|
| 1 | +// This file is duplicating CloudFormationDeployments define in https://github.com/aws/aws-cdk/blob/master/packages/aws-cdk/lib/api/cloudformation-deployments.ts |
| 2 | +// This was needed to overwrite isAssetManifestArtifact function which was preventing proper assets to be created due to difference between cloudAssembly in @aws-cdk vs. aws-cdk-lib |
| 3 | + |
| 4 | +/* eslint-disable @typescript-eslint/ban-ts-comment */ |
| 5 | +/* eslint-disable @typescript-eslint/explicit-function-return-type */ |
| 6 | +/* eslint-disable @typescript-eslint/member-ordering */ |
| 7 | +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ |
| 8 | +/* eslint-disable @typescript-eslint/member-delimiter-style */ |
| 9 | +/* eslint-disable newline-before-return */ |
| 10 | +/* eslint-disable func-style */ |
| 11 | +import * as cxapi from '@aws-cdk/cx-api'; |
| 12 | +import { AssetManifest } from 'cdk-assets'; |
| 13 | +import { debug } from 'aws-cdk/lib/logging'; |
| 14 | +import { publishAssets } from 'aws-cdk/lib/util/asset-publishing'; |
| 15 | +import { Mode } from 'aws-cdk/lib/api/aws-auth/credentials'; |
| 16 | +import { ISDK } from 'aws-cdk/lib/api/aws-auth/sdk'; |
| 17 | +import { SdkProvider } from 'aws-cdk/lib/api/aws-auth/sdk-provider'; |
| 18 | +import { deployStack, DeployStackResult, destroyStack } from 'aws-cdk/lib/api/deploy-stack'; |
| 19 | +import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate } from 'aws-cdk/lib/api/nested-stack-helpers'; |
| 20 | +import { ToolkitInfo } from 'aws-cdk/lib/api/toolkit-info'; |
| 21 | +import { CloudFormationStack, Template } from 'aws-cdk/lib/api/util/cloudformation'; |
| 22 | +import { replaceEnvPlaceholders } from 'aws-cdk/lib/api/util/placeholders'; |
| 23 | +import { |
| 24 | + ProvisionerProps, |
| 25 | + DeployStackOptions, |
| 26 | + DestroyStackOptions, |
| 27 | + prepareSdkWithLookupRoleFor, |
| 28 | + StackExistsOptions, |
| 29 | + PreparedSdkForEnvironment, |
| 30 | +} from 'aws-cdk/lib/api/cloudformation-deployments'; |
| 31 | + |
| 32 | +/** |
| 33 | + * Helper class for CloudFormation deployments |
| 34 | + * |
| 35 | + * Looks us the right SDK and Bootstrap stack to deploy a given |
| 36 | + * stack artifact. |
| 37 | + */ |
| 38 | +export class CloudFormationDeployments { |
| 39 | + private readonly sdkProvider: SdkProvider; |
| 40 | + |
| 41 | + constructor(props: ProvisionerProps) { |
| 42 | + this.sdkProvider = props.sdkProvider; |
| 43 | + } |
| 44 | + |
| 45 | + public async readCurrentTemplateWithNestedStacks( |
| 46 | + rootStackArtifact: cxapi.CloudFormationStackArtifact |
| 47 | + ): Promise<Template> { |
| 48 | + const sdk = await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact); |
| 49 | + return (await loadCurrentTemplateWithNestedStacks(rootStackArtifact, sdk)).deployedTemplate; |
| 50 | + } |
| 51 | + |
| 52 | + public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> { |
| 53 | + debug(`Reading existing template for stack ${stackArtifact.displayName}.`); |
| 54 | + const sdk = await this.prepareSdkWithLookupOrDeployRole(stackArtifact); |
| 55 | + return loadCurrentTemplate(stackArtifact, sdk); |
| 56 | + } |
| 57 | + |
| 58 | + public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> { |
| 59 | + const { stackSdk, resolvedEnvironment, cloudFormationRoleArn } = await this.prepareSdkFor( |
| 60 | + options.stack, |
| 61 | + options.roleArn |
| 62 | + ); |
| 63 | + |
| 64 | + const toolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, stackSdk, options.toolkitStackName); |
| 65 | + |
| 66 | + // Publish any assets before doing the actual deploy |
| 67 | + await this.publishStackAssets(options.stack, toolkitInfo); |
| 68 | + |
| 69 | + // Do a verification of the bootstrap stack version |
| 70 | + await this.validateBootstrapStackVersion( |
| 71 | + options.stack.stackName, |
| 72 | + options.stack.requiresBootstrapStackVersion, |
| 73 | + options.stack.bootstrapStackVersionSsmParameter, |
| 74 | + toolkitInfo |
| 75 | + ); |
| 76 | + |
| 77 | + return deployStack({ |
| 78 | + stack: options.stack, |
| 79 | + resolvedEnvironment, |
| 80 | + deployName: options.deployName, |
| 81 | + notificationArns: options.notificationArns, |
| 82 | + quiet: options.quiet, |
| 83 | + sdk: stackSdk, |
| 84 | + sdkProvider: this.sdkProvider, |
| 85 | + roleArn: cloudFormationRoleArn, |
| 86 | + reuseAssets: options.reuseAssets, |
| 87 | + toolkitInfo, |
| 88 | + tags: options.tags, |
| 89 | + execute: options.execute, |
| 90 | + changeSetName: options.changeSetName, |
| 91 | + force: options.force, |
| 92 | + parameters: options.parameters, |
| 93 | + usePreviousParameters: options.usePreviousParameters, |
| 94 | + progress: options.progress, |
| 95 | + ci: options.ci, |
| 96 | + rollback: options.rollback, |
| 97 | + hotswap: options.hotswap, |
| 98 | + extraUserAgent: options.extraUserAgent, |
| 99 | + }); |
| 100 | + } |
| 101 | + |
| 102 | + public async destroyStack(options: DestroyStackOptions): Promise<void> { |
| 103 | + const { stackSdk, cloudFormationRoleArn: roleArn } = await this.prepareSdkFor(options.stack, options.roleArn); |
| 104 | + |
| 105 | + return destroyStack({ |
| 106 | + sdk: stackSdk, |
| 107 | + roleArn, |
| 108 | + stack: options.stack, |
| 109 | + deployName: options.deployName, |
| 110 | + quiet: options.quiet, |
| 111 | + }); |
| 112 | + } |
| 113 | + |
| 114 | + public async stackExists(options: StackExistsOptions): Promise<boolean> { |
| 115 | + const { stackSdk } = await this.prepareSdkFor(options.stack, undefined, Mode.ForReading); |
| 116 | + const stack = await CloudFormationStack.lookup( |
| 117 | + stackSdk.cloudFormation(), |
| 118 | + options.deployName ?? options.stack.stackName |
| 119 | + ); |
| 120 | + return stack.exists; |
| 121 | + } |
| 122 | + |
| 123 | + private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<ISDK> { |
| 124 | + // try to assume the lookup role |
| 125 | + try { |
| 126 | + const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact); |
| 127 | + if (result.didAssumeRole) { |
| 128 | + return result.sdk; |
| 129 | + } |
| 130 | + } catch {} |
| 131 | + // fall back to the deploy role |
| 132 | + return (await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading)).stackSdk; |
| 133 | + } |
| 134 | + |
| 135 | + /** |
| 136 | + * Get the environment necessary for touching the given stack |
| 137 | + * |
| 138 | + * Returns the following: |
| 139 | + * |
| 140 | + * - The resolved environment for the stack (no more 'unknown-account/unknown-region') |
| 141 | + * - SDK loaded with the right credentials for calling `CreateChangeSet`. |
| 142 | + * - The Execution Role that should be passed to CloudFormation. |
| 143 | + */ |
| 144 | + private async prepareSdkFor( |
| 145 | + stack: cxapi.CloudFormationStackArtifact, |
| 146 | + roleArn?: string, |
| 147 | + mode = Mode.ForWriting |
| 148 | + ): Promise<PreparedSdkForEnvironment> { |
| 149 | + if (!stack.environment) { |
| 150 | + throw new Error(`The stack ${stack.displayName} does not have an environment`); |
| 151 | + } |
| 152 | + |
| 153 | + const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment); |
| 154 | + |
| 155 | + // Substitute any placeholders with information about the current environment |
| 156 | + const arns = await replaceEnvPlaceholders( |
| 157 | + { |
| 158 | + assumeRoleArn: stack.assumeRoleArn, |
| 159 | + |
| 160 | + // Use the override if given, otherwise use the field from the stack |
| 161 | + cloudFormationRoleArn: roleArn ?? stack.cloudFormationExecutionRoleArn, |
| 162 | + }, |
| 163 | + resolvedEnvironment, |
| 164 | + this.sdkProvider |
| 165 | + ); |
| 166 | + |
| 167 | + const stackSdk = await this.sdkProvider.forEnvironment(resolvedEnvironment, mode, { |
| 168 | + assumeRoleArn: arns.assumeRoleArn, |
| 169 | + assumeRoleExternalId: stack.assumeRoleExternalId, |
| 170 | + }); |
| 171 | + |
| 172 | + return { |
| 173 | + stackSdk: stackSdk.sdk, |
| 174 | + resolvedEnvironment, |
| 175 | + cloudFormationRoleArn: arns.cloudFormationRoleArn, |
| 176 | + }; |
| 177 | + } |
| 178 | + |
| 179 | + /** |
| 180 | + * Publish all asset manifests that are referenced by the given stack |
| 181 | + */ |
| 182 | + private async publishStackAssets(stack: cxapi.CloudFormationStackArtifact, toolkitInfo: ToolkitInfo) { |
| 183 | + const stackEnv = await this.sdkProvider.resolveEnvironment(stack.environment); |
| 184 | + const assetArtifacts = stack.dependencies.filter(isAssetManifestArtifact); |
| 185 | + |
| 186 | + for (const assetArtifact of assetArtifacts) { |
| 187 | + await this.validateBootstrapStackVersion( |
| 188 | + stack.stackName, |
| 189 | + assetArtifact.requiresBootstrapStackVersion, |
| 190 | + assetArtifact.bootstrapStackVersionSsmParameter, |
| 191 | + toolkitInfo |
| 192 | + ); |
| 193 | + |
| 194 | + const manifest = AssetManifest.fromFile(assetArtifact.file); |
| 195 | + await publishAssets(manifest, this.sdkProvider, stackEnv); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + /** |
| 200 | + * Validate that the bootstrap stack has the right version for this stack |
| 201 | + */ |
| 202 | + private async validateBootstrapStackVersion( |
| 203 | + stackName: string, |
| 204 | + requiresBootstrapStackVersion: number | undefined, |
| 205 | + bootstrapStackVersionSsmParameter: string | undefined, |
| 206 | + toolkitInfo: ToolkitInfo |
| 207 | + ) { |
| 208 | + if (requiresBootstrapStackVersion === undefined) { |
| 209 | + return; |
| 210 | + } |
| 211 | + |
| 212 | + try { |
| 213 | + await toolkitInfo.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter); |
| 214 | + } catch (e) { |
| 215 | + // @ts-ignore |
| 216 | + throw new Error(`${stackName}: ${e.message}`); |
| 217 | + } |
| 218 | + } |
| 219 | +} |
| 220 | + |
| 221 | +function isAssetManifestArtifact(art: cxapi.CloudArtifact): art is cxapi.AssetManifestArtifact { |
| 222 | + return Object.keys(art).includes('file'); |
| 223 | +} |
0 commit comments