diff --git a/.changeset/sixty-hornets-impress.md b/.changeset/sixty-hornets-impress.md new file mode 100644 index 000000000..683543eea --- /dev/null +++ b/.changeset/sixty-hornets-impress.md @@ -0,0 +1,7 @@ +--- +"create-sst": patch +"@sst/console": patch +"sst": patch +--- + +Update CDK to 2.171.1 diff --git a/.github/workflows/aws-cdk.yml b/.github/workflows/aws-cdk.yml index 7b5a9167b..5d234e7a4 100644 --- a/.github/workflows/aws-cdk.yml +++ b/.github/workflows/aws-cdk.yml @@ -41,8 +41,18 @@ jobs: tar -xvf cdk.tar.gz PACKAGE_JSON=./package/package.json VERSION=$(jq -r .version $PACKAGE_JSON)-1 - jq ' - .dependencies += {chalk: .devDependencies.chalk, yaml: .devDependencies.yaml, promptly: .devDependencies.promptly, archiver: .devDependencies.archiver, "fs-extra": .devDependencies["fs-extra"], "strip-ansi": .devDependencies["strip-ansi"] } | + jq -c ' + .dependencies += ( + .devDependencies | + with_entries( + select( + .key != "@aws-cdk/cdk-build-tools" and + .key != "@aws-cdk/pkglint" and + .key != "@aws-cdk/yargs-gen" and + .key != "cdk-assets" + ) + ) + ) | .name = "sst-aws-cdk" ' $PACKAGE_JSON > tmp.json mv tmp.json $PACKAGE_JSON diff --git a/packages/console/package.json b/packages/console/package.json index d602ef1ce..105fa512f 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -9,18 +9,18 @@ "preview": "vite preview" }, "dependencies": { - "@aws-sdk/client-cloudformation": "3.405.0", - "@aws-sdk/client-cloudwatch": "3.405.0", - "@aws-sdk/client-cloudwatch-logs": "3.405.0", - "@aws-sdk/client-cognito-identity-provider": "3.405.0", - "@aws-sdk/client-dynamodb": "3.405.0", - "@aws-sdk/client-lambda": "3.405.0", - "@aws-sdk/client-rds-data": "3.405.0", - "@aws-sdk/client-s3": "3.405.0", - "@aws-sdk/client-ssm": "3.405.0", + "@aws-sdk/client-cloudformation": "3.454.0", + "@aws-sdk/client-cloudwatch": "3.454.0", + "@aws-sdk/client-cloudwatch-logs": "3.454.0", + "@aws-sdk/client-cognito-identity-provider": "3.454.0", + "@aws-sdk/client-dynamodb": "3.454.0", + "@aws-sdk/client-lambda": "3.454.0", + "@aws-sdk/client-rds-data": "3.454.0", + "@aws-sdk/client-s3": "3.454.0", + "@aws-sdk/client-ssm": "3.454.0", "@aws-sdk/fetch-http-handler": "^3.374.0", - "@aws-sdk/s3-request-presigner": "3.405.0", - "@aws-sdk/util-dynamodb": "^3.405.0", + "@aws-sdk/s3-request-presigner": "3.454.0", + "@aws-sdk/util-dynamodb": "^3.454.0", "@fontsource/jetbrains-mono": "^4.5.0", "@radix-ui/colors": "^0.1.8", "@radix-ui/react-accordion": "^0.1.5", diff --git a/packages/create-sst/bin/presets/base/javascript/preset.mjs b/packages/create-sst/bin/presets/base/javascript/preset.mjs index 98a63dda3..786be3d98 100644 --- a/packages/create-sst/bin/presets/base/javascript/preset.mjs +++ b/packages/create-sst/bin/presets/base/javascript/preset.mjs @@ -3,7 +3,7 @@ import { extract, patch, install } from "create-sst"; export default [ extract(), install({ - packages: ["sst@^2", "aws-cdk-lib@2.161.1", "constructs@10.3.0"], + packages: ["sst@^2", "aws-cdk-lib@2.171.1", "constructs@10.3.0"], dev: true, }), ]; diff --git a/packages/create-sst/bin/presets/dropin/astro/preset.mjs b/packages/create-sst/bin/presets/dropin/astro/preset.mjs index 61c7eecd5..b75938615 100644 --- a/packages/create-sst/bin/presets/dropin/astro/preset.mjs +++ b/packages/create-sst/bin/presets/dropin/astro/preset.mjs @@ -5,7 +5,7 @@ export default [ install({ packages: [ "sst@^2", - "aws-cdk-lib@2.161.1", + "aws-cdk-lib@2.171.1", "constructs@10.3.0", "astro-sst", ], diff --git a/packages/create-sst/bin/presets/dropin/container/preset.mjs b/packages/create-sst/bin/presets/dropin/container/preset.mjs index 0135826fa..f26a93385 100644 --- a/packages/create-sst/bin/presets/dropin/container/preset.mjs +++ b/packages/create-sst/bin/presets/dropin/container/preset.mjs @@ -3,7 +3,7 @@ import { patch, append, extract, install } from "create-sst"; export default [ extract(), install({ - packages: ["sst@^2", "aws-cdk-lib@2.161.1", "constructs@10.3.0"], + packages: ["sst@^2", "aws-cdk-lib@2.171.1", "constructs@10.3.0"], dev: true, }), patch({ diff --git a/packages/create-sst/bin/presets/dropin/nextjs/preset.mjs b/packages/create-sst/bin/presets/dropin/nextjs/preset.mjs index 76d767274..188f04c28 100644 --- a/packages/create-sst/bin/presets/dropin/nextjs/preset.mjs +++ b/packages/create-sst/bin/presets/dropin/nextjs/preset.mjs @@ -3,7 +3,7 @@ import { patch, append, extract, install } from "create-sst"; export default [ extract(), install({ - packages: ["sst@^2", "aws-cdk-lib@2.161.1", "constructs@10.3.0"], + packages: ["sst@^2", "aws-cdk-lib@2.171.1", "constructs@10.3.0"], dev: true, }), patch({ diff --git a/packages/create-sst/bin/presets/dropin/remix/preset.mjs b/packages/create-sst/bin/presets/dropin/remix/preset.mjs index 86aaa0317..ef053a6b9 100644 --- a/packages/create-sst/bin/presets/dropin/remix/preset.mjs +++ b/packages/create-sst/bin/presets/dropin/remix/preset.mjs @@ -3,7 +3,7 @@ import { patch, append, extract, install } from "create-sst"; export default [ extract(), install({ - packages: ["sst@^2", "aws-cdk-lib@2.161.1", "constructs@10.3.0"], + packages: ["sst@^2", "aws-cdk-lib@2.171.1", "constructs@10.3.0"], dev: true, }), patch({ diff --git a/packages/create-sst/bin/presets/dropin/solid/preset.mjs b/packages/create-sst/bin/presets/dropin/solid/preset.mjs index f74268edc..dc3147a01 100644 --- a/packages/create-sst/bin/presets/dropin/solid/preset.mjs +++ b/packages/create-sst/bin/presets/dropin/solid/preset.mjs @@ -5,7 +5,7 @@ export default [ install({ packages: [ "sst@^2", - "aws-cdk-lib@2.161.1", + "aws-cdk-lib@2.171.1", "constructs@10.3.0", "solid-start-sst", ], diff --git a/packages/create-sst/bin/presets/dropin/svelte/preset.mjs b/packages/create-sst/bin/presets/dropin/svelte/preset.mjs index e56c918a2..103e0e266 100644 --- a/packages/create-sst/bin/presets/dropin/svelte/preset.mjs +++ b/packages/create-sst/bin/presets/dropin/svelte/preset.mjs @@ -5,7 +5,7 @@ export default [ install({ packages: [ "sst@^2", - "aws-cdk-lib@2.161.1", + "aws-cdk-lib@2.171.1", "constructs@10.3.0", "svelte-kit-sst", ], diff --git a/packages/sst/package.json b/packages/sst/package.json index d4e9b1bba..1d110c110 100644 --- a/packages/sst/package.json +++ b/packages/sst/package.json @@ -35,10 +35,10 @@ }, "homepage": "https://sst.dev", "dependencies": { - "@aws-cdk/aws-lambda-python-alpha": "2.161.1-alpha.0", + "@aws-cdk/aws-lambda-python-alpha": "2.171.1-alpha.0", "@aws-cdk/cloud-assembly-schema": "38.0.1", - "@aws-cdk/cloudformation-diff": "2.161.1", - "@aws-cdk/cx-api": "2.161.1", + "@aws-cdk/cloudformation-diff": "2.171.1", + "@aws-cdk/cx-api": "2.171.1", "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-cloudformation": "^3.454.0", "@aws-sdk/client-ecs": "^3.454.0", @@ -63,11 +63,11 @@ "@smithy/signature-v4": "^2.0.16", "@trpc/server": "9.16.0", "adm-zip": "^0.5.10", - "aws-cdk-lib": "2.161.1", + "aws-cdk-lib": "2.171.1", "aws-iot-device-sdk": "^2.2.13", "aws-sdk": "^2.1501.0", "builtin-modules": "3.2.0", - "cdk-assets": "2.155.6", + "cdk-assets": "^3.0.0-rc.48", "chalk": "^5.2.0", "chokidar": "^3.5.3", "ci-info": "^3.7.0", @@ -95,7 +95,7 @@ "ora": "^6.1.2", "react": "^18.0.0", "remeda": "^1.3.0", - "sst-aws-cdk": "2.161.1-2", + "sst-aws-cdk": "2.171.1-4", "tree-kill": "^1.2.2", "undici": "^5.12.0", "uuid": "^9.0.0", diff --git a/packages/sst/src/cdk/deploy-stack.ts b/packages/sst/src/cdk/deploy-stack.ts index 027c16147..fc23da093 100644 --- a/packages/sst/src/cdk/deploy-stack.ts +++ b/packages/sst/src/cdk/deploy-stack.ts @@ -1,19 +1,27 @@ import * as cxapi from "@aws-cdk/cx-api"; -import type { CloudFormation } from "aws-sdk"; +import type { + CreateChangeSetCommandInput, + CreateStackCommandInput, + DescribeChangeSetCommandOutput, + ExecuteChangeSetCommandInput, + UpdateStackCommandInput, + Tag, +} from "@aws-sdk/client-cloudformation"; import * as uuid from "uuid"; -import { - TemplateBodyParameter, - makeBodyParameter, -} from "sst-aws-cdk/lib/api/util/template-body-parameter.js"; +import type { + SDK, + SdkProvider, + ICloudFormationClient, +} from "sst-aws-cdk/lib/api/aws-auth/index.js"; +import type { EnvironmentResources } from "sst-aws-cdk/lib/api/environment-resources.js"; import { addMetadataAssetsToManifest } from "sst-aws-cdk/lib/assets.js"; -import { Tag } from "sst-aws-cdk/lib/cdk-toolkit.js"; import { debug, print, warning } from "sst-aws-cdk/lib/logging.js"; -import { AssetManifestBuilder } from "sst-aws-cdk/lib/util/asset-manifest-builder.js"; -import { publishAssets } from "sst-aws-cdk/lib/util/asset-publishing.js"; -import { ISDK, SdkProvider } from "sst-aws-cdk/lib/api/aws-auth/index.js"; -import { EnvironmentResources } from "sst-aws-cdk/lib/api/environment-resources.js"; import { CfnEvaluationException } from "sst-aws-cdk/lib/api/evaluate-cloudformation-template.js"; -import { HotswapMode, ICON } from "sst-aws-cdk/lib/api/hotswap/common.js"; +import { + HotswapMode, + HotswapPropertyOverrides, + ICON, +} from "sst-aws-cdk/lib/api/hotswap/common.js"; import { tryHotswapDeployment } from "sst-aws-cdk/lib/api/hotswap-deployments.js"; import { changeSetHasNoChanges, @@ -27,18 +35,56 @@ import { ResourcesToImport, } from "sst-aws-cdk/lib/api/util/cloudformation.js"; import { - // StackActivityMonitor, - StackActivityProgress, + StackActivityMonitor, + type StackActivityProgress, } from "sst-aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.js"; +import { + type TemplateBodyParameter, + makeBodyParameter, +} from "sst-aws-cdk/lib/api/util/template-body-parameter.js"; +import { AssetManifestBuilder } from "sst-aws-cdk/lib/util/asset-manifest-builder.js"; +import { determineAllowCrossAccountAssetPublishing } from "sst-aws-cdk/lib/api/util/checks.js"; +import { publishAssets } from "sst-aws-cdk/lib/util/asset-publishing.js"; +import { StringWithoutPlaceholders } from "sst-aws-cdk/lib/api/util/placeholders.js"; import { blue } from "colorette"; import { callWithRetry } from "./util.js"; -export interface DeployStackResult { +export type DeployStackResult = + | SuccessfulDeployStackResult + | NeedRollbackFirstDeployStackResult + | ReplacementRequiresNoRollbackStackResult; + +/** Successfully deployed a stack */ +export interface SuccessfulDeployStackResult { + readonly type: "did-deploy-stack"; readonly noOp: boolean; readonly outputs: { [name: string]: string }; readonly stackArn: string; } +/** The stack is currently in a failpaused state, and needs to be rolled back before the deployment */ +export interface NeedRollbackFirstDeployStackResult { + readonly type: "failpaused-need-rollback-first"; + readonly reason: "not-norollback" | "replacement"; +} + +/** The upcoming change has a replacement, which requires deploying without --no-rollback */ +export interface ReplacementRequiresNoRollbackStackResult { + readonly type: "replacement-requires-norollback"; +} + +export function assertIsSuccessfulDeployStackResult( + x: DeployStackResult +): asserts x is SuccessfulDeployStackResult { + if (x.type !== "did-deploy-stack") { + throw new Error( + `Unexpected deployStack result. This should not happen: ${JSON.stringify( + x + )}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose.` + ); + } +} + export interface DeployStackOptions { /** * The stack to be deployed @@ -64,19 +110,17 @@ export interface DeployStackOptions { * Should have been initialized with the correct role with which * stack operations should be performed. */ - readonly sdk: ISDK; + readonly sdk: SDK; /** * SDK provider (seeded with default credentials) * - * Will exclusively be used to assume publishing credentials (which must - * start out from current credentials regardless of whether we've assumed an - * action role to touch the stack or not). - * - * Used for the following purposes: - * - * - Publish legacy assets. - * - Upload large CloudFormation templates to the staging bucket. + * Will be used to: + * - Publish assets, either legacy assets or large CFN templates + * that aren't themselves assets from a manifest. (Needs an SDK + * Provider because the file publishing role is declared as part + * of the asset). + * - Hotswap */ readonly sdkProvider: SdkProvider; @@ -88,9 +132,13 @@ export interface DeployStackOptions { /** * Role to pass to CloudFormation to execute the change set * - * @default - Role specified on stack, otherwise current + * To obtain a `StringWithoutPlaceholders`, run a regular + * string though `TargetEnvironment.replacePlaceholders`. + * + * @default - No execution role; CloudFormation either uses the role currently associated with + * the stack, or otherwise uses current AWS credentials. */ - readonly roleArn?: string; + readonly roleArn?: StringWithoutPlaceholders; /** * Notification ARNs to pass to CloudFormation to notify when the change set has completed @@ -191,6 +239,11 @@ export interface DeployStackOptions { */ readonly hotswap?: HotswapMode; + /** + * Extra properties that configure hotswap behavior + */ + readonly hotswapPropertyOverrides?: HotswapPropertyOverrides; + /** * The extra string to append to the User-Agent header when performing AWS SDK calls. * @@ -262,7 +315,7 @@ export async function deployStack( debug( `Found existing stack ${deployName} that had previously failed creation. Deleting it before attempting to re-create it.` ); - await cfn.deleteStack({ StackName: deployName }).promise(); + await cfn.deleteStack({ StackName: deployName }); const deletedStack = await waitForStackDelete(cfn, deployName); if (deletedStack && deletedStack.stackStatus.name !== "DELETE_COMPLETE") { throw new Error( @@ -311,6 +364,7 @@ export async function deployStack( if (options.hotswap) { } return { + type: "did-deploy-stack", noOp: true, outputs: cloudFormationStack.outputs, stackArn: cloudFormationStack.stackId, @@ -324,19 +378,30 @@ export async function deployStack( options.resolvedEnvironment, legacyAssets, options.envResources, - options.sdk, options.overrideTemplate ); + let bootstrapStackName: string | undefined; + try { + bootstrapStackName = (await options.envResources.lookupToolkit()).stackName; + } catch (e) { + debug(`Could not determine the bootstrap stack name: ${e}`); + } await publishAssets( legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv, { parallel: options.assetParallelism, + allowCrossAccount: await determineAllowCrossAccountAssetPublishing( + options.sdk, + bootstrapStackName + ), } ); const hotswapMode = options.hotswap; + const hotswapPropertyOverrides = + options.hotswapPropertyOverrides ?? new HotswapPropertyOverrides(); if (hotswapMode && hotswapMode !== HotswapMode.FULL_DEPLOYMENT) { // attempt to short-circuit the deployment if possible try { @@ -345,7 +410,8 @@ export async function deployStack( stackParams.values, cloudFormationStack, stackArtifact, - hotswapMode + hotswapMode, + hotswapPropertyOverrides ); if (hotswapDeploymentResult) { return hotswapDeploymentResult; @@ -368,6 +434,7 @@ export async function deployStack( options.sdk.appendCustomUserAgent("cdk-hotswap/fallback"); } else { return { + type: "did-deploy-stack", noOp: true, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs, @@ -386,18 +453,18 @@ export async function deployStack( return fullDeployment.performDeployment(); } -type CommonPrepareOptions = keyof CloudFormation.CreateStackInput & - keyof CloudFormation.UpdateStackInput & - keyof CloudFormation.CreateChangeSetInput; -type CommonExecuteOptions = keyof CloudFormation.CreateStackInput & - keyof CloudFormation.UpdateStackInput & - keyof CloudFormation.ExecuteChangeSetInput; +type CommonPrepareOptions = keyof CreateStackCommandInput & + keyof UpdateStackCommandInput & + keyof CreateChangeSetCommandInput; +type CommonExecuteOptions = keyof CreateStackCommandInput & + keyof UpdateStackCommandInput & + keyof ExecuteChangeSetCommandInput; /** * This class shares state and functionality between the different full deployment modes */ class FullCloudFormationDeployment { - private readonly cfn: ReturnType; + private readonly cfn: ICloudFormationClient; private readonly stackName: string; private readonly update: boolean; private readonly verb: string; @@ -457,12 +524,10 @@ class FullCloudFormationDeployment { debug("No changes are to be performed on %s.", this.stackName); if (execute) { debug("Deleting empty change set %s", changeSetDescription.ChangeSetId); - await this.cfn - .deleteChangeSet({ - StackName: this.stackName, - ChangeSetName: changeSetName, - }) - .promise(); + await this.cfn.deleteChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + }); } if (this.options.force) { @@ -478,6 +543,7 @@ class FullCloudFormationDeployment { } return { + type: "did-deploy-stack", noOp: true, outputs: this.cloudFormationStack.outputs, stackArn: changeSetDescription.StackId!, @@ -490,12 +556,31 @@ class FullCloudFormationDeployment { changeSetDescription.ChangeSetId ); return { + type: "did-deploy-stack", noOp: false, outputs: this.cloudFormationStack.outputs, stackArn: changeSetDescription.StackId!, }; } + // If there are replacements in the changeset, check the rollback flag and stack status + const replacement = hasReplacement(changeSetDescription); + const isPausedFailState = + this.cloudFormationStack.stackStatus.isRollbackable; + const rollback = this.options.rollback ?? true; + if (isPausedFailState && replacement) { + return { type: "failpaused-need-rollback-first", reason: "replacement" }; + } + if (isPausedFailState && !rollback) { + return { + type: "failpaused-need-rollback-first", + reason: "not-norollback", + }; + } + if (!rollback && replacement) { + return { type: "replacement-requires-norollback" }; + } + return this.executeChangeSet(changeSetDescription); } @@ -505,21 +590,19 @@ class FullCloudFormationDeployment { debug( `Attempting to create ChangeSet with name ${changeSetName} to ${this.verb} stack ${this.stackName}` ); - const changeSet = await this.cfn - .createChangeSet({ - StackName: this.stackName, - ChangeSetName: changeSetName, - ChangeSetType: this.options.resourcesToImport - ? "IMPORT" - : this.update - ? "UPDATE" - : "CREATE", - ResourcesToImport: this.options.resourcesToImport, - Description: `CDK Changeset for execution ${this.uuid}`, - ClientToken: `create${this.uuid}`, - ...this.commonPrepareOptions(), - }) - .promise(); + const changeSet = await this.cfn.createChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + ChangeSetType: this.options.resourcesToImport + ? "IMPORT" + : this.update + ? "UPDATE" + : "CREATE", + ResourcesToImport: this.options.resourcesToImport, + Description: `CDK Changeset for execution ${this.uuid}`, + ClientToken: `create${this.uuid}`, + ...this.commonPrepareOptions(), + }); debug( "Initiated creation of changeset: %s; waiting for it to finish creating...", @@ -532,22 +615,20 @@ class FullCloudFormationDeployment { } private async executeChangeSet( - changeSet: CloudFormation.DescribeChangeSetOutput - ): Promise { + changeSet: DescribeChangeSetCommandOutput + ): Promise { debug( "Initiating execution of changeset %s on stack %s", changeSet.ChangeSetId, this.stackName ); - await this.cfn - .executeChangeSet({ - StackName: this.stackName, - ChangeSetName: changeSet.ChangeSetName!, - ClientRequestToken: `exec${this.uuid}`, - ...this.commonExecuteOptions(), - }) - .promise(); + await this.cfn.executeChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSet.ChangeSetName!, + ClientRequestToken: `exec${this.uuid}`, + ...this.commonExecuteOptions(), + }); debug( "Execution of changeset %s on stack %s has started; waiting for the update to complete...", @@ -568,12 +649,10 @@ class FullCloudFormationDeployment { debug( `Removing existing change set with name ${changeSetName} if it exists` ); - await this.cfn - .deleteChangeSet({ - StackName: this.stackName, - ChangeSetName: changeSetName, - }) - .promise(); + await this.cfn.deleteChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + }); } } @@ -590,12 +669,10 @@ class FullCloudFormationDeployment { terminationProtection, this.stackName ); - await this.cfn - .updateTerminationProtection({ - StackName: this.stackName, - EnableTerminationProtection: terminationProtection, - }) - .promise(); + await this.cfn.updateTerminationProtection({ + StackName: this.stackName, + EnableTerminationProtection: terminationProtection, + }); debug( "Termination protection updated to %s for stack %s", terminationProtection, @@ -604,25 +681,26 @@ class FullCloudFormationDeployment { } } - private async directDeployment(): Promise { + private async directDeployment(): Promise< + SuccessfulDeployStackResult | undefined + > { const startTime = new Date(); if (this.update) { await this.updateTerminationProtection(); try { - await this.cfn - .updateStack({ - StackName: this.stackName, - ClientRequestToken: `update${this.uuid}`, - ...this.commonPrepareOptions(), - ...this.commonExecuteOptions(), - }) - .promise(); + await this.cfn.updateStack({ + StackName: this.stackName, + ClientRequestToken: `update${this.uuid}`, + ...this.commonPrepareOptions(), + ...this.commonExecuteOptions(), + }); } catch (err: any) { if (err.message === "No updates are to be performed.") { debug("No updates are to be performed for stack %s", this.stackName); return { + type: "did-deploy-stack", noOp: true, outputs: this.cloudFormationStack.outputs, stackArn: this.cloudFormationStack.stackId, @@ -638,17 +716,15 @@ class FullCloudFormationDeployment { const terminationProtection = this.stackArtifact.terminationProtection ?? false; - await this.cfn - .createStack({ - StackName: this.stackName, - ClientRequestToken: `create${this.uuid}`, - ...(terminationProtection - ? { EnableTerminationProtection: true } - : undefined), - ...this.commonPrepareOptions(), - ...this.commonExecuteOptions(), - }) - .promise(); + await this.cfn.createStack({ + StackName: this.stackName, + ClientRequestToken: `create${this.uuid}`, + ...(terminationProtection + ? { EnableTerminationProtection: true } + : undefined), + ...this.commonPrepareOptions(), + ...this.commonExecuteOptions(), + }); if (this.options.noMonitor) return; return this.monitorDeployment(startTime, undefined); @@ -658,20 +734,20 @@ class FullCloudFormationDeployment { private async monitorDeployment( startTime: Date, expectedChanges: number | undefined - ): Promise { - // const monitor = this.options.quiet - // ? undefined - // : StackActivityMonitor.withDefaultPrinter( - // this.cfn, - // this.stackName, - // this.stackArtifact, - // { - // resourcesTotal: expectedChanges, - // progress: this.options.progress, - // changeSetCreationTime: startTime, - // ci: this.options.ci, - // } - // ).start(); + ): Promise { + // const monitor = this.options.quiet + // ? undefined + // : StackActivityMonitor.withDefaultPrinter( + // this.cfn, + // this.stackName, + // this.stackArtifact, + // { + // resourcesTotal: expectedChanges, + // progress: this.options.progress, + // changeSetCreationTime: startTime, + // ci: this.options.ci, + // } + // ).start(); let finalState = this.cloudFormationStack; try { @@ -691,6 +767,7 @@ class FullCloudFormationDeployment { } debug("Stack %s has completed updating", this.stackName); return { + type: "did-deploy-stack", noOp: false, outputs: finalState.outputs, stackArn: finalState.stackId, @@ -701,7 +778,7 @@ class FullCloudFormationDeployment { * Return the options that are shared between CreateStack, UpdateStack and CreateChangeSet */ private commonPrepareOptions(): Partial< - Pick + Pick > { return { Capabilities: [ @@ -725,7 +802,7 @@ class FullCloudFormationDeployment { * deployed everywhere yet. */ private commonExecuteOptions(): Partial< - Pick + Pick > { const shouldDisableRollback = this.options.rollback === false; @@ -742,7 +819,7 @@ export interface DestroyStackOptions { */ stack: cxapi.CloudFormationStackArtifact; - sdk: ISDK; + sdk: SDK; roleArn?: string; deployName?: string; quiet?: boolean; @@ -761,14 +838,12 @@ export async function destroyStack(options: DestroyStackOptions) { const monitor = options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, { - ci: options.ci, - }).start(); + ci: options.ci, + }).start(); */ try { - await cfn - .deleteStack({ StackName: deployName, RoleARN: options.roleArn }) - .promise(); + await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }); const destroyedStack = await waitForStackDelete(cfn, deployName); if ( destroyedStack && @@ -919,3 +994,15 @@ function arrayEquals(a: any[], b: any[]): boolean { a.every((item) => b.includes(item)) && b.every((item) => a.includes(item)) ); } + +function hasReplacement(cs: DescribeChangeSetCommandOutput) { + return (cs.Changes ?? []).some((c) => { + // @ts-ignore + const a = c.ResourceChange?.PolicyAction; + return ( + a === "ReplaceAndDelete" || + a === "ReplaceAndRetain" || + a === "ReplaceAndSnapshot" + ); + }); +} diff --git a/packages/sst/src/cdk/deployments-wrapper.ts b/packages/sst/src/cdk/deployments-wrapper.ts index 5c9077706..5c731e5b3 100644 --- a/packages/sst/src/cdk/deployments-wrapper.ts +++ b/packages/sst/src/cdk/deployments-wrapper.ts @@ -7,8 +7,7 @@ import { TemplateParameters, waitForStackDelete, } from "sst-aws-cdk/lib/api/util/cloudformation.js"; -import { Mode } from "sst-aws-cdk/lib/api/aws-auth/credentials.js"; -import { ISDK } from "sst-aws-cdk/lib/api/aws-auth/sdk.js"; +import { SDK } from "sst-aws-cdk/lib/api/aws-auth/sdk.js"; import { EnvironmentResources } from "sst-aws-cdk/lib/api/environment-resources.js"; import { addMetadataAssetsToManifest } from "sst-aws-cdk/lib/assets.js"; import { publishAssets } from "sst-aws-cdk/lib/util/asset-publishing.js"; @@ -21,6 +20,7 @@ import { } from "./deployments.js"; import { DeployStackOptions } from "./deploy-stack.js"; import { lazy } from "../util/lazy.js"; +import { StringWithoutPlaceholders } from "sst-aws-cdk/lib/api/util/placeholders.js"; export async function publishDeployAssets( sdkProvider: SdkProvider, @@ -31,29 +31,17 @@ export async function publishDeployAssets( envResources, stackSdk, resolvedEnvironment, - cloudFormationRoleArn, + executionRoleArn, } = await useDeployment().get(sdkProvider, options); - // TODO - // old - //await deployment.publishStackAssets(options.stack, toolkitInfo, { - // buildAssets: options.buildAssets ?? true, - // publishOptions: { - // quiet: options.quiet, - // parallel: options.assetParallelism, - // }, - //}); - - // new const assetArtifacts = options.stack.dependencies.filter( cxapi.AssetManifestArtifact.isAssetManifestArtifact ); for (const asset of assetArtifacts) { const manifest = AssetManifest.fromFile(asset.file); - //await buildAssets(manifest, sdkProvider, resolvedEnvironment, { - //}); await publishAssets(manifest, sdkProvider, resolvedEnvironment, { buildAssets: true, + allowCrossAccount: true, quiet: options.quiet, parallel: options.assetParallelism, }); @@ -68,7 +56,7 @@ export async function publishDeployAssets( quiet: options.quiet, sdk: stackSdk, sdkProvider, - roleArn: cloudFormationRoleArn, + roleArn: executionRoleArn, reuseAssets: options.reuseAssets, envResources, tags: options.tags, @@ -93,9 +81,9 @@ const useDeployment = lazy(() => { { deployment: Deployments; envResources: EnvironmentResources; - stackSdk: ISDK; + stackSdk: SDK; resolvedEnvironment: Environment; - cloudFormationRoleArn?: string; + executionRoleArn?: StringWithoutPlaceholders; } >(); return { @@ -103,15 +91,12 @@ const useDeployment = lazy(() => { const region = options.stack.environment.region; if (!state.has(region)) { const deployment = new Deployments({ sdkProvider }); - const { - stackSdk, - resolvedEnvironment, - cloudFormationRoleArn, - envResources, - } = await deployment.prepareSdkFor( - options.stack, - options.roleArn, - Mode.ForWriting + const env = await deployment.envs.accessStackForMutableStackOperations( + options.stack + ); + const envResources = env.resources; + const executionRoleArn = await env.replacePlaceholders( + options.roleArn ?? options.stack.cloudFormationExecutionRoleArn ); // Do a verification of the bootstrap stack version @@ -125,9 +110,9 @@ const useDeployment = lazy(() => { state.set(region, { deployment, envResources, - stackSdk, - resolvedEnvironment, - cloudFormationRoleArn, + stackSdk: env.sdk, + resolvedEnvironment: env.resolvedEnvironment, + executionRoleArn, }); } return state.get(region)!; @@ -150,7 +135,7 @@ async function deployStack(options: DeployStackOptions): Promise { debug( `Found existing stack ${deployName} that had previously failed creation. Deleting it before attempting to re-create it.` ); - await cfn.deleteStack({ StackName: deployName }).promise(); + await cfn.deleteStack({ StackName: deployName }); const deletedStack = await waitForStackDelete(cfn, deployName); if (deletedStack && deletedStack.stackStatus.name !== "DELETE_COMPLETE") { throw new Error( @@ -191,7 +176,6 @@ async function deployStack(options: DeployStackOptions): Promise { options.resolvedEnvironment, legacyAssets, options.envResources, - options.sdk, options.overrideTemplate ); await publishAssets( @@ -200,6 +184,7 @@ async function deployStack(options: DeployStackOptions): Promise { stackEnv, { parallel: options.assetParallelism, + allowCrossAccount: true, } ); diff --git a/packages/sst/src/cdk/deployments.ts b/packages/sst/src/cdk/deployments.ts index 69f917ee1..f45286f17 100644 --- a/packages/sst/src/cdk/deployments.ts +++ b/packages/sst/src/cdk/deployments.ts @@ -1,82 +1,59 @@ // Copied from https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/cloudformation-deployments.ts +import { randomUUID } from "crypto"; import * as cxapi from "@aws-cdk/cx-api"; import * as cdk_assets from "cdk-assets"; import { AssetManifest, IManifestEntry } from "cdk-assets"; -import { Tag } from "sst-aws-cdk/lib/cdk-toolkit.js"; -import { debug, warning, error } from "sst-aws-cdk/lib/logging.js"; -import { - buildAssets, - publishAssets, - BuildAssetsOptions, - PublishAssetsOptions, - PublishingAws, - EVENT_TO_LOGGER, -} from "sst-aws-cdk/lib/util/asset-publishing.js"; -import { Mode } from "sst-aws-cdk/lib/api/aws-auth/credentials.js"; -import { ISDK } from "sst-aws-cdk/lib/api/aws-auth/sdk.js"; -import { - CredentialsOptions, - SdkForEnvironment, - SdkProvider, -} from "sst-aws-cdk/lib/api/aws-auth/sdk-provider.js"; +import type { Tag } from "sst-aws-cdk/lib/cdk-toolkit.js"; +import { debug, warning } from "sst-aws-cdk/lib/logging.js"; +import { EnvironmentAccess } from "sst-aws-cdk/lib/api/environment-access.js"; +import type { SdkProvider } from "sst-aws-cdk/lib/api/aws-auth/sdk-provider.js"; import { + type DeploymentMethod, deployStack, DeployStackResult, destroyStack, - DeploymentMethod, } from "./deploy-stack.js"; +import { type EnvironmentResources } from "sst-aws-cdk/lib/api/environment-resources.js"; +import { EnvironmentResourcesRegistry } from "sst-aws-cdk/lib/api/environment-resources.js"; import { - EnvironmentResources, - EnvironmentResourcesRegistry, -} from "sst-aws-cdk/lib/api/environment-resources.js"; + HotswapMode, + HotswapPropertyOverrides, +} from "sst-aws-cdk/lib/api/hotswap/common.js"; import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate, - RootTemplateWithNestedStacks, + type RootTemplateWithNestedStacks, } from "sst-aws-cdk/lib/api/nested-stack-helpers.js"; +import { DEFAULT_TOOLKIT_STACK_NAME } from "sst-aws-cdk/lib/api/toolkit-info.js"; +import { determineAllowCrossAccountAssetPublishing } from "sst-aws-cdk/lib/api/util/checks.js"; import { CloudFormationStack, - Template, + type ResourceIdentifierSummaries, ResourcesToImport, - ResourceIdentifierSummaries, + stabilizeStack, + Template, + uploadStackTemplateAssets, } from "sst-aws-cdk/lib/api/util/cloudformation.js"; -import { StackActivityProgress } from "sst-aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.js"; -import { replaceEnvPlaceholders } from "sst-aws-cdk/lib/api/util/placeholders.js"; +import { + StackActivityMonitor, + StackActivityProgress, +} from "sst-aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.js"; +import { StackEventPoller } from "sst-aws-cdk/lib/api/util/cloudformation/stack-event-poller.js"; +import { RollbackChoice } from "sst-aws-cdk/lib/api/util/cloudformation/stack-status.js"; import { makeBodyParameter } from "sst-aws-cdk/lib/api/util/template-body-parameter.js"; import { AssetManifestBuilder } from "sst-aws-cdk/lib/util/asset-manifest-builder.js"; +import { + buildAssets, + type BuildAssetsOptions, + EVENT_TO_LOGGER, + publishAssets, + type PublishAssetsOptions, + PublishingAws, +} from "sst-aws-cdk/lib/util/asset-publishing.js"; import { callWithRetry } from "./util.js"; -import { HotswapMode } from "sst-aws-cdk/lib/api/hotswap/common.js"; -/** - * SDK obtained by assuming the lookup role - * for a given environment - */ -export interface PreparedSdkWithLookupRoleForEnvironment { - /** - * The SDK for the given environment - */ - readonly sdk: ISDK; - - /** - * The resolved environment for the stack - * (no more 'unknown-account/unknown-region') - */ - readonly resolvedEnvironment: cxapi.Environment; - - /** - * Whether or not the assume role was successful. - * If the assume role was not successful (false) - * then that means that the 'sdk' returned contains - * the default credentials (not the assume role credentials) - */ - readonly didAssumeRole: boolean; - - /** - * An object for accessing the bootstrap resources in this environment - */ - readonly envResources: EnvironmentResources; -} +const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23; export interface DeployStackOptions { /** @@ -206,6 +183,11 @@ export interface DeployStackOptions { */ readonly hotswap?: HotswapMode; + /** + * Properties that configure hotswap behavior + */ + readonly hotswapPropertyOverrides?: HotswapPropertyOverrides; + /** * The extra string to append to the User-Agent header when performing AWS SDK calls. * @@ -240,19 +222,83 @@ export interface DeployStackOptions { ignoreNoStacks?: boolean; } -interface AssetOptions { +export interface RollbackStackOptions { /** - * Stack with assets to build. + * Stack to roll back */ readonly stack: cxapi.CloudFormationStackArtifact; /** - * Name of the toolkit stack, if not the default name. + * Execution role for the deployment (pass through to CloudFormation) + * + * @default - Current role + */ + readonly roleArn?: string; + + /** + * Don't show stack deployment events, just wait + * + * @default false + */ + readonly quiet?: boolean; + + /** + * Whether we are on a CI system + * + * @default false + */ + readonly ci?: boolean; + + /** + * Name of the toolkit stack, if not the default name * * @default 'CDKToolkit' */ readonly toolkitStackName?: string; + /** + * Whether to force a rollback or not + * + * Forcing a rollback will orphan all undeletable resources. + * + * @default false + */ + readonly force?: boolean; + + /** + * Orphan the resources with the given logical IDs + * + * @default - No orphaning + */ + readonly orphanLogicalIds?: string[]; + + /** + * Display mode for stack deployment progress. + * + * @default - StackActivityProgress.Bar - stack events will be displayed for + * the resource currently being deployed. + */ + readonly progress?: StackActivityProgress; + + /** + * Whether to validate the version of the bootstrap stack permissions + * + * @default true + */ + readonly validateBootstrapStackVersion?: boolean; +} + +export interface RollbackStackResult { + readonly notInRollbackableState?: boolean; + readonly success?: boolean; +} + +interface AssetOptions { + /** + * Stack with assets to build. + */ + readonly stack: cxapi.CloudFormationStackArtifact; + /** * Execution role for the building. * @@ -307,51 +353,46 @@ export interface DeploymentsProps { } /** - * SDK obtained by assuming the deploy role - * for a given environment + * Scope for a single set of deployments from a set of Cloud Assembly Artifacts + * + * Manages lookup of SDKs, Bootstrap stacks, etc. */ -export interface PreparedSdkForEnvironment { - /** - * The SDK for the given environment - */ - readonly stackSdk: ISDK; +export class Deployments { + public readonly envs: EnvironmentAccess; /** - * The resolved environment for the stack - * (no more 'unknown-account/unknown-region') - */ - readonly resolvedEnvironment: cxapi.Environment; - /** - * The Execution Role that should be passed to CloudFormation. + * SDK provider for asset publishing (do not use for anything else). * - * @default - no execution role is used + * This SDK provider is only allowed to be used for that purpose, nothing else. + * + * It's not a different object, but the field name should imply that this + * object should not be used directly, except to pass to asset handling routines. */ - readonly cloudFormationRoleArn?: string; + private readonly assetSdkProvider: SdkProvider; /** - * Access class for environmental resources to help the deployment + * SDK provider for passing to deployStack + * + * This SDK provider is only allowed to be used for that purpose, nothing else. + * + * It's not a different object, but the field name should imply that this + * object should not be used directly, except to pass to `deployStack`. */ - readonly envResources: EnvironmentResources; -} + private readonly deployStackSdkProvider: SdkProvider; -/** - * Scope for a single set of deployments from a set of Cloud Assembly Artifacts - * - * Manages lookup of SDKs, Bootstrap stacks, etc. - */ -export class Deployments { - private readonly sdkProvider: SdkProvider; - private readonly sdkCache = new Map(); private readonly publisherCache = new Map< AssetManifest, cdk_assets.AssetPublishing >(); - private readonly environmentResources: EnvironmentResourcesRegistry; + + private _allowCrossAccountAssetPublishing: boolean | undefined; constructor(private readonly props: DeploymentsProps) { - this.sdkProvider = props.sdkProvider; - this.environmentResources = new EnvironmentResourcesRegistry( - props.toolkitStackName + this.assetSdkProvider = props.sdkProvider; + this.deployStackSdkProvider = props.sdkProvider; + this.envs = new EnvironmentAccess( + props.sdkProvider, + props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME ); } @@ -361,18 +402,19 @@ export class Deployments { public async resolveEnvironment( stack: cxapi.CloudFormationStackArtifact ): Promise { - return this.sdkProvider.resolveEnvironment(stack.environment); + return this.envs.resolveStackEnvironment(stack); } public async readCurrentTemplateWithNestedStacks( rootStackArtifact: cxapi.CloudFormationStackArtifact, retrieveProcessedTemplate: boolean = false ): Promise { - const sdk = (await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact)) - .stackSdk; + const env = await this.envs.accessStackForLookupBestEffort( + rootStackArtifact + ); return loadCurrentTemplateWithNestedStacks( rootStackArtifact, - sdk, + env.sdk, retrieveProcessedTemplate ); } @@ -381,9 +423,8 @@ export class Deployments { stackArtifact: cxapi.CloudFormationStackArtifact ): Promise