DeploymentConfig.java
package com.cloudforge.core.config;
import com.cloudforge.core.annotation.ConfigField;
import com.cloudforge.core.annotation.FieldTag;
import com.cloudforge.core.enums.AuthMode;
import com.cloudforge.core.enums.ComplianceFrameworkType;
import com.cloudforge.core.enums.ComplianceMode;
import com.cloudforge.core.enums.LoadBalancerType;
import com.cloudforge.core.enums.NetworkMode;
import com.cloudforge.core.enums.RuntimeType;
import com.cloudforge.core.local.DeploymentTarget;
import com.cloudforge.core.enums.SecurityProfile;
import com.cloudforge.core.enums.TopologyType;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Universal deployment configuration for CloudForge applications.
*
* <p>This is the canonical configuration structure used by both interactive deployers
* and non-interactive deployment tools. It maps directly to deployment-context.json
* and can be serialized/deserialized via Jackson.</p>
*
* <p><b>Architecture:</b> This class lives in cloudforge-core (the contract layer) as
* it defines the data model interface between libraries and consumers. This ensures
* cfc-testing and other consumers always use the latest configuration schema without
* duplication.</p>
*
* @since CloudForge 3.0.0
*/
public class DeploymentConfig {
// ========== Basic Configuration ==========
/** CloudFormation stack name */
@ConfigField(
displayName = "Stack Name",
description = "CloudFormation stack name (lowercase alphanumeric with hyphens)",
category = "basic",
required = true,
pattern = "^[a-zA-Z][a-zA-Z0-9-]{0,127}$",
example = "my-app-stack",
order = 10
)
public String stackName;
/** Environment name (e.g., "dev", "staging", "production") */
@ConfigField(
displayName = "Environment",
description = "Deployment environment name",
category = "basic",
allowedValues = {"dev", "staging", "prod"},
example = "dev",
order = 20
)
public String environment;
/** Application identifier (e.g., "jenkins", "gitlab", "vault") */
@ConfigField(
displayName = "Application ID",
description = "Application identifier from available plugins",
category = "basic",
required = true,
order = 5
)
public String applicationId;
/** Human-readable application name */
@ConfigField(
displayName = "Application Name",
description = "Human-readable application display name",
category = "basic",
order = 6
)
public String applicationName;
/** ApplicationSpec instance (not serialized to JSON) */
@JsonIgnore
public ApplicationSpec applicationSpec;
// ========== Domain Configuration ==========
/**
* Subdomain prefix (e.g., "ci", "gitlab") — ordered ahead of {@link #domain}: you pick what
* this app is called (the subdomain) before which domain it hangs off of, and the combined
* result (subdomain + "." + domain) is what actually gets requested, not the other way
* around.
*/
@ConfigField(
displayName = "Subdomain",
description = "Subdomain prefix (e.g., 'ci' for ci.example.com)",
category = "domain",
pattern = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
example = "ci",
order = 10
)
public String subdomain;
/** Primary domain (e.g., "example.com") */
@ConfigField(
displayName = "Domain",
description = "Primary domain for the application (e.g., example.com)",
category = "domain",
pattern = "^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$",
example = "example.com",
order = 20
)
public String domain;
/**
* Fully qualified domain name — always {@code subdomain + "." + domain} once both are set;
* this field only matters when you need to override that computed result directly (e.g. a
* domain structure the subdomain+domain pair can't express). {@code @JsonIgnore}d
* deliberately: it's a derived/override value, not sent as its own JSON key — the previous
* description ("overrides domain+subdomain") had the relationship backwards, reading as if
* *this* field were the primary input and domain/subdomain were the fallback, when it's the
* other way around.
*/
@ConfigField(
displayName = "FQDN (advanced override)",
description = "Computed as Subdomain + \".\" + Domain — only set this directly to override that computed value",
category = "domain",
order = 30
)
@JsonIgnore // Computed/override field, not serialized under its own key
public String fqdn;
/** Enable SSL certificate via ACM */
@ConfigField(
displayName = "Enable SSL",
description = "Enable SSL/TLS with AWS Certificate Manager",
category = "domain",
order = 30
)
public Boolean enableSsl;
/**
* ARN of an existing ACM certificate to use for the ALB's HTTPS listener, instead of
* provisioning a new one. Takes priority over both of {@code FargateRuntimeConfiguration}'s
* other two certificate paths — the DNS-validated public cert (needs a Route53 hosted zone
* this deployment controls) and the AWS Private CA cert (issued for the bare ALB DNS name,
* NOT trusted by browsers — see that class's own comments). This is how a deployment gets a
* genuinely publicly-trusted certificate without either of those: import your own cert
* (issued by any public CA — ACM's own DNS/email validation, Let's Encrypt, a purchased
* cert, ...) into ACM yourself first (e.g. {@code aws acm import-certificate}, entirely
* within your own account — the key material never has to pass through this deployment
* config or CloudFormation), then point this at the resulting ARN.
*/
@ConfigField(
displayName = "Existing Certificate ARN",
description = "ARN of an ACM certificate you've already issued/imported — used as-is instead of provisioning a new one",
category = "domain",
pattern = "^arn:aws[a-zA-Z-]*:acm:[a-z0-9-]+:\\d{12}:certificate/[a-f0-9-]+$",
example = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012",
visibleWhen = "enableSsl == true",
order = 35
)
public String certificateArn;
// ========== Runtime Configuration ==========
/** Runtime type (FARGATE or EC2) */
@ConfigField(
displayName = "Runtime Type",
description = "Container runtime: FARGATE (serverless) or EC2 (instance-based)",
category = "basic",
allowedValues = {"FARGATE", "EC2"},
required = true,
order = 30
)
public RuntimeType runtime;
/** Topology type — genuinely selectable, not just an auto-derived display value, so a future
* topology (or an app implementing more than one applicable interface) isn't locked out of
* being chosen explicitly. No {@code allowedValues} override here on purpose: leaving it
* unset lets the schema builder enumerate every {@link TopologyType} constant automatically —
* a hardcoded single-value override would hide CMS_SERVICE/JENKINS_SERVICE/S3_WEBSITE from
* the wizard entirely, locking out a CmsSpec app's actual topology even though {@link
* DeploymentContextPreparer} already defaults to it correctly. */
@ConfigField(
displayName = "Topology",
description = "Deployment topology pattern",
category = "basic",
order = 40
)
public TopologyType topology;
/** Security profile (DEV, STAGING, PRODUCTION) */
@ConfigField(
displayName = "Security Profile",
description = "Security profile determines compliance requirements and defaults",
category = "security",
allowedValues = {"DEV", "STAGING", "PRODUCTION"},
required = true,
order = 10
)
public SecurityProfile securityProfile = SecurityProfile.DEV;
// ========== Network Configuration ==========
/** Network mode for VPC topology */
@ConfigField(
displayName = "Network Mode",
description = "VPC topology. Private with NAT is recommended for production; public is suitable for development; isolated requires VPC endpoints.",
category = "network",
allowedValues = {"public", "private-with-nat", "isolated"},
example = "private-with-nat",
order = 10
)
public NetworkMode networkMode = NetworkMode.PUBLIC;
/** Load balancer type */
@ConfigField(
displayName = "Load Balancer Type",
description = "ALB (HTTP/HTTPS) or NLB (TCP/UDP) - ALB required for OIDC",
category = "network",
order = 20
)
public LoadBalancerType lbType = LoadBalancerType.ALB;
/** Create Route53 hosted zone */
@ConfigField(
displayName = "Create Route53 Zone",
description = "Create new Route53 hosted zone for the domain",
category = "domain",
order = 40
)
public Boolean createZone = false;
/** Enable VPC flow logs */
@ConfigField(
displayName = "Enable Flow Logs",
description = "Enable VPC flow logs for network traffic analysis",
category = "network",
order = 30
)
public Boolean enableFlowlogs = null; // Null means security profile determines default
/** Enable AWS WAF */
@ConfigField(
displayName = "Enable WAF",
description = "Enable AWS Web Application Firewall for ALB protection",
category = "network",
visibleWhen = "lbType == alb",
order = 40
)
public Boolean wafEnabled;
/** HTTPS-only mode (no HTTP listener when SSL enabled) */
@ConfigField(
displayName = "HTTPS Strict Mode",
description = "Disable HTTP listener when SSL enabled (no HTTP→HTTPS redirect)",
category = "network",
visibleWhen = "enableSsl == true",
order = 45
)
public Boolean httpsStrictEnabled;
/** Enable ALB access logs to S3 */
@ConfigField(
displayName = "ALB Access Logging",
description = "Log all ALB requests to S3 for auditing",
category = "network",
visibleWhen = "lbType == alb",
order = 50
)
public Boolean albAccessLogging = false;
/** Enable CloudFront CDN */
@ConfigField(
displayName = "Enable CloudFront",
description = "Enable CloudFront CDN for global content delivery",
category = "network",
tags = {FieldTag.BILLING_IMPACT},
order = 60
)
@JsonAlias("cloudfront")
public Boolean cloudfrontEnabled;
/** CIDR for bastion/VPN SSH access */
@ConfigField(
displayName = "Bastion CIDR",
description = "CIDR for bastion/VPN SSH access (PRODUCTION profile)",
category = "network",
pattern = "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$",
example = "10.0.1.0/24",
order = 70
)
public String bastionCidr = "10.0.1.0/24";
// ========== Resource Configuration ==========
/** Minimum instance capacity for auto-scaling */
@ConfigField(
displayName = "Min Instance Capacity",
description = "Minimum number of instances for auto-scaling",
category = "resources",
min = 1,
max = 100,
order = 10
)
public int minInstanceCapacity = 1;
/** Maximum instance capacity for auto-scaling */
@ConfigField(
displayName = "Max Instance Capacity",
description = "Maximum number of instances for auto-scaling",
category = "resources",
min = 1,
max = 100,
validators = {"CapacityValidator"},
tags = {FieldTag.BILLING_IMPACT},
order = 20
)
public int maxInstanceCapacity = 1;
/** CPU target utilization percentage for auto-scaling */
@ConfigField(
displayName = "CPU Target Utilization (%)",
description = "Target CPU utilization for auto-scaling triggers",
category = "resources",
min = 10,
max = 90,
visibleWhen = "maxInstanceCapacity > 1",
order = 30
)
public int cpuTargetUtilization = 60;
/** Fargate CPU units (256, 512, 1024, 2048, 4096) */
@ConfigField(
displayName = "CPU (Fargate units)",
description = "Fargate CPU units: 256=0.25vCPU, 512=0.5vCPU, 1024=1vCPU, etc.",
category = "resources",
allowedValues = {"256", "512", "1024", "2048", "4096", "8192", "16384"},
visibleWhen = "runtime == FARGATE",
validators = {"FargateCpuMemoryValidator"},
tags = {FieldTag.BILLING_IMPACT},
order = 40
)
public int cpu = 1024;
/** Fargate memory in MB */
@ConfigField(
displayName = "Memory (MB)",
description = "Container memory in MB (must be valid for CPU selection)",
category = "resources",
min = 512,
max = 122880,
visibleWhen = "runtime == FARGATE",
validators = {"FargateCpuMemoryValidator"},
tags = {FieldTag.BILLING_IMPACT},
order = 50
)
public int memory = 2048;
/** EC2 instance type (e.g., "t3.micro", "t3.small") */
@ConfigField(
displayName = "EC2 Instance Type",
description = "EC2 instance size for compute capacity",
category = "resources",
allowedValues = {
"t3.micro", "t3.small", "t3.medium", "t3.large", "t3.xlarge",
"t3a.micro", "t3a.small", "t3a.medium", "t3a.large",
"m5.large", "m5.xlarge", "m5.2xlarge",
"c5.large", "c5.xlarge", "c5.2xlarge"
},
visibleWhen = "runtime == EC2",
defaultFrom = "defaultInstanceType",
tags = {FieldTag.BILLING_IMPACT},
order = 60
)
public String instanceType = "t3.micro";
/** Override container image tag */
@ConfigField(
displayName = "Container Image",
description = "Override container image tag (e.g., 'v1.2.3' or '2024.1')",
category = "resources",
example = "v1.2.3",
order = 65
)
public String containerImage;
// ========== Storage Configuration ==========
/** Retain EFS/EBS volumes on stack deletion */
@ConfigField(
displayName = "Retain Storage",
description = "Retain EFS/EBS volumes on stack deletion (agnostic - works for any workload)",
category = "storage",
order = 10
)
public Boolean retainStorage = false;
/** Reuse existing EFS by ID (for disaster recovery workflows) */
@ConfigField(
displayName = "Existing File System ID",
description = "Reuse existing EFS by ID (for disaster recovery workflows)",
category = "storage",
example = "fs-12345678",
order = 20
)
public String existingFileSystemId;
/** S3 bucket for artifacts */
@ConfigField(
displayName = "Artifacts Bucket",
description = "S3 bucket name for build artifacts",
category = "storage",
order = 30
)
public String artifactsBucket;
/** S3 prefix for artifacts */
@ConfigField(
displayName = "Artifacts Prefix",
description = "S3 prefix for build artifacts",
category = "storage",
example = "jenkins/job/${JOB_NAME}/${BUILD_NUMBER}",
order = 40
)
public String artifactsPrefix = "jenkins/job/${JOB_NAME}/${BUILD_NUMBER}";
// ========== Authentication Configuration ==========
/** Authentication mode */
@ConfigField(
displayName = "Authentication Mode",
description = "Authentication integration type: ALB-level OIDC, application-level, or none",
category = "security",
visibleWhen = "supportsOidc",
order = 20
)
public AuthMode authMode = AuthMode.NONE;
/** OIDC provider (none, cognito, identity-center, external-idp) */
@ConfigField(
displayName = "OIDC Provider",
description = "Identity provider for OIDC authentication",
category = "security",
allowedValues = {"none", "cognito", "identity-center", "external-idp", "cloudforge-manager"},
visibleWhen = "authMode != none",
order = 30
)
public String oidcProvider = "none";
/** Auto-provision new Cognito User Pool */
@ConfigField(
displayName = "Auto-Provision Cognito",
description = "Create a new Cognito User Pool for authentication",
category = "security",
visibleWhen = "oidcProvider == cognito",
order = 40
)
public Boolean cognitoAutoProvision = false;
/** Cognito User Pool name */
@ConfigField(
displayName = "Cognito User Pool Name",
description = "Name for the Cognito User Pool",
category = "security",
visibleWhen = "cognitoAutoProvision == true",
order = 50
)
public String cognitoUserPoolName = null;
/** Cognito domain prefix (must be globally unique) */
@ConfigField(
displayName = "Cognito Domain Prefix",
description = "Globally unique prefix for Cognito hosted UI domain",
category = "security",
pattern = "^[a-z][a-z0-9-]{0,62}$",
visibleWhen = "cognitoAutoProvision == true",
order = 60
)
public String cognitoDomainPrefix = null;
/** Enable MFA for Cognito */
@ConfigField(
displayName = "Enable MFA",
description = "Require multi-factor authentication for Cognito users",
category = "security",
visibleWhen = "cognitoAutoProvision == true",
order = 70
)
public Boolean cognitoMfaEnabled = false;
/** Cognito MFA method */
@ConfigField(
displayName = "MFA Method",
description = "MFA method: totp (authenticator app), sms, or both",
category = "security",
allowedValues = {"totp", "sms", "both"},
visibleWhen = "cognitoMfaEnabled == true",
order = 75
)
public String cognitoMfaMethod = "both";
/**
* Overrides the security-profile default for whether users can self-register on the
* Cognito hosted UI (development defaults to allowed, staging/production to admin-only).
* Left {@code null}, the profile default applies unchanged. Self-signup is not enforced
* by any compliance framework rule today — a self-registered user still gets no group
* membership and resolves to the lowest-privilege role — but enabling it while a compliance
* framework is active (advisory or enforce) logs a warning at synthesis time, since open
* self-registration into an ops panel is a common audit finding independent of what role
* it grants.
*/
@ConfigField(
displayName = "Enable Self-Signup",
description = "Override the security profile's default for self-service Cognito signup",
category = "security",
visibleWhen = "cognitoAutoProvision == true",
order = 78
)
public Boolean cognitoSelfSignupEnabled = null;
/** Create admin and user groups in Cognito */
@ConfigField(
displayName = "Create User Groups",
description = "Create admin and user groups in Cognito",
category = "security",
visibleWhen = "cognitoAutoProvision == true",
order = 80
)
public Boolean cognitoCreateGroups = true;
/** Admin group name */
@ConfigField(
displayName = "Admin Group Name",
description = "Name for the Cognito admin group",
category = "security",
visibleWhen = "cognitoCreateGroups == true",
order = 90
)
public String cognitoAdminGroupName = null;
/** User group name */
@ConfigField(
displayName = "User Group Name",
description = "Name for the Cognito users group",
category = "security",
visibleWhen = "cognitoCreateGroups == true",
order = 100
)
public String cognitoUserGroupName = null;
/** Initial admin email address */
@ConfigField(
displayName = "Initial Admin Email",
description = "Email address for the first admin user",
category = "security",
pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
visibleWhen = "cognitoAutoProvision == true",
example = "admin@example.com",
order = 110
)
public String cognitoInitialAdminEmail = null;
/** Initial admin phone number (E.164 format) */
@ConfigField(
displayName = "Initial Admin Phone",
description = "Phone number for admin user (E.164 format: +1234567890)",
category = "security",
pattern = "^\\+[1-9]\\d{1,14}$",
visibleWhen = "cognitoMfaEnabled == true",
example = "+15551234567",
order = 120
)
public String cognitoInitialAdminPhone = null;
/** Existing Cognito User Pool ID */
@ConfigField(
displayName = "Existing User Pool ID",
description = "ID of existing Cognito User Pool to use",
category = "security",
visibleWhen = "oidcProvider == cognito && cognitoAutoProvision == false",
order = 130
)
public String cognitoUserPoolId = null;
/** Existing Cognito App Client ID */
@ConfigField(
displayName = "Existing App Client ID",
description = "ID of existing Cognito App Client to use",
category = "security",
visibleWhen = "oidcProvider == cognito && cognitoAutoProvision == false",
order = 140
)
public String cognitoAppClientId = null;
// ========== External OIDC Configuration ==========
/** OIDC issuer URL */
@ConfigField(
displayName = "OIDC Issuer URL",
description = "Issuer URL from your identity provider (e.g., https://login.example.com)",
category = "security",
visibleWhen = "oidcProvider == external-idp",
pattern = "^https://.*$",
example = "https://login.example.com",
order = 200
)
public String oidcIssuer = null;
/** CloudForge Manager's own public URL, used as the OIDC issuer when this app trusts a
* CloudForge Manager install as its identity provider (see
* com.cloudforgeci.manager.auth.oidcprovider's own package javadoc). Authorization/token/
* userinfo endpoints are computed from this one URL (CloudForge Manager's OIDC-provider
* endpoints are at fixed paths — /oauth2/authorize, /oauth2/token, /userinfo — so unlike
* the generic external-idp option above, only the base URL needs entering here, not each
* endpoint separately). */
@ConfigField(
displayName = "CloudForge Manager URL",
description = "Public URL of the CloudForge Manager install this app trusts as its identity provider",
category = "security",
visibleWhen = "oidcProvider == cloudforge-manager",
pattern = "^https?://.*$",
example = "https://manager.example.com",
order = 195
)
public String cloudforgeManagerIssuerUrl = null;
/** OIDC authorization endpoint */
@ConfigField(
displayName = "Authorization Endpoint",
description = "OIDC authorization endpoint URL",
category = "security",
visibleWhen = "oidcProvider == external-idp",
pattern = "^https://.*$",
order = 210
)
public String oidcAuthorizationEndpoint = null;
/** OIDC token endpoint */
@ConfigField(
displayName = "Token Endpoint",
description = "OIDC token endpoint URL",
category = "security",
visibleWhen = "oidcProvider == external-idp",
pattern = "^https://.*$",
order = 220
)
public String oidcTokenEndpoint = null;
/** OIDC user info endpoint */
@ConfigField(
displayName = "User Info Endpoint",
description = "OIDC user info endpoint URL",
category = "security",
visibleWhen = "oidcProvider == external-idp",
pattern = "^https://.*$",
order = 230
)
public String oidcUserInfoEndpoint = null;
/** OIDC client ID — shared by external-idp and cloudforge-manager, both providers where an
* admin registers this app's client credentials by hand (external IdP's own console, or
* CloudForge Manager's Trusted Apps settings page) rather than CloudForge auto-provisioning
* them, so the field means the same thing either way. */
@ConfigField(
displayName = "OIDC Client ID",
description = "Client ID from your identity provider",
category = "security",
visibleWhen = "oidcProvider == external-idp || oidcProvider == cloudforge-manager",
order = 240
)
public String oidcClientId = null;
/** OIDC client secret name in Secrets Manager — same sharing rationale as {@link
* #oidcClientId}. */
@ConfigField(
displayName = "Client Secret (Secrets Manager)",
description = "Name of the secret in AWS Secrets Manager containing the client secret",
category = "security",
visibleWhen = "oidcProvider == external-idp || oidcProvider == cloudforge-manager",
sensitive = true,
order = 250
)
public String oidcClientSecretName = null;
// ========== Optional Ports Configuration ==========
// These enable optional services on applications that support them.
// Ports are NOT exposed by default - must be explicitly enabled.
/** Enable JNLP build agent port (Jenkins: 50000) */
@ConfigField(
displayName = "Enable Build Agents",
description = "Enable JNLP build agent port for distributed builds (Jenkins: 50000)",
category = "ports",
visibleWhen = "supportsAgents",
order = 10
)
public boolean enableAgents = false;
/** Enable Git SSH port (GitLab: 22, Gitea: 2222) */
@ConfigField(
displayName = "Enable Git SSH",
description = "Enable Git SSH access for repository cloning (GitLab: 22, Gitea: 2222)",
category = "ports",
visibleWhen = "supportsSsh",
order = 20
)
public boolean enableSsh = false;
/** Enable SMTP email port (Mattermost: 587) */
@ConfigField(
displayName = "Enable SMTP",
description = "Enable SMTP port for outbound email (port 587)",
category = "ports",
visibleWhen = "supportsSmtp",
order = 30
)
public boolean enableSmtp = false;
/** Enable SMTP TLS email port (Mattermost: 465) */
@ConfigField(
displayName = "Enable SMTPS",
description = "Enable SMTP over TLS port for secure outbound email (port 465)",
category = "ports",
visibleWhen = "supportsSmtps",
order = 40
)
public boolean enableSmtps = false;
/** Enable clustering ports (Mattermost: 8074-8075, Vault: 8201) */
@ConfigField(
displayName = "Enable Clustering",
description = "Enable inter-node clustering ports for high availability",
category = "ports",
visibleWhen = "supportsClustering",
order = 50
)
public boolean enableClustering = false;
/** Enable container registry port (GitLab: 5050, Nexus: 5000-5002) */
@ConfigField(
displayName = "Enable Docker Registry",
description = "Enable container registry ports (GitLab: 5050, Nexus: 5000-5002)",
category = "ports",
visibleWhen = "supportsDockerRegistry",
order = 60
)
public boolean enableDockerRegistry = false;
/** Enable Prometheus metrics port (GitLab: 9090) */
@ConfigField(
displayName = "Enable Metrics Port",
description = "Enable Prometheus metrics endpoint port (typically 9090)",
category = "ports",
visibleWhen = "supportsMetrics",
order = 70
)
public boolean enableMetrics = false;
/** Enable Notary content trust port (Harbor: 4443) */
@ConfigField(
displayName = "Enable Notary",
description = "Enable Docker Content Trust Notary port (Harbor: 4443)",
category = "ports",
visibleWhen = "supportsNotary",
order = 80
)
public boolean enableNotary = false;
/** Enable Trivy vulnerability scanner port (Harbor: 8080) */
@ConfigField(
displayName = "Enable Trivy Scanner",
description = "Enable Trivy vulnerability scanner port (Harbor: 8080)",
category = "ports",
visibleWhen = "supportsTrivy",
order = 90
)
public boolean enableTrivy = false;
/** Enable Redis Sentinel port (Redis: 26379) */
@ConfigField(
displayName = "Enable Sentinel",
description = "Enable Redis Sentinel port for HA monitoring (port 26379)",
category = "ports",
visibleWhen = "supportsSentinel",
order = 100
)
public boolean enableSentinel = false;
/** Enable Redis Cluster bus port (Redis: 16379) */
@ConfigField(
displayName = "Enable Cluster Bus",
description = "Enable Redis Cluster bus port for cluster gossip (port 16379)",
category = "ports",
visibleWhen = "supportsClusterBus",
order = 110
)
public boolean enableCluster = false;
// ========== IAM Identity Center Configuration ==========
/** Auto-provision SAML application in IAM Identity Center */
@ConfigField(
displayName = "Auto-Provision Identity Center",
description = "Create SAML application in IAM Identity Center",
category = "security",
visibleWhen = "oidcProvider == identity-center",
order = 260
)
public Boolean autoProvisionIdentityCenter = false;
/** IAM Identity Center (SSO) Instance ARN */
@ConfigField(
displayName = "SSO Instance ARN",
description = "ARN of IAM Identity Center instance",
category = "security",
visibleWhen = "oidcProvider == identity-center",
order = 270
)
public String ssoInstanceArn = null;
/** SSO Group ID */
@ConfigField(
displayName = "SSO Group ID",
description = "IAM Identity Center group UUID",
category = "security",
visibleWhen = "oidcProvider == identity-center",
order = 275
)
public String ssoGroupId = null;
/** SSO Target Account ID */
@ConfigField(
displayName = "SSO Target Account ID",
description = "12-digit AWS account ID for SSO target",
category = "security",
pattern = "^\\d{12}$",
visibleWhen = "oidcProvider == identity-center",
order = 278
)
public String ssoTargetAccountId = null;
/** Identity Center group name for user assignment */
@ConfigField(
displayName = "Identity Center Group",
description = "IAM Identity Center group for user assignment",
category = "security",
visibleWhen = "oidcProvider == identity-center",
order = 280
)
public String identityCenterGroupName = null;
// ========== Database Configuration ==========
/**
* Provision RDS database for application.
* Only shown for applications with optional database support (e.g., Metabase, Grafana).
* Applications requiring database (e.g., Mattermost, GitLab) always provision one.
*/
@ConfigField(
displayName = "Provision RDS Database",
description = "Create managed RDS database for high availability and automatic backups",
category = "database",
visibleWhen = "supportsDatabase && !requiresDatabase",
required = false,
order = 10
)
public Boolean provisionDatabase = false;
/**
* Database engine (e.g., postgres, mysql, mariadb).
* Default comes from ApplicationSpec.databaseRequirement().engine()
*
* <p><b>Deliberately no static default here (nor on the other {@code database*} fields below
* with a {@code defaultFrom}):</b> {@link com.cloudforge.core.config.DeploymentContextPreparer}
* only applies a field's {@code defaultFrom}-resolved ApplicationSpec value when the field is
* currently null/blank — a non-null Java initializer (this used to be {@code = "postgres"})
* permanently looks "already set" and blocks that resolution forever. That silently forced
* every app onto engine=postgres/version=15/instanceClass=db.t3.small/storage=20GB/name=appdb
* unless a caller explicitly overrode every one of these fields together — for a MySQL-only
* app like WordPress, a form that filled in {@code databaseEngine=mysql} but left {@code
* databaseVersion} untouched produced an impossible "mysql15" RDS parameter group family and
* failed CloudFormation with {@code CREATE_FAILED}. {@code cloudforge-api}'s {@code
* ApplicationFactory} merges these fields against {@code ApplicationSpec.databaseRequirement()}
* with the same null-check pattern, now consistently reachable too.</p>
*/
@ConfigField(
displayName = "Database Engine",
description = "RDS database engine type",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
allowedValues = {"postgres", "mysql", "mariadb", "aurora-postgresql", "aurora-mysql"},
defaultFrom = "databaseRequirement().engine",
example = "postgres",
order = 20
)
public String databaseEngine;
/**
* Database engine version.
* Default comes from ApplicationSpec.databaseRequirement().version()
*/
@ConfigField(
displayName = "Database Version",
description = "Database engine version (e.g., 15 for PostgreSQL 15)",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
example = "15",
defaultFrom = "databaseRequirement().version",
order = 30
)
public String databaseVersion;
/**
* RDS instance class (e.g., db.t3.small, db.m5.large).
* DESTRUCTIVE: Changing this requires resource replacement.
* BILLING_IMPACT: Larger instances cost more.
*/
@ConfigField(
displayName = "Database Instance Class",
description = "RDS instance type - larger instances provide more CPU/memory",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
allowedValues = {
"db.t3.micro", "db.t3.small", "db.t3.medium", "db.t3.large",
"db.m5.large", "db.m5.xlarge", "db.m5.2xlarge",
"db.r5.large", "db.r5.xlarge", "db.r5.2xlarge"
},
example = "db.t3.small",
defaultFrom = "databaseRequirement().instanceClass",
tags = {FieldTag.DESTRUCTIVE, FieldTag.BILLING_IMPACT},
order = 40
)
public String databaseInstanceClass;
/**
* Allocated storage in GB.
* BILLING_IMPACT: More storage costs more.
*/
@ConfigField(
displayName = "Database Storage (GB)",
description = "Allocated storage for RDS database in GB",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
min = 20,
max = 65536,
example = "100",
defaultFrom = "databaseRequirement().allocatedStorageGB",
tags = {FieldTag.BILLING_IMPACT},
order = 50
)
public Integer databaseAllocatedStorageGB;
/**
* Enable Multi-AZ deployment for high availability.
* BILLING_IMPACT: Multi-AZ doubles database costs.
*/
@ConfigField(
displayName = "Multi-AZ Deployment",
description = "Deploy database across multiple availability zones for high availability",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
tags = {FieldTag.BILLING_IMPACT},
order = 60
)
public Boolean databaseMultiAz = false;
/**
* Optional number of RDS read replicas. When unset, an application may provide
* its own default (CloudForge Manager defaults to one); zero explicitly disables
* replicas for applications that support them.
*/
@ConfigField(
displayName = "Database Read Replicas",
description = "Number of read-only RDS replicas to deploy (0 disables replicas)",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
min = 0,
max = 5,
example = "1",
tags = {FieldTag.BILLING_IMPACT},
order = 65
)
public Integer databaseReadReplicaCount = null;
/**
* Database name.
* IMMUTABLE: Cannot be changed after creation.
*/
@ConfigField(
displayName = "Database Name",
description = "Initial database name to create",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
pattern = "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
example = "appdb",
defaultFrom = "databaseRequirement().databaseName",
tags = {FieldTag.IMMUTABLE},
order = 70
)
public String databaseName;
/**
* Backup retention period in days.
* Compliance frameworks may override: PCI-DSS (90 days), HIPAA (30 days), SOC2 (14 days).
*/
@ConfigField(
displayName = "Backup Retention (Days)",
description = "Number of days to retain automated backups (0 = disabled)",
category = "database",
visibleWhen = "provisionDatabase",
dependsOn = "provisionDatabase",
min = 0,
max = 35,
example = "7",
order = 80
)
public Integer databaseBackupRetentionDays = 7;
// ========== Redis Session Store (CloudForge Manager) ==========
/**
* Provision an ElastiCache Redis cluster and bind CloudForge Manager's own container to it
* as a shared session store (CFC_MANAGER_SESSION_MODE=redis), so every Manager instance
* behind the same ALB recognizes sessions any of the others created — the missing piece for
* running Manager itself horizontally scaled. Only {@code ApplicationFactory} acts on this,
* and only when {@code applicationId == cloudforge-manager}; {@code visibleWhen} below keeps
* it out of every other application's deploy form for the same reason. Requires RDS
* (embedded H2 isn't safe to share across instances) — see {@code
* ManagerDeploymentPreset.rdsWithRedisSessions(...)} in cloudforge-manager-deployment, the
* typed pairing this mirrors. Single-instance Manager (the common case) needs nothing beyond
* the in-memory default and should leave this false.
*/
@ConfigField(
displayName = "Redis Session Store",
description = "Provision ElastiCache Redis and share sessions across every CloudForge "
+ "Manager instance — required for running Manager itself horizontally scaled behind an ALB",
category = "database",
visibleWhen = "applicationId == cloudforge-manager && provisionDatabase",
dependsOn = "provisionDatabase",
tags = {FieldTag.BILLING_IMPACT},
order = 90
)
public Boolean provisionManagerRedisSessions = false;
/**
* Provision a dedicated AWS Secrets Manager entry holding the AES cipher key CloudForge
* Manager uses to encrypt cross-account connection secrets (external IDs) at rest — see
* {@code SecretCipher}/{@code AesGcmSecretCipher} in cloudforge-manager. Injected into
* Manager's own ECS task as {@code CFC_MANAGER_ACCOUNT_SECRET_KEY} via {@code
* ecs.Secret.fromSecretsManager(...)}, mirroring how {@code CFC_MANAGER_DATABASE_PASSWORD}
* is already delivered — never a literal value in the task definition. Only {@code
* ApplicationFactory} acts on this, and only when {@code applicationId ==
* cloudforge-manager}. Defaults to {@code true} (unlike {@code
* provisionManagerRedisSessions}) because without it Manager silently falls back to {@code
* PlaintextSecretCipher} — leaving it on is the secure-by-default choice for any real AWS
* deployment of Manager, not an opt-in scaling feature like Redis. Independent of {@code
* provisionDatabase}: even an embedded-H2 Manager deployment benefits from encrypting the
* secrets it holds.
*/
@ConfigField(
displayName = "Account Connection Cipher Key",
description = "Provision a Secrets Manager entry for the AES key CloudForge Manager uses "
+ "to encrypt cross-account connection secrets — leave enabled unless you are "
+ "supplying CFC_MANAGER_ACCOUNT_SECRET_KEY through another mechanism",
category = "database",
visibleWhen = "applicationId == cloudforge-manager",
tags = {FieldTag.BILLING_IMPACT},
order = 91
)
public Boolean provisionManagerAccountCipherKey = true;
/**
* A customer's LicenseSeat license key ({@code LS-XXXX-XXXX-XXXX-XXXX}), injected at deploy
* time so an install activates immediately instead of requiring a follow-up visit to the
* owner-only License settings screen after the stack comes up. When set, {@code
* ApplicationFactory} provisions a dedicated Secrets Manager entry for it and {@code
* ContainerFactory} binds that as {@code CFC_MANAGER_LICENSESEAT_LICENSE_KEY} — the exact
* env var {@code ManagerRuntimeConfiguration}'s "stopgap" path already reads (see its {@code
* LicenseSeat} record javadoc, cloudforge-manager). Purely additive: the in-app License
* settings screen still works, and a key activated through it still takes precedence (see
* {@code LicenseKeyStore}, cloudforge-manager) — this only seeds the initial value.
*
* <p>{@code category = "database"} despite not being a database setting: it sits right next
* to {@code provisionManagerAccountCipherKey} above for the same reason that field does —
* {@code InteractiveDeployer.configureDatabaseOptions()} is the one place that currently
* discovers and prompts for manager-only fields by reflection ({@code
* ConfigurationIntrospector.discoverVisibleFields(..., "database")}); a genuine "security"
* category discovery pass would also surface ~30 unrelated existing security fields that
* were never meant for CLI prompting.
*/
@ConfigField(
displayName = "License Key",
description = "LicenseSeat customer license key (LS-XXXX-XXXX-XXXX-XXXX) to activate this "
+ "install with on first boot, delivered via a dedicated Secrets Manager entry",
category = "database",
visibleWhen = "applicationId == cloudforge-manager",
sensitive = true,
order = 92
)
public String managerLicenseKey;
/** Enable RDS deletion protection remediation */
@ConfigField(
displayName = "RDS Deletion Protection Remediation",
description = "Enable automatic remediation of RDS deletion protection compliance violations",
category = "compliance",
visibleWhen = "provisionDatabase && awsConfigEnabled",
dependsOn = "awsConfigEnabled",
order = 400
)
public Boolean enableRdsDeletionProtectionRemediation = false;
/** Enable RDS auto minor version upgrade remediation */
@ConfigField(
displayName = "RDS Auto Minor Version Upgrade Remediation",
description = "Enable automatic remediation of RDS auto minor version upgrade compliance violations",
category = "compliance",
visibleWhen = "provisionDatabase && awsConfigEnabled",
dependsOn = "awsConfigEnabled",
order = 410
)
public Boolean enableRdsAutoMinorVersionUpgradeRemediation = false;
// ========== Compliance Configuration ==========
/**
* Compliance frameworks to enable.
*
* <p>Supports comma-separated string format in JSON for backward compatibility:
* <pre>{"complianceFrameworks": "soc2,pci-dss,hipaa"}</pre>
*
* <p>In Java code, use the type-safe List:
* <pre>config.complianceFrameworks.contains(ComplianceFrameworkType.HIPAA)</pre>
*/
@ConfigField(
displayName = "Compliance Frameworks",
description = "Compliance frameworks to enable (soc2, pci-dss, hipaa, gdpr)",
category = "compliance",
// Was free-text-only (no allowedValues) — the Manager UI's builder rendered this as a
// bare "comma, separated, values" text box for an array field with real, fixed,
// machine-checkable options, exactly the kind of field a typo silently corrupts. Adding
// allowedValues lets the UI render checkboxes instead and auto-fill the exact expected
// token, matching ComplianceFrameworkType's own JSON values.
allowedValues = {"soc2", "pci-dss", "hipaa", "gdpr"},
example = "soc2,pci-dss",
order = 300
)
@JsonDeserialize(using = ComplianceFrameworkListConverter.Deserializer.class)
@JsonSerialize(using = ComplianceFrameworkListConverter.Serializer.class)
public List<ComplianceFrameworkType> complianceFrameworks = new ArrayList<>();
/**
* Compliance validation mode controlling how validation failures are handled.
*/
@ConfigField(
displayName = "Compliance Mode",
description = "How to handle compliance validation failures",
category = "compliance",
allowedValues = {"enforce", "advisory", "disabled"},
order = 310
)
public ComplianceMode complianceMode; // null = use defaultForProfile(securityProfile)
/** CloudWatch Logs retention days */
@ConfigField(
displayName = "Log Retention (Days)",
description = "CloudWatch Logs retention period in days",
category = "monitoring",
allowedValues = {"1", "3", "5", "7", "14", "30", "60", "90", "120", "150", "180", "365", "400", "545", "731", "1827", "3653"},
order = 10
)
public String logRetentionDays = null;
// ========== Monitoring Configuration ==========
/** Enable CloudWatch monitoring */
@ConfigField(
displayName = "Enable Monitoring",
description = "Enable CloudWatch metrics and alarms",
category = "monitoring",
order = 20
)
public Boolean enableMonitoring = true;
/** Enable encryption at rest */
@ConfigField(
displayName = "Enable Encryption",
description = "Enable encryption at rest for all resources",
category = "security",
order = 320
)
public Boolean enableEncryption = true;
/** Enable AWS Config */
@ConfigField(
displayName = "Enable AWS Config",
description = "Enable AWS Config for configuration compliance monitoring",
category = "compliance",
order = 30
)
public Boolean awsConfigEnabled = false;
/** Create AWS Config infrastructure */
@ConfigField(
displayName = "Create Config Infrastructure",
description = "Create AWS Config recorder and delivery channel (only one per region)",
category = "compliance",
visibleWhen = "awsConfigEnabled == true",
order = 40
)
public Boolean createConfigInfrastructure = true;
/** Enable GuardDuty threat detection */
@ConfigField(
displayName = "Enable GuardDuty",
description = "Enable Amazon GuardDuty for threat detection",
category = "compliance",
order = 50
)
public Boolean guardDutyEnabled = false;
/** Create GuardDuty detector (account-region singleton) */
@ConfigField(
displayName = "Create GuardDuty Detector",
description = "Create GuardDuty detector (only one per account/region)",
category = "compliance",
visibleWhen = "guardDutyEnabled == true",
order = 60
)
public Boolean createGuardDutyDetector = false;
/** GuardDuty alerts configured (EventBridge to SNS/SIEM) */
@ConfigField(
displayName = "Configure GuardDuty Alerts",
description = "Configure EventBridge rules to forward GuardDuty findings to SNS/SIEM",
category = "compliance",
visibleWhen = "guardDutyEnabled == true",
order = 65
)
public Boolean guardDutyAlertsConfigured = false;
/** Certificate expiration monitoring enabled */
@ConfigField(
displayName = "Certificate Expiration Monitoring",
description = "Enable CloudWatch alarms for ACM certificate expiration",
category = "monitoring",
visibleWhen = "enableSsl == true",
order = 68
)
public Boolean certificateExpirationMonitoring = false;
/** Enable CloudTrail for API audit logging */
@ConfigField(
displayName = "Enable CloudTrail",
description = "Enable CloudTrail for API audit logging",
category = "compliance",
order = 70
)
public Boolean cloudTrailEnabled = false;
/** Enable security monitoring */
@ConfigField(
displayName = "Security Monitoring",
description = "Enable security monitoring features",
category = "compliance",
order = 72
)
public Boolean securityMonitoringEnabled = false;
/** Enable EFS encryption in transit */
@ConfigField(
displayName = "EFS Encryption in Transit",
description = "Enable EFS encryption in transit for data protection",
category = "security",
order = 340
)
public Boolean efsEncryptionInTransitEnabled = true;
/** Restrict security group egress to VPC CIDR only (requires VPC endpoints for AWS service access) */
@ConfigField(
displayName = "Restrict Security Group Egress",
description = "Restrict security group egress to VPC CIDR only. Requires VPC endpoints for AWS services (CloudWatch, RDS monitoring). If false, allows 0.0.0.0/0 egress (default CDK behavior).",
category = "security",
order = 345
)
public Boolean restrictSecurityGroupEgress = false;
/** Enable automated backups (null = use security profile default) */
@ConfigField(
displayName = "Automated Backups",
description = "Enable automated backups for EFS and databases (null = profile default: PRODUCTION=true, others=false)",
category = "storage",
order = 50
)
public Boolean automatedBackupEnabled = null;
/** Enable cross-region backups (null = use security profile default) */
@ConfigField(
displayName = "Cross-Region Backups",
description = "Enable cross-region backup replication for disaster recovery (null = profile default)",
category = "storage",
tags = {FieldTag.BILLING_IMPACT},
order = 60
)
public Boolean crossRegionBackupEnabled = null;
// ========== Advanced Monitoring & Threat Protection ==========
/** Enable Amazon Macie for PII/PHI discovery (HIPAA/GDPR) */
@ConfigField(
displayName = "Enable Macie",
description = "Enable Amazon Macie for PII/PHI discovery (required for HIPAA/GDPR)",
category = "compliance",
tags = {FieldTag.BILLING_IMPACT},
order = 80
)
public Boolean macieEnabled = false;
/** Enable Macie automated discovery jobs */
@ConfigField(
displayName = "Macie Auto-Discovery",
description = "Enable Macie automated discovery jobs for continuous scanning",
category = "compliance",
visibleWhen = "macieEnabled == true",
dependsOn = "macieEnabled",
tags = {FieldTag.BILLING_IMPACT},
order = 90
)
public Boolean macieAutomatedDiscovery = false;
/** Enable AWS Security Hub for centralized security findings */
@ConfigField(
displayName = "Enable Security Hub",
description = "Enable AWS Security Hub for centralized security findings",
category = "compliance",
order = 100
)
public Boolean securityHubEnabled = false;
/** Enable Amazon Inspector for vulnerability scanning */
@ConfigField(
displayName = "Enable Inspector",
description = "Enable Amazon Inspector for vulnerability scanning",
category = "compliance",
order = 110
)
public Boolean inspectorEnabled = false;
/** Enable anti-malware scanning */
@ConfigField(
displayName = "Anti-Malware Scanning",
description = "Enable anti-malware scanning for uploaded files",
category = "compliance",
order = 120
)
public Boolean antiMalwareEnabled = false;
/** Enable file integrity monitoring */
@ConfigField(
displayName = "File Integrity Monitoring",
description = "Monitor critical files for unauthorized changes",
category = "compliance",
order = 130
)
public Boolean fileIntegrityMonitoring = false;
/** Enable container runtime security monitoring */
@ConfigField(
displayName = "Container Runtime Security",
description = "Monitor container runtime for suspicious activity",
category = "compliance",
order = 140
)
public Boolean containerRuntimeSecurity = false;
/** Enable container image vulnerability scanning */
@ConfigField(
displayName = "Container Image Scanning",
description = "Scan container images for known vulnerabilities",
category = "compliance",
order = 150
)
public Boolean containerImageScanning = false;
/** Enable AWS Audit Manager */
@ConfigField(
displayName = "Enable Audit Manager",
description = "Enable AWS Audit Manager for compliance evidence collection",
category = "compliance",
tags = {FieldTag.BILLING_IMPACT},
order = 160
)
public Boolean auditManagerEnabled = false;
/** Enable CloudWatch Logs KMS encryption */
@ConfigField(
displayName = "CloudWatch Logs KMS Encryption",
description = "Encrypt CloudWatch Logs with customer-managed KMS keys (required for PCI-DSS, HIPAA, SOC2)",
category = "compliance",
tags = {FieldTag.BILLING_IMPACT},
order = 170
)
public Boolean cloudWatchLogsKmsEncryptionEnabled = false;
/** Enable CloudTrail Insights */
@ConfigField(
displayName = "CloudTrail Insights",
description = "Enable CloudTrail Insights for API activity anomaly detection (required for SOC2, NIST)",
category = "compliance",
tags = {FieldTag.BILLING_IMPACT},
visibleWhen = "cloudTrailEnabled == true",
dependsOn = "cloudTrailEnabled",
order = 180
)
public Boolean cloudTrailInsightsEnabled = false;
/** Enable Route53 Query Logging */
@ConfigField(
displayName = "Route53 Query Logging",
description = "Enable DNS query logging for Route53 hosted zones (required for SOC2, NIST)",
category = "compliance",
order = 190
)
public Boolean route53QueryLoggingEnabled = false;
/** Enable S3 Object Lock for audit buckets (HIPAA/PCI-DSS immutability requirement) */
@ConfigField(
displayName = "S3 Object Lock",
description = "Enable S3 Object Lock for compliance audit buckets to ensure immutability (HIPAA § 164.312(c)(1), PCI-DSS Req 10.7)",
category = "compliance",
order = 195
)
public Boolean s3ObjectLockEnabled = false;
/** Enable S3 versioning remediation */
@ConfigField(
displayName = "S3 Versioning Remediation",
description = "Enable automatic remediation of S3 versioning compliance violations",
category = "compliance",
visibleWhen = "awsConfigEnabled == true",
order = 420
)
public Boolean enableS3VersioningRemediation = false;
/** Enable CloudTrail bucket access logging remediation */
@ConfigField(
displayName = "CloudTrail Bucket Access Logging Remediation",
description = "Enable automatic remediation of CloudTrail S3 bucket access logging violations",
category = "compliance",
visibleWhen = "awsConfigEnabled == true && cloudTrailEnabled == true",
order = 430
)
public Boolean enableCloudTrailBucketAccessRemediation = false;
// ========== Health Check Configuration ==========
@ConfigField(
displayName = "Health Check Grace Period (seconds)",
description = "Time to wait before starting health checks after container starts. GitLab needs 600s due to database migrations.",
category = "resources",
min = 60,
max = 900,
defaultFrom = "defaultHealthCheckGracePeriod",
order = 600
)
public int healthCheckGracePeriod = 300;
@ConfigField(
displayName = "Health Check Interval (seconds)",
description = "Time between health checks",
category = "resources",
min = 5,
max = 300,
order = 610
)
public int healthCheckInterval = 30;
@ConfigField(
displayName = "Health Check Timeout (seconds)",
description = "Time to wait for health check response",
category = "resources",
min = 2,
max = 60,
order = 620
)
public int healthCheckTimeout = 5;
@ConfigField(
displayName = "Healthy Threshold Count",
description = "Number of consecutive successful health checks before marking healthy",
category = "resources",
min = 1,
max = 10,
order = 630
)
public int healthyThreshold = 2;
@ConfigField(
displayName = "Unhealthy Threshold Count",
description = "Number of consecutive failed health checks before marking unhealthy",
category = "resources",
min = 1,
max = 10,
order = 640
)
public int unhealthyThreshold = 3;
// ========== Region Configuration ==========
/** AWS region (e.g., "us-east-1", "us-west-2") */
@ConfigField(
displayName = "AWS Region",
description = "AWS region for deployment",
category = "basic",
allowedValues = {
"us-east-1", "us-east-2", "us-west-1", "us-west-2",
"eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1", "eu-north-1",
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2",
"ap-south-1", "sa-east-1", "ca-central-1"
},
required = true,
order = 50
)
public String region = "us-east-1";
/**
* Target AWS account id to pin CDK's {@code Environment.account} to at synth time.
*
* <p>Deliberately <b>not</b> a {@code @ConfigField} — {@link
* com.cloudforge.core.config.ConfigurationIntrospector} and {@link
* com.cloudforge.core.config.DeploymentContextPreparer} both skip unannotated fields, so this
* never appears in the generated deployment-context form/schema and never gets a default
* resolved for it. It exists purely for callers that already know the target account
* programmatically (cross-account deploy: CloudForge Manager resolves this from an assumed
* IAM role before synthesis — see {@code cloudforge-manager}'s account-connection feature,
* which owns every concept of "which account" beyond this bare pass-through field).</p>
*
* <p>When {@code null} (the default — every existing caller), behavior is completely
* unchanged from before this field existed: {@code cloudforge-api}'s {@code
* CloudForgeSynthesizer} falls back to its prior {@code CDK_DEFAULT_ACCOUNT}-env-var-or-omit
* resolution, producing the same account-agnostic template it always has.</p>
*/
public String account;
/**
* GDPR data transfer approval flag for non-EU deployments.
*/
@ConfigField(
displayName = "GDPR Data Transfer Approved",
description = "Confirm proper data transfer mechanisms (SCCs, BCRs) are in place for non-EU deployments with GDPR",
category = "compliance",
visibleWhen = "complianceFrameworks contains gdpr",
dependsOn = "complianceFrameworks",
order = 330
)
public Boolean gdprDataTransferApproved = false;
/**
* Availability zone suffixes for deployment (region-relative — "a" means whichever zone the
* deploy's own region calls "a", not a specific full zone name, since the same suffix set is
* valid regardless of which region gets picked elsewhere in the form).
*/
@ConfigField(
displayName = "Availability Zones",
description = "Availability zone suffixes to deploy across (leave empty for automatic selection)",
category = "network",
allowedValues = {"a", "b", "c", "d"},
example = "a,b",
order = 5
)
public String[] availabilityZones;
/** Enable auto-scaling */
@ConfigField(
displayName = "Enable Auto-Scaling",
description = "Enable automatic scaling based on CPU utilization",
category = "resources",
order = 5
)
public Boolean enableAutoScaling = false;
// ========== JSON Serialization/Deserialization ==========
/**
* Creates a pre-configured ObjectMapper for DeploymentConfig serialization.
*
* <p>Configuration:
* <ul>
* <li>Field visibility (not getters) for public field serialization</li>
* <li>Enums serialized as strings</li>
* <li>Unknown properties ignored for forward compatibility</li>
* <li>Null values excluded from output</li>
* </ul>
*/
private static ObjectMapper createMapper() {
ObjectMapper mapper = JsonMapper.builder()
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// serializationInclusion(Include) is deprecated since Jackson 2.20 in favor of
// defaultPropertyInclusion(Value); ALL_NON_NULL is the equivalent Value constant.
.defaultPropertyInclusion(JsonInclude.Value.ALL_NON_NULL)
.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
.build();
return mapper;
}
/**
* Load DeploymentConfig from a JSON file (e.g., deployment-context.json).
*
* @param path Path to the JSON file
* @return DeploymentConfig populated from JSON
* @throws IOException if file cannot be read or parsed
*/
public static DeploymentConfig fromFile(Path path) throws IOException {
return createMapper().readValue(path.toFile(), DeploymentConfig.class);
}
/**
* Load DeploymentConfig from a JSON file path string.
*
* @param filePath Path to the JSON file
* @return DeploymentConfig populated from JSON
* @throws IOException if file cannot be read or parsed
*/
public static DeploymentConfig fromFile(String filePath) throws IOException {
return fromFile(Path.of(filePath));
}
/**
* Load DeploymentConfig from a JSON string.
*
* @param json JSON string
* @return DeploymentConfig populated from JSON
* @throws JsonProcessingException if JSON cannot be parsed
*/
public static DeploymentConfig fromJson(String json) throws JsonProcessingException {
return createMapper().readValue(json, DeploymentConfig.class);
}
/**
* Load DeploymentConfig from a Map (e.g., CDK context).
*
* <p>Uses Jackson's type-safe conversion to handle:
* <ul>
* <li>String → Enum conversion via @JsonCreator methods</li>
* <li>String/Number → Boolean conversion (supports "1", "yes", "0", "no")</li>
* <li>Comma-separated strings → List conversion (complianceFrameworks)</li>
* <li>Unknown properties are ignored for forward compatibility</li>
* </ul>
*
* @param map Map containing configuration key-value pairs
* @return DeploymentConfig populated from the map
*/
public static DeploymentConfig fromMap(java.util.Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return new DeploymentConfig();
}
// Normalize the map: handle CDK context key rename and boolean string coercion
java.util.Map<String, Object> normalized = new java.util.LinkedHashMap<>(map);
if (normalized.containsKey("env") && !normalized.containsKey("environment")) {
normalized.put("environment", normalized.get("env"));
}
// Coerce boolean-like string values before Jackson processing
normalized.replaceAll((key, value) -> coerceBooleanIfNeeded(value));
return createMapper().convertValue(normalized, DeploymentConfig.class);
}
/**
* Coerces common boolean-like string values to actual Boolean objects.
* Supports: "yes", "on" → true; "no", "off" → false.
*
* <p>Note: "1" and "0" are NOT coerced to boolean because they could be valid
* integer values (e.g., minInstanceCapacity="1"). Jackson handles "true"/"false"
* natively, so we only need to handle non-standard boolean representations.</p>
*/
private static Object coerceBooleanIfNeeded(Object value) {
if (value instanceof String str) {
String lower = str.trim().toLowerCase();
return switch (lower) {
// Note: "1" and "0" removed - they're ambiguous with integer values
case "yes", "on" -> Boolean.TRUE;
case "no", "off" -> Boolean.FALSE;
default -> value; // Keep original string for Jackson to parse
};
}
return value;
}
/**
* Serialize this DeploymentConfig to a JSON string.
*
* @return JSON string representation
* @throws JsonProcessingException if serialization fails
*/
public String toJson() throws JsonProcessingException {
return createMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this);
}
/**
* Save this DeploymentConfig to a JSON file.
*
* @param path Path to write the JSON file
* @throws IOException if file cannot be written
*/
public void toFile(Path path) throws IOException {
createMapper().writerWithDefaultPrettyPrinter().writeValue(path.toFile(), this);
}
/**
* Save this DeploymentConfig to a JSON file path string.
*
* @param filePath Path to write the JSON file
* @throws IOException if file cannot be written
*/
public void toFile(String filePath) throws IOException {
toFile(Path.of(filePath));
}
// ========== Compliance Framework Helpers ==========
/**
* Returns the compliance frameworks as a comma-separated string.
* Provided for backward compatibility with code expecting the old string format.
*
* @return comma-separated framework string (e.g., "soc2,pci-dss,hipaa")
*/
public String getComplianceFrameworksAsString() {
return ComplianceFrameworkType.toCommaSeparated(complianceFrameworks);
}
/**
* Checks if a specific compliance framework is enabled.
*
* @param framework the framework to check
* @return true if the framework is in the list
*/
public boolean hasComplianceFramework(ComplianceFrameworkType framework) {
return complianceFrameworks != null && complianceFrameworks.contains(framework);
}
/**
* Checks if any compliance framework is enabled.
*
* @return true if at least one framework is configured
*/
public boolean hasAnyComplianceFramework() {
return complianceFrameworks != null && !complianceFrameworks.isEmpty();
}
// ========== CloudForge Manager (deploy history client) ==========
/**
* Base URL of a running CloudForge Manager (e.g. http://127.0.0.1:1958).
* Optional — when unset/unreachable, deploy history POSTs are skipped.
*/
@ConfigField(
displayName = "Manager URL",
description = "Shared CloudForge Manager base URL for deploy-history posts after option 2/8",
category = "operations",
required = false,
example = "http://127.0.0.1:1958",
propertyKey = "cfc.manager.url",
order = 9000
)
public String managerUrl;
/**
* Default inventory target for Manager clients (ministack | aws).
*/
@ConfigField(
displayName = "Manager Target",
description = "Deployment target recorded in history entries",
category = "operations",
required = false,
allowedValues = {"aws", "ministack", "localstack"},
example = "ministack",
propertyKey = "cfc.manager.target",
order = 9010
)
@JsonSerialize(using = DeploymentTargetConverter.Serializer.class)
@JsonDeserialize(using = DeploymentTargetConverter.Deserializer.class)
public DeploymentTarget managerTarget;
/**
* Bearer token for POST /api/v1/history. Prefer CFC_MANAGER_HISTORY_TOKEN env — do not commit.
*/
@ConfigField(
displayName = "Manager History Token",
description = "Bearer token for append-only deploy history API (prefer env CFC_MANAGER_HISTORY_TOKEN)",
category = "operations",
required = false,
sensitive = true,
propertyKey = "cfc.manager.history-token",
order = 9020
)
public String managerHistoryToken;
/**
* Opt-in: grants CloudForge Manager's own task/instance role the direct-deploy IAM
* capabilities ({@code CFN_DEPLOY}/{@code SC_PROVISION} — see
* {@code ManagerAwsCapabilityCatalog}), condition-scoped to CloudForge-tagged resources.
* Only has any effect when this deployment's {@code applicationId} is
* {@code cloudforge-manager}; a no-op for every other application. Defaults to false — Manager
* deploying AWS infrastructure on a caller's behalf (via {@code deploy:create}/
* {@code deploy:catalog}) is a materially broader permission grant than Manager's normal
* inventory/operator role, so it must be explicitly requested per deployment, not inherited
* automatically from IAM profile.
*/
@ConfigField(
displayName = "Manager Direct Deploy",
description = "Grant CloudForge Manager's task role permission to create/update AWS "
+ "infrastructure directly (CFN CreateStack/UpdateStack + Service Catalog "
+ "ProvisionProduct), scoped to CloudForge-tagged resources. Only applies when "
+ "applicationId is cloudforge-manager.",
category = "operations",
required = false,
tags = {FieldTag.REQUIRES_APPROVAL, FieldTag.EXPERIMENTAL},
propertyKey = "cfc.manager.direct-deploy-enabled",
order = 9030
)
public Boolean managerDirectDeployEnabled = false;
/**
* Convert this DeploymentConfig to a Map for CDK context.
*
* <p>Special handling:
* <ul>
* <li>Renames "environment" to "env" for CDK compatibility</li>
* <li>Excludes null values</li>
* </ul>
*
* @return Map suitable for CDK App context
*/
@SuppressWarnings("unchecked")
public Map<String, Object> toContextMap() {
ObjectMapper mapper = createMapper();
Map<String, Object> context = mapper.convertValue(this, Map.class);
// CDK uses "env" not "environment"
if (context.containsKey("environment")) {
context.put("env", context.remove("environment"));
}
return context;
}
/**
* Context map safe for durable deploy history.
*
* <p>Same shape as {@link #toContextMap()}, but omits fields annotated with
* {@code @ConfigField(sensitive = true)} (and their {@link JsonAlias} names).
* New non-sensitive {@code @ConfigField}s appear automatically.</p>
*
* @return redacted map suitable for Manager history {@code detail.deploymentContext}
*/
public Map<String, Object> toHistoryContextMap() {
Map<String, Object> context = new LinkedHashMap<>(toContextMap());
for (String key : sensitiveContextKeys()) {
context.remove(key);
}
return context;
}
private static Set<String> sensitiveContextKeys() {
Set<String> keys = new HashSet<>();
for (Field field : DeploymentConfig.class.getDeclaredFields()) {
ConfigField annotation = field.getAnnotation(ConfigField.class);
if (annotation == null || !annotation.sensitive()) {
continue;
}
keys.add(field.getName());
JsonAlias alias = field.getAnnotation(JsonAlias.class);
if (alias != null) {
keys.addAll(List.of(alias.value()));
}
// toContextMap renames environment → env
if ("environment".equals(field.getName())) {
keys.add("env");
}
}
return keys;
}
}