From 059e0b4297c877d2743e6ebe69f925c021e11c38 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 16 Jan 2025 14:58:22 +0000 Subject: [PATCH 1/7] api changes --- .../toolkit/lib/actions/synth/index.ts | 2 +- .../lib/api/cloud-assembly/private/index.ts | 5 +++ .../cloud-assembly/private/stack-selectors.ts | 5 +++ .../@aws-cdk/toolkit/lib/toolkit/toolkit.ts | 43 +++++++++++-------- 4 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/index.ts create mode 100644 packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/stack-selectors.ts diff --git a/packages/@aws-cdk/toolkit/lib/actions/synth/index.ts b/packages/@aws-cdk/toolkit/lib/actions/synth/index.ts index 2cb1420cc0987..2cbdea734ee6e 100644 --- a/packages/@aws-cdk/toolkit/lib/actions/synth/index.ts +++ b/packages/@aws-cdk/toolkit/lib/actions/synth/index.ts @@ -4,7 +4,7 @@ export interface SynthOptions { /** * Select the stacks */ - readonly stacks: StackSelector; + readonly stacks?: StackSelector; /** * After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION) diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/index.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/index.ts new file mode 100644 index 0000000000000..1733ca0c300d3 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/index.ts @@ -0,0 +1,5 @@ +export * from './context-aware-source'; +export * from './exec'; +export * from './prepare-source'; +export * from './source-builder'; +export * from './stack-selectors'; diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/stack-selectors.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/stack-selectors.ts new file mode 100644 index 0000000000000..31f114b9d82d8 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/stack-selectors.ts @@ -0,0 +1,5 @@ +import { StackSelectionStrategy, StackSelector } from '../stack-selector'; + +export const ALL_STACKS: StackSelector = { + strategy: StackSelectionStrategy.ALL_STACKS, +}; diff --git a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts index b781c392035fd..9f1ac4c2b43f6 100644 --- a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts @@ -15,10 +15,10 @@ import { patternsArrayForWatch, WatchOptions } from '../actions/watch'; import { SdkOptions } from '../api/aws-auth'; import { DEFAULT_TOOLKIT_STACK_NAME, SdkProvider, SuccessfulDeployStackResult, StackCollection, Deployments, HotswapMode, StackActivityProgress, ResourceMigrator, obscureTemplate, serializeStructure, tagsForStack, CliIoHost, validateSnsTopicArn, Concurrency, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode } from '../api/aws-cdk'; import { CachedCloudAssemblySource, IdentityCloudAssemblySource, StackAssembly, ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assembly'; -import { CloudAssemblySourceBuilder } from '../api/cloud-assembly/private/source-builder'; +import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/private'; import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; -import { asSdkLogger, withAction, Timer, confirm, data, error, highlight, info, success, warn, ActionAwareIoHost, debug } from '../api/io/private'; +import { asSdkLogger, withAction, Timer, confirm, error, highlight, info, success, warn, ActionAwareIoHost, debug } from '../api/io/private'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -38,7 +38,7 @@ export interface ToolkitOptions { /** * The IoHost implementation, handling the inline interactions between the Toolkit and an integration. */ - // ioHost: IIoHost; + ioHost?: IIoHost; /** * Configuration options for the SDK. @@ -78,10 +78,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab public constructor(private readonly props: ToolkitOptions = {}) { super(); - // @todo open ioHost up - this.ioHost = CliIoHost.getIoHost(); - // this.ioHost = options.ioHost; - + this.ioHost = props.ioHost ?? CliIoHost.getIoHost(); this.toolkitStackName = props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; } @@ -121,26 +118,38 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab /** * Synth Action */ - public async synth(cx: ICloudAssemblySource, options: SynthOptions): Promise { + public async synth(cx: ICloudAssemblySource, options: SynthOptions = {}): Promise { const ioHost = withAction(this.ioHost, 'synth'); const assembly = await this.assemblyFromSource(cx); - const stacks = assembly.selectStacksV2(options.stacks); + const stacks = assembly.selectStacksV2(options.stacks ?? ALL_STACKS); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHost); // if we have a single stack, print it to STDOUT + const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; + const assemblyData = { + assemblyDirectory: stacks.assembly.directory, + stacksCount: stacks.stackCount, + stackIds: stacks.hierarchicalIds, + }; + if (stacks.stackCount === 1) { - const template = stacks.firstStack?.template; + const firstStack = stacks.firstStack!; + const template = firstStack.template; const obscuredTemplate = obscureTemplate(template); - await ioHost.notify(info('', 'CDK_TOOLKIT_I0001', { - raw: template, - json: serializeStructure(obscuredTemplate, true), - yaml: serializeStructure(obscuredTemplate, false), - }, - )); + await ioHost.notify(info(message, 'CDK_TOOLKIT_I0001', { + ...assemblyData, + stack: { + stackName: firstStack.stackName, + hierarchicalId: firstStack.hierarchicalId, + template, + stringifiedJson: serializeStructure(obscuredTemplate, true), + stringifiedYaml: serializeStructure(obscuredTemplate, false), + }, + })); } else { // not outputting template to stdout, let's explain things to the user a little bit... - await ioHost.notify(success(`Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`)); + await ioHost.notify(success(message, 'CDK_TOOLKIT_I0002', assemblyData)); await ioHost.notify(info(`Supply a stack id (${stacks.stackArtifacts.map((s) => chalk.green(s.hierarchicalId)).join(', ')}) to display its template.`)); } From 9ebb093c1fcfde8d18260f8b0ed044ddb153fbc2 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 16 Jan 2025 13:17:01 +0000 Subject: [PATCH 2/7] test(toolkit): tests for synth --- aws-cdk.code-workspace | 1 + packages/@aws-cdk/toolkit/.gitignore | 3 + .../test/_fixtures/two-empty-stacks/app.js | 7 ++ .../test/_fixtures/two-empty-stacks/cdk.json | 3 + .../@aws-cdk/toolkit/test/_helpers/index.ts | 7 ++ .../toolkit/test/_helpers/test-io-host.ts | 13 +++ .../toolkit/test/actions/synth.test.ts | 101 ++++++++++++++++++ .../api/cloud-assembly/source-builder.test.ts | 1 + .../toolkit/test/toolkit/toolkit.test.ts | 8 ++ packages/@aws-cdk/toolkit/tsconfig.json | 5 +- 10 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/app.js create mode 100644 packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/cdk.json create mode 100644 packages/@aws-cdk/toolkit/test/_helpers/index.ts create mode 100644 packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts create mode 100644 packages/@aws-cdk/toolkit/test/actions/synth.test.ts create mode 100644 packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts create mode 100644 packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts diff --git a/aws-cdk.code-workspace b/aws-cdk.code-workspace index 406bc32a95401..49a0a85e992a7 100644 --- a/aws-cdk.code-workspace +++ b/aws-cdk.code-workspace @@ -31,6 +31,7 @@ "name": "aws-custom-resource-sdk-adapter", "rootPath": "packages/@aws-cdk/aws-custom-resource-sdk-adapter" }, + { "name": "toolkit", "rootPath": "packages/@aws-cdk/toolkit" }, { "name": "user-input-gen", "rootPath": "tools/@aws-cdk/user-input-gen" } ] }, diff --git a/packages/@aws-cdk/toolkit/.gitignore b/packages/@aws-cdk/toolkit/.gitignore index da75d711cfa41..a338de02a4e69 100644 --- a/packages/@aws-cdk/toolkit/.gitignore +++ b/packages/@aws-cdk/toolkit/.gitignore @@ -26,6 +26,9 @@ build-info.json lib/**/*.wasm lib/**/*.yaml +# Include test resources +!test/_fixtures/**/*.js + # Include config files !.eslintrc.js !jest.config.js diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/app.js b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/app.js new file mode 100644 index 0000000000000..303299ec8b1cf --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/app.js @@ -0,0 +1,7 @@ +import * as cdk from 'aws-cdk-lib/core'; + +const app = new cdk.App(); +new cdk.Stack(app, 'AppStack1'); +new cdk.Stack(app, 'AppStack2'); + +app.synth(); diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/cdk.json b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/cdk.json new file mode 100644 index 0000000000000..7f138728ebb7d --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "node app.js" +} diff --git a/packages/@aws-cdk/toolkit/test/_helpers/index.ts b/packages/@aws-cdk/toolkit/test/_helpers/index.ts new file mode 100644 index 0000000000000..3e1d7589a70fd --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_helpers/index.ts @@ -0,0 +1,7 @@ +import * as path from 'path'; + +export * from './test-io-host'; + +export function fixture(name: string, app = 'app.js'): string { + return path.normalize(path.join(__dirname, '..', 'fixtures', name, app)); +} diff --git a/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts b/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts new file mode 100644 index 0000000000000..db878d1088492 --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts @@ -0,0 +1,13 @@ +import { IIoHost, IoMessage, IoRequest } from '../../lib'; + +/** + * A test implementation of IIoHost that does nothing but can by spied on. + */ +export class TestIoHost implements IIoHost { + public async notify(_msg: IoMessage): Promise { + // do nothing + } + public async requestResponse(msg: IoRequest): Promise { + return msg.defaultResponse; + } +} diff --git a/packages/@aws-cdk/toolkit/test/actions/synth.test.ts b/packages/@aws-cdk/toolkit/test/actions/synth.test.ts new file mode 100644 index 0000000000000..d67a8275fa3c5 --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/actions/synth.test.ts @@ -0,0 +1,101 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as core from 'aws-cdk-lib/core'; +import { Toolkit } from '../../lib/toolkit'; +import { fixture, TestIoHost } from '../_helpers'; + +const ioHost = new TestIoHost(); +const notifySpy = jest.spyOn(ioHost, 'notify'); +const requestResponseSpy = jest.spyOn(ioHost, 'requestResponse'); +const cdk = new Toolkit({ ioHost }); + +const cxFromBuilder = async () => { + return cdk.fromAssemblyBuilder(async () => { + const app = new core.App(); + new core.Stack(app, 'Stack1'); + new core.Stack(app, 'Stack2'); + + // @todo fix api + return app.synth() as any; + }); +}; + +const cxFromApp = async (name: string) => { + return cdk.fromCdkApp(`node ${fixture(name)}`); +}; + +beforeEach(() => { + requestResponseSpy.mockClear(); + notifySpy.mockClear(); +}); + +describe('synth', () => { + test('synth from builder', async () => { + // WHEN + const cx = await cxFromBuilder(); + await cdk.synth(cx); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'synth', + level: 'info', + message: expect.stringContaining('Successfully synthesized'), + })); + }); + + test('synth from app', async () => { + // WHEN + await cdk.synth(await cxFromApp('two-empty-stacks')); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'synth', + level: 'info', + message: expect.stringContaining('Successfully synthesized'), + })); + }); + + test('single stack returns the stack', async () => { + // WHEN + const cx = await cdk.fromAssemblyBuilder(async () => { + const app = new core.App(); + const stack = new core.Stack(app, 'Stack1'); + new s3.Bucket(stack, 'MyBucket'); + return app.synth() as any; + }); + + await cdk.synth(cx); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'synth', + level: 'info', + code: 'CDK_TOOLKIT_I0001', + message: expect.stringContaining('Successfully synthesized'), + data: expect.objectContaining({ + stacksCount: 1, + stack: expect.objectContaining({ + hierarchicalId: 'Stack1', + stackName: 'Stack1', + stringifiedJson: expect.not.stringContaining('CheckBootstrapVersion'), + }), + }), + })); + }); + + test('multiple stacks returns the ids', async () => { + // WHEN + await cdk.synth(await cxFromApp('two-empty-stacks')); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'synth', + level: 'info', + code: 'CDK_TOOLKIT_I0002', + message: expect.stringContaining('Successfully synthesized'), + data: expect.objectContaining({ + stacksCount: 2, + stackIds: ['AppStack1', 'AppStack2'], + }), + })); + }); +}); diff --git a/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts b/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts new file mode 100644 index 0000000000000..24cff3247ee22 --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts @@ -0,0 +1 @@ +test('test', () => expect(true).toBe(true)); diff --git a/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts b/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts new file mode 100644 index 0000000000000..00a9f2f4fc0af --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/toolkit/toolkit.test.ts @@ -0,0 +1,8 @@ +/** + * NOTE: This test suite should only contain tests for creating the Toolkit and its methods. + * + * - Actions: Tests for each action go into the `test/actions` directory + * - Source Builders: Tests for the Cloud Assembly Source Builders are in `test/api/cloud-assembly/source-builder.test.ts` + */ + +test('test', () => expect(true).toBe(true)); diff --git a/packages/@aws-cdk/toolkit/tsconfig.json b/packages/@aws-cdk/toolkit/tsconfig.json index e5e0cf1849574..c4081f47c5db2 100644 --- a/packages/@aws-cdk/toolkit/tsconfig.json +++ b/packages/@aws-cdk/toolkit/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "esModuleInterop": true, + "esModuleInterop": false, "skipLibCheck": true, "target": "es2022", "allowJs": true, @@ -25,6 +25,7 @@ "references": [ { "path": "../cx-api" }, { "path": "../cloudformation-diff" }, - { "path": "../../aws-cdk" } + { "path": "../../aws-cdk" }, + { "path": "../../aws-cdk-lib" } ] } From ced7269feb56914016b73f1761df1a60ec1c1d81 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Thu, 16 Jan 2025 18:52:03 -0500 Subject: [PATCH 3/7] test(toolkit): tests for deploy --- .../toolkit/lib/actions/deploy/index.ts | 10 +- .../@aws-cdk/toolkit/lib/toolkit/toolkit.ts | 17 ++- packages/@aws-cdk/toolkit/package.json | 1 + .../@aws-cdk/toolkit/test/_helpers/index.ts | 2 +- .../toolkit/test/actions/deploy.test.ts | 122 ++++++++++++++++++ 5 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk/toolkit/test/actions/deploy.test.ts diff --git a/packages/@aws-cdk/toolkit/lib/actions/deploy/index.ts b/packages/@aws-cdk/toolkit/lib/actions/deploy/index.ts index 60098cd16fb5a..b92b5ec411602 100644 --- a/packages/@aws-cdk/toolkit/lib/actions/deploy/index.ts +++ b/packages/@aws-cdk/toolkit/lib/actions/deploy/index.ts @@ -110,8 +110,10 @@ export class StackParameters { export interface BaseDeployOptions { /** * Criteria for selecting stacks to deploy + * + * @default - all stacks */ - readonly stacks: StackSelector; + readonly stacks?: StackSelector; /** * @deprecated set on toolkit @@ -148,9 +150,9 @@ export interface BaseDeployOptions { * A 'hotswap' deployment will attempt to short-circuit CloudFormation * and update the affected resources like Lambda functions directly. * - * @default - `HotswapMode.FALL_BACK` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments + * @default - no hotswap */ - readonly hotswap: HotswapMode; + readonly hotswap?: HotswapMode; /** * Rollback failed deployments @@ -182,7 +184,7 @@ export interface DeployOptions extends BaseDeployOptions { /** * What kind of security changes require approval * - * @default RequireApproval.Broadening + * @default RequireApproval.NEVER */ readonly requireApproval?: RequireApproval; diff --git a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts index 9f1ac4c2b43f6..e2c08656d0bd1 100644 --- a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts @@ -183,11 +183,11 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab /** * 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 ioHost = withAction(this.ioHost, 'deploy'); const timer = Timer.start(); const assembly = await this.assemblyFromSource(cx); - const stackCollection = assembly.selectStacksV2(options.stacks); + const stackCollection = assembly.selectStacksV2(options.stacks ?? ALL_STACKS); await this.validateStacksMetadata(stackCollection, ioHost); const synthTime = timer.end(); @@ -423,9 +423,16 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab await ioHost.notify(info(`${chalk.cyan(stack.id)}.${chalk.cyan(name)} = ${chalk.underline(chalk.cyan(value))}`)); } - await ioHost.notify(info('Stack ARN:')); - - await ioHost.notify(data(deployResult.stackArn)); + const obscuredTemplate = obscureTemplate(stack.template); + await ioHost.notify(info(`Stack ARN:${deployResult.stackArn}`, 'CDK_TOOLKIT_I0002', { + stack: { + stackName: stack.stackName, + hierarchicalId: stack.hierarchicalId, + template: stack.template, + stringifiedJson: serializeStructure(obscuredTemplate, true), + stringifiedYaml: serializeStructure(obscuredTemplate, false), + }, + })); } catch (e: any) { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " diff --git a/packages/@aws-cdk/toolkit/package.json b/packages/@aws-cdk/toolkit/package.json index bac4a77dd6edf..fcd7a7df090e8 100644 --- a/packages/@aws-cdk/toolkit/package.json +++ b/packages/@aws-cdk/toolkit/package.json @@ -47,6 +47,7 @@ "@types/node": "^18.18.14", "aws-cdk": "0.0.0", "aws-cdk-lib": "0.0.0", + "aws-sdk-client-mock": "^4.0.1", "esbuild": "^0.24.0", "jest": "^29.7.0", "typescript": "~5.6.3" diff --git a/packages/@aws-cdk/toolkit/test/_helpers/index.ts b/packages/@aws-cdk/toolkit/test/_helpers/index.ts index 3e1d7589a70fd..838babf5daee1 100644 --- a/packages/@aws-cdk/toolkit/test/_helpers/index.ts +++ b/packages/@aws-cdk/toolkit/test/_helpers/index.ts @@ -3,5 +3,5 @@ import * as path from 'path'; export * from './test-io-host'; export function fixture(name: string, app = 'app.js'): string { - return path.normalize(path.join(__dirname, '..', 'fixtures', name, app)); + return path.normalize(path.join(__dirname, '..', '_fixtures', name, app)); } diff --git a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts new file mode 100644 index 0000000000000..fcff9d1dd86fb --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts @@ -0,0 +1,122 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as core from 'aws-cdk-lib/core'; +import { Toolkit } from '../../lib/toolkit'; +import { fixture, TestIoHost } from '../_helpers'; +import { CloudFormationClient, DescribeChangeSetCommand, DescribeStacksCommand, StackStatus } from '@aws-sdk/client-cloudformation'; +import { mockClient } from 'aws-sdk-client-mock'; +import { StackSelectionStrategy } from '../../lib'; + +const ioHost = new TestIoHost(); +const notifySpy = jest.spyOn(ioHost, 'notify'); +const requestResponseSpy = jest.spyOn(ioHost, 'requestResponse'); +const cdk = new Toolkit({ ioHost }); +const mockCloudFormationClient = mockClient(CloudFormationClient); + +const cxFromBuilder = async () => { + return cdk.fromAssemblyBuilder(async () => { + const app = new core.App(); + new core.Stack(app, 'Stack1'); + new core.Stack(app, 'Stack2'); + + // @todo fix api + return app.synth() as any; + }); +}; + +const cxFromApp = async (name: string) => { + return cdk.fromCdkApp(`node ${fixture(name)}`); +}; + +beforeEach(() => { + requestResponseSpy.mockClear(); + notifySpy.mockClear(); + mockCloudFormationClient.reset(); + mockCloudFormationClient.onAnyCommand().resolves({}); + mockCloudFormationClient.on(DescribeChangeSetCommand).resolves({ + Status: StackStatus.CREATE_COMPLETE, + Changes: [], + }); + mockCloudFormationClient + .on(DescribeStacksCommand) + // First call, no stacks exis + .resolvesOnce({ + Stacks: [], + }) + // Second call, stack has been created + .resolves({ + Stacks: [ + { + StackStatus: StackStatus.CREATE_COMPLETE, + StackStatusReason: 'It is magic', + EnableTerminationProtection: false, + StackName: 'MagicalStack', + CreationTime: new Date(), + }, + ], + }); +}); + +describe('deploy', () => { + test('deploy from builder', async () => { + // WHEN + const cx = await cxFromBuilder(); + await cdk.deploy(cx); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + message: expect.stringContaining('Deployment time:'), + })); + }); + + test('deploy from app', async () => { + // WHEN + await cdk.deploy(await cxFromApp('two-empty-stacks')); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + message: expect.stringContaining('Deployment time:'), + })); + }); + + test('deploy no resources results in warning', async () => { + // WHEN + const cx = await cxFromBuilder(); + await cdk.deploy(cx, { + stacks: { + strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE, + patterns: ['Stack1'], + }, + }); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + message: expect.stringContaining('Stack ARN:'), + data: expect.objectContaining({ + stack: expect.objectContaining({ + hierarchicalId: 'Stack1', + stackName: 'Stack1', + stringifiedJson: expect.not.stringContaining('CheckBootstrapVersion'), + }), + }), + })); + + expect(notifySpy).not.toHaveBeenCalledWith(expect.objectContaining({ + action: 'deploy', + level: 'info', + message: expect.stringContaining('Stack ARN:'), + data: expect.objectContaining({ + stack: expect.objectContaining({ + hierarchicalId: 'Stack2', + stackName: 'Stack2', + stringifiedJson: expect.not.stringContaining('CheckBootstrapVersion'), + }), + }), + })); + }); +}); From 77863b7c6806a04e0e8bc4a40e80c90e8f738d0d Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Fri, 17 Jan 2025 13:44:25 -0500 Subject: [PATCH 4/7] require approval is live --- .../lib/actions/diff/private/helpers.ts | 24 +++++++++++++ .../toolkit/lib/actions/diff/private/index.ts | 1 + .../@aws-cdk/toolkit/lib/toolkit/toolkit.ts | 24 ++++++------- .../toolkit/test/actions/deploy.test.ts | 36 +++++++++++-------- 4 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 packages/@aws-cdk/toolkit/lib/actions/diff/private/helpers.ts create mode 100644 packages/@aws-cdk/toolkit/lib/actions/diff/private/index.ts diff --git a/packages/@aws-cdk/toolkit/lib/actions/diff/private/helpers.ts b/packages/@aws-cdk/toolkit/lib/actions/diff/private/helpers.ts new file mode 100644 index 0000000000000..98155f3d02c0e --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/diff/private/helpers.ts @@ -0,0 +1,24 @@ +import { DescribeChangeSetOutput, fullDiff } from '@aws-cdk/cloudformation-diff'; +import * as cxapi from '@aws-cdk/cx-api'; +import { ToolkitError } from '../../../api/errors'; +import { RequireApproval } from '../../deploy'; + +/** + * Return whether the diff has security-impacting changes that need confirmation + */ +export function diffRequiresApproval( + oldTemplate: any, + newTemplate: cxapi.CloudFormationStackArtifact, + requireApproval: RequireApproval, + changeSet?: DescribeChangeSetOutput, +): boolean { + // @todo return or print the full diff. + const diff = fullDiff(oldTemplate, newTemplate.template, changeSet); + + switch (requireApproval) { + case RequireApproval.NEVER: return false; + case RequireApproval.ANY_CHANGE: return diff.permissionsAnyChanges; + case RequireApproval.BROADENING: return diff.permissionsBroadened; + default: throw new ToolkitError(`Unrecognized approval level: ${requireApproval}`); + } +} diff --git a/packages/@aws-cdk/toolkit/lib/actions/diff/private/index.ts b/packages/@aws-cdk/toolkit/lib/actions/diff/private/index.ts new file mode 100644 index 0000000000000..c5f595cf9d428 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/actions/diff/private/index.ts @@ -0,0 +1 @@ +export * from './helpers'; diff --git a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts index e2c08656d0bd1..056e25471f25d 100644 --- a/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts @@ -8,6 +8,7 @@ import { AssetBuildTime, DeployOptions, RequireApproval } from '../actions/deplo import { buildParameterMap, removePublishedAssets } from '../actions/deploy/private'; import { DestroyOptions } from '../actions/destroy'; import { DiffOptions } from '../actions/diff'; +import { diffRequiresApproval } from '../actions/diff/private'; import { ListOptions } from '../actions/list'; import { RollbackOptions } from '../actions/rollback'; import { SynthOptions } from '../actions/synth'; @@ -207,7 +208,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab }); await migrator.tryMigrateResources(stackCollection, options); - // const requireApproval = options.requireApproval ?? RequireApproval.BROADENING; + const requireApproval = options.requireApproval ?? RequireApproval.NEVER; const parameterMap = buildParameterMap(options.parameters?.parameters); @@ -281,17 +282,16 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab return; } - // @TODO - // if (requireApproval !== RequireApproval.NEVER) { - // const currentTemplate = await deployments.readCurrentTemplate(stack); - // if (printSecurityDiff(currentTemplate, stack, requireApproval)) { - // await askUserConfirmation( - // concurrency, - // '"--require-approval" is enabled and stack includes security-sensitive updates', - // 'Do you wish to deploy these changes', - // ); - // } - // } + if (requireApproval !== RequireApproval.NEVER) { + const currentTemplate = await deployments.readCurrentTemplate(stack); + if (diffRequiresApproval(currentTemplate, stack, requireApproval)) { + const motivation = '"--require-approval" is enabled and stack includes security-sensitive updates.'; + const question = `${motivation}\nDo you wish to deploy these changes`; + // @todo reintroduce concurrency and corked logging in CliHost + const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I5060', question, motivation, true, concurrency)); + if (!confirmed) { throw new ToolkitError('Aborted by user'); } + } + } // Following are the same semantics we apply with respect to Notification ARNs (dictated by the SDK) // diff --git a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts index fcff9d1dd86fb..ffa665b2e77a1 100644 --- a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts @@ -1,10 +1,10 @@ -import * as s3 from 'aws-cdk-lib/aws-s3'; -import * as core from 'aws-cdk-lib/core'; -import { Toolkit } from '../../lib/toolkit'; -import { fixture, TestIoHost } from '../_helpers'; import { CloudFormationClient, DescribeChangeSetCommand, DescribeStacksCommand, StackStatus } from '@aws-sdk/client-cloudformation'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as core from 'aws-cdk-lib/core'; import { mockClient } from 'aws-sdk-client-mock'; -import { StackSelectionStrategy } from '../../lib'; +import { RequireApproval, StackSelectionStrategy } from '../../lib'; +import { Toolkit } from '../../lib/toolkit'; +import { TestIoHost } from '../_helpers'; const ioHost = new TestIoHost(); const notifySpy = jest.spyOn(ioHost, 'notify'); @@ -23,10 +23,6 @@ const cxFromBuilder = async () => { }); }; -const cxFromApp = async (name: string) => { - return cdk.fromCdkApp(`node ${fixture(name)}`); -}; - beforeEach(() => { requestResponseSpy.mockClear(); notifySpy.mockClear(); @@ -70,19 +66,31 @@ describe('deploy', () => { })); }); - test('deploy from app', async () => { + test('request response when require approval is set', async () => { // WHEN - await cdk.deploy(await cxFromApp('two-empty-stacks')); + const cx = await cdk.fromAssemblyBuilder(async () => { + const app = new core.App(); + const stack = new core.Stack(app, 'Stack1'); + new iam.Role(stack, 'Role', { + assumedBy: new iam.ArnPrincipal('arn'), + }); + return app.synth() as any; + }); + + await cdk.deploy(cx, { + requireApproval: RequireApproval.ANY_CHANGE, + }); // THEN - expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + expect(requestResponseSpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'deploy', level: 'info', - message: expect.stringContaining('Deployment time:'), + code: 'CDK_TOOLKIT_I5060', + message: expect.stringContaining('Do you wish to deploy these changes'), })); }); - test('deploy no resources results in warning', async () => { + test('stack information is returned when successfully deployed', async () => { // WHEN const cx = await cxFromBuilder(); await cdk.deploy(cx, { From 3832cd440e21e0427e30d8a270f859e1182f7fae Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Fri, 17 Jan 2025 14:19:13 -0500 Subject: [PATCH 5/7] mock deployments --- .../toolkit/test/actions/deploy.test.ts | 89 +++++++------------ 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts index ffa665b2e77a1..59d09b427484d 100644 --- a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts @@ -1,8 +1,6 @@ -import { CloudFormationClient, DescribeChangeSetCommand, DescribeStacksCommand, StackStatus } from '@aws-sdk/client-cloudformation'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as core from 'aws-cdk-lib/core'; -import { mockClient } from 'aws-sdk-client-mock'; -import { RequireApproval, StackSelectionStrategy } from '../../lib'; +import { RequireApproval } from '../../lib'; import { Toolkit } from '../../lib/toolkit'; import { TestIoHost } from '../_helpers'; @@ -10,7 +8,23 @@ const ioHost = new TestIoHost(); const notifySpy = jest.spyOn(ioHost, 'notify'); const requestResponseSpy = jest.spyOn(ioHost, 'requestResponse'); const cdk = new Toolkit({ ioHost }); -const mockCloudFormationClient = mockClient(CloudFormationClient); + +jest.mock('../../lib/api/aws-cdk', () => { + return { + ...jest.requireActual('../../lib/api/aws-cdk'), + Deployments: jest.fn().mockImplementation(() => ({ + deployStack: jest.fn().mockResolvedValue({ + type: 'did-deploy-stack', + stackArn: 'arn:aws:cloudformation:region:account:stack/test-stack', + outputs: {}, + noOp: false, + }), + resolveEnvironment: jest.fn().mockResolvedValue({}), + isSingleAssetPublished: jest.fn().mockResolvedValue(true), + readCurrentTemplate: jest.fn().mockResolvedValue({ Resources: {} }), + })), + }; +}); const cxFromBuilder = async () => { return cdk.fromAssemblyBuilder(async () => { @@ -26,30 +40,7 @@ const cxFromBuilder = async () => { beforeEach(() => { requestResponseSpy.mockClear(); notifySpy.mockClear(); - mockCloudFormationClient.reset(); - mockCloudFormationClient.onAnyCommand().resolves({}); - mockCloudFormationClient.on(DescribeChangeSetCommand).resolves({ - Status: StackStatus.CREATE_COMPLETE, - Changes: [], - }); - mockCloudFormationClient - .on(DescribeStacksCommand) - // First call, no stacks exis - .resolvesOnce({ - Stacks: [], - }) - // Second call, stack has been created - .resolves({ - Stacks: [ - { - StackStatus: StackStatus.CREATE_COMPLETE, - StackStatusReason: 'It is magic', - EnableTerminationProtection: false, - StackName: 'MagicalStack', - CreationTime: new Date(), - }, - ], - }); + jest.clearAllMocks(); }); describe('deploy', () => { @@ -90,41 +81,27 @@ describe('deploy', () => { })); }); - test('stack information is returned when successfully deployed', async () => { + test('skips response by default', async () => { // WHEN - const cx = await cxFromBuilder(); + const cx = await cdk.fromAssemblyBuilder(async () => { + const app = new core.App(); + const stack = new core.Stack(app, 'Stack1'); + new iam.Role(stack, 'Role', { + assumedBy: new iam.ArnPrincipal('arn'), + }); + return app.synth() as any; + }); + await cdk.deploy(cx, { - stacks: { - strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE, - patterns: ['Stack1'], - }, + requireApproval: RequireApproval.NEVER, }); // THEN - expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - action: 'deploy', - level: 'info', - message: expect.stringContaining('Stack ARN:'), - data: expect.objectContaining({ - stack: expect.objectContaining({ - hierarchicalId: 'Stack1', - stackName: 'Stack1', - stringifiedJson: expect.not.stringContaining('CheckBootstrapVersion'), - }), - }), - })); - - expect(notifySpy).not.toHaveBeenCalledWith(expect.objectContaining({ + expect(requestResponseSpy).not.toHaveBeenCalledWith(expect.objectContaining({ action: 'deploy', level: 'info', - message: expect.stringContaining('Stack ARN:'), - data: expect.objectContaining({ - stack: expect.objectContaining({ - hierarchicalId: 'Stack2', - stackName: 'Stack2', - stringifiedJson: expect.not.stringContaining('CheckBootstrapVersion'), - }), - }), + code: 'CDK_TOOLKIT_I5060', + message: expect.stringContaining('Do you wish to deploy these changes'), })); }); }); From 82466bea5b1fa4851471c69e1214a4420b73acfa Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Fri, 17 Jan 2025 14:35:56 -0500 Subject: [PATCH 6/7] use testiohost --- .../@aws-cdk/toolkit/test/_helpers/test-io-host.ts | 12 ++++++++---- .../@aws-cdk/toolkit/test/actions/deploy.test.ts | 11 ++++------- packages/@aws-cdk/toolkit/test/actions/synth.test.ts | 11 ++++++----- .../test/api/cloud-assembly/source-builder.test.ts | 3 ++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts b/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts index d8d0dd1641a3d..8b80aeed1602d 100644 --- a/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts +++ b/packages/@aws-cdk/toolkit/test/_helpers/test-io-host.ts @@ -5,20 +5,24 @@ import { IIoHost, IoMessage, IoMessageLevel, IoRequest, isMessageRelevantForLeve * Optional set a level to filter out all irrelevant messages. */ export class TestIoHost implements IIoHost { - public readonly spy: jest.Mock; + public readonly notifySpy: jest.Mock; + public readonly requestSpy: jest.Mock; constructor(public level: IoMessageLevel = 'info') { - this.spy = jest.fn(); + this.notifySpy = jest.fn(); + this.requestSpy = jest.fn(); } public async notify(msg: IoMessage): Promise { if (isMessageRelevantForLevel(msg, this.level)) { - this.spy(msg); + this.notifySpy(msg); } } public async requestResponse(msg: IoRequest): Promise { - await this.notify(msg); + if (isMessageRelevantForLevel(msg, this.level)) { + this.requestSpy(msg); + } return msg.defaultResponse; } } diff --git a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts index 59d09b427484d..31b2e9e86a9e7 100644 --- a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts @@ -5,8 +5,6 @@ import { Toolkit } from '../../lib/toolkit'; import { TestIoHost } from '../_helpers'; const ioHost = new TestIoHost(); -const notifySpy = jest.spyOn(ioHost, 'notify'); -const requestResponseSpy = jest.spyOn(ioHost, 'requestResponse'); const cdk = new Toolkit({ ioHost }); jest.mock('../../lib/api/aws-cdk', () => { @@ -38,8 +36,7 @@ const cxFromBuilder = async () => { }; beforeEach(() => { - requestResponseSpy.mockClear(); - notifySpy.mockClear(); + ioHost.notifySpy.mockClear(); jest.clearAllMocks(); }); @@ -50,7 +47,7 @@ describe('deploy', () => { await cdk.deploy(cx); // THEN - expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'deploy', level: 'info', message: expect.stringContaining('Deployment time:'), @@ -73,7 +70,7 @@ describe('deploy', () => { }); // THEN - expect(requestResponseSpy).toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.requestSpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'deploy', level: 'info', code: 'CDK_TOOLKIT_I5060', @@ -97,7 +94,7 @@ describe('deploy', () => { }); // THEN - expect(requestResponseSpy).not.toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.requestSpy).not.toHaveBeenCalledWith(expect.objectContaining({ action: 'deploy', level: 'info', code: 'CDK_TOOLKIT_I5060', diff --git a/packages/@aws-cdk/toolkit/test/actions/synth.test.ts b/packages/@aws-cdk/toolkit/test/actions/synth.test.ts index 3b64136b1d397..79b1af63c9835 100644 --- a/packages/@aws-cdk/toolkit/test/actions/synth.test.ts +++ b/packages/@aws-cdk/toolkit/test/actions/synth.test.ts @@ -5,7 +5,8 @@ const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); beforeEach(() => { - ioHost.spy.mockClear(); + ioHost.notifySpy.mockClear(); + ioHost.requestSpy.mockClear(); }); describe('synth', () => { @@ -15,7 +16,7 @@ describe('synth', () => { await toolkit.synth(cx); // THEN - expect(ioHost.spy).toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'synth', level: 'info', message: expect.stringContaining('Successfully synthesized'), @@ -28,7 +29,7 @@ describe('synth', () => { await toolkit.synth(cx); // THEN - expect(ioHost.spy).toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'synth', level: 'info', message: expect.stringContaining('Successfully synthesized'), @@ -41,7 +42,7 @@ describe('synth', () => { await toolkit.synth(cx); // THEN - expect(ioHost.spy).toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'synth', level: 'info', code: 'CDK_TOOLKIT_I0001', @@ -62,7 +63,7 @@ describe('synth', () => { await toolkit.synth(await appFixture(toolkit, 'two-empty-stacks')); // THEN - expect(ioHost.spy).toHaveBeenCalledWith(expect.objectContaining({ + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ action: 'synth', level: 'info', code: 'CDK_TOOLKIT_I0002', diff --git a/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts b/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts index 9e5ec588963c4..ac3862de03c37 100644 --- a/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts +++ b/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts @@ -5,7 +5,8 @@ const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); beforeEach(() => { - ioHost.spy.mockClear(); + ioHost.notifySpy.mockClear(); + ioHost.requestSpy.mockClear(); }); describe('fromAssemblyBuilder', () => { From 09ea55af8cb439977de7c225ce2f433f95e2e2bf Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Fri, 17 Jan 2025 15:14:24 -0500 Subject: [PATCH 7/7] pr feedback --- .../test/_fixtures/stack-with-role/index.ts | 11 +++++ .../toolkit/test/actions/deploy.test.ts | 46 ++++--------------- 2 files changed, 20 insertions(+), 37 deletions(-) create mode 100644 packages/@aws-cdk/toolkit/test/_fixtures/stack-with-role/index.ts diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-role/index.ts b/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-role/index.ts new file mode 100644 index 0000000000000..38c3f675b7549 --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-role/index.ts @@ -0,0 +1,11 @@ +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as core from 'aws-cdk-lib/core'; + +export default async() => { + const app = new core.App(); + const stack = new core.Stack(app, 'Stack1'); + new iam.Role(stack, 'Role', { + assumedBy: new iam.ArnPrincipal('arn'), + }); + return app.synth() as any; +}; diff --git a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts index 31b2e9e86a9e7..8604cf5bc4648 100644 --- a/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit/test/actions/deploy.test.ts @@ -1,11 +1,9 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as core from 'aws-cdk-lib/core'; import { RequireApproval } from '../../lib'; import { Toolkit } from '../../lib/toolkit'; -import { TestIoHost } from '../_helpers'; +import { builderFixture, TestIoHost } from '../_helpers'; const ioHost = new TestIoHost(); -const cdk = new Toolkit({ ioHost }); +const toolkit = new Toolkit({ ioHost }); jest.mock('../../lib/api/aws-cdk', () => { return { @@ -24,27 +22,17 @@ jest.mock('../../lib/api/aws-cdk', () => { }; }); -const cxFromBuilder = async () => { - return cdk.fromAssemblyBuilder(async () => { - const app = new core.App(); - new core.Stack(app, 'Stack1'); - new core.Stack(app, 'Stack2'); - - // @todo fix api - return app.synth() as any; - }); -}; - beforeEach(() => { ioHost.notifySpy.mockClear(); + ioHost.requestSpy.mockClear(); jest.clearAllMocks(); }); describe('deploy', () => { test('deploy from builder', async () => { // WHEN - const cx = await cxFromBuilder(); - await cdk.deploy(cx); + const cx = await builderFixture(toolkit, 'two-empty-stacks'); + await toolkit.deploy(cx); // THEN expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ @@ -56,16 +44,8 @@ describe('deploy', () => { test('request response when require approval is set', async () => { // WHEN - const cx = await cdk.fromAssemblyBuilder(async () => { - const app = new core.App(); - const stack = new core.Stack(app, 'Stack1'); - new iam.Role(stack, 'Role', { - assumedBy: new iam.ArnPrincipal('arn'), - }); - return app.synth() as any; - }); - - await cdk.deploy(cx, { + const cx = await builderFixture(toolkit, 'stack-with-role'); + await toolkit.deploy(cx, { requireApproval: RequireApproval.ANY_CHANGE, }); @@ -80,16 +60,8 @@ describe('deploy', () => { test('skips response by default', async () => { // WHEN - const cx = await cdk.fromAssemblyBuilder(async () => { - const app = new core.App(); - const stack = new core.Stack(app, 'Stack1'); - new iam.Role(stack, 'Role', { - assumedBy: new iam.ArnPrincipal('arn'), - }); - return app.synth() as any; - }); - - await cdk.deploy(cx, { + const cx = await builderFixture(toolkit, 'stack-with-role'); + await toolkit.deploy(cx, { requireApproval: RequireApproval.NEVER, });