SonarQubeApplicationSpec.java

package com.cloudforgeci.api.application.cicd;

import com.cloudforge.core.annotation.ApplicationPlugin;
import com.cloudforge.core.interfaces.ApplicationSpec;
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;

/**
 * SonarQube ApplicationSpec -- continuous code quality and security analysis.
 *
 * <p><strong>Deployment:</strong></p>
 * <ul>
 *   <li>Fargate: {@code sonarqube:lts-community} Docker image</li>
 *   <li>EC2: installs SonarQube from the official ZIP distribution</li>
 * </ul>
 *
 * @see <a href="https://docs.sonarsource.com/sonarqube/">SonarQube Documentation</a>
 */
@ApplicationPlugin(
    value = "sonarqube",
    category = "code-quality",
    displayName = "SonarQube",
    description = "Continuous code quality and security inspection platform",
    defaultCpu = 2048,
    defaultMemory = 4096,
    defaultInstanceType = "t3.medium",
    supportsFargate = true,
    supportsEc2 = true,
    supportsOidc = false
)
public class SonarQubeApplicationSpec implements ApplicationSpec {

    // ========== Application Identity ==========

    @Override
    public String applicationId() {
        return "sonarqube";
    }

    // SonarQube Community Edition has no clustering/HA support (a Data Center Edition feature),
    // so ScalingFactory rejects multi-instance settings for it.
    @Override
    public boolean supportsAutoScaling() {
        return false;
    }

    // ========== Container Configuration (Fargate) ==========

    @Override
    public String defaultContainerImage() {
        return "sonarqube:lts-community";
    }

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

    @Override
    public List<OptionalPort> optionalPorts() {
        // Metrics are available on the primary port (/api/system/health, /api/monitoring/*) --
        // no additional ports needed.
        return List.of();
    }

    @Override
    public String containerDataPath() {
        return "/opt/sonarqube/data";
    }

    @Override
    public String efsDataPath() {
        return "/sonarqube";
    }

    @Override
    public String volumeName() {
        return "sonarqubeData";
    }

    @Override
    public String containerUser() {
        return "1000:1000"; // SonarQube runs as user 1000
    }

    @Override
    public String efsPermissions() {
        return "755";
    }

    @Override
    public String healthCheckPath() {
        return "/api/system/health";
    }

    @Override
    public Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
        Map<String, String> environment = new HashMap<>();

        if (fqdn != null && !fqdn.isBlank()) {
            String serverUrl = (sslEnabled ? "https://" : "http://") + fqdn;
            environment.put("SONAR_WEB_CONTEXT", "/");
            environment.put("SONAR_WEB_HOST", "0.0.0.0");
            environment.put("SONAR_WEB_PORT", String.valueOf(applicationPort()));
            environment.put("SONAR_WEB_PUBLIC_URL", serverUrl);
        }

        environment.put("SONAR_WEB_JAVAADDITIONALOPTS", "-XX:+UseG1GC -Xmx2g -Xms512m");
        environment.put("SONAR_CE_JAVAADDITIONALOPTS", "-XX:+UseG1GC -Xmx1g -Xms256m");

        return environment;
    }

    // ========== EC2 Configuration ==========

    @Override
    public String ebsDeviceName() {
        return "/dev/xvdh";
    }

    @Override
    public String ec2DataPath() {
        return "/opt/sonarqube/data";
    }

    @Override
    public List<String> ec2LogPaths() {
        return List.of(
            "/opt/sonarqube/logs/sonar.log",
            "/opt/sonarqube/logs/web.log",
            "/opt/sonarqube/logs/ce.log",
            "/var/log/userdata.log",
            "/var/log/messages"
        );
    }

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

        builder.addCommands(
            "# Install Java 17",
            "command -v dnf >/dev/null && dnf -y install java-17-amazon-corretto-headless unzip || " +
            "yum -y install java-17-amazon-corretto-headless unzip",
            "echo 'Java 17 installed' >> /var/log/userdata.log"
        );

        String sonarVersion = "10.3.0.82913"; // LTS version
        builder.addCommands(
            "# Download SonarQube",
            "cd /tmp",
            "wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-" + sonarVersion + ".zip",
            "unzip sonarqube-" + sonarVersion + ".zip",
            "mv sonarqube-" + sonarVersion + " /opt/sonarqube",
            "echo 'SonarQube downloaded and extracted' >> /var/log/userdata.log"
        );

        builder.addCommands(
            "# Create SonarQube user",
            "useradd -r -s /bin/bash sonarqube || true",
            "echo 'SonarQube user created' >> /var/log/userdata.log"
        );

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

        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
            );
        }

        builder.addCommands(
            "# Configure SonarQube",
            "mkdir -p /opt/sonarqube/data/es7",
            "mkdir -p /opt/sonarqube/logs",
            "mkdir -p /opt/sonarqube/temp",
            "chown -R sonarqube:sonarqube /opt/sonarqube",
            "echo 'SonarQube directories configured' >> /var/log/userdata.log"
        );

        builder.addCommands(
            "# Configure SonarQube properties",
            "cat > /opt/sonarqube/conf/sonar.properties <<'EOF'",
            "# SonarQube Server Configuration",
            "sonar.web.host=0.0.0.0",
            "sonar.web.port=9000",
            "sonar.web.context=/",
            "sonar.path.data=" + ec2DataPath(),
            "sonar.path.logs=/opt/sonarqube/logs",
            "sonar.path.temp=/opt/sonarqube/temp",
            "",
            "# Public URL (configure via ALB DNS or custom domain)",
            "# sonar.core.serverBaseURL=https://your-domain.com",
            "",
            "# Elasticsearch",
            "sonar.search.javaOpts=-Xmx512m -Xms512m -XX:MaxDirectMemorySize=256m -XX:+HeapDumpOnOutOfMemoryError",
            "",
            "# Web Server",
            "sonar.web.javaOpts=-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError",
            "",
            "# Compute Engine",
            "sonar.ce.javaOpts=-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError",
            "EOF",
            "chown sonarqube:sonarqube /opt/sonarqube/conf/sonar.properties",
            "echo 'SonarQube properties configured' >> /var/log/userdata.log"
        );

        builder.addCommands(
            "# Create SonarQube systemd service",
            "cat > /etc/systemd/system/sonarqube.service <<'EOF'",
            "[Unit]",
            "Description=SonarQube service",
            "After=network.target network-online.target",
            "Requires=network-online.target",
            "",
            "[Service]",
            "Type=forking",
            "ExecStart=/opt/sonarqube/bin/linux-x86-64/sonar.sh start",
            "ExecStop=/opt/sonarqube/bin/linux-x86-64/sonar.sh stop",
            "ExecReload=/opt/sonarqube/bin/linux-x86-64/sonar.sh restart",
            "User=sonarqube",
            "Group=sonarqube",
            "Restart=on-failure",
            "RestartSec=10",
            "LimitNOFILE=65536",
            "LimitNPROC=4096",
            "",
            "[Install]",
            "WantedBy=multi-user.target",
            "EOF",
            "echo 'SonarQube systemd service created' >> /var/log/userdata.log"
        );

        builder.addCommands(
            "# Set system limits for SonarQube (Elasticsearch requirements)",
            "echo 'sonarqube - nofile 65536' >> /etc/security/limits.conf",
            "echo 'sonarqube - nproc 4096' >> /etc/security/limits.conf",
            "echo 'vm.max_map_count=262144' >> /etc/sysctl.conf",
            "sysctl -w vm.max_map_count=262144",
            "echo 'System limits configured for SonarQube' >> /var/log/userdata.log"
        );

        builder.addCommands(
            "# Start SonarQube",
            "systemctl daemon-reload",
            "systemctl enable sonarqube",
            "systemctl start sonarqube",
            "echo 'SonarQube service started' >> /var/log/userdata.log",
            "",
            "# Wait for SonarQube to fully start",
            "sleep 60",
            "",
            "# Check SonarQube status",
            "if systemctl is-active --quiet sonarqube; then",
            "  echo 'SonarQube is running' >> /var/log/userdata.log",
            "  echo 'Access SonarQube via ALB endpoint' >> /var/log/userdata.log",
            "  echo 'Default credentials: admin / admin' >> /var/log/userdata.log",
            "else",
            "  echo 'ERROR: SonarQube failed to start' >> /var/log/userdata.log",
            "  tail -50 /opt/sonarqube/logs/sonar.log >> /var/log/userdata.log",
            "fi"
        );
    }

    // ========== OIDC Integration ==========

    @Override
    public boolean supportsOidcIntegration() {
        return false; // SonarQube Community Edition doesn't support OIDC (Enterprise feature)
    }

    @Override
    public OidcIntegration getOidcIntegration() {
        return null;
    }

    @Override
    public String toString() {
        return "SonarQubeApplicationSpec{" +
                "applicationId='sonarqube'" +
                ", defaultImage='sonarqube:lts-community'" +
                ", applicationPort=9000" +
                ", containerDataPath='/opt/sonarqube/data'" +
                ", ec2DataPath='/opt/sonarqube/data'" +
                '}';
    }
}