import * as cdk from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; import { SpicyVpc, SpicyVpcProps, SubnetCidrConfig } from '../constructs/spicy-vpc'; /** * Props for SpicyVpcStack * Extends SpicyVpcProps but makes tags optional since we can derive from context */ export interface SpicyVpcStackProps extends cdk.StackProps { /** * VPC configuration - all SpicyVpcProps except tags (derived from stack props) */ readonly vpcConfig?: Omit; /** * Owner tag */ readonly ownerTag: string; /** * Product tag */ readonly productTag: string; /** * Component tag */ readonly componentTag: string; /** * Build identifier (e.g., git SHA) */ readonly buildTag?: string; } /** * SpicyVpcStack - A CloudFormation stack that creates a Spicy VPC * * This stack can be deployed via CDK CLI or from Jenkins pipelines. * Configuration can be provided via: * 1. Direct props * 2. CDK context (cdk deploy -c key=value) * 3. Environment variables */ export class SpicyVpcStack extends cdk.Stack { /** The VPC construct */ public readonly spicyVpc: SpicyVpc; constructor(scope: Construct, id: string, props: SpicyVpcStackProps) { super(scope, id, props); // Create the VPC this.spicyVpc = new SpicyVpc(this, 'SpicyVpc', { ...props.vpcConfig, tags: { owner: props.ownerTag, product: props.productTag, component: props.componentTag, build: props.buildTag, }, }); } /** * Create a SpicyVpcStack from CDK context values * This is useful when deploying from Jenkins where context is passed via -c flags */ static fromContext(scope: Construct, id: string, stackProps?: cdk.StackProps): SpicyVpcStack { const app = scope.node.root as cdk.App; // Required context values const ownerTag = app.node.tryGetContext('ownerTag') ?? 'SpicyTeam'; const productTag = app.node.tryGetContext('productTag') ?? 'spicy'; const componentTag = app.node.tryGetContext('componentTag') ?? 'spicy-VPC'; const buildTag = app.node.tryGetContext('build'); // Optional VPC configuration from context const vpcCidr = app.node.tryGetContext('vpcCidr') as string | undefined; const vpcTenancy = app.node.tryGetContext('vpcTenancy') as 'default' | 'dedicated' | undefined; const numberOfAzs = app.node.tryGetContext('numberOfAzs') as string | undefined; const availabilityZones = app.node.tryGetContext('availabilityZones') as string | undefined; const createPrivateSubnets = app.node.tryGetContext('createPrivateSubnets') as string | undefined; const createAdditionalPrivateSubnets = app.node.tryGetContext('createAdditionalPrivateSubnets') as | string | undefined; const publicSubnetTag = app.node.tryGetContext('publicSubnetTag') as string | undefined; const privateSubnetATag = app.node.tryGetContext('privateSubnetATag') as string | undefined; const privateSubnetBTag = app.node.tryGetContext('privateSubnetBTag') as string | undefined; const enableIpv6 = app.node.tryGetContext('enableIpv6') as string | undefined; // Interface endpoint overrides const disableEcrEndpoint = app.node.tryGetContext('disableEcrEndpoint') as string | undefined; const disableEcrDockerEndpoint = app.node.tryGetContext('disableEcrDockerEndpoint') as string | undefined; const disableCloudwatchLogsEndpoint = app.node.tryGetContext('disableCloudwatchLogsEndpoint') as string | undefined; const disableStsEndpoint = app.node.tryGetContext('disableStsEndpoint') as string | undefined; const disableSecretsManagerEndpoint = app.node.tryGetContext('disableSecretsManagerEndpoint') as string | undefined; const disableSsmEndpoint = app.node.tryGetContext('disableSsmEndpoint') as string | undefined; // Parse subnet CIDRs from context const subnetCidrs = SpicyVpcStack.parseSubnetCidrsFromContext(app); // Build VPC config using a mutable object then cast const vpcConfigBuilder: { vpcCidr?: string; vpcTenancy?: 'default' | 'dedicated'; numberOfAzs?: 2 | 3 | 4; availabilityZones?: string[]; createPrivateSubnets?: boolean; createAdditionalPrivateSubnets?: boolean; publicSubnetTag?: string; privateSubnetATag?: string; privateSubnetBTag?: string; subnetCidrs?: SpicyVpcProps['subnetCidrs']; enableIpv6?: boolean; interfaceEndpoints?: SpicyVpcProps['interfaceEndpoints']; } = {}; if (vpcCidr) vpcConfigBuilder.vpcCidr = vpcCidr; if (vpcTenancy) vpcConfigBuilder.vpcTenancy = vpcTenancy; if (numberOfAzs) vpcConfigBuilder.numberOfAzs = parseInt(numberOfAzs, 10) as 2 | 3 | 4; if (availabilityZones) { vpcConfigBuilder.availabilityZones = availabilityZones.split(','); } if (createPrivateSubnets !== undefined) { vpcConfigBuilder.createPrivateSubnets = createPrivateSubnets === 'true'; } if (createAdditionalPrivateSubnets !== undefined) { vpcConfigBuilder.createAdditionalPrivateSubnets = createAdditionalPrivateSubnets === 'true'; } if (publicSubnetTag) vpcConfigBuilder.publicSubnetTag = publicSubnetTag; if (privateSubnetATag) vpcConfigBuilder.privateSubnetATag = privateSubnetATag; if (privateSubnetBTag) vpcConfigBuilder.privateSubnetBTag = privateSubnetBTag; if (subnetCidrs && Object.keys(subnetCidrs).length > 0) { vpcConfigBuilder.subnetCidrs = subnetCidrs; } if (enableIpv6 !== undefined) { vpcConfigBuilder.enableIpv6 = enableIpv6 !== 'false'; } // Build interface endpoint config if any overrides provided if ( disableEcrEndpoint || disableEcrDockerEndpoint || disableCloudwatchLogsEndpoint || disableStsEndpoint || disableSecretsManagerEndpoint || disableSsmEndpoint ) { vpcConfigBuilder.interfaceEndpoints = { ecr: disableEcrEndpoint !== 'true', ecrDocker: disableEcrDockerEndpoint !== 'true', cloudwatchLogs: disableCloudwatchLogsEndpoint !== 'true', sts: disableStsEndpoint !== 'true', secretsManager: disableSecretsManagerEndpoint !== 'true', ssm: disableSsmEndpoint !== 'true', }; } return new SpicyVpcStack(scope, id, { ...stackProps, ownerTag, productTag, componentTag, buildTag, vpcConfig: vpcConfigBuilder, }); } /** * Parse subnet CIDR configurations from CDK context */ private static parseSubnetCidrsFromContext(app: cdk.App): SpicyVpcProps['subnetCidrs'] { const cidrs: SpicyVpcProps['subnetCidrs'] = {}; const azLetters = ['a', 'b', 'c', 'd'] as const; for (const az of azLetters) { const azUpper = az.toUpperCase(); const publicCidr = app.node.tryGetContext(`publicSubnet${azUpper}Cidr`); const private1Cidr = app.node.tryGetContext(`privateSubnet${azUpper}1Cidr`); const private2Cidr = app.node.tryGetContext(`privateSubnet${azUpper}2Cidr`); if (publicCidr || private1Cidr || private2Cidr) { const config: SubnetCidrConfig = { publicCidr: publicCidr ?? '', private1Cidr: private1Cidr ?? '', }; if (private2Cidr) { config.private2Cidr = private2Cidr; } cidrs[az] = config; } } return cidrs; } }