From 621d725be1e6accfdd3be3c7c505ef09c727a914 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 3 Apr 2025 12:00:18 +0200 Subject: [PATCH 1/7] feat(toolkit): add a return type for `toolkit.deploy()` `toolkit.deploy()` now returns information about the stacks it deployed. --- .../lib/cloud-assembly/schema.ts | 7 +- .../schema/cloud-assembly.schema.json | 2 +- .../cloud-assembly-schema/schema/version.json | 4 +- .../@aws-cdk/toolkit-lib/lib/toolkit/index.ts | 1 + .../toolkit-lib/lib/toolkit/toolkit.ts | 24 +- .../@aws-cdk/toolkit-lib/lib/toolkit/types.ts | 65 ++++ .../test/_fixtures/nested-assembly/app.ts | 7 + .../test/_fixtures/nested-assembly/cdk.json | 3 + .../StageStack130339B27.assets.json | 19 ++ .../StageStack130339B27.template.json | 314 ++++++++++++++++++ .../cdk.out/assembly-Stage/cdk.out | 1 + .../cdk.out/assembly-Stage/manifest.json | 66 ++++ .../_fixtures/nested-assembly/cdk.out/cdk.out | 1 + .../nested-assembly/cdk.out/manifest.json | 18 + .../nested-assembly/cdk.out/tree.json | 1 + .../toolkit-lib/test/actions/deploy.test.ts | 82 ++++- 16 files changed, 607 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts index 4ac55eccf..e593328e9 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts @@ -100,7 +100,12 @@ export interface ArtifactManifest { readonly properties?: ArtifactProperties; /** - * A string that represents this artifact. Should only be used in user interfaces. + * A string that can be shown to a user to uniquely identify this artifact inside a cloud assembly tree + * + * Is used by the CLI to present a list of stacks to the user in a way that + * makes sense to them. Even though the property name "display name" doesn't + * imply it, this field is used to select stacks as well, so all stacks should + * have a unique display name. * * @default - no display name */ diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 06d442784..855b0ba0b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -83,7 +83,7 @@ ] }, "displayName": { - "description": "A string that represents this artifact. Should only be used in user interfaces. (Default - no display name)", + "description": "A string that can be shown to a user to uniquely identify this artifact inside a cloud assembly tree\n\nIs used by the CLI to present a list of stacks to the user in a way that\nmakes sense to them. Even though the property name \"display name\" doesn't\nimply it, this field is used to select stacks as well, so all stacks should\nhave a unique display name. (Default - no display name)", "type": "string" } }, diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 8e791e665..d173fd1d1 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,4 @@ { - "schemaHash": "78936b0f9299bbe47497dd77f8065d71e65d8d739a0413ad7698ad03b22ef83e", + "schemaHash": "96958a4c40e0a00e850f0c14dd6e9c0fc8db0b075f1f155cea41ab198c0514be", "revision": 43 -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts index a69665dca..c450e67f2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts @@ -1,3 +1,4 @@ export * from './toolkit'; export * from './non-interactive-io-host'; +export * from './types'; export * from '../api/shared-public'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 762dda640..4aa0d6118 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -7,6 +7,7 @@ import * as uuid from 'uuid'; import { NonInteractiveIoHost } from './non-interactive-io-host'; import type { ToolkitServices } from './private'; import { assemblyFromSource } from './private'; +import type { DeployResult } from './types'; import type { BootstrapEnvironments, BootstrapOptions, BootstrapResult, EnvironmentBootstrapResult } from '../actions/bootstrap'; import { BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions } from '../actions/deploy'; @@ -413,7 +414,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { * * Deploys the selected stacks into an AWS account */ - public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { + public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'deploy'); const assembly = await assemblyFromSource(ioHelper, cx); return this._deploy(assembly, 'deploy', options); @@ -422,7 +423,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { /** * Helper to allow deploy being called as part of the watch action. */ - private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: ExtendedDeployOptions = {}) { + private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: ExtendedDeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, action); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); @@ -430,9 +431,13 @@ export class Toolkit extends CloudAssemblySourceBuilder { await this.validateStacksMetadata(stackCollection, ioHelper); const synthDuration = await synthSpan.end(); + const ret: DeployResult = { + stacks: [], + }; + if (stackCollection.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('This app contains no stacks')); - return; + return ret; } const deployments = await this.deploymentsForAction('deploy'); @@ -655,6 +660,17 @@ export class Toolkit extends CloudAssemblySourceBuilder { await ioHelper.notify(IO.CDK_TOOLKIT_I5901.msg(buffer.join('\n'))); } await ioHelper.notify(IO.CDK_TOOLKIT_I5901.msg(`Stack ARN:\n${deployResult.stackArn}`)); + + ret.stacks.push({ + stackName: stack.stackName, + environment: { + account: stack.environment.account, + region: stack.environment.region, + }, + stackArn: deployResult.stackArn, + outputs: deployResult.outputs, + hierarchicalId: stack.hierarchicalId, + }); } catch (e: any) { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " @@ -715,6 +731,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { buildAsset, publishAsset, }); + + return ret; } /** diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts new file mode 100644 index 000000000..7fa2e4f1f --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -0,0 +1,65 @@ +/** + * Result interface for toolkit.deploy operation + */ +export interface DeployResult { + /** + * Map of deployed stacks by artifact ID. + */ + readonly stacks: DeployedStack[]; +} + +/** + * Information about a deployed stack + */ +export interface DeployedStack { + /** + * The name of the deployed stack + * + * A stack name is unique inside its environment, but not unique globally. + */ + readonly stackName: string; + + /** + * The environment where the stack was deployed + * + * This environment is always concrete, because even though the CDK app's + * stack may be region-agnostic, in order to be deployed it will have to have + * been specialized. + */ + readonly environment: Environment; + + /** + * Hierarchical identifier + * + * This uniquely identifies the stack inside the CDK app. + * + * In practice this will be the stack's construct path, but unfortunately the + * Cloud Assembly contract doesn't require or guarantee that. + */ + readonly hierarchicalId: string; + + /** + * The ARN of the deployed stack + */ + readonly stackArn: string; + + /** + * The outputs of the deployed CloudFormation stack + */ + readonly outputs: { [key: string]: string }; +} + +/** + * An environment, which is an (account, region) pair + */ +export interface Environment { + /** + * The account number + */ + readonly account: string; + + /** + * The region number + */ + readonly region: string; +} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts new file mode 100644 index 000000000..cc7b2e03c --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts @@ -0,0 +1,7 @@ +import * as cdk from 'aws-cdk-lib/core'; + +const app = new cdk.App({ autoSynth: false }); +const stage = new cdk.Stage(app, 'Stage'); +new cdk.Stack(stage, 'Stack1'); + +app.synth(); diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json new file mode 100644 index 000000000..b49bb19e0 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "$(npx -c 'which tsx') app.ts" +} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json new file mode 100644 index 000000000..4028f38b9 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json @@ -0,0 +1,19 @@ +{ + "version": "40.0.0", + "files": { + "7f4fb64a3afca08edbdbaa369e00317cb253697278406a83c78e849e89045f9d": { + "source": { + "path": "StageStack130339B27.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "7f4fb64a3afca08edbdbaa369e00317cb253697278406a83c78e849e89045f9d.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json new file mode 100644 index 000000000..a7668a10b --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json @@ -0,0 +1,314 @@ +{ + "Resources": { + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/zPSM7Qw1TNQTCwv1k1OydbNyUzSCy5JTM7WyctPSdXLKtYvMzLSMzQEKskqzszULSrNK8nMTdULgtAAJ5JKTz8AAAA=" + }, + "Metadata": { + "aws:cdk:path": "Stage/Stack1/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Conditions": { + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-3" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-4" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-2" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "il-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + } + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out new file mode 100644 index 000000000..1e02a2deb --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out @@ -0,0 +1 @@ +{"version":"40.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json new file mode 100644 index 000000000..52119eb46 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json @@ -0,0 +1,66 @@ +{ + "version": "40.0.0", + "artifacts": { + "StageStack130339B27.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "StageStack130339B27.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "StageStack130339B27": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "StageStack130339B27.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/7f4fb64a3afca08edbdbaa369e00317cb253697278406a83c78e849e89045f9d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "StageStack130339B27.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + }, + "stackName": "Stage-Stack1" + }, + "dependencies": [ + "StageStack130339B27.assets" + ], + "metadata": { + "/Stage/Stack1/CDKMetadata/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadata" + } + ], + "/Stage/Stack1/CDKMetadata/Condition": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadataAvailable" + } + ], + "/Stage/Stack1/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Stage/Stack1/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "Stage/Stack1" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out new file mode 100644 index 000000000..1e02a2deb --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out @@ -0,0 +1 @@ +{"version":"40.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json new file mode 100644 index 000000000..53a5301de --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json @@ -0,0 +1,18 @@ +{ + "version": "40.0.0", + "artifacts": { + "assembly-Stage": { + "type": "cdk:cloud-assembly", + "properties": { + "directoryName": "assembly-Stage", + "displayName": "Stage" + } + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json new file mode 100644 index 000000000..00871b5fa --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","children":{"Stage":{"id":"Stage","path":"Stage","children":{"Stack1":{"id":"Stack1","path":"Stage/Stack1","children":{"CDKMetadata":{"id":"CDKMetadata","path":"Stage/Stack1/CDKMetadata","children":{"Default":{"id":"Default","path":"Stage/Stack1/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.185.0"}},"Condition":{"id":"Condition","path":"Stage/Stack1/CDKMetadata/Condition","constructInfo":{"fqn":"aws-cdk-lib.CfnCondition","version":"2.185.0"}}},"constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"BootstrapVersion":{"id":"BootstrapVersion","path":"Stage/Stack1/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.185.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"Stage/Stack1/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.185.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.185.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.Stage","version":"2.185.0"}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}},"constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.185.0"}}} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts index 9c7feed9f..313e79f53 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts @@ -2,7 +2,7 @@ import { RequireApproval, StackParameters } from '../../lib'; import * as awsCdkApi from '../../lib/api/aws-cdk'; import type { DeployStackOptions, DeployStackResult } from '../../lib/api/aws-cdk'; import { Toolkit } from '../../lib/toolkit'; -import { builderFixture, TestIoHost } from '../_helpers'; +import { builderFixture, cdkOutFixture, TestIoHost } from '../_helpers'; import { MockSdk } from '../util/aws-cdk'; let ioHost: TestIoHost; @@ -279,6 +279,86 @@ describe('deploy', () => { successfulDeployment(); }); }); + + test('deploy returns stack information', async () => { + // GIVEN + mockDeployStack.mockResolvedValue({ + type: 'did-deploy-stack', + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + }, + noOp: false, + }); + + // WHEN + const cx = await builderFixture(toolkit, 'two-empty-stacks'); + const result = await toolkit.deploy(cx); + + // THEN + expect(result).toEqual({ + stacks: [ + { + stackName: 'Stack1', + hierarchicalId: 'Stack1', + environment: { + // This wouldn't normally work like this, but this is the information in the manifest so that's what we assert + account: 'unknown-account', + region: 'unknown-region', + }, + // This just comes from the mocked function above + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + }, + }, + { + stackName: 'Stack2', + hierarchicalId: 'Stack2', + environment: { + // This wouldn't normally work like this, but this is the information in the manifest so that's what we assert + account: 'unknown-account', + region: 'unknown-region', + }, + // This just comes from the mocked function above + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + // omg + }, + }, + ], + }); + }); + + test('deploy contains nested assembly hierarchical id', async () => { + // GIVEN + mockDeployStack.mockResolvedValue({ + type: 'did-deploy-stack', + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + }, + noOp: false, + }); + + // WHEN + const cx = await cdkOutFixture(toolkit, 'nested-assembly'); + const result = await toolkit.deploy(cx); + + // THEN + expect(result).toEqual({ + stacks: [ + expect.objectContaining({ + hierarchicalId: 'Stage/Stack1', + }), + ], + }); + }); }); function successfulDeployment() { From aa56eb11aa87357d6995e192438ae4d190d86cca Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Apr 2025 14:06:59 +0000 Subject: [PATCH 2/7] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/cloud-assembly-schema/schema/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index d173fd1d1..8caae8e30 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,4 @@ { "schemaHash": "96958a4c40e0a00e850f0c14dd6e9c0fc8db0b075f1f155cea41ab198c0514be", "revision": 43 -} +} \ No newline at end of file From aecffa60bef6e37f9fa903d76b2095205c560c95 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 3 Apr 2025 12:00:18 +0200 Subject: [PATCH 3/7] feat(toolkit): add a return type for `toolkit.deploy()` `toolkit.deploy()` now returns information about the stacks it deployed. --- .../lib/cloud-assembly/schema.ts | 7 +- .../schema/cloud-assembly.schema.json | 2 +- .../cloud-assembly-schema/schema/version.json | 4 +- .../@aws-cdk/toolkit-lib/lib/toolkit/index.ts | 1 + .../toolkit-lib/lib/toolkit/toolkit.ts | 24 +- .../@aws-cdk/toolkit-lib/lib/toolkit/types.ts | 65 ++++ .../test/_fixtures/nested-assembly/app.ts | 7 + .../test/_fixtures/nested-assembly/cdk.json | 3 + .../StageStack130339B27.assets.json | 19 ++ .../StageStack130339B27.template.json | 314 ++++++++++++++++++ .../cdk.out/assembly-Stage/cdk.out | 1 + .../cdk.out/assembly-Stage/manifest.json | 66 ++++ .../_fixtures/nested-assembly/cdk.out/cdk.out | 1 + .../nested-assembly/cdk.out/manifest.json | 18 + .../nested-assembly/cdk.out/tree.json | 1 + .../toolkit-lib/test/actions/deploy.test.ts | 82 ++++- 16 files changed, 607 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts index 4ac55eccf..e593328e9 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts @@ -100,7 +100,12 @@ export interface ArtifactManifest { readonly properties?: ArtifactProperties; /** - * A string that represents this artifact. Should only be used in user interfaces. + * A string that can be shown to a user to uniquely identify this artifact inside a cloud assembly tree + * + * Is used by the CLI to present a list of stacks to the user in a way that + * makes sense to them. Even though the property name "display name" doesn't + * imply it, this field is used to select stacks as well, so all stacks should + * have a unique display name. * * @default - no display name */ diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 06d442784..855b0ba0b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -83,7 +83,7 @@ ] }, "displayName": { - "description": "A string that represents this artifact. Should only be used in user interfaces. (Default - no display name)", + "description": "A string that can be shown to a user to uniquely identify this artifact inside a cloud assembly tree\n\nIs used by the CLI to present a list of stacks to the user in a way that\nmakes sense to them. Even though the property name \"display name\" doesn't\nimply it, this field is used to select stacks as well, so all stacks should\nhave a unique display name. (Default - no display name)", "type": "string" } }, diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 8e791e665..d173fd1d1 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,4 @@ { - "schemaHash": "78936b0f9299bbe47497dd77f8065d71e65d8d739a0413ad7698ad03b22ef83e", + "schemaHash": "96958a4c40e0a00e850f0c14dd6e9c0fc8db0b075f1f155cea41ab198c0514be", "revision": 43 -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts index a69665dca..c450e67f2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts @@ -1,3 +1,4 @@ export * from './toolkit'; export * from './non-interactive-io-host'; +export * from './types'; export * from '../api/shared-public'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 762dda640..4aa0d6118 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -7,6 +7,7 @@ import * as uuid from 'uuid'; import { NonInteractiveIoHost } from './non-interactive-io-host'; import type { ToolkitServices } from './private'; import { assemblyFromSource } from './private'; +import type { DeployResult } from './types'; import type { BootstrapEnvironments, BootstrapOptions, BootstrapResult, EnvironmentBootstrapResult } from '../actions/bootstrap'; import { BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions } from '../actions/deploy'; @@ -413,7 +414,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { * * Deploys the selected stacks into an AWS account */ - public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { + public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'deploy'); const assembly = await assemblyFromSource(ioHelper, cx); return this._deploy(assembly, 'deploy', options); @@ -422,7 +423,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { /** * Helper to allow deploy being called as part of the watch action. */ - private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: ExtendedDeployOptions = {}) { + private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: ExtendedDeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, action); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); @@ -430,9 +431,13 @@ export class Toolkit extends CloudAssemblySourceBuilder { await this.validateStacksMetadata(stackCollection, ioHelper); const synthDuration = await synthSpan.end(); + const ret: DeployResult = { + stacks: [], + }; + if (stackCollection.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('This app contains no stacks')); - return; + return ret; } const deployments = await this.deploymentsForAction('deploy'); @@ -655,6 +660,17 @@ export class Toolkit extends CloudAssemblySourceBuilder { await ioHelper.notify(IO.CDK_TOOLKIT_I5901.msg(buffer.join('\n'))); } await ioHelper.notify(IO.CDK_TOOLKIT_I5901.msg(`Stack ARN:\n${deployResult.stackArn}`)); + + ret.stacks.push({ + stackName: stack.stackName, + environment: { + account: stack.environment.account, + region: stack.environment.region, + }, + stackArn: deployResult.stackArn, + outputs: deployResult.outputs, + hierarchicalId: stack.hierarchicalId, + }); } catch (e: any) { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " @@ -715,6 +731,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { buildAsset, publishAsset, }); + + return ret; } /** diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts new file mode 100644 index 000000000..d4bbd6e8c --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -0,0 +1,65 @@ +/** + * Result interface for toolkit.deploy operation + */ +export interface DeployResult { + /** + * List of stacks deployed by this operation + */ + readonly stacks: DeployedStack[]; +} + +/** + * Information about a deployed stack + */ +export interface DeployedStack { + /** + * The name of the deployed stack + * + * A stack name is unique inside its environment, but not unique globally. + */ + readonly stackName: string; + + /** + * The environment where the stack was deployed + * + * This environment is always concrete, because even though the CDK app's + * stack may be region-agnostic, in order to be deployed it will have to have + * been specialized. + */ + readonly environment: Environment; + + /** + * Hierarchical identifier + * + * This uniquely identifies the stack inside the CDK app. + * + * In practice this will be the stack's construct path, but unfortunately the + * Cloud Assembly contract doesn't require or guarantee that. + */ + readonly hierarchicalId: string; + + /** + * The ARN of the deployed stack + */ + readonly stackArn: string; + + /** + * The outputs of the deployed CloudFormation stack + */ + readonly outputs: { [key: string]: string }; +} + +/** + * An environment, which is an (account, region) pair + */ +export interface Environment { + /** + * The account number + */ + readonly account: string; + + /** + * The region number + */ + readonly region: string; +} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts new file mode 100644 index 000000000..cc7b2e03c --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/app.ts @@ -0,0 +1,7 @@ +import * as cdk from 'aws-cdk-lib/core'; + +const app = new cdk.App({ autoSynth: false }); +const stage = new cdk.Stage(app, 'Stage'); +new cdk.Stack(stage, 'Stack1'); + +app.synth(); diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json new file mode 100644 index 000000000..b49bb19e0 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "$(npx -c 'which tsx') app.ts" +} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json new file mode 100644 index 000000000..4028f38b9 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.assets.json @@ -0,0 +1,19 @@ +{ + "version": "40.0.0", + "files": { + "7f4fb64a3afca08edbdbaa369e00317cb253697278406a83c78e849e89045f9d": { + "source": { + "path": "StageStack130339B27.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "7f4fb64a3afca08edbdbaa369e00317cb253697278406a83c78e849e89045f9d.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json new file mode 100644 index 000000000..a7668a10b --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/StageStack130339B27.template.json @@ -0,0 +1,314 @@ +{ + "Resources": { + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/zPSM7Qw1TNQTCwv1k1OydbNyUzSCy5JTM7WyctPSdXLKtYvMzLSMzQEKskqzszULSrNK8nMTdULgtAAJ5JKTz8AAAA=" + }, + "Metadata": { + "aws:cdk:path": "Stage/Stack1/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Conditions": { + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-3" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-4" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-2" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "il-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + } + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out new file mode 100644 index 000000000..1e02a2deb --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/cdk.out @@ -0,0 +1 @@ +{"version":"40.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json new file mode 100644 index 000000000..52119eb46 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/assembly-Stage/manifest.json @@ -0,0 +1,66 @@ +{ + "version": "40.0.0", + "artifacts": { + "StageStack130339B27.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "StageStack130339B27.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "StageStack130339B27": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "StageStack130339B27.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/7f4fb64a3afca08edbdbaa369e00317cb253697278406a83c78e849e89045f9d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "StageStack130339B27.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + }, + "stackName": "Stage-Stack1" + }, + "dependencies": [ + "StageStack130339B27.assets" + ], + "metadata": { + "/Stage/Stack1/CDKMetadata/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadata" + } + ], + "/Stage/Stack1/CDKMetadata/Condition": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadataAvailable" + } + ], + "/Stage/Stack1/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Stage/Stack1/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "Stage/Stack1" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out new file mode 100644 index 000000000..1e02a2deb --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/cdk.out @@ -0,0 +1 @@ +{"version":"40.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json new file mode 100644 index 000000000..53a5301de --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/manifest.json @@ -0,0 +1,18 @@ +{ + "version": "40.0.0", + "artifacts": { + "assembly-Stage": { + "type": "cdk:cloud-assembly", + "properties": { + "directoryName": "assembly-Stage", + "displayName": "Stage" + } + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json new file mode 100644 index 000000000..00871b5fa --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/nested-assembly/cdk.out/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","children":{"Stage":{"id":"Stage","path":"Stage","children":{"Stack1":{"id":"Stack1","path":"Stage/Stack1","children":{"CDKMetadata":{"id":"CDKMetadata","path":"Stage/Stack1/CDKMetadata","children":{"Default":{"id":"Default","path":"Stage/Stack1/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.185.0"}},"Condition":{"id":"Condition","path":"Stage/Stack1/CDKMetadata/Condition","constructInfo":{"fqn":"aws-cdk-lib.CfnCondition","version":"2.185.0"}}},"constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"BootstrapVersion":{"id":"BootstrapVersion","path":"Stage/Stack1/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.185.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"Stage/Stack1/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.185.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.185.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.Stage","version":"2.185.0"}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}},"constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.185.0"}}} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts index 9c7feed9f..313e79f53 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts @@ -2,7 +2,7 @@ import { RequireApproval, StackParameters } from '../../lib'; import * as awsCdkApi from '../../lib/api/aws-cdk'; import type { DeployStackOptions, DeployStackResult } from '../../lib/api/aws-cdk'; import { Toolkit } from '../../lib/toolkit'; -import { builderFixture, TestIoHost } from '../_helpers'; +import { builderFixture, cdkOutFixture, TestIoHost } from '../_helpers'; import { MockSdk } from '../util/aws-cdk'; let ioHost: TestIoHost; @@ -279,6 +279,86 @@ describe('deploy', () => { successfulDeployment(); }); }); + + test('deploy returns stack information', async () => { + // GIVEN + mockDeployStack.mockResolvedValue({ + type: 'did-deploy-stack', + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + }, + noOp: false, + }); + + // WHEN + const cx = await builderFixture(toolkit, 'two-empty-stacks'); + const result = await toolkit.deploy(cx); + + // THEN + expect(result).toEqual({ + stacks: [ + { + stackName: 'Stack1', + hierarchicalId: 'Stack1', + environment: { + // This wouldn't normally work like this, but this is the information in the manifest so that's what we assert + account: 'unknown-account', + region: 'unknown-region', + }, + // This just comes from the mocked function above + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + }, + }, + { + stackName: 'Stack2', + hierarchicalId: 'Stack2', + environment: { + // This wouldn't normally work like this, but this is the information in the manifest so that's what we assert + account: 'unknown-account', + region: 'unknown-region', + }, + // This just comes from the mocked function above + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + // omg + }, + }, + ], + }); + }); + + test('deploy contains nested assembly hierarchical id', async () => { + // GIVEN + mockDeployStack.mockResolvedValue({ + type: 'did-deploy-stack', + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: { + OutputKey1: 'OutputValue1', + OutputKey2: 'OutputValue2', + }, + noOp: false, + }); + + // WHEN + const cx = await cdkOutFixture(toolkit, 'nested-assembly'); + const result = await toolkit.deploy(cx); + + // THEN + expect(result).toEqual({ + stacks: [ + expect.objectContaining({ + hierarchicalId: 'Stage/Stack1', + }), + ], + }); + }); }); function successfulDeployment() { From d7a7cb63159dda402912cb0104c4c4de528b480f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 3 Apr 2025 16:10:58 +0200 Subject: [PATCH 4/7] feat(toolkit): add a return type for `toolkit.destroy()` `toolkit.destroy()` now returns information about the stacks it deployed. --- .../toolkit-lib/lib/toolkit/toolkit.ts | 27 ++++++++++--- .../@aws-cdk/toolkit-lib/lib/toolkit/types.ts | 39 +++++++++++++++++++ .../lib/api/cloudformation/stack-helpers.ts | 2 +- .../lib/api/deployments/deploy-stack.ts | 16 +++++++- .../lib/api/deployments/deployments.ts | 2 +- 5 files changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 59fb5df19..080b5494a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -6,7 +6,7 @@ import * as fs from 'fs-extra'; import { NonInteractiveIoHost } from './non-interactive-io-host'; import type { ToolkitServices } from './private'; import { assemblyFromSource } from './private'; -import type { DeployResult } from './types'; +import type { DeployResult, DestroyResult } from './types'; import type { BootstrapEnvironments, BootstrapOptions, BootstrapResult, EnvironmentBootstrapResult } from '../actions/bootstrap'; import { BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions } from '../actions/deploy'; @@ -854,7 +854,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { * * Destroys the selected Stacks. */ - public async destroy(cx: ICloudAssemblySource, options: DestroyOptions): Promise { + public async destroy(cx: ICloudAssemblySource, options: DestroyOptions): Promise { const ioHelper = asIoHelper(this.ioHost, 'destroy'); const assembly = await assemblyFromSource(ioHelper, cx); return this._destroy(assembly, 'destroy', options); @@ -863,18 +863,23 @@ export class Toolkit extends CloudAssemblySourceBuilder { /** * Helper to allow destroy being called as part of the deploy action. */ - private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise { + private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise { const ioHelper = asIoHelper(this.ioHost, action); const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks }); // The stacks will have been ordered for deployment, so reverse them for deletion. const stacks = (await assembly.selectStacksV2(options.stacks)).reversed(); await synthSpan.end(); + const ret: DestroyResult = { + stacks: [], + }; + const motivation = 'Destroying stacks is an irreversible action'; const question = `Are you sure you want to delete: ${chalk.red(stacks.hierarchicalIds.join(', '))}`; const confirmed = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I7010.req(question, { motivation })); if (!confirmed) { - return ioHelper.notify(IO.CDK_TOOLKIT_E7010.msg('Aborted by user')); + await ioHelper.notify(IO.CDK_TOOLKIT_E7010.msg('Aborted by user')); + return ret; } const destroySpan = await ioHelper.span(SPAN.DESTROY_ACTION).begin({ @@ -890,11 +895,21 @@ export class Toolkit extends CloudAssemblySourceBuilder { stack, }); const deployments = await this.deploymentsForAction(action); - await deployments.destroyStack({ + const result = await deployments.destroyStack({ stack, deployName: stack.stackName, roleArn: options.roleArn, }); + + ret.stacks.push({ + environment: { + account: stack.environment.account, + region: stack.environment.region, + }, + stackName: stack.stackName, + stackArn: result.stackArn, + }); + await ioHelper.notify(IO.CDK_TOOLKIT_I7900.msg(chalk.green(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`), stack)); await singleDestroySpan.end(); } catch (e: any) { @@ -902,6 +917,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { throw e; } } + + return ret; } finally { await destroySpan.end(); } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts index 732a1ef7a..2e86b82eb 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -128,3 +128,42 @@ export interface Environment { */ readonly region: string; } + +/** + * Result interface for toolkit.destroy operation + */ +export interface DestroyResult { + /** + * List of stacks destroyed by this operation + */ + readonly stacks: DestroyedStack[]; +} + +/** + * A stack targeted by a destroy operation + */ +export interface DestroyedStack { + /** + * The name of the stack + * + * A stack name is unique inside its environment, but not unique globally. + */ + readonly stackName: string; + + /** + * The environment of the stack + * + * This environment is always concrete, because even though the CDK app's + * stack may be region-agnostic, in order to be deployed it will have to have + * been specialized. + */ + readonly environment: Environment; + + /** + * The ARN of the stack that was destroyed, if any. + * + * If the stack didn't exist to begin with, the operation will succeed but + * this value will be undefined. + */ + readonly stackArn?: string; +} diff --git a/packages/aws-cdk/lib/api/cloudformation/stack-helpers.ts b/packages/aws-cdk/lib/api/cloudformation/stack-helpers.ts index 77c3bb76b..1ae6d2fd5 100644 --- a/packages/aws-cdk/lib/api/cloudformation/stack-helpers.ts +++ b/packages/aws-cdk/lib/api/cloudformation/stack-helpers.ts @@ -85,7 +85,7 @@ export class CloudFormationStack { } /** - * The stack's ID + * The stack's ID (which is the same as its ARN) * * Throws if the stack doesn't exist. */ diff --git a/packages/aws-cdk/lib/api/deployments/deploy-stack.ts b/packages/aws-cdk/lib/api/deployments/deploy-stack.ts index 66fcb6737..6c38d423c 100644 --- a/packages/aws-cdk/lib/api/deployments/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deployments/deploy-stack.ts @@ -663,13 +663,23 @@ export interface DestroyStackOptions { deployName?: string; } -export async function destroyStack(options: DestroyStackOptions, ioHelper: IoHelper) { +export interface DestroyStackResult { + /** + * The ARN of the stack that was destroyed, if any. + * + * If the stack didn't exist to begin with, the operation will succeed + * but this value will be undefined. + */ + readonly stackArn?: string; +} + +export async function destroyStack(options: DestroyStackOptions, ioHelper: IoHelper): Promise { const deployName = options.deployName || options.stack.stackName; const cfn = options.sdk.cloudFormation(); const currentStack = await CloudFormationStack.lookup(cfn, deployName); if (!currentStack.exists) { - return; + return {}; } const monitor = new StackActivityMonitor({ cfn, @@ -685,6 +695,8 @@ export async function destroyStack(options: DestroyStackOptions, ioHelper: IoHel if (destroyedStack && destroyedStack.stackStatus.name !== 'DELETE_COMPLETE') { throw new ToolkitError(`Failed to destroy ${deployName}: ${destroyedStack.stackStatus}`); } + + return { stackArn: currentStack.stackId }; } catch (e: any) { throw new ToolkitError(suffixWithErrors(formatErrorMessage(e), monitor.errors)); } finally { diff --git a/packages/aws-cdk/lib/api/deployments/deployments.ts b/packages/aws-cdk/lib/api/deployments/deployments.ts index bca4bd1aa..dc0e2e4df 100644 --- a/packages/aws-cdk/lib/api/deployments/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments/deployments.ts @@ -570,7 +570,7 @@ export class Deployments { ); } - public async destroyStack(options: DestroyStackOptions): Promise { + public async destroyStack(options: DestroyStackOptions) { const env = await this.envs.accessStackForMutableStackOperations(options.stack); const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn); From 5393392831d899d33e6c1cc28b628190a645931c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Apr 2025 11:58:14 +0000 Subject: [PATCH 5/7] chore: self mutation Signed-off-by: github-actions --- packages/@aws-cdk/cloud-assembly-schema/schema/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index d173fd1d1..8caae8e30 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,4 @@ { "schemaHash": "96958a4c40e0a00e850f0c14dd6e9c0fc8db0b075f1f155cea41ab198c0514be", "revision": 43 -} +} \ No newline at end of file From a537a753bd550108d94a44d5f83d5f297dfa3b73 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Apr 2025 13:28:25 +0000 Subject: [PATCH 6/7] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/THIRD_PARTY_LICENSES | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index 32dfaffd3..33b930cb1 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -21564,26 +21564,6 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** fs-extra@10.1.0 - https://www.npmjs.com/package/fs-extra/v/10.1.0 | MIT -(The MIT License) - -Copyright (c) 2011-2017 JP Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---------------- ** fs-extra@9.1.0 - https://www.npmjs.com/package/fs-extra/v/9.1.0 | MIT From 4b5c1e04cc31e8764c54df5aacb0dbf8c56c2d8c Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Sun, 6 Apr 2025 19:50:45 +0200 Subject: [PATCH 7/7] feat(toolkit): add a return type for toolkit.rollback() (#334) `toolkit.rollback()` now returns information about the stacks it rolled back. Includes some refactoring that deduplicates return information between deploy/destroy and rollback. Closes https://github.com/aws/aws-cdk/issues/33188 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions Co-authored-by: github-actions --- .../toolkit-lib/lib/toolkit/toolkit.ts | 25 +++++- .../@aws-cdk/toolkit-lib/lib/toolkit/types.ts | 84 +++++++++---------- .../toolkit-lib/test/actions/rollback.test.ts | 26 +++++- .../lib/api/deployments/deployments.ts | 17 ++-- 4 files changed, 94 insertions(+), 58 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 080b5494a..b22541d7f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -6,7 +6,7 @@ import * as fs from 'fs-extra'; import { NonInteractiveIoHost } from './non-interactive-io-host'; import type { ToolkitServices } from './private'; import { assemblyFromSource } from './private'; -import type { DeployResult, DestroyResult } from './types'; +import type { DeployResult, DestroyResult, RollbackResult } from './types'; import type { BootstrapEnvironments, BootstrapOptions, BootstrapResult, EnvironmentBootstrapResult } from '../actions/bootstrap'; import { BootstrapSource } from '../actions/bootstrap'; import { AssetBuildTime, type DeployOptions } from '../actions/deploy'; @@ -796,7 +796,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { * * Rolls back the selected stacks. */ - public async rollback(cx: ICloudAssemblySource, options: RollbackOptions): Promise { + public async rollback(cx: ICloudAssemblySource, options: RollbackOptions): Promise { const ioHelper = asIoHelper(this.ioHost, 'rollback'); const assembly = await assemblyFromSource(ioHelper, cx); return this._rollback(assembly, 'rollback', options); @@ -805,16 +805,20 @@ export class Toolkit extends CloudAssemblySourceBuilder { /** * Helper to allow rollback being called as part of the deploy or watch action. */ - private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { + private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { const ioHelper = asIoHelper(this.ioHost, action); const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks }); const stacks = await assembly.selectStacksV2(options.stacks); await this.validateStacksMetadata(stacks, ioHelper); await synthSpan.end(); + const ret: RollbackResult = { + stacks: [], + }; + if (stacks.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E6001.msg('No stacks selected')); - return; + return ret; } let anyRollbackable = false; @@ -839,6 +843,16 @@ export class Toolkit extends CloudAssemblySourceBuilder { anyRollbackable = true; } await rollbackSpan.end(); + + ret.stacks.push({ + environment: { + account: stack.environment.account, + region: stack.environment.region, + }, + stackName: stack.stackName, + stackArn: stackResult.stackArn, + result: stackResult.notInRollbackableState ? 'already-stable' : 'rolled-back', + }); } catch (e: any) { await ioHelper.notify(IO.CDK_TOOLKIT_E6900.msg(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, { error: e })); throw new ToolkitError('Rollback failed (use --force to orphan failing resources)'); @@ -847,6 +861,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { if (!anyRollbackable) { throw new ToolkitError('No stacks were in a state that could be rolled back'); } + + return ret; } /** @@ -908,6 +924,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { }, stackName: stack.stackName, stackArn: result.stackArn, + stackExisted: result.stackArn !== undefined, }); await ioHelper.notify(IO.CDK_TOOLKIT_I7900.msg(chalk.green(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`), stack)); diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts index 09eb25eee..626c522be 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -9,18 +9,18 @@ export interface DeployResult { } /** - * Information about a deployed stack + * Properties that describe a physically deployed stack */ -export interface DeployedStack { +export interface PhysicalStack { /** - * The name of the deployed stack + * The name of the stack * * A stack name is unique inside its environment, but not unique globally. */ readonly stackName: string; /** - * The environment where the stack was deployed + * The environment of the stack * * This environment is always concrete, because even though the CDK app's * stack may be region-agnostic, in order to be deployed it will have to have @@ -28,6 +28,16 @@ export interface DeployedStack { */ readonly environment: Environment; + /** + * The ARN of the stack + */ + readonly stackArn: Arn extends 'arnOptional' ? string | undefined : string; +} + +/** + * Information about a deployed stack + */ +export interface DeployedStack extends PhysicalStack { /** * Hierarchical identifier * @@ -38,11 +48,6 @@ export interface DeployedStack { */ readonly hierarchicalId: string; - /** - * The ARN of the deployed stack - */ - readonly stackArn: string; - /** * The outputs of the deployed CloudFormation stack */ @@ -63,6 +68,7 @@ export interface Environment { */ readonly region: string; } + /** * Result interface for toolkit.deploy operation */ @@ -76,23 +82,7 @@ export interface DeployResult { /** * Information about a deployed stack */ -export interface DeployedStack { - /** - * The name of the deployed stack - * - * A stack name is unique inside its environment, but not unique globally. - */ - readonly stackName: string; - - /** - * The environment where the stack was deployed - * - * This environment is always concrete, because even though the CDK app's - * stack may be region-agnostic, in order to be deployed it will have to have - * been specialized. - */ - readonly environment: Environment; - +export interface DeployedStack extends PhysicalStack { /** * Hierarchical identifier * @@ -103,11 +93,6 @@ export interface DeployedStack { */ readonly hierarchicalId: string; - /** - * The ARN of the deployed stack - */ - readonly stackArn: string; - /** * The outputs of the deployed CloudFormation stack */ @@ -142,30 +127,37 @@ export interface DestroyResult { /** * A stack targeted by a destroy operation */ -export interface DestroyedStack { +export interface DestroyedStack extends PhysicalStack<'arnOptional'> { /** - * The name of the stack + * Whether the stack existed to begin with * - * A stack name is unique inside its environment, but not unique globally. + * If `!stackExisted`, the stack didn't exist, wasn't deleted, and `stackArn` + * will be `undefined`. */ - readonly stackName: string; + readonly stackExisted: boolean; +} +/** + * Result interface for toolkit.rollback operation + */ +export interface RollbackResult { /** - * The environment of the stack - * - * This environment is always concrete, because even though the CDK app's - * stack may be region-agnostic, in order to be deployed it will have to have - * been specialized. + * List of stacks rolled back by this operation */ - readonly environment: Environment; + readonly stacks: RolledBackStack[]; +} +/** + * A stack targeted by a rollback operation + */ +export interface RolledBackStack extends PhysicalStack { /** - * The ARN of the stack that was destroyed, if any. + * What operation we did for this stack * - * If the stack didn't exist to begin with, the operation will succeed but - * this value will be undefined. + * Either: we did roll it back, or we didn't need to roll it back because + * it was already stable. */ - readonly stackArn?: string; + readonly result: StackRollbackResult; } /** * Result interface for toolkit.deploy operation @@ -297,3 +289,5 @@ export interface Environment { */ readonly region: string; } + +export type StackRollbackResult = 'rolled-back' | 'already-stable'; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/rollback.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/rollback.test.ts index 6b49f7fd4..114d26dce 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/rollback.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/rollback.test.ts @@ -22,6 +22,7 @@ beforeEach(() => { mockRollbackStack.mockResolvedValue({ notInRollbackableState: false, success: true, + stackArn: 'arn:stack', }); }); @@ -29,10 +30,33 @@ describe('rollback', () => { test('successful rollback', async () => { // WHEN const cx = await builderFixture(toolkit, 'two-empty-stacks'); - await toolkit.rollback(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + const result = await toolkit.rollback(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); // THEN successfulRollback(); + + expect(result).toEqual({ + stacks: [ + { + environment: { + account: 'unknown-account', + region: 'unknown-region', + }, + result: 'rolled-back', + stackArn: 'arn:stack', + stackName: 'Stack1', + }, + { + environment: { + account: 'unknown-account', + region: 'unknown-region', + }, + result: 'rolled-back', + stackArn: 'arn:stack', + stackName: 'Stack2', + }, + ], + }); }); test('rollback not in rollbackable state', async () => { diff --git a/packages/aws-cdk/lib/api/deployments/deployments.ts b/packages/aws-cdk/lib/api/deployments/deployments.ts index f04e3a8fd..8319842d2 100644 --- a/packages/aws-cdk/lib/api/deployments/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments/deployments.ts @@ -229,10 +229,10 @@ export interface RollbackStackOptions { readonly validateBootstrapStackVersion?: boolean; } -export interface RollbackStackResult { - readonly notInRollbackableState?: boolean; - readonly success?: boolean; -} +export type RollbackStackResult = { readonly stackArn: string } & ( + | { readonly notInRollbackableState: true } + | { readonly success: true; notInRollbackableState?: undefined } +); interface AssetOptions { /** @@ -467,14 +467,15 @@ export class Deployments { // We loop in case of `--force` and the stack ends up in `CONTINUE_UPDATE_ROLLBACK`. let maxLoops = 10; while (maxLoops--) { - let cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName); + const cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName); + const stackArn = cloudFormationStack.stackId; const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn); switch (cloudFormationStack.stackStatus.rollbackChoice) { case RollbackChoice.NONE: await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(`Stack ${deployName} does not need a rollback: ${cloudFormationStack.stackStatus}`)); - return { notInRollbackableState: true }; + return { stackArn: cloudFormationStack.stackId, notInRollbackableState: true }; case RollbackChoice.START_ROLLBACK: await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Initiating rollback of stack ${deployName}`)); @@ -516,7 +517,7 @@ export class Deployments { await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg( `Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`, )); - return { notInRollbackableState: true }; + return { stackArn, notInRollbackableState: true }; default: throw new ToolkitError(`Unexpected rollback choice: ${cloudFormationStack.stackStatus.rollbackChoice}`); @@ -552,7 +553,7 @@ export class Deployments { } if (finalStackState.stackStatus.isRollbackSuccess || !stackErrorMessage) { - return { success: true }; + return { stackArn, success: true }; } // Either we need to ignore some resources to continue the rollback, or something went wrong