MagentoApplicationSpec.java

package com.cloudforgeci.api.application.cms;

import com.cloudforge.core.annotation.CmsPlugin;
import com.cloudforge.core.interfaces.CmsSpec;
import com.cloudforge.core.interfaces.DatabaseSpec;
import com.cloudforge.core.interfaces.Ec2Context;
import com.cloudforge.core.interfaces.OidcIntegration;
import com.cloudforge.core.interfaces.PhpRuntimeConfig;
import com.cloudforge.core.interfaces.UserDataBuilder;
import com.cloudforge.core.oidc.MagentoOidcIntegration;
import com.cloudforgeci.api.core.PhpUserDataBuilder;

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

/**
 * Magento 2 E-commerce ApplicationSpec implementation.
 *
 * <p>Magento is an e-commerce platform owned by Adobe with B2B and B2C
 * catalog, order, and storefront features.</p>
 *
 * <h2>Key Features:</h2>
 * <ul>
 *   <li>PHP 8.2 with advanced extensions</li>
 *   <li>MySQL 8.0 / MariaDB 10.6+ required</li>
 *   <li>Elasticsearch/OpenSearch for catalog search</li>
 *   <li>Redis for cache and sessions</li>
 *   <li>Varnish for full-page caching</li>
 *   <li>RabbitMQ for message queues (optional)</li>
 * </ul>
 *
 * <h2>Resource Requirements:</h2>
 * <p>Magento is resource-intensive and requires significant CPU,
 * memory, and storage compared to other PHP platforms.</p>
 *
 * @since 3.1.0
 * @see CmsSpec
 */
@CmsPlugin(
    value = "magento",
    category = "ecommerce",
    displayName = "Magento 2 / Adobe Commerce",
    description = "E-commerce platform for B2B and B2C online stores",
    phpVersion = "8.2",
    defaultCpu = 4096,
    defaultMemory = 8192,
    defaultInstanceType = "t3.xlarge",
    supportsOidc = true,
    oidcMethod = "miniOrange OIDC Module",
    requiresDatabase = true,
    supportedDatabases = {"mysql", "mariadb"},
    supportsS3Media = true,
    supportsObjectCache = true,
    supportsMultisite = true,
    websiteUrl = "https://business.adobe.com/products/magento/magento-commerce.html",
    defaultImage = "magento/magento-cloud-docker-php:8.2-fpm"
)
public class MagentoApplicationSpec implements CmsSpec, DatabaseSpec {

    // ========== Constants ==========

    protected static final String APPLICATION_ID = "magento";
    protected static final String DEFAULT_IMAGE = "magento/magento-cloud-docker-php:8.2-fpm";
    protected static final int APPLICATION_PORT = 80;
    protected static final String CONTAINER_DATA_PATH = "/var/www/html";
    protected static final String EFS_DATA_PATH = "/magento";
    protected static final String VOLUME_NAME = "magentoData";
    protected static final String CONTAINER_USER = "33:33";
    protected static final String EFS_PERMISSIONS = "755";

    protected static final String PHP_VERSION = "8.2";
    protected static final List<String> PHP_EXTENSIONS = List.of(
        "bcmath", "ctype", "curl", "dom", "fileinfo", "gd", "hash",
        "iconv", "intl", "json", "libxml", "mbstring", "openssl",
        "pdo_mysql", "simplexml", "soap", "sockets", "sodium",
        "spl", "tokenizer", "xmlwriter", "xsl", "zip", "zlib",
        "opcache", "redis", "imagick"
    );

    // ========== ApplicationSpec Implementation ==========

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

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

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

    @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 String efsPermissions() {
        return EFS_PERMISSIONS;
    }

    @Override
    public String healthCheckPath() {
        return "/health_check.php";
    }

    @Override
    public int defaultHealthCheckGracePeriod() {
        return 600; // 10 minutes - Magento startup is slow
    }

    // ========== CmsSpec Implementation ==========

    @Override
    public String phpVersion() {
        return PHP_VERSION;
    }

    @Override
    public List<String> requiredPhpExtensions() {
        return PHP_EXTENSIONS;
    }

    @Override
    public Map<String, String> phpFpmConfig() {
        return Map.of(
            "pm", "dynamic",
            "pm.max_children", "100",
            "pm.start_servers", "20",
            "pm.min_spare_servers", "10",
            "pm.max_spare_servers", "50",
            "pm.max_requests", "1000"
        );
    }

    @Override
    public Map<String, String> opcacheConfig() {
        return Map.of(
            "opcache.enable", "1",
            "opcache.memory_consumption", "512",
            "opcache.interned_strings_buffer", "64",
            "opcache.max_accelerated_files", "60000",
            "opcache.revalidate_freq", "0",
            "opcache.validate_timestamps", "0",
            "opcache.save_comments", "1",
            "opcache.fast_shutdown", "1"
        );
    }

    @Override
    public int phpMemoryLimit() {
        return 2048; // 2GB for Magento
    }

    @Override
    public int phpMaxExecutionTime() {
        return 18000; // 5 hours for reindexing
    }

    @Override
    public int phpUploadMaxFilesize() {
        return 128;
    }

    @Override
    public int phpPostMaxSize() {
        return 128;
    }

    @Override
    public boolean supportsS3MediaStorage() {
        return true;
    }

    @Override
    public String s3MediaPlugin() {
        return "magento/module-aws-s3";
    }

    @Override
    public String mediaUploadPath() {
        return "/var/www/html/pub/media";
    }

    @Override
    public boolean supportsCdnIntegration() {
        return true;
    }

    @Override
    public List<String> cdnMediaPaths() {
        return List.of("/pub/media/*", "/media/*");
    }

    @Override
    public List<String> cdnStaticPaths() {
        return List.of("/pub/static/*", "/static/*");
    }

    @Override
    public List<String> cdnAdminPaths() {
        // Admin path is customizable in real Magento installs; override this method if you've changed it.
        return List.of("/admin/*", "/backend/*", "/setup/*");
    }

    @Override
    public Map<String, String> redisEnvVars(String host, int port) {
        Map<String, String> env = new HashMap<>();
        env.put("REDIS_HOST", host);
        env.put("REDIS_PORT", String.valueOf(port));
        // Default cache backend (database 0)
        env.put("MAGENTO_CACHE_BACKEND_REDIS_SERVER", host);
        env.put("MAGENTO_CACHE_BACKEND_REDIS_PORT", String.valueOf(port));
        env.put("MAGENTO_CACHE_BACKEND_REDIS_DATABASE", "0");
        // Full-page cache backend (database 1)
        env.put("MAGENTO_PAGE_CACHE_BACKEND_REDIS_SERVER", host);
        env.put("MAGENTO_PAGE_CACHE_BACKEND_REDIS_PORT", String.valueOf(port));
        env.put("MAGENTO_PAGE_CACHE_BACKEND_REDIS_DATABASE", "1");
        // Session backend (database 2)
        env.put("MAGENTO_SESSION_BACKEND_REDIS_SERVER", host);
        env.put("MAGENTO_SESSION_BACKEND_REDIS_PORT", String.valueOf(port));
        env.put("MAGENTO_SESSION_BACKEND_REDIS_DATABASE", "2");
        return env;
    }

    @Override
    public Map<String, String> databaseEnvVars(String host, int port, String name, String user) {
        Map<String, String> env = new HashMap<>();
        env.put("DB_HOST", host);
        env.put("DB_PORT", String.valueOf(port));
        env.put("DB_NAME", name);
        env.put("DB_USER", user);
        env.put("MAGENTO_DATABASE_HOST", host);
        env.put("MAGENTO_DATABASE_PORT", String.valueOf(port));
        env.put("MAGENTO_DATABASE_NAME", name);
        env.put("MAGENTO_DATABASE_USER", user);
        return env;
    }

    @Override
    public boolean supportsObjectCache() {
        return true;
    }

    @Override
    public String preferredCacheBackend() {
        return "redis";
    }

    @Override
    public String objectCachePlugin() {
        return null; // Native Redis support
    }

    @Override
    public boolean hasScheduledTasks() {
        return true;
    }

    @Override
    public boolean useSystemCron() {
        return true;
    }

    @Override
    public Map<String, String> cronCommands(String siteUrl) {
        Map<String, String> crons = new HashMap<>();
        // Magento cron groups
        crons.put("* * * * *", "php /var/www/html/bin/magento cron:run --group=default");
        crons.put("* * * * *", "php /var/www/html/bin/magento cron:run --group=index");
        crons.put("*/5 * * * *", "php /var/www/html/bin/magento cron:run --group=consumers");
        crons.put("*/15 * * * *", "php /var/www/html/bin/magento cron:run --group=ddg_automation");
        return crons;
    }

    @Override
    public boolean supportsMultisite() {
        return true;
    }

    @Override
    public String multisiteMode() {
        return "domain"; // Magento uses store views with separate domains
    }

    @Override
    public String cmsCategory() {
        return "ecommerce";
    }

    @Override
    public String preferredWebServer() {
        return "nginx";
    }

    @Override
    public String documentRoot() {
        return CONTAINER_DATA_PATH + "/pub";
    }

    @Override
    public String cliTool() {
        return "bin/magento";
    }

    @Override
    public List<String> cliToolInstallCommands() {
        return List.of(); // Magento CLI is part of the installation
    }

    // ========== DatabaseSpec Implementation ==========

    @Override
    public DatabaseRequirement databaseRequirement() {
        return DatabaseRequirement.required("mysql", "8.0")
            .withInstanceClass("db.r6g.large")
            .withStorage(100)
            .withDatabaseName("magento");
    }

    @Override
    public int backupRetentionDays() {
        return 14;
    }

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

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

    @Override
    public String ec2DataPath() {
        return "/var/www/html";
    }

    @Override
    public List<String> ec2LogPaths() {
        return List.of(
            "/var/log/nginx/access.log",
            "/var/log/nginx/error.log",
            "/var/log/php-fpm/error.log",
            "/var/www/html/var/log/system.log",
            "/var/www/html/var/log/exception.log",
            "/var/www/html/var/log/debug.log",
            "/var/log/userdata.log"
        );
    }

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

        // Install PHP
        PhpRuntimeConfig phpConfig = PhpRuntimeConfig.forMagento();
        PhpUserDataBuilder.installPhp(builder, phpConfig);
        PhpUserDataBuilder.installNginx(builder);
        PhpUserDataBuilder.installComposer(builder);

        // Install Elasticsearch
        builder.addCommands(
            "# Install Elasticsearch",
            "rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch",
            "cat > /etc/yum.repos.d/elasticsearch.repo << 'EOF'",
            "[elasticsearch]",
            "name=Elasticsearch repository",
            "baseurl=https://artifacts.elastic.co/packages/8.x/yum",
            "gpgcheck=1",
            "gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch",
            "enabled=1",
            "autorefresh=1",
            "type=rpm-md",
            "EOF",
            "dnf install -y elasticsearch",
            "systemctl enable elasticsearch",
            "systemctl start elasticsearch",
            "echo 'Elasticsearch installed' >> /var/log/userdata.log"
        );

        // Mount storage
        String[] userParts = containerUser().split(":");
        if (context.hasEfs()) {
            builder.mountEfs(
                context.efsId().orElseThrow(),
                context.accessPointId().orElseThrow(),
                ec2DataPath(),
                userParts[0], userParts[1]
            );
        } else {
            builder.mountEbs(ebsDeviceName(), ec2DataPath(), userParts[0], userParts[1]);
        }

        // Set permissions
        builder.addCommands(
            "# Set Magento permissions",
            "chown -R nginx:nginx " + ec2DataPath(),
            "find " + ec2DataPath() + " -type d -exec chmod 755 {} \\;",
            "find " + ec2DataPath() + " -type f -exec chmod 644 {} \\;",
            "chmod -R 775 " + ec2DataPath() + "/var",
            "chmod -R 775 " + ec2DataPath() + "/pub/static",
            "chmod -R 775 " + ec2DataPath() + "/pub/media",
            "chmod -R 775 " + ec2DataPath() + "/generated",
            "",
            "# Start services",
            "systemctl restart nginx php-fpm",
            "echo 'Magento installation complete' >> /var/log/userdata.log"
        );

        // Install CloudWatch agent
        String logGroupName = String.format("/cloudforge/%s/%s",
            context.stackName(), applicationId());
        builder.installCloudWatchAgent(logGroupName, ec2LogPaths());
    }

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

        // Magento mode
        env.put("MAGE_MODE", "production");

        // Base URLs
        if (fqdn != null && !fqdn.isBlank()) {
            String baseUrl = (sslEnabled ? "https://" : "http://") + fqdn + "/";
            env.put("MAGENTO_BACKEND_FRONTNAME", "admin");
            env.put("MAGENTO_BASE_URL", baseUrl);
            env.put("MAGENTO_BASE_URL_SECURE", sslEnabled ? baseUrl : "");
            env.put("MAGENTO_USE_SECURE", sslEnabled ? "1" : "0");
            env.put("MAGENTO_USE_SECURE_ADMIN", sslEnabled ? "1" : "0");
        }

        // Performance settings
        env.put("MAGENTO_CACHE_BACKEND", "redis");
        env.put("MAGENTO_SESSION_SAVE", "redis");

        return env;
    }

    // ========== OIDC Support ==========

    @Override
    public boolean supportsOidcIntegration() {
        return true;
    }

    @Override
    public OidcIntegration getOidcIntegration() {
        return new MagentoOidcIntegration();
    }

    @Override
    public List<String> getSupportedAuthModes() {
        return List.of("application-oidc", "alb-oidc", "none");
    }

    // ========== Magento Specific ==========

    /**
     * Returns whether Elasticsearch/OpenSearch is required.
     *
     * @return true (Magento 2.4+ requires search engine)
     */
    public boolean requiresSearchEngine() {
        return true;
    }

    /**
     * Returns the preferred search engine.
     *
     * @return "opensearch" or "elasticsearch"
     */
    public String preferredSearchEngine() {
        return "opensearch";
    }

    /**
     * Returns whether Varnish full-page cache is recommended.
     *
     * @return true for production deployments
     */
    public boolean supportsVarnish() {
        return true;
    }

    /**
     * Returns whether RabbitMQ is supported for async operations.
     *
     * @return true
     */
    public boolean supportsRabbitMq() {
        return true;
    }

    /**
     * Returns PhpRuntimeConfig optimized for Magento.
     *
     * @return PHP runtime configuration
     */
    public static PhpRuntimeConfig getPhpConfig() {
        return PhpRuntimeConfig.forMagento();
    }
}