diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index b728d4849f44d..9074ba75961f3 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -639,6 +639,12 @@ class BuiltinLambdaStack extends cdk.Stack { } } +class NotificationArnPropStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sns.Topic(this, 'topic'); + } +} class AppSyncHotswapStack extends cdk.Stack { constructor(parent, id, props) { super(parent, id, props); @@ -708,6 +714,10 @@ switch (stackSet) { new DockerStack(app, `${stackPrefix}-docker`); new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); + new NotificationArnPropStack(app, `${stackPrefix}-notification-arn-prop`, { + notificationArns: [`arn:aws:sns:${defaultEnv.region}:${defaultEnv.account}:${stackPrefix}-test-topic-prop`], + }); + // SSO stacks new SsoInstanceAccessControlConfig(app, `${stackPrefix}-sso-access-control`); new SsoAssignment(app, `${stackPrefix}-sso-assignment`); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index 4f62c15d62482..578d90e90497b 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -33,6 +33,7 @@ import { withCDKMigrateFixture, withExtendedTimeoutFixture, randomString, + withoutBootstrap, } from '../../lib'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime @@ -276,9 +277,12 @@ integTest( }), ); +// bootstrapping also performs synthesis. As it turns out, bootstrap-stage synthesis still causes the lookups to be cached, meaning that the lookup never +// happens when we actually call `cdk synth --no-lookups`. This results in the error never being thrown, because it never tries to lookup anything. +// Fix this by not trying to bootstrap; there's no need to bootstrap anyway, since the test never tries to deploy anything. integTest( 'context in stage propagates to top', - withDefaultFixture(async (fixture) => { + withoutBootstrap(async (fixture) => { await expect( fixture.cdkSynth({ // This will make it error to prove that the context bubbles up, and also that we can fail on command @@ -613,12 +617,13 @@ integTest( ); integTest( - 'deploy with notification ARN', + 'deploy with notification ARN as flag', withDefaultFixture(async (fixture) => { - const topicName = `${fixture.stackNamePrefix}-test-topic`; + const topicName = `${fixture.stackNamePrefix}-test-topic-flag`; const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName })); const topicArn = response.TopicArn!; + try { await fixture.cdkDeploy('test-2', { options: ['--notification-arns', topicArn], @@ -641,6 +646,31 @@ integTest( }), ); +integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-test-topic-prop`; + + const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName })); + const topicArn = response.TopicArn!; + + try { + await fixture.cdkDeploy('notification-arn-prop'); + + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arn-prop'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + } finally { + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topicArn, + }), + ); + } +})); + // NOTE: this doesn't currently work with modern-style synthesis, as the bootstrap // role by default will not have permission to iam:PassRole the created role. integTest( diff --git a/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md b/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md index 634630f6e9b41..beadd60aa4ed2 100644 --- a/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md +++ b/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md @@ -73,6 +73,7 @@ Flags come in three types: | [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | | [@aws-cdk/aws-s3:keepNotificationInImportedBucket](#aws-cdkaws-s3keepnotificationinimportedbucket) | When enabled, Adding notifications to a bucket in the current stack will not remove notification from imported stack. | 2.155.0 | (fix) | | [@aws-cdk/aws-stepfunctions-tasks:useNewS3UriParametersForBedrockInvokeModelTask](#aws-cdkaws-stepfunctions-tasksusenews3uriparametersforbedrockinvokemodeltask) | When enabled, use new props for S3 URI field in task definition of state machine for bedrock invoke model. | 2.156.0 | (fix) | +| [@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions](#aws-cdkaws-ecsreduceec2fargatecloudwatchpermissions) | When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration | 2.159.0 | (fix) | @@ -134,7 +135,8 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, - "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true } } ``` @@ -1378,4 +1380,22 @@ When this feature flag is enabled, specify newly introduced props 's3InputUri' a **Compatibility with old behavior:** Disable the feature flag to use input and output path fields for s3 URI +### @aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions + +*When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration* (fix) + +Currently, we automatically add a number of cloudwatch permissions to the task role when no cloudwatch log group is +specified as logConfiguration and it will grant 'Resources': ['*'] to the task role. + +When this feature flag is enabled, we will only grant the necessary permissions when users specify cloudwatch log group. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| 2.159.0 | `false` | `true` | + +**Compatibility with old behavior:** Disable the feature flag to continue grant permissions to log group when no log group is specified + + diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 750fe90d3e2d4..31cc267e1ecc8 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -82,12 +82,12 @@ "semver": "^7.6.3" }, "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": "^36.0.5" + "@aws-cdk/cloud-assembly-schema": "^38.0.0" }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "@aws-cdk/cloud-assembly-schema": "^38.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^29.5.12", "@types/mock-fs": "^4.13.4", diff --git a/packages/@aws-cdk/integ-runner/package.json b/packages/@aws-cdk/integ-runner/package.json index 22d521d68601c..84312ef8dd01a 100644 --- a/packages/@aws-cdk/integ-runner/package.json +++ b/packages/@aws-cdk/integ-runner/package.json @@ -71,11 +71,12 @@ }, "dependencies": { "chokidar": "^3.6.0", - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "@aws-cdk/cloud-assembly-schema": "^38.0.0", "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "cdk-assets": "^2.154.0", "@aws-cdk/aws-service-spec": "^0.1.24", - "cdk-assets": "^2.151.29", + "@aws-cdk/cdk-cli-wrapper": "0.0.0", "aws-cdk": "0.0.0", "chalk": "^4", diff --git a/packages/aws-cdk-lib/core/README.md b/packages/aws-cdk-lib/core/README.md index 140d8920c44de..aac7abe87c167 100644 --- a/packages/aws-cdk-lib/core/README.md +++ b/packages/aws-cdk-lib/core/README.md @@ -1242,6 +1242,18 @@ const stack = new Stack(app, 'StackName', { }); ``` +### Receiving CloudFormation Stack Events + +You can add one or more SNS Topic ARNs to any Stack: + +```ts +const stack = new Stack(app, 'StackName', { + notificationArns: ['arn:aws:sns:us-east-1:23456789012:Topic'], +}); +``` + +Stack events will be sent to any SNS Topics in this list. + ### CfnJson `CfnJson` allows you to postpone the resolution of a JSON blob from diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts index 1017f172a850e..c985c538cac81 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts @@ -48,6 +48,7 @@ export function addStackArtifactToAssembly( terminationProtection: stack.terminationProtection, tags: nonEmptyDict(stack.tags.tagValues()), validateOnSynth: session.validateOnSynth, + notificationArns: stack._notificationArns, ...stackProps, ...stackNameProperty, }; diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index ce3cb9c9b9fd8..a12ca414491fc 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -127,6 +127,13 @@ export interface StackProps { */ readonly tags?: { [key: string]: string }; + /** + * SNS Topic ARNs that will receive stack events. + * + * @default - no notfication arns. + */ + readonly notificationArns?: string[]; + /** * Synthesis method to use while deploying this stack * @@ -364,6 +371,13 @@ export class Stack extends Construct implements ITaggable { */ public readonly _crossRegionReferences: boolean; + /** + * SNS Notification ARNs to receive stack events. + * + * @internal + */ + public readonly _notificationArns: string[]; + /** * Logical ID generation strategy */ @@ -451,6 +465,14 @@ export class Stack extends Construct implements ITaggable { } this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags); + for (const notificationArn of props.notificationArns ?? []) { + if (Token.isUnresolved(notificationArn)) { + throw new Error(`Stack '${id}' includes one or more tokens in its notification ARNs: ${props.notificationArns}`); + } + } + + this._notificationArns = props.notificationArns ?? []; + if (!VALID_STACK_NAME_REGEX.test(this.stackName)) { throw new Error(`Stack name must match the regular expression: ${VALID_STACK_NAME_REGEX.toString()}, got '${this.stackName}'`); } diff --git a/packages/aws-cdk-lib/core/test/stack.test.ts b/packages/aws-cdk-lib/core/test/stack.test.ts index 82be67b19499b..1f53e0f990337 100644 --- a/packages/aws-cdk-lib/core/test/stack.test.ts +++ b/packages/aws-cdk-lib/core/test/stack.test.ts @@ -2075,6 +2075,32 @@ describe('stack', () => { expect(asm.getStackArtifact(stack2.artifactId).tags).toEqual(expected); }); + test('stack notification arns are reflected in the stack artifact properties', () => { + // GIVEN + const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const app = new App({ stackTraces: false }); + const stack1 = new Stack(app, 'stack1', { + notificationArns: NOTIFICATION_ARNS, + }); + + // WHEN + const asm = app.synth(); + + // THEN + expect(asm.getStackArtifact(stack1.artifactId).notificationArns).toEqual(NOTIFICATION_ARNS); + }); + + test('throws if stack notification arns contain tokens', () => { + // GIVEN + const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const app = new App({ stackTraces: false }); + + // THEN + expect(() => new Stack(app, 'stack1', { + notificationArns: [...NOTIFICATION_ARNS, Aws.URL_SUFFIX], + })).toThrow('includes one or more tokens in its notification ARNs'); + }); + test('Termination Protection is reflected in Cloud Assembly artifact', () => { // if the root is an app, invoke "synth" to avoid double synthesis const app = new App(); diff --git a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts index 7cf279c96d924..d73e2a5b33dd7 100644 --- a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -54,6 +54,11 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly tags: { [id: string]: string }; + /** + * SNS Topics that will receive stack events. + */ + public readonly notificationArns: string[]; + /** * The physical name of this stack. */ @@ -158,6 +163,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { // We get the tags from 'properties' if available (cloud assembly format >= 6.0.0), otherwise // from the stack metadata this.tags = properties.tags ?? this.tagsFromMetadata(); + this.notificationArns = properties.notificationArns ?? []; this.assumeRoleArn = properties.assumeRoleArn; this.assumeRoleExternalId = properties.assumeRoleExternalId; this.cloudFormationExecutionRoleArn = properties.cloudFormationExecutionRoleArn; diff --git a/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts b/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts index 85009cedd7c23..81d5b4a0c3186 100644 --- a/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts +++ b/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts @@ -21,6 +21,24 @@ afterEach(() => { rimraf(builder.outdir); }); +test('read notification arns from artifact properties', () => { +// GIVEN + const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + builder.addArtifact('Stack', { + ...stackBase, + properties: { + ...stackBase.properties, + notificationArns: NOTIFICATION_ARNS, + }, + }); + + // WHEN + const assembly = builder.buildAssembly(); + + // THEN + expect(assembly.getStackByName('Stack').notificationArns).toEqual(NOTIFICATION_ARNS); +}); + test('read tags from artifact properties', () => { // GIVEN builder.addArtifact('Stack', { diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index bb7f78cfe3553..3003b87c37138 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -122,7 +122,7 @@ "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "@aws-cdk/cloud-assembly-schema": "^38.0.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index c9c934dcee4f2..b8b44a60bc556 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -644,6 +644,12 @@ async function canSkipDeploy( return false; } + // Notification arns have changed + if (!arrayEquals(cloudFormationStack.notificationArns, deployStackOptions.notificationArns ?? [])) { + debug(`${deployName}: notification arns have changed`); + return false; + } + // Termination protection has been updated if (!!deployStackOptions.stack.terminationProtection !== !!cloudFormationStack.terminationProtection) { debug(`${deployName}: termination protection has been updated`); @@ -694,3 +700,7 @@ function suffixWithErrors(msg: string, errors?: string[]) { ? `${msg}: ${errors.join(', ')}` : msg; } + +function arrayEquals(a: any[], b: any[]): boolean { + return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); +} diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index 23e95f6d618e5..2361871e2bef0 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -138,12 +138,21 @@ export class CloudFormationStack { /** * The stack's current tags * - * Empty list of the stack does not exist + * Empty list if the stack does not exist */ public get tags(): CloudFormation.Tags { return this.stack?.Tags || []; } + /** + * SNS Topic ARNs that will receive stack events. + * + * Empty list if the stack does not exist + */ + public get notificationArns(): CloudFormation.NotificationARNs { + return this.stack?.NotificationARNs ?? []; + } + /** * Return the names of all current parameters to the stack * diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index af64056e2fc29..d6c6000092f6d 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -161,7 +161,6 @@ export class CdkToolkit { let changeSet = undefined; if (options.changeSet) { - let stackExists = false; try { stackExists = await this.props.deployments.stackExists({ @@ -214,14 +213,6 @@ export class CdkToolkit { return this.watch(options); } - if (options.notificationArns) { - options.notificationArns.map( arn => { - if (!validateSnsTopicArn(arn)) { - throw new Error(`Notification arn ${arn} is not a valid arn for an SNS topic`); - } - }); - } - const startSynthTime = new Date().getTime(); const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly, options.ignoreNoStacks); @@ -318,7 +309,17 @@ export class CdkToolkit { } } - const stackIndex = stacks.indexOf(stack)+1; + let notificationArns: string[] = []; + notificationArns = notificationArns.concat(options.notificationArns ?? []); + notificationArns = notificationArns.concat(stack.notificationArns); + + notificationArns.map(arn => { + if (!validateSnsTopicArn(arn)) { + throw new Error(`Notification arn ${arn} is not a valid arn for an SNS topic`); + } + }); + + const stackIndex = stacks.indexOf(stack) + 1; print('%s: deploying... [%s/%s]', chalk.bold(stack.displayName), stackIndex, stackCollection.stackCount); const startDeployTime = new Date().getTime(); @@ -335,7 +336,7 @@ export class CdkToolkit { roleArn: options.roleArn, toolkitStackName: options.toolkitStackName, reuseAssets: options.reuseAssets, - notificationArns: options.notificationArns, + notificationArns, tags, execute: options.execute, changeSetName: options.changeSetName, diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 79f9e8064b5d6..7946d4256d6e5 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -96,7 +96,7 @@ "xml-js": "^1.6.11" }, "dependencies": { - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "@aws-cdk/cloud-assembly-schema": "^38.0.0", "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", @@ -104,7 +104,7 @@ "archiver": "^5.3.2", "aws-sdk": "^2.1691.0", "camelcase": "^6.3.0", - "cdk-assets": "^2.151.29", + "cdk-assets": "^2.154.0", "cdk-from-cfn": "^0.162.0", "chalk": "^4", "chokidar": "^3.6.0", diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 666d4f43410ec..4aec7cc9ff7d1 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -460,6 +460,42 @@ test('deploy is not skipped if parameters are different', async () => { })); }); +test('deploy is skipped if notificationArns are the same', async () => { + // GIVEN + givenTemplateIs(FAKE_STACK.template); + givenStackExists({ + NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], + }); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + stack: FAKE_STACK, + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], + }); + + // THEN + expect(cfnMocks.createChangeSet).not.toHaveBeenCalled(); +}); + +test('deploy is not skipped if notificationArns are different', async () => { + // GIVEN + givenTemplateIs(FAKE_STACK.template); + givenStackExists({ + NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], + }); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + stack: FAKE_STACK, + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MagicTopic'], + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalled(); +}); + test('if existing stack failed to create, it is deleted and recreated', async () => { // GIVEN givenStackExists( @@ -624,7 +660,7 @@ test('deploy is not skipped if stack is in a _FAILED state', async () => { await deployStack({ ...standardDeployStackArguments(), usePreviousParameters: true, - }).catch(() => {}); + }).catch(() => { }); // THEN expect(cfnMocks.createChangeSet).toHaveBeenCalled(); diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 9a1cc7f493886..f67c35ad8dae7 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -72,6 +72,8 @@ import { RequireApproval } from '../lib/diff'; import { Configuration } from '../lib/settings'; import { flatten } from '../lib/util'; +process.env.CXAPI_DISABLE_SELECT_BY_ID = '1'; + let cloudExecutable: MockCloudExecutable; let bootstrapper: jest.Mocked; let stderrMock: jest.SpyInstance; @@ -290,11 +292,11 @@ describe('readCurrentTemplate', () => { // GIVEN // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest mockForEnvironment = jest.fn().mockImplementationOnce(() => { throw new Error('error'); }) - .mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true };}); + .mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; }); mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; mockCloudExecutable.sdkProvider.stubSSM({ getParameter() { - return { }; + return {}; }, }); const cdkToolkit = new CdkToolkit({ @@ -336,7 +338,7 @@ describe('readCurrentTemplate', () => { }); mockCloudExecutable.sdkProvider.stubSSM({ getParameter() { - return { }; + return {}; }, }); @@ -505,108 +507,253 @@ describe('deploy', () => { }); }); - test('with sns notification arns', async () => { - // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, - 'Test-Stack-B': { Baz: 'Zinga!' }, - }, notificationArns), + describe('sns notification arns', () => { + beforeEach(() => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, + MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, + MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, + ], + }); }); - // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, + test('with sns notification arns as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await toolkit.deploy({ + // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); - }); - test('fail with incorrect sns notification arns', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, - }, notificationArns), + test('fail with incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-A'] }, + test('with sns notification arns in the executable', async () => { + // GIVEN + const expectedNotificationArns = [ + 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + }); + + test('fail with incorrect sns notification arns in the executable', async () => { + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); + + test('with sns notification arns in the executable and as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + + const expectedNotificationArns = notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']); + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); + }); + + test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); + + test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); + test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); }); + }); - test('globless bootstrap uses environment without question', async () => { + test('globless bootstrap uses environment without question', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); - - // WHEN - await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); + const toolkit = defaultToolkitSetup(); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '56789', - region: 'south-pole', - name: 'aws://56789/south-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + // WHEN + await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); - test('globby bootstrap uses whats in the stacks', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); - cloudExecutable.configuration.settings.set(['app'], 'something'); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '56789', + region: 'south-pole', + name: 'aws://56789/south-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - // WHEN - await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); + test('globby bootstrap uses whats in the stacks', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + cloudExecutable.configuration.settings.set(['app'], 'something'); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'bermuda-triangle-1', - name: 'aws://123456789012/bermuda-triangle-1', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + // WHEN + await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); - test('bootstrap can be invoked without the --app argument', async () => { - // GIVEN - cloudExecutable.configuration.settings.clear(); - const mockSynthesize = jest.fn(); - cloudExecutable.synthesize = mockSynthesize; + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - const toolkit = defaultToolkitSetup(); + test('bootstrap can be invoked without the --app argument', async () => { + // GIVEN + cloudExecutable.configuration.settings.clear(); + const mockSynthesize = jest.fn(); + cloudExecutable.synthesize = mockSynthesize; - // WHEN - await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); + const toolkit = defaultToolkitSetup(); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'west-pole', - name: 'aws://123456789012/west-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + // WHEN + await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); - expect(cloudExecutable.hasApp).toEqual(false); - expect(mockSynthesize).not.toHaveBeenCalled(); - }); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'west-pole', + name: 'aws://123456789012/west-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + + expect(cloudExecutable.hasApp).toEqual(false); + expect(mockSynthesize).not.toHaveBeenCalled(); }); }); @@ -614,7 +761,7 @@ describe('destroy', () => { test('destroy correct stack', async () => { const toolkit = defaultToolkitSetup(); - await expect(() => { + expect(() => { return toolkit.destroy({ selector: { patterns: ['Test-Stack-A/Test-Stack-C'] }, exclusively: true, @@ -877,10 +1024,6 @@ describe('synth', () => { expect(mockData.mock.calls.length).toEqual(0); }); - afterEach(() => { - process.env.STACKS_TO_VALIDATE = undefined; - }); - describe('migrate', () => { const testResourcePath = [__dirname, 'commands', 'test-resources']; const templatePath = [...testResourcePath, 'templates']; @@ -1016,13 +1159,13 @@ describe('synth', () => { }); }); - test('causes synth to fail if autoValidate=true', async() => { + test('causes synth to fail if autoValidate=true', async () => { const toolkit = defaultToolkitSetup(); const autoValidate = true; await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined(); }); - test('causes synth to succeed if autoValidate=false', async() => { + test('causes synth to succeed if autoValidate=false', async () => { const toolkit = defaultToolkitSetup(); const autoValidate = false; await toolkit.synth([], false, true, autoValidate); @@ -1030,7 +1173,7 @@ describe('synth', () => { }); }); - test('stack has error and was explicitly selected', async() => { + test('stack has error and was explicitly selected', async () => { cloudExecutable = new MockCloudExecutable({ stacks: [ MockStack.MOCK_STACK_A, @@ -1146,7 +1289,8 @@ class MockStack { ], }, depends: [MockStack.MOCK_STACK_C.stackName], - } + }; + public static readonly MOCK_STACK_WITH_ERROR: TestStackArtifact = { stackName: 'witherrors', env: 'aws://123456789012/bermuda-triangle-1', @@ -1178,6 +1322,39 @@ class MockStack { }, }, } + public static readonly MOCK_STACK_WITH_NOTIFICATION_ARNS: TestStackArtifact = { + stackName: 'Test-Stack-Notification-Arns', + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'], + template: { Resources: { TemplateName: 'Test-Stack-Notification-Arns' } }, + env: 'aws://123456789012/bermuda-triangle-1337', + metadata: { + '/Test-Stack-Notification-Arns': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Foo', value: 'Bar' }, + ], + }, + ], + }, + } + + public static readonly MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS: TestStackArtifact = { + stackName: 'Test-Stack-Bad-Notification-Arns', + notificationArns: ['arn:1337:123456789012:sns:bad'], + template: { Resources: { TemplateName: 'Test-Stack-Bad-Notification-Arns' } }, + env: 'aws://123456789012/bermuda-triangle-1337', + metadata: { + '/Test-Stack-Bad-Notification-Arns': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Foo', value: 'Bar' }, + ], + }, + ], + }, + } } class FakeCloudFormation extends Deployments { @@ -1195,9 +1372,7 @@ class FakeCloudFormation extends Deployments { Object.entries(tags).map(([Key, Value]) => ({ Key, Value })) .sort((l, r) => l.Key.localeCompare(r.Key)); } - if (expectedNotificationArns) { - this.expectedNotificationArns = expectedNotificationArns; - } + this.expectedNotificationArns = expectedNotificationArns ?? []; } public deployStack(options: DeployStackOptions): Promise { @@ -1205,7 +1380,11 @@ class FakeCloudFormation extends Deployments { MockStack.MOCK_STACK_A.stackName, MockStack.MOCK_STACK_B.stackName, MockStack.MOCK_STACK_C.stackName, + // MockStack.MOCK_STACK_D deliberately omitted. MockStack.MOCK_STACK_WITH_ASSET.stackName, + MockStack.MOCK_STACK_WITH_ERROR.stackName, + MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName, + MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS.stackName, ]).toContain(options.stack.stackName); if (this.expectedTags[options.stack.stackName]) { @@ -1236,8 +1415,12 @@ class FakeCloudFormation extends Deployments { return Promise.resolve({}); case MockStack.MOCK_STACK_WITH_ASSET.stackName: return Promise.resolve({}); + case MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName: + return Promise.resolve({}); + case MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS.stackName: + return Promise.resolve({}); default: - return Promise.reject(`Not an expected mock stack: ${stack.stackName}`); + throw new Error(`not an expected mock stack: ${stack.stackName}`); } } } diff --git a/packages/aws-cdk/test/util.ts b/packages/aws-cdk/test/util.ts index 879d6572f369b..1f059836d670d 100644 --- a/packages/aws-cdk/test/util.ts +++ b/packages/aws-cdk/test/util.ts @@ -16,6 +16,7 @@ export interface TestStackArtifact { env?: string; depends?: string[]; metadata?: cxapi.StackMetadata; + notificationArns?: string[]; /** Old-style assets */ assets?: cxschema.AssetMetadataEntry[]; @@ -101,6 +102,7 @@ function addAttributes(assembly: TestAssembly, builder: cxapi.CloudAssemblyBuild ...stack.properties, templateFile, terminationProtection: stack.terminationProtection, + notificationArns: stack.notificationArns, }, displayName: stack.displayName, }); diff --git a/yarn.lock b/yarn.lock index 44bb741b65481..3c101cd0d04e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -59,20 +59,20 @@ "@aws-cdk/service-spec-types" "^0.0.91" "@cdklabs/tskb" "^0.0.3" -"@aws-cdk/cloud-assembly-schema@^36.0.24": - version "36.0.24" - resolved "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-36.0.24.tgz#f6f05615223e800771ca99c88ad631c32b33d642" - integrity sha512-dHyb4lvd6nbNHLVvdyxVPgwc0MyzN3VzIJnWwGJWKOIwVqL7hvU2NkQQrktY9T2MtdhzUdDFm9qluxuLRV5Cfw== +"@aws-cdk/cloud-assembly-schema@^38.0.0": + version "38.0.1" + resolved "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz#cdf4684ae8778459e039cd44082ea644a3504ca9" + integrity sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w== dependencies: jsonschema "^1.4.1" semver "^7.6.3" -"@aws-cdk/cx-api@^2.157.0": - version "2.157.0" - resolved "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-2.157.0.tgz#c0721f1d27778dd740c98f12efaf89b105cf89ce" - integrity sha512-PRZAbVVPyhcrnNW4tmSKIp8WMxjo9ImSa1K7MAbK8ufafD+KFWGQgxhVIl3bY6WVeZVaduoXRD2gQZwUaDj3XQ== +"@aws-cdk/cx-api@^2.158.0": + version "2.159.0" + resolved "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-2.159.0.tgz#567c0ae0d7a6fc2f7cb9bda7e6cb23fac8d99094" + integrity sha512-HVkHCKQjVi3PCSOF22zLztZMEL+cJcyVvFctS3vXPetgl77L+e/onaGt1AUwRcNY44tvbqJm3oIVQt2HqM3q7w== dependencies: - semver "^7.6.2" + semver "^7.6.3" "@aws-cdk/lambda-layer-kubectl-v24@^2.0.242": version "2.0.242" @@ -8754,13 +8754,13 @@ case@1.6.3, case@^1.6.3: resolved "https://registry.npmjs.org/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== -cdk-assets@^2.151.29: - version "2.151.30" - resolved "https://registry.npmjs.org/cdk-assets/-/cdk-assets-2.151.30.tgz#6b8bc669540641370c18de8b103821cc1c44ec3e" - integrity sha512-adI0yZIJDh/rz/9FUMcAKokc/9BSj2NzTkVYe03Ca4347SHsWzzLwo3fRFywfCg4/QNGvh9aNNNz6YR7zLII9g== +cdk-assets@^2.154.0: + version "2.154.0" + resolved "https://registry.npmjs.org/cdk-assets/-/cdk-assets-2.154.0.tgz#675d239c0156ca05c4a2809b30858c843f984ead" + integrity sha512-8M3zLHCx8nj5Fv5ubEps53jh22NN9G7ZLuq1AJwPdXZP7+nb4q5tdl2Ah2ZPMM/dob9u3KTwNeN34oLKHfDzbw== dependencies: - "@aws-cdk/cloud-assembly-schema" "^36.0.24" - "@aws-cdk/cx-api" "^2.157.0" + "@aws-cdk/cloud-assembly-schema" "^38.0.0" + "@aws-cdk/cx-api" "^2.158.0" archiver "^5.3.2" aws-sdk "^2.1691.0" glob "^7.2.3" @@ -16090,7 +16090,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2, semver@^7.6.3: +semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: version "7.6.3" resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -16556,16 +16556,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@*, string-width@^1.0.1, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.0.1, string-width@^5.1.2: +"string-width-cjs@npm:string-width@^4.2.0", string-width@*, string-width@^1.0.1, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.0.1, string-width@^5.1.2: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16630,7 +16621,7 @@ stringify-package@^1.0.1: resolved "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -16644,13 +16635,6 @@ strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -17648,7 +17632,7 @@ workerpool@^6.5.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17666,15 +17650,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"