HarborApplicationSpec.java

package com.cloudforgeci.api.application.artifactregistry;

import com.cloudforge.core.annotation.ApplicationPlugin;

import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.DatabaseSpec;
import com.cloudforge.core.interfaces.DatabaseSpec.DatabaseConnection;
import com.cloudforge.core.interfaces.DatabaseSpec.DatabaseRequirement;
import com.cloudforge.core.interfaces.Ec2Context;
import com.cloudforge.core.interfaces.OidcIntegration;
import com.cloudforge.core.interfaces.UserDataBuilder;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Harbor Container Registry ApplicationSpec implementation.
 *
 * <p>Harbor is an open-source container registry with security, identity, and management.</p>
 *
 * <p><strong>Key Features:</strong></p>
 * <ul>
 *   <li>Container image vulnerability scanning</li>
 *   <li>Image signing and verification</li>
 *   <li>Role-based access control (RBAC)</li>
 *   <li>Replication across registries</li>
 *   <li>Audit logging</li>
 * </ul>
 *
 * <p><strong>Compliance Use Cases:</strong></p>
 * <ul>
 *   <li>SOC2: Container image provenance and audit trails</li>
 *   <li>PCI-DSS: Secure storage of payment processing containers</li>
 *   <li>HIPAA: Vulnerability scanning for healthcare containers</li>
 * </ul>
 *
 * <p><strong>Fintech Applications:</strong></p>
 * <ul>
 *   <li>Store Docker images for payment processing services</li>
 *   <li>Vulnerability scanning for financial applications</li>
 *   <li>Image signing for regulatory compliance</li>
 *   <li>Content trust and notary integration</li>
 * </ul>
 *
 * <p><strong>Database Requirements:</strong></p>
 * <ul>
 *   <li><b>REQUIRED:</b> PostgreSQL 10+ via RDS</li>
 *   <li>Harbor does NOT support embedded databases for production</li>
 *   <li>Recommended: PostgreSQL with db.t3.medium or larger</li>
 * </ul>
 *
 * <p><strong>Security Note:</strong></p>
 * <ul>
 *   <li>Enable content trust for image signing</li>
 *   <li>Configure vulnerability scanning</li>
 *   <li>Use OIDC/LDAP for authentication</li>
 *   <li>Enable audit logging</li>
 * </ul>
 *
 * @see <a href="https://goharbor.io/docs/">Harbor Documentation</a>
 */
@ApplicationPlugin(
    value = "harbor",
    category = "artifactregistry",
    displayName = "Harbor",
    description = "Container image registry with security scanning",
    defaultCpu = 2048,
    defaultMemory = 4096,
    defaultInstanceType = "t3.medium",
    supportsFargate = true,
    supportsEc2 = true,
    supportsOidc = false,
    supportsDatabase = true,
    requiresDatabase = true
)

public class HarborApplicationSpec implements ApplicationSpec, DatabaseSpec {

    private static final String APPLICATION_ID = "harbor";
    private static final String DEFAULT_IMAGE = "goharbor/harbor-core:v2.9.0";
    private static final int APPLICATION_PORT = 80;
    private static final String CONTAINER_DATA_PATH = "/data";
    private static final String EFS_DATA_PATH = "/harbor";
    private static final String VOLUME_NAME = "harborData";
    private static final String CONTAINER_USER = "10000:10000"; // harbor user
    private static final String EFS_PERMISSIONS = "755";
    private static final String EBS_DEVICE_NAME = "/dev/xvdh";
    private static final String EC2_DATA_PATH = "/data/harbor";
    private static final List<String> EC2_LOG_PATHS = List.of(
        "/var/log/harbor/harbor.log",
        "/var/log/userdata.log"
    );

    @Override
    public String applicationId() {
        return APPLICATION_ID;
    }

    @Override
    public String defaultContainerImage() {
        return DEFAULT_IMAGE;
    }

    @Override
    public int applicationPort() {
        return APPLICATION_PORT;
    }

    @Override
    public java.util.List<OptionalPort> optionalPorts() {
        return java.util.List.of(
            // Notary - inbound for content trust and image signing
            OptionalPort.inboundTcp(4443, "enableNotary", "Notary (Content Trust)"),
            // Trivy - inbound for vulnerability scanning API
            OptionalPort.inboundTcp(8080, "enableTrivy", "Trivy Scanner")
        );
    }

    @Override
    public String containerDataPath() {
        return CONTAINER_DATA_PATH;
    }

    @Override
    public String efsDataPath() {
        return EFS_DATA_PATH;
    }

    @Override
    public String volumeName() {
        return VOLUME_NAME;
    }

    @Override
    public String containerUser() {
        return CONTAINER_USER;
    }

    @Override
    public DatabaseRequirement databaseRequirement() {
        // Harbor REQUIRES PostgreSQL for registry metadata
        return DatabaseRequirement.required("postgres", "13")
            .withInstanceClass("db.t3.medium")
            .withStorage(50)
            .withDatabaseName("registry");
    }

    @Override
    public Map<String, String> databaseParameters() {
        // PostgreSQL optimization for Harbor registry workload
        return Map.of(
            "max_connections", "250",
            "shared_buffers", "{DBInstanceClassMemory/4096}",
            "work_mem", "16MB",
            "maintenance_work_mem", "256MB",
            "log_statement", "ddl"
        );
    }

    @Override
    public int backupRetentionDays() {
        return 30; // Harbor contains container registry metadata
    }

    @Override
    public Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
        // Delegate to new method with null database connection for backward compatibility
        return containerEnvironmentVariables(fqdn, sslEnabled, authMode, null);
    }

    /**
     * Container environment variables with database connection support.
     *
     * <p>Configures Harbor to use RDS PostgreSQL for registry metadata.
     * Harbor REQUIRES a database for all deployments.</p>
     */
    public Map<String, String> containerEnvironmentVariables(
            String fqdn, boolean sslEnabled, String authMode, DatabaseConnection dbConn) {
        Map<String, String> environment = new HashMap<>();

        // Configure hostname
        if (fqdn != null && !fqdn.isBlank()) {
            environment.put("HARBOR_HOSTNAME", fqdn);
            environment.put("HARBOR_EXTERNAL_URL", (sslEnabled ? "https://" : "http://") + fqdn);
        }

        // Proxy/Load Balancer configuration - CRITICAL for ALB deployments
        // Trust X-Forwarded-* headers from ALB for proper IP logging and HTTPS detection
        // Harbor's nginx config handles this, but we set the external URL correctly above
        // which tells Harbor it's behind a reverse proxy
        environment.put("HTTP_PROXY", "");  // Don't use outbound proxy
        environment.put("HTTPS_PROXY", "");  // Don't use outbound proxy
        // Harbor detects ALB by checking X-Forwarded-Proto header automatically

        // Database configuration (REQUIRED for Harbor)
        if (dbConn != null) {
            // Use RDS PostgreSQL for registry metadata
            environment.put("POSTGRESQL_HOST", dbConn.endpoint());
            environment.put("POSTGRESQL_PORT", String.valueOf(dbConn.port()));
            environment.put("POSTGRESQL_DATABASE", dbConn.databaseName());
            environment.put("POSTGRESQL_USERNAME", dbConn.username());
            // Password is injected via ECS secret as GITLAB_DATABASE_PASSWORD
            // Harbor will read it as POSTGRESQL_PASSWORD when ECS injects it
            // Don't set it here - ContainerFactory adds it as an ECS secret
            environment.put("POSTGRESQL_SSLMODE", "require");
        } else {
            // NOTE: Harbor REQUIRES a database - this should never happen
            // Set placeholder that will fail fast if database is missing
            environment.put("POSTGRESQL_HOST", "MISSING_DATABASE_CONNECTION");
        }

        return environment;
    }

    @Override
    public String efsPermissions() {
        return EFS_PERMISSIONS;
    }

    @Override
    public String ebsDeviceName() {
        return EBS_DEVICE_NAME;
    }

    @Override
    public String ec2DataPath() {
        return EC2_DATA_PATH;
    }

    @Override
    public List<String> ec2LogPaths() {
        return EC2_LOG_PATHS;
    }

    @Override
    public void configureUserData(UserDataBuilder builder, Ec2Context context) {
        builder.addSystemUpdate();

        // Install Docker and Docker Compose
        builder.addCommands(
            "# Install Docker",
            "yum install -y docker",
            "systemctl enable docker",
            "systemctl start docker",
            "echo 'Docker installed' >> /var/log/userdata.log",
            "",
            "# Install Docker Compose",
            "curl -L \"https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose",
            "chmod +x /usr/local/bin/docker-compose",
            "echo 'Docker Compose installed' >> /var/log/userdata.log"
        );

        // Install CloudWatch Agent
        String logGroupName = String.format("/aws/%s/%s/%s",
            context.stackName(),
            context.runtimeType(),
            context.securityProfile());
        builder.installCloudWatchAgent(logGroupName, ec2LogPaths());

        // Mount storage
        String[] userParts = containerUser().split(":");
        String uid = userParts[0];
        String gid = userParts[1];

        if (context.hasEfs()) {
            builder.mountEfs(
                context.efsId().orElseThrow(),
                context.accessPointId().orElseThrow(),
                ec2DataPath(),
                uid,
                gid
            );
        } else {
            builder.mountEbs(
                ebsDeviceName(),
                ec2DataPath(),
                uid,
                gid
            );
        }

        // Download and configure Harbor
        builder.addCommands(
            "# Retrieve Harbor passwords from Secrets Manager or generate secure defaults",
            "HARBOR_ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id ${STACK_NAME:-harbor}/admin-password --query SecretString --output text 2>/dev/null || openssl rand -base64 16)",
            "HARBOR_DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id ${STACK_NAME:-harbor}/db-password --query SecretString --output text 2>/dev/null || openssl rand -base64 16)",
            "echo \"Generated Harbor admin password (save this): $HARBOR_ADMIN_PASSWORD\" >> /var/log/userdata.log",
            "",
            "# Download Harbor installer",
            "cd /opt",
            "curl -L https://github.com/goharbor/harbor/releases/download/v2.9.0/harbor-offline-installer-v2.9.0.tgz -o harbor.tgz",
            "tar xzvf harbor.tgz",
            "cd harbor",
            "",
            "# Create Harbor configuration",
            "cat > harbor.yml <<'EOF'",
            "# Harbor Configuration",
            "",
            "hostname: harbor.example.com",
            "",
            "# HTTP settings",
            "http:",
            "  port: 80",
            "",
            "# HTTPS settings (configure for production)",
            "# https:",
            "#   port: 443",
            "#   certificate: /data/cert/server.crt",
            "#   private_key: /data/cert/server.key",
            "",
            "# Harbor admin password - retrieve from Secrets Manager",
            "harbor_admin_password: $HARBOR_ADMIN_PASSWORD",
            "",
            "# Database configuration",
            "database:",
            "  password: $HARBOR_DB_PASSWORD",
            "  max_idle_conns: 100",
            "  max_open_conns: 900",
            "",
            "# Data volume",
            "data_volume: " + ec2DataPath(),
            "",
            "# Log settings",
            "log:",
            "  level: info",
            "  local:",
            "    rotate_count: 50",
            "    rotate_size: 200M",
            "    location: /var/log/harbor",
            "",
            "_version: '2.9.0'",
            "EOF",
            "",
            "# Prepare Harbor installation",
            "./prepare",
            "",
            "# Install and start Harbor",
            "./install.sh --with-trivy --with-chartmuseum",
            "",
            "echo 'Harbor installation complete' >> /var/log/userdata.log",
            "echo 'Access Harbor at http://harbor.example.com' >> /var/log/userdata.log",
            "echo 'Admin password was logged above - store it securely' >> /var/log/userdata.log"
        );
    }

    @Override
    public boolean supportsOidcIntegration() {
        // Harbor has built-in OIDC support but no OidcIntegration implementation yet
        // Return false until getOidcIntegration() returns a valid implementation
        return false;
    }

    @Override
    public OidcIntegration getOidcIntegration() {
        // Harbor has built-in OIDC support
        // TODO: Implement HarborOidcIntegration to configure harbor.yml with OIDC settings
        return null;
    }

    @Override
    public String toString() {
        return "HarborApplicationSpec{" +
                "applicationId='" + APPLICATION_ID + '\'' +
                ", defaultImage='" + DEFAULT_IMAGE + '\'' +
                ", applicationPort=" + APPLICATION_PORT +
                '}';
    }
}