ApplicationSpec.java
package com.cloudforge.core.interfaces;
import com.cloudforge.core.annotation.ApplicationPlugin;
import java.util.List;
/**
* Application specification interface defining application-specific configuration.
* This enables CloudForge to deploy any application (Jenkins, GitLab, Vault, etc.)
* using the same infrastructure patterns.
*
* <p>Implementations provide configuration for both container (Fargate) and
* EC2 deployments, supporting both EFS and EBS storage strategies.</p>
*
* <p>CloudForge 3.0.0: Universal Application Support</p>
*
* <p>Example implementations:</p>
* <ul>
* <li>JenkinsApplicationSpec: Jenkins CI/CD automation server</li>
* <li>GitLabApplicationSpec: GitLab DevOps platform</li>
* <li>GrafanaApplicationSpec: Grafana metrics visualization</li>
* <li>PostgreSQLApplicationSpec: PostgreSQL database</li>
* <li>VaultApplicationSpec: HashiCorp Vault secrets management</li>
* <li>+ 9 more built-in applications</li>
* </ul>
*
* <h2>Plugin Metadata:</h2>
* <p>Implementations should be annotated with {@link ApplicationPlugin} for auto-discovery
* and metadata support:</p>
* <pre>{@code
* @ApplicationPlugin(
* value = "jenkins",
* category = "cicd",
* displayName = "Jenkins",
* description = "Open-source automation server for CI/CD"
* )
* public class JenkinsApplicationSpec implements ApplicationSpec {
* // ...
* }
* }</pre>
*/
public interface ApplicationSpec {
// ========== Application Identity ==========
/**
* Returns a unique identifier for this application.
* Used for logging, metrics, and resource naming.
*
* @return application identifier (e.g., "jenkins", "gitlab", "vault")
*/
String applicationId();
// ========== Container Configuration ==========
/**
* Returns the default container image for this application.
* Can be overridden by deployment context configuration.
*
* @return container image string (e.g., "jenkins/jenkins:lts")
*/
String defaultContainerImage();
/**
* Returns the primary application port.
* This is the port the application listens on inside the container.
*
* @return application port (e.g., 8080 for Jenkins)
*/
int applicationPort();
/**
* Returns the container path where application data is stored.
* This is where the volume will be mounted inside the container.
*
* @return container mount path (e.g., "/var/jenkins_home")
*/
String containerDataPath();
/**
* Returns the EFS path for this application's data.
* This is the path within the EFS filesystem.
*
* @return EFS path (e.g., "/jenkins")
*/
String efsDataPath();
/**
* Returns the volume name for this application.
* Used to reference the volume in task definitions.
*
* @return volume name (e.g., "jenkinsHome")
*/
String volumeName();
/**
* Returns the container user (UID:GID) to run as.
* Important for file permissions when using EFS.
*
* @return user in format "UID:GID" (e.g., "1000:1000")
*/
String containerUser();
/**
* Returns the EFS permissions for the access point.
*
* @return permissions string (e.g., "750")
*/
String efsPermissions();
/**
* Configures application-specific environment variables for the container.
*
* <p>Applications can override this to provide custom environment variables
* based on deployment configuration (FQDN, SSL, authMode, etc.). The infrastructure
* passes the FQDN, SSL settings, and authentication mode for applications that need
* reverse proxy configuration or authentication-specific setup.</p>
*
* <p>Example use cases:</p>
* <ul>
* <li>Jenkins: JAVA_OPTS, JENKINS_OPTS for reverse proxy configuration, skip setup wizard for application-oidc</li>
* <li>GitLab: GITLAB_OMNIBUS_CONFIG for external URL configuration and OIDC setup</li>
* <li>Vault: VAULT_ADDR for API endpoint configuration</li>
* </ul>
*
* @param fqdn The fully qualified domain name (may be null)
* @param sslEnabled Whether SSL is enabled
* @param authMode The authentication mode (may be null, e.g., "none", "alb-oidc", "application-oidc")
* @return Map of environment variable key-value pairs (never null, may be empty)
*/
default java.util.Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
return java.util.Collections.emptyMap();
}
/**
* Returns the health check path for ALB/ELB health checks.
*
* <p>Different applications expose health endpoints at different paths:</p>
* <ul>
* <li>Jenkins: /login</li>
* <li>GitLab: /users/sign_in</li>
* <li>Grafana: /api/health</li>
* <li>Metabase: /api/health</li>
* </ul>
*
* @return health check path (e.g., "/login", "/api/health")
*/
default String healthCheckPath() {
return "/";
}
// ========== EC2 Configuration ==========
/**
* Returns the EBS device name for EC2 instances when not using EFS.
* This is the device that will be formatted and mounted for application data.
*
* @return EBS device path (e.g., "/dev/xvdh")
*/
String ebsDeviceName();
/**
* Returns the EC2 data path where application stores persistent data.
* This may differ from containerDataPath depending on application packaging.
*
* @return EC2 mount path (e.g., "/var/lib/jenkins")
*/
String ec2DataPath();
/**
* Returns CloudWatch log file paths for EC2 monitoring.
* These files will be streamed to CloudWatch Logs for centralized logging.
*
* @return list of absolute log file paths (e.g., ["/var/log/jenkins/jenkins.log"])
*/
List<String> ec2LogPaths();
/**
* Configure EC2 UserData script for application installation and setup.
*
* <p>The implementation should use the UserDataBuilder to add application-specific
* installation commands while leveraging infrastructure helpers for storage mounting
* and CloudWatch configuration.</p>
*
* <p>The infrastructure handles:</p>
* <ul>
* <li>System updates</li>
* <li>EFS vs EBS storage mounting (based on availability)</li>
* <li>CloudWatch Agent installation and configuration</li>
* <li>File permissions and ownership</li>
* </ul>
*
* <p>The application provides:</p>
* <ul>
* <li>Application installation commands (yum/dnf install, etc.)</li>
* <li>Application configuration</li>
* <li>Service startup commands</li>
* </ul>
*
* @param builder The UserDataBuilder providing infrastructure helpers
* @param context The Ec2Context providing runtime information
*/
void configureUserData(UserDataBuilder builder, Ec2Context context);
// ========== OIDC Integration (Optional) ==========
/**
* Returns whether this application supports OIDC integration.
*
* <p>Applications with built-in OIDC support (GitLab, Grafana, SonarQube) or
* plugin support (Jenkins) should return true.</p>
*
* @return true if application can integrate with OIDC providers
*/
default boolean supportsOidcIntegration() {
return false;
}
/**
* Returns the OIDC integration handler for this application.
*
* <p>This provides application-specific configuration for integrating with
* Cognito or IAM Identity Center OIDC.</p>
*
* @return OIDC integration handler, or null if not supported
*/
default OidcIntegration getOidcIntegration() {
return null;
}
/**
* Returns the list of supported authentication modes for this application.
*
* <p>CloudForge supports three authentication modes:</p>
* <ul>
* <li><b>application-oidc</b>: OIDC authentication integrated within the application (requires getOidcIntegration() != null)</li>
* <li><b>alb-oidc</b>: OIDC authentication at ALB level (works for all applications)</li>
* <li><b>none</b>: No authentication (public access or manually configured)</li>
* </ul>
*
* <p>The list is ordered by preference. The first mode is the recommended default.</p>
*
* <p>Default behavior:</p>
* <ul>
* <li>If application has OIDC integration → ["application-oidc", "alb-oidc", "none"]</li>
* <li>If application claims OIDC support but lacks integration → ["alb-oidc", "none"]</li>
* <li>If application doesn't support OIDC → ["none"]</li>
* </ul>
*
* @return List of supported auth modes in order of preference (never null, never empty)
*/
default List<String> getSupportedAuthModes() {
if (getOidcIntegration() != null) {
// Application has working OIDC integration - prefer application-oidc
return List.of("application-oidc", "alb-oidc", "none");
} else if (supportsOidcIntegration()) {
// Application claims OIDC support but doesn't implement it - use alb-oidc
return List.of("alb-oidc", "none");
} else {
// No OIDC support at all
return List.of("none");
}
}
/**
* Returns the recommended (default) authentication mode for this application.
*
* <p>This is the first mode from {@link #getSupportedAuthModes()}.</p>
*
* @return the recommended auth mode (e.g., "application-oidc", "alb-oidc", "none")
*/
default String getRecommendedAuthMode() {
return getSupportedAuthModes().get(0);
}
// ========== Path-Based Authentication ==========
/**
* Returns paths that require authentication when using ALB-level OIDC.
*
* <p>When this list is non-empty and authMode is "alb-oidc", the ALB will:
* <ul>
* <li>Require OIDC authentication for requests matching these paths</li>
* <li>Allow unauthenticated access to all other paths</li>
* </ul>
*
* <p>When this list is empty (default), ALL paths require authentication.</p>
*
* <p>Path patterns support ALB path-pattern syntax:</p>
* <ul>
* <li>Exact: "/admin"</li>
* <li>Prefix wildcard: "/admin/*"</li>
* <li>Extension: "*.php"</li>
* </ul>
*
* <p>Example for phpBB (protect admin and installer):</p>
* <pre>{@code
* @Override
* public List<String> protectedPaths() {
* return List.of("/adm/*", "/install/*");
* }
* }</pre>
*
* <p>Example for WordPress (protect wp-admin):</p>
* <pre>{@code
* @Override
* public List<String> protectedPaths() {
* return List.of("/wp-admin/*", "/wp-login.php");
* }
* }</pre>
*
* <p>Users can override these defaults via DeploymentContext:</p>
* <ul>
* <li>protectedPaths: Override/replace the application defaults</li>
* <li>additionalProtectedPaths: Add to the application defaults</li>
* <li>publicPaths: Explicitly mark paths as public (overrides protected)</li>
* </ul>
*
* @return list of path patterns requiring authentication (empty = protect everything)
* @see #publicPaths()
*/
default List<String> protectedPaths() {
return List.of(); // Default: protect everything when auth is enabled
}
/**
* Returns paths that should always be public (no authentication required).
*
* <p>These paths are excluded from authentication even when they would
* otherwise be protected. Useful for health checks, public APIs, etc.</p>
*
* <p>Common use cases:</p>
* <ul>
* <li>Health check endpoints: "/health", "/api/health"</li>
* <li>Public API endpoints: "/api/public/*"</li>
* <li>Static assets: "/static/*", "/assets/*"</li>
* </ul>
*
* @return list of path patterns that should be public (empty by default)
*/
default List<String> publicPaths() {
return List.of(); // Default: no explicit public paths
}
// ========== Optional Ports (Security-Conscious) ==========
/**
* Optional service port that can be enabled via deployment configuration.
*
* <p>Ports are NOT exposed by default - must be explicitly enabled via the configKey
* in deployment configuration. This follows the principle of least privilege.</p>
*
* @param port The port number
* @param protocol The protocol ("tcp" or "udp")
* @param configKey The DeploymentContext key to enable this port (e.g., "enableSmtp")
* @param service Human-readable service name for logging/prompts
* @param inbound true if port accepts inbound connections (requires security group rule),
* false if outbound only (container connects out, no SG rule needed)
*/
record OptionalPort(int port, String protocol, String configKey, String service, boolean inbound) {
/**
* Convenience constructor for inbound TCP ports.
*
* @param port the port number
* @param configKey the deployment config key to enable this port
* @param service the service name using this port
* @return an OptionalPort configured for inbound TCP
*/
public static OptionalPort inboundTcp(int port, String configKey, String service) {
return new OptionalPort(port, "tcp", configKey, service, true);
}
/**
* Convenience constructor for outbound TCP ports (no security group rule needed).
*
* @param port the port number
* @param configKey the deployment config key to enable this port
* @param service the service name using this port
* @return an OptionalPort configured for outbound TCP
*/
public static OptionalPort outboundTcp(int port, String configKey, String service) {
return new OptionalPort(port, "tcp", configKey, service, false);
}
}
/**
* Returns optional ports that can be enabled via deployment configuration.
*
* <p>These ports are NOT exposed by default. Users must set the corresponding
* configKey to true in their deployment configuration to enable each port.</p>
*
* <p>Example implementation for Mattermost:</p>
* <pre>{@code
* @Override
* public List<OptionalPort> optionalPorts() {
* return List.of(
* OptionalPort.outboundTcp(587, "enableSmtp", "SMTP Email"),
* OptionalPort.inboundTcp(8074, "enableClustering", "Cluster Gossip")
* );
* }
* }</pre>
*
* <p>User enables in deployment-context.json:</p>
* <pre>{@code
* {
* "enableSmtp": true,
* "enableClustering": true
* }
* }</pre>
*
* @return list of optional ports (empty by default - most apps only need primary port)
*/
default List<OptionalPort> optionalPorts() {
return List.of();
}
// ========== Plugin Metadata Methods ==========
/**
* Get the application category from the {@link ApplicationPlugin} annotation.
*
* @return the category (e.g., "cicd", "monitoring", "database")
*/
default String category() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return "unknown";
}
return annotation.category();
}
/**
* Get the human-readable display name for this application.
*
* @return the display name, defaulting to capitalized {@link #applicationId()} if not specified
*/
default String displayName() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
String id = applicationId();
return id.substring(0, 1).toUpperCase() + id.substring(1);
}
String displayName = annotation.displayName();
if (displayName.isEmpty()) {
String id = applicationId();
return id.substring(0, 1).toUpperCase() + id.substring(1);
}
return displayName;
}
/**
* Get the application description.
*
* @return the application description
*/
default String description() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return "";
}
return annotation.description();
}
/**
* Get the default Fargate CPU units.
*
* @return the default CPU units (256, 512, 1024, 2048, 4096)
*/
default int defaultCpu() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return 1024; // Default 1 vCPU
}
return annotation.defaultCpu();
}
/**
* Get the default Fargate memory in MB.
*
* @return the default memory in MB
*/
default int defaultMemory() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return 2048; // Default 2GB
}
return annotation.defaultMemory();
}
/**
* Get the default EC2 instance type.
*
* @return the default instance type (e.g., "t3.small")
*/
default String defaultInstanceType() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return "t3.small";
}
return annotation.defaultInstanceType();
}
/**
* Check if this application supports Fargate deployment.
*
* @return true if Fargate is supported
*/
default boolean supportsFargate() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return true; // Default to supported
}
return annotation.supportsFargate();
}
/**
* Check if this application supports EC2 deployment.
*
* @return true if EC2 is supported
*/
default boolean supportsEc2() {
ApplicationPlugin annotation = getClass().getAnnotation(ApplicationPlugin.class);
if (annotation == null) {
return true; // Default to supported
}
return annotation.supportsEc2();
}
/**
* Get the recommended health check grace period for this application.
*
* <p>The grace period is how long ECS/ALB waits before starting health checks
* after a container starts. Applications with longer initialization times
* (like GitLab) need longer grace periods.</p>
*
* <p>Default values:</p>
* <ul>
* <li>Most applications: 300 seconds (5 minutes)</li>
* <li>GitLab: 600 seconds (10 minutes) - due to database migrations and initialization</li>
* <li>Other database-heavy apps may also need longer periods</li>
* </ul>
*
* @return recommended health check grace period in seconds
*/
default int defaultHealthCheckGracePeriod() {
return 300; // Default 5 minutes for most applications
}
}