@aws-cdk-testing/cli-integ 2.173.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.eslintrc.js +9 -0
  2. package/LICENSE +2 -1
  3. package/bin/query-github.js +3 -3
  4. package/bin/query-github.ts +56 -0
  5. package/bin/run-suite.js +3 -3
  6. package/bin/run-suite.ts +140 -0
  7. package/bin/stage-distribution.js +3 -2
  8. package/bin/stage-distribution.ts +267 -0
  9. package/bin/test-root.ts +3 -0
  10. package/lib/aws.js +9 -6
  11. package/lib/aws.ts +263 -0
  12. package/lib/corking.ts +33 -0
  13. package/lib/eventually.js +3 -3
  14. package/lib/eventually.ts +42 -0
  15. package/lib/files.js +3 -2
  16. package/lib/files.ts +80 -0
  17. package/lib/github.js +6 -5
  18. package/lib/github.ts +43 -0
  19. package/lib/index.ts +13 -0
  20. package/lib/integ-test.ts +81 -0
  21. package/lib/lists.ts +9 -0
  22. package/lib/memoize.ts +14 -0
  23. package/lib/npm.ts +41 -0
  24. package/lib/package-sources/release-source.js +3 -2
  25. package/lib/package-sources/release-source.ts +81 -0
  26. package/lib/package-sources/repo-source.ts +111 -0
  27. package/lib/package-sources/repo-tools/npm.js +5 -4
  28. package/lib/package-sources/repo-tools/npm.ts +48 -0
  29. package/lib/package-sources/source.ts +35 -0
  30. package/lib/package-sources/subprocess.ts +15 -0
  31. package/lib/resource-pool.js +2 -2
  32. package/lib/resource-pool.ts +140 -0
  33. package/lib/resources.ts +4 -0
  34. package/lib/shell.js +8 -5
  35. package/lib/shell.ts +168 -0
  36. package/lib/staging/codeartifact.js +11 -8
  37. package/lib/staging/codeartifact.ts +387 -0
  38. package/lib/staging/maven.js +5 -3
  39. package/lib/staging/maven.ts +95 -0
  40. package/lib/staging/npm.ts +62 -0
  41. package/lib/staging/nuget.ts +75 -0
  42. package/lib/staging/parallel-shell.js +2 -2
  43. package/lib/staging/parallel-shell.ts +51 -0
  44. package/lib/staging/pypi.ts +50 -0
  45. package/lib/staging/usage-dir.ts +99 -0
  46. package/lib/with-aws.js +3 -2
  47. package/lib/with-aws.ts +67 -0
  48. package/lib/with-cdk-app.js +23 -14
  49. package/lib/with-cdk-app.ts +742 -0
  50. package/lib/with-cli-lib.ts +134 -0
  51. package/lib/with-packages.ts +15 -0
  52. package/lib/with-sam.js +7 -4
  53. package/lib/with-sam.ts +288 -0
  54. package/lib/with-temporary-directory.ts +35 -0
  55. package/lib/with-timeout.ts +33 -0
  56. package/lib/xpmutex.js +2 -2
  57. package/lib/xpmutex.ts +218 -0
  58. package/package.json +84 -62
  59. package/resources/cloud-assemblies/0.36.0/cdk.out +1 -0
  60. package/resources/cloud-assemblies/1.10.0-lookup-default-vpc/cdk.out +1 -0
  61. package/resources/cloud-assemblies/1.10.0-request-azs/cdk.out +1 -0
  62. package/tests/cli-integ-tests/bootstrapping.integtest.js +22 -13
  63. package/tests/cli-integ-tests/bootstrapping.integtest.ts +493 -0
  64. package/tests/cli-integ-tests/cli-lib.integtest.js +3 -2
  65. package/tests/cli-integ-tests/cli-lib.integtest.ts +90 -0
  66. package/tests/cli-integ-tests/cli.integtest.js +76 -49
  67. package/tests/cli-integ-tests/cli.integtest.ts +2874 -0
  68. package/tests/cli-integ-tests/garbage-collection.integtest.js +2 -2
  69. package/tests/cli-integ-tests/garbage-collection.integtest.ts +392 -0
  70. package/tests/init-csharp/init-csharp.integtest.ts +15 -0
  71. package/tests/init-fsharp/init-fsharp.integtest.ts +15 -0
  72. package/tests/init-go/init-go.integtest.ts +23 -0
  73. package/tests/init-java/init-java.integtest.ts +14 -0
  74. package/tests/init-javascript/init-javascript.integtest.ts +59 -0
  75. package/tests/init-python/init-python.integtest.ts +20 -0
  76. package/tests/init-typescript-app/init-typescript-app.integtest.ts +66 -0
  77. package/tests/init-typescript-lib/init-typescript-lib.integtest.ts +13 -0
  78. package/tests/tool-integrations/amplify.integtest.ts +43 -0
  79. package/tests/tool-integrations/with-tool-context.ts +14 -0
  80. package/tests/uberpackage/uberpackage.integtest.ts +11 -0
  81. package/resources/cdk-apps/cfn-include-app/.gitignore +0 -1
@@ -0,0 +1,2874 @@
1
+ import { existsSync, promises as fs } from 'fs';
2
+ import * as querystring from 'node:querystring';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import {
6
+ CreateStackCommand,
7
+ DescribeStackResourcesCommand,
8
+ DescribeStacksCommand,
9
+ GetTemplateCommand,
10
+ ListChangeSetsCommand,
11
+ UpdateStackCommand,
12
+ waitUntilStackUpdateComplete,
13
+ } from '@aws-sdk/client-cloudformation';
14
+ import { DescribeServicesCommand } from '@aws-sdk/client-ecs';
15
+ import {
16
+ CreateRoleCommand,
17
+ DeleteRoleCommand,
18
+ DeleteRolePolicyCommand,
19
+ ListRolePoliciesCommand,
20
+ PutRolePolicyCommand,
21
+ } from '@aws-sdk/client-iam';
22
+ import { InvokeCommand } from '@aws-sdk/client-lambda';
23
+ import { PutObjectLockConfigurationCommand } from '@aws-sdk/client-s3';
24
+ import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns';
25
+ import { AssumeRoleCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
26
+ import * as mockttp from 'mockttp';
27
+ import { CompletedRequest } from 'mockttp';
28
+ import {
29
+ cloneDirectory,
30
+ integTest,
31
+ randomInteger,
32
+ randomString,
33
+ RESOURCES_DIR,
34
+ retry,
35
+ shell,
36
+ sleep,
37
+ withCDKMigrateFixture,
38
+ withDefaultFixture,
39
+ withExtendedTimeoutFixture,
40
+ withoutBootstrap,
41
+ withSamIntegrationFixture,
42
+ withSpecificFixture,
43
+ } from '../../lib';
44
+
45
+ jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime
46
+
47
+ describe('ci', () => {
48
+ integTest(
49
+ 'output to stderr',
50
+ withDefaultFixture(async (fixture) => {
51
+ const deployOutput = await fixture.cdkDeploy('test-2', { captureStderr: true, onlyStderr: true });
52
+ const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], {
53
+ captureStderr: true,
54
+ onlyStderr: true,
55
+ });
56
+ const destroyOutput = await fixture.cdkDestroy('test-2', { captureStderr: true, onlyStderr: true });
57
+ expect(deployOutput).not.toEqual('');
58
+ expect(destroyOutput).not.toEqual('');
59
+ expect(diffOutput).not.toEqual('');
60
+ }),
61
+ );
62
+ describe('ci=true', () => {
63
+ integTest(
64
+ 'output to stdout',
65
+ withDefaultFixture(async (fixture) => {
66
+ const execOptions = {
67
+ captureStderr: true,
68
+ onlyStderr: true,
69
+ modEnv: {
70
+ CI: 'true',
71
+ JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: 'true',
72
+ JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: 'true',
73
+ JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: 'true',
74
+ },
75
+ };
76
+
77
+ const deployOutput = await fixture.cdkDeploy('test-2', execOptions);
78
+ const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], execOptions);
79
+ const destroyOutput = await fixture.cdkDestroy('test-2', execOptions);
80
+ expect(deployOutput).toEqual('');
81
+ expect(destroyOutput).toEqual('');
82
+ expect(diffOutput).toEqual('');
83
+ }),
84
+ );
85
+ });
86
+ });
87
+
88
+ integTest(
89
+ 'VPC Lookup',
90
+ withDefaultFixture(async (fixture) => {
91
+ fixture.log('Making sure we are clean before starting.');
92
+ await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } });
93
+
94
+ fixture.log('Setting up: creating a VPC with known tags');
95
+ await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } });
96
+ fixture.log('Setup complete!');
97
+
98
+ fixture.log('Verifying we can now import that VPC');
99
+ await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } });
100
+ }),
101
+ );
102
+
103
+ // testing a construct with a builtin Nodejs Lambda Function.
104
+ // In this case we are testing the s3.Bucket construct with the
105
+ // autoDeleteObjects prop set to true, which creates a Lambda backed
106
+ // CustomResource. Since the compiled Lambda code (e.g. __entrypoint__.js)
107
+ // is bundled as part of the CDK package, we want to make sure we don't
108
+ // introduce changes to the compiled code that could prevent the Lambda from
109
+ // executing. If we do, this test will timeout and fail.
110
+ integTest(
111
+ 'Construct with builtin Lambda function',
112
+ withDefaultFixture(async (fixture) => {
113
+ await fixture.cdkDeploy('builtin-lambda-function');
114
+ fixture.log('Setup complete!');
115
+ await fixture.cdkDestroy('builtin-lambda-function');
116
+ }),
117
+ );
118
+
119
+ // this is to ensure that asset bundling for apps under a stage does not break
120
+ integTest(
121
+ 'Stage with bundled Lambda function',
122
+ withDefaultFixture(async (fixture) => {
123
+ await fixture.cdkDeploy('bundling-stage/BundlingStack');
124
+ fixture.log('Setup complete!');
125
+ await fixture.cdkDestroy('bundling-stage/BundlingStack');
126
+ }),
127
+ );
128
+
129
+ integTest(
130
+ 'Two ways of showing the version',
131
+ withDefaultFixture(async (fixture) => {
132
+ const version1 = await fixture.cdk(['version'], { verbose: false });
133
+ const version2 = await fixture.cdk(['--version'], { verbose: false });
134
+
135
+ expect(version1).toEqual(version2);
136
+ }),
137
+ );
138
+
139
+ integTest(
140
+ 'Termination protection',
141
+ withDefaultFixture(async (fixture) => {
142
+ const stackName = 'termination-protection';
143
+ await fixture.cdkDeploy(stackName);
144
+
145
+ // Try a destroy that should fail
146
+ await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error');
147
+
148
+ // Can update termination protection even though the change set doesn't contain changes
149
+ await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } });
150
+ await fixture.cdkDestroy(stackName);
151
+ }),
152
+ );
153
+
154
+ integTest(
155
+ 'cdk synth',
156
+ withDefaultFixture(async (fixture) => {
157
+ await fixture.cdk(['synth', fixture.fullStackName('test-1')]);
158
+ expect(fixture.template('test-1')).toEqual(
159
+ expect.objectContaining({
160
+ Resources: {
161
+ topic69831491: {
162
+ Type: 'AWS::SNS::Topic',
163
+ Metadata: {
164
+ 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`,
165
+ },
166
+ },
167
+ },
168
+ }),
169
+ );
170
+
171
+ expect(
172
+ await fixture.cdkSynth({
173
+ options: [fixture.fullStackName('test-1')],
174
+ }),
175
+ ).not.toEqual(
176
+ expect.stringContaining(`
177
+ Rules:
178
+ CheckBootstrapVersion:`),
179
+ );
180
+
181
+ await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false });
182
+ expect(fixture.template('test-2')).toEqual(
183
+ expect.objectContaining({
184
+ Resources: {
185
+ topic152D84A37: {
186
+ Type: 'AWS::SNS::Topic',
187
+ Metadata: {
188
+ 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`,
189
+ },
190
+ },
191
+ topic2A4FB547F: {
192
+ Type: 'AWS::SNS::Topic',
193
+ Metadata: {
194
+ 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`,
195
+ },
196
+ },
197
+ },
198
+ }),
199
+ );
200
+ }),
201
+ );
202
+
203
+ integTest(
204
+ 'ssm parameter provider error',
205
+ withDefaultFixture(async (fixture) => {
206
+ await expect(
207
+ fixture.cdk(
208
+ ['synth', fixture.fullStackName('missing-ssm-parameter'), '-c', 'test:ssm-parameter-name=/does/not/exist'],
209
+ {
210
+ allowErrExit: true,
211
+ },
212
+ ),
213
+ ).resolves.toContain('SSM parameter not available in account');
214
+ }),
215
+ );
216
+
217
+ integTest(
218
+ 'automatic ordering',
219
+ withDefaultFixture(async (fixture) => {
220
+ // Deploy the consuming stack which will include the producing stack
221
+ await fixture.cdkDeploy('order-consuming');
222
+
223
+ // Destroy the providing stack which will include the consuming stack
224
+ await fixture.cdkDestroy('order-providing');
225
+ }),
226
+ );
227
+
228
+ integTest(
229
+ 'automatic ordering with concurrency',
230
+ withDefaultFixture(async (fixture) => {
231
+ // Deploy the consuming stack which will include the producing stack
232
+ await fixture.cdkDeploy('order-consuming', { options: ['--concurrency', '2'] });
233
+
234
+ // Destroy the providing stack which will include the consuming stack
235
+ await fixture.cdkDestroy('order-providing');
236
+ }),
237
+ );
238
+
239
+ integTest(
240
+ '--exclusively selects only selected stack',
241
+ withDefaultFixture(async (fixture) => {
242
+ // Deploy the "depends-on-failed" stack, with --exclusively. It will NOT fail (because
243
+ // of --exclusively) and it WILL create an output we can check for to confirm that it did
244
+ // get deployed.
245
+ const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json');
246
+ await fs.mkdir(path.dirname(outputsFile), { recursive: true });
247
+
248
+ await fixture.cdkDeploy('depends-on-failed', {
249
+ options: ['--exclusively', '--outputs-file', outputsFile],
250
+ });
251
+
252
+ // Verify the output to see that the stack deployed
253
+ const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString());
254
+ expect(outputs).toEqual({
255
+ [`${fixture.stackNamePrefix}-depends-on-failed`]: {
256
+ TopicName: `${fixture.stackNamePrefix}-depends-on-failedMyTopic`,
257
+ },
258
+ });
259
+ }),
260
+ );
261
+
262
+ integTest(
263
+ 'context setting',
264
+ withDefaultFixture(async (fixture) => {
265
+ await fs.writeFile(
266
+ path.join(fixture.integTestDir, 'cdk.context.json'),
267
+ JSON.stringify({
268
+ contextkey: 'this is the context value',
269
+ }),
270
+ );
271
+ try {
272
+ await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value');
273
+
274
+ // Test that deleting the contextkey works
275
+ await fixture.cdk(['context', '--reset', 'contextkey']);
276
+ await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value');
277
+
278
+ // Test that forced delete of the context key does not throw
279
+ await fixture.cdk(['context', '-f', '--reset', 'contextkey']);
280
+ } finally {
281
+ await fs.unlink(path.join(fixture.integTestDir, 'cdk.context.json'));
282
+ }
283
+ }),
284
+ );
285
+
286
+ // bootstrapping also performs synthesis. As it turns out, bootstrap-stage synthesis still causes the lookups to be cached, meaning that the lookup never
287
+ // happens when we actually call `cdk synth --no-lookups`. This results in the error never being thrown, because it never tries to lookup anything.
288
+ // Fix this by not trying to bootstrap; there's no need to bootstrap anyway, since the test never tries to deploy anything.
289
+ integTest(
290
+ 'context in stage propagates to top',
291
+ withoutBootstrap(async (fixture) => {
292
+ await expect(
293
+ fixture.cdkSynth({
294
+ // This will make it error to prove that the context bubbles up, and also that we can fail on command
295
+ options: ['--no-lookups'],
296
+ modEnv: {
297
+ INTEG_STACK_SET: 'stage-using-context',
298
+ },
299
+ allowErrExit: true,
300
+ }),
301
+ ).resolves.toContain('Context lookups have been disabled');
302
+ }),
303
+ );
304
+
305
+ integTest(
306
+ 'deploy',
307
+ withDefaultFixture(async (fixture) => {
308
+ const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false });
309
+
310
+ // verify the number of resources in the stack
311
+ const response = await fixture.aws.cloudFormation.send(
312
+ new DescribeStackResourcesCommand({
313
+ StackName: stackArn,
314
+ }),
315
+ );
316
+ expect(response.StackResources?.length).toEqual(2);
317
+ }),
318
+ );
319
+
320
+ integTest(
321
+ 'deploy --method=direct',
322
+ withDefaultFixture(async (fixture) => {
323
+ const stackArn = await fixture.cdkDeploy('test-2', {
324
+ options: ['--method=direct'],
325
+ captureStderr: false,
326
+ });
327
+
328
+ // verify the number of resources in the stack
329
+ const response = await fixture.aws.cloudFormation.send(
330
+ new DescribeStackResourcesCommand({
331
+ StackName: stackArn,
332
+ }),
333
+ );
334
+ expect(response.StackResources?.length).toBeGreaterThan(0);
335
+ }),
336
+ );
337
+
338
+ integTest(
339
+ 'deploy all',
340
+ withDefaultFixture(async (fixture) => {
341
+ const arns = await fixture.cdkDeploy('test-*', { captureStderr: false });
342
+
343
+ // verify that we only deployed both stacks (there are 2 ARNs in the output)
344
+ expect(arns.split('\n').length).toEqual(2);
345
+ }),
346
+ );
347
+
348
+ integTest(
349
+ 'deploy all concurrently',
350
+ withDefaultFixture(async (fixture) => {
351
+ const arns = await fixture.cdkDeploy('test-*', {
352
+ captureStderr: false,
353
+ options: ['--concurrency', '2'],
354
+ });
355
+
356
+ // verify that we only deployed both stacks (there are 2 ARNs in the output)
357
+ expect(arns.split('\n').length).toEqual(2);
358
+ }),
359
+ );
360
+
361
+ integTest('doubly nested stack',
362
+ withDefaultFixture(async (fixture) => {
363
+ await fixture.cdkDeploy('with-doubly-nested-stack', {
364
+ captureStderr: false,
365
+ });
366
+ }));
367
+
368
+ integTest(
369
+ 'nested stack with parameters',
370
+ withDefaultFixture(async (fixture) => {
371
+ // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances
372
+ // of this test to run in parallel, othewise they will attempt to create the same SNS topic.
373
+ const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', {
374
+ options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`],
375
+ captureStderr: false,
376
+ });
377
+
378
+ // verify that we only deployed a single stack (there's a single ARN in the output)
379
+ expect(stackArn.split('\n').length).toEqual(1);
380
+
381
+ // verify the number of resources in the stack
382
+ const response = await fixture.aws.cloudFormation.send(
383
+ new DescribeStackResourcesCommand({
384
+ StackName: stackArn,
385
+ }),
386
+ );
387
+ expect(response.StackResources?.length).toEqual(1);
388
+ }),
389
+ );
390
+
391
+ integTest(
392
+ 'deploy without execute a named change set',
393
+ withDefaultFixture(async (fixture) => {
394
+ const changeSetName = 'custom-change-set-name';
395
+ const stackArn = await fixture.cdkDeploy('test-2', {
396
+ options: ['--no-execute', '--change-set-name', changeSetName],
397
+ captureStderr: false,
398
+ });
399
+ // verify that we only deployed a single stack (there's a single ARN in the output)
400
+ expect(stackArn.split('\n').length).toEqual(1);
401
+
402
+ const response = await fixture.aws.cloudFormation.send(
403
+ new DescribeStacksCommand({
404
+ StackName: stackArn,
405
+ }),
406
+ );
407
+ expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');
408
+
409
+ //verify a change set was created with the provided name
410
+ const changeSetResponse = await fixture.aws.cloudFormation.send(
411
+ new ListChangeSetsCommand({
412
+ StackName: stackArn,
413
+ }),
414
+ );
415
+ const changeSets = changeSetResponse.Summaries || [];
416
+ expect(changeSets.length).toEqual(1);
417
+ expect(changeSets[0].ChangeSetName).toEqual(changeSetName);
418
+ expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
419
+ }),
420
+ );
421
+
422
+ integTest(
423
+ 'security related changes without a CLI are expected to fail',
424
+ withDefaultFixture(async (fixture) => {
425
+ // redirect /dev/null to stdin, which means there will not be tty attached
426
+ // since this stack includes security-related changes, the deployment should
427
+ // immediately fail because we can't confirm the changes
428
+ const stackName = 'iam-test';
429
+ await expect(
430
+ fixture.cdkDeploy(stackName, {
431
+ options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true.
432
+ neverRequireApproval: false,
433
+ }),
434
+ ).rejects.toThrow('exited with error');
435
+
436
+ // Ensure stack was not deployed
437
+ await expect(
438
+ fixture.aws.cloudFormation.send(
439
+ new DescribeStacksCommand({
440
+ StackName: fixture.fullStackName(stackName),
441
+ }),
442
+ ),
443
+ ).rejects.toThrow('does not exist');
444
+ }),
445
+ );
446
+
447
+ integTest(
448
+ 'deploy wildcard with outputs',
449
+ withDefaultFixture(async (fixture) => {
450
+ const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json');
451
+ await fs.mkdir(path.dirname(outputsFile), { recursive: true });
452
+
453
+ await fixture.cdkDeploy(['outputs-test-*'], {
454
+ options: ['--outputs-file', outputsFile],
455
+ });
456
+
457
+ const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString());
458
+ expect(outputs).toEqual({
459
+ [`${fixture.stackNamePrefix}-outputs-test-1`]: {
460
+ TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`,
461
+ },
462
+ [`${fixture.stackNamePrefix}-outputs-test-2`]: {
463
+ TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`,
464
+ },
465
+ });
466
+ }),
467
+ );
468
+
469
+ integTest(
470
+ 'deploy with parameters',
471
+ withDefaultFixture(async (fixture) => {
472
+ const stackArn = await fixture.cdkDeploy('param-test-1', {
473
+ options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`],
474
+ captureStderr: false,
475
+ });
476
+
477
+ const response = await fixture.aws.cloudFormation.send(
478
+ new DescribeStacksCommand({
479
+ StackName: stackArn,
480
+ }),
481
+ );
482
+
483
+ expect(response.Stacks?.[0].Parameters).toContainEqual({
484
+ ParameterKey: 'TopicNameParam',
485
+ ParameterValue: `${fixture.stackNamePrefix}bazinga`,
486
+ });
487
+ }),
488
+ );
489
+
490
+ integTest(
491
+ 'update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one',
492
+ withDefaultFixture(async (fixture) => {
493
+ // GIVEN
494
+ await expect(
495
+ fixture.cdkDeploy('param-test-1', {
496
+ options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`],
497
+ captureStderr: false,
498
+ }),
499
+ ).rejects.toThrow('exited with error');
500
+
501
+ const response = await fixture.aws.cloudFormation.send(
502
+ new DescribeStacksCommand({
503
+ StackName: fixture.fullStackName('param-test-1'),
504
+ }),
505
+ );
506
+
507
+ const stackArn = response.Stacks?.[0].StackId;
508
+ expect(response.Stacks?.[0].StackStatus).toEqual('ROLLBACK_COMPLETE');
509
+
510
+ // WHEN
511
+ const newStackArn = await fixture.cdkDeploy('param-test-1', {
512
+ options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`],
513
+ captureStderr: false,
514
+ });
515
+
516
+ const newStackResponse = await fixture.aws.cloudFormation.send(
517
+ new DescribeStacksCommand({
518
+ StackName: newStackArn,
519
+ }),
520
+ );
521
+
522
+ // THEN
523
+ expect(stackArn).not.toEqual(newStackArn); // new stack was created
524
+ expect(newStackResponse.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
525
+ expect(newStackResponse.Stacks?.[0].Parameters).toContainEqual({
526
+ ParameterKey: 'TopicNameParam',
527
+ ParameterValue: `${fixture.stackNamePrefix}allgood`,
528
+ });
529
+ }),
530
+ );
531
+
532
+ integTest(
533
+ 'stack in UPDATE_ROLLBACK_COMPLETE state can be updated',
534
+ withDefaultFixture(async (fixture) => {
535
+ // GIVEN
536
+ const stackArn = await fixture.cdkDeploy('param-test-1', {
537
+ options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`],
538
+ captureStderr: false,
539
+ });
540
+
541
+ let response = await fixture.aws.cloudFormation.send(
542
+ new DescribeStacksCommand({
543
+ StackName: stackArn,
544
+ }),
545
+ );
546
+
547
+ expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
548
+
549
+ // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE
550
+ await expect(
551
+ fixture.cdkDeploy('param-test-1', {
552
+ options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`],
553
+ captureStderr: false,
554
+ }),
555
+ ).rejects.toThrow('exited with error');
556
+
557
+ response = await fixture.aws.cloudFormation.send(
558
+ new DescribeStacksCommand({
559
+ StackName: stackArn,
560
+ }),
561
+ );
562
+
563
+ expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE');
564
+
565
+ // WHEN
566
+ await fixture.cdkDeploy('param-test-1', {
567
+ options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`],
568
+ captureStderr: false,
569
+ });
570
+
571
+ response = await fixture.aws.cloudFormation.send(
572
+ new DescribeStacksCommand({
573
+ StackName: stackArn,
574
+ }),
575
+ );
576
+
577
+ // THEN
578
+ expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE');
579
+ expect(response.Stacks?.[0].Parameters).toContainEqual({
580
+ ParameterKey: 'TopicNameParam',
581
+ ParameterValue: `${fixture.stackNamePrefix}allgood`,
582
+ });
583
+ }),
584
+ );
585
+
586
+ integTest(
587
+ 'deploy with wildcard and parameters',
588
+ withDefaultFixture(async (fixture) => {
589
+ await fixture.cdkDeploy('param-test-*', {
590
+ options: [
591
+ '--parameters',
592
+ `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`,
593
+ '--parameters',
594
+ `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`,
595
+ '--parameters',
596
+ `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`,
597
+ '--parameters',
598
+ `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`,
599
+ ],
600
+ });
601
+ }),
602
+ );
603
+
604
+ integTest(
605
+ 'deploy with parameters multi',
606
+ withDefaultFixture(async (fixture) => {
607
+ const paramVal1 = `${fixture.stackNamePrefix}bazinga`;
608
+ const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`;
609
+
610
+ const stackArn = await fixture.cdkDeploy('param-test-3', {
611
+ options: ['--parameters', `DisplayNameParam=${paramVal1}`, '--parameters', `OtherDisplayNameParam=${paramVal2}`],
612
+ captureStderr: false,
613
+ });
614
+
615
+ const response = await fixture.aws.cloudFormation.send(
616
+ new DescribeStacksCommand({
617
+ StackName: stackArn,
618
+ }),
619
+ );
620
+
621
+ expect(response.Stacks?.[0].Parameters).toContainEqual({
622
+ ParameterKey: 'DisplayNameParam',
623
+ ParameterValue: paramVal1,
624
+ });
625
+ expect(response.Stacks?.[0].Parameters).toContainEqual({
626
+ ParameterKey: 'OtherDisplayNameParam',
627
+ ParameterValue: paramVal2,
628
+ });
629
+ }),
630
+ );
631
+
632
+ integTest(
633
+ 'deploy with notification ARN as flag',
634
+ withDefaultFixture(async (fixture) => {
635
+ const topicName = `${fixture.stackNamePrefix}-test-topic-flag`;
636
+
637
+ const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName }));
638
+ const topicArn = response.TopicArn!;
639
+
640
+ try {
641
+ await fixture.cdkDeploy('notification-arns', {
642
+ options: ['--notification-arns', topicArn],
643
+ });
644
+
645
+ // verify that the stack we deployed has our notification ARN
646
+ const describeResponse = await fixture.aws.cloudFormation.send(
647
+ new DescribeStacksCommand({
648
+ StackName: fixture.fullStackName('notification-arns'),
649
+ }),
650
+ );
651
+ expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]);
652
+ } finally {
653
+ await fixture.aws.sns.send(
654
+ new DeleteTopicCommand({
655
+ TopicArn: topicArn,
656
+ }),
657
+ );
658
+ }
659
+ }),
660
+ );
661
+
662
+ integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixture) => {
663
+ const topicName = `${fixture.stackNamePrefix}-test-topic-prop`;
664
+
665
+ const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName }));
666
+ const topicArn = response.TopicArn!;
667
+
668
+ try {
669
+ await fixture.cdkDeploy('notification-arns', {
670
+ modEnv: {
671
+ INTEG_NOTIFICATION_ARNS: topicArn,
672
+
673
+ },
674
+ });
675
+
676
+ // verify that the stack we deployed has our notification ARN
677
+ const describeResponse = await fixture.aws.cloudFormation.send(
678
+ new DescribeStacksCommand({
679
+ StackName: fixture.fullStackName('notification-arns'),
680
+ }),
681
+ );
682
+ expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]);
683
+ } finally {
684
+ await fixture.aws.sns.send(
685
+ new DeleteTopicCommand({
686
+ TopicArn: topicArn,
687
+ }),
688
+ );
689
+ }
690
+ }));
691
+
692
+ // https://github.com/aws/aws-cdk/issues/32153
693
+ integTest('deploy preserves existing notification arns when not specified', withDefaultFixture(async (fixture) => {
694
+ const topicName = `${fixture.stackNamePrefix}-topic`;
695
+
696
+ const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName }));
697
+ const topicArn = response.TopicArn!;
698
+
699
+ try {
700
+ await fixture.cdkDeploy('notification-arns');
701
+
702
+ // add notification arns externally to cdk
703
+ await fixture.aws.cloudFormation.send(
704
+ new UpdateStackCommand({
705
+ StackName: fixture.fullStackName('notification-arns'),
706
+ UsePreviousTemplate: true,
707
+ NotificationARNs: [topicArn],
708
+ }),
709
+ );
710
+
711
+ await waitUntilStackUpdateComplete(
712
+ {
713
+ client: fixture.aws.cloudFormation,
714
+ maxWaitTime: 600,
715
+ },
716
+ { StackName: fixture.fullStackName('notification-arns') },
717
+ );
718
+
719
+ // deploy again
720
+ await fixture.cdkDeploy('notification-arns');
721
+
722
+ // make sure the notification arn is preserved
723
+ const describeResponse = await fixture.aws.cloudFormation.send(
724
+ new DescribeStacksCommand({
725
+ StackName: fixture.fullStackName('notification-arns'),
726
+ }),
727
+ );
728
+ expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]);
729
+ } finally {
730
+ await fixture.aws.sns.send(
731
+ new DeleteTopicCommand({
732
+ TopicArn: topicArn,
733
+ }),
734
+ );
735
+ }
736
+ }));
737
+
738
+ integTest('deploy deletes ALL notification arns when empty array is passed', withDefaultFixture(async (fixture) => {
739
+ const topicName = `${fixture.stackNamePrefix}-topic`;
740
+
741
+ const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName }));
742
+ const topicArn = response.TopicArn!;
743
+
744
+ try {
745
+ await fixture.cdkDeploy('notification-arns', {
746
+ modEnv: {
747
+ INTEG_NOTIFICATION_ARNS: topicArn,
748
+ },
749
+ });
750
+
751
+ // make sure the arn was added
752
+ let describeResponse = await fixture.aws.cloudFormation.send(
753
+ new DescribeStacksCommand({
754
+ StackName: fixture.fullStackName('notification-arns'),
755
+ }),
756
+ );
757
+ expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]);
758
+
759
+ // deploy again with empty array
760
+ await fixture.cdkDeploy('notification-arns', {
761
+ modEnv: {
762
+ INTEG_NOTIFICATION_ARNS: '',
763
+ },
764
+ });
765
+
766
+ // make sure the arn was deleted
767
+ describeResponse = await fixture.aws.cloudFormation.send(
768
+ new DescribeStacksCommand({
769
+ StackName: fixture.fullStackName('notification-arns'),
770
+ }),
771
+ );
772
+ expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([]);
773
+ } finally {
774
+ await fixture.aws.sns.send(
775
+ new DeleteTopicCommand({
776
+ TopicArn: topicArn,
777
+ }),
778
+ );
779
+ }
780
+ }));
781
+
782
+ integTest('deploy with notification ARN as prop and flag', withDefaultFixture(async (fixture) => {
783
+ const topic1Name = `${fixture.stackNamePrefix}-topic1`;
784
+ const topic2Name = `${fixture.stackNamePrefix}-topic1`;
785
+
786
+ const topic1Arn = (await fixture.aws.sns.send(new CreateTopicCommand({ Name: topic1Name }))).TopicArn!;
787
+ const topic2Arn = (await fixture.aws.sns.send(new CreateTopicCommand({ Name: topic2Name }))).TopicArn!;
788
+
789
+ try {
790
+ await fixture.cdkDeploy('notification-arns', {
791
+ modEnv: {
792
+ INTEG_NOTIFICATION_ARNS: topic1Arn,
793
+
794
+ },
795
+ options: ['--notification-arns', topic2Arn],
796
+ });
797
+
798
+ // verify that the stack we deployed has our notification ARN
799
+ const describeResponse = await fixture.aws.cloudFormation.send(
800
+ new DescribeStacksCommand({
801
+ StackName: fixture.fullStackName('notification-arns'),
802
+ }),
803
+ );
804
+ expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topic1Arn, topic2Arn]);
805
+ } finally {
806
+ await fixture.aws.sns.send(
807
+ new DeleteTopicCommand({
808
+ TopicArn: topic1Arn,
809
+ }),
810
+ );
811
+ await fixture.aws.sns.send(
812
+ new DeleteTopicCommand({
813
+ TopicArn: topic2Arn,
814
+ }),
815
+ );
816
+ }
817
+ }));
818
+
819
+ // NOTE: this doesn't currently work with modern-style synthesis, as the bootstrap
820
+ // role by default will not have permission to iam:PassRole the created role.
821
+ integTest(
822
+ 'deploy with role',
823
+ withDefaultFixture(async (fixture) => {
824
+ if (fixture.packages.majorVersion() !== '1') {
825
+ return; // Nothing to do
826
+ }
827
+
828
+ const roleName = `${fixture.stackNamePrefix}-test-role`;
829
+
830
+ await deleteRole();
831
+
832
+ const createResponse = await fixture.aws.iam.send(
833
+ new CreateRoleCommand({
834
+ RoleName: roleName,
835
+ AssumeRolePolicyDocument: JSON.stringify({
836
+ Version: '2012-10-17',
837
+ Statement: [
838
+ {
839
+ Action: 'sts:AssumeRole',
840
+ Principal: { Service: 'cloudformation.amazonaws.com' },
841
+ Effect: 'Allow',
842
+ },
843
+ {
844
+ Action: 'sts:AssumeRole',
845
+ Principal: { AWS: (await fixture.aws.sts.send(new GetCallerIdentityCommand({}))).Arn },
846
+ Effect: 'Allow',
847
+ },
848
+ ],
849
+ }),
850
+ }),
851
+ );
852
+
853
+ if (!createResponse.Role) {
854
+ throw new Error('Role is expected to be present!!');
855
+ }
856
+
857
+ if (!createResponse.Role.Arn) {
858
+ throw new Error('Role arn is expected to be present!!');
859
+ }
860
+
861
+ const roleArn = createResponse.Role.Arn;
862
+ try {
863
+ await fixture.aws.iam.send(
864
+ new PutRolePolicyCommand({
865
+ RoleName: roleName,
866
+ PolicyName: 'DefaultPolicy',
867
+ PolicyDocument: JSON.stringify({
868
+ Version: '2012-10-17',
869
+ Statement: [
870
+ {
871
+ Action: '*',
872
+ Resource: '*',
873
+ Effect: 'Allow',
874
+ },
875
+ ],
876
+ }),
877
+ }),
878
+ );
879
+
880
+ await retry(fixture.output, 'Trying to assume fresh role', retry.forSeconds(300), async () => {
881
+ await fixture.aws.sts.send(
882
+ new AssumeRoleCommand({
883
+ RoleArn: roleArn,
884
+ RoleSessionName: 'testing',
885
+ }),
886
+ );
887
+ });
888
+
889
+ // In principle, the role has replicated from 'us-east-1' to wherever we're testing.
890
+ // Give it a little more sleep to make sure CloudFormation is not hitting a box
891
+ // that doesn't have it yet.
892
+ await sleep(5000);
893
+
894
+ await fixture.cdkDeploy('test-2', {
895
+ options: ['--role-arn', roleArn],
896
+ });
897
+
898
+ // Immediately delete the stack again before we delete the role.
899
+ //
900
+ // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack
901
+ // operations will fail when CloudFormation tries to assume the role that's already gone.
902
+ await fixture.cdkDestroy('test-2');
903
+ } finally {
904
+ await deleteRole();
905
+ }
906
+
907
+ async function deleteRole() {
908
+ try {
909
+ const response = await fixture.aws.iam.send(new ListRolePoliciesCommand({ RoleName: roleName }));
910
+
911
+ if (!response.PolicyNames) {
912
+ throw new Error('Policy names cannot be undefined for deleteRole() function');
913
+ }
914
+
915
+ for (const policyName of response.PolicyNames) {
916
+ await fixture.aws.iam.send(
917
+ new DeleteRolePolicyCommand({
918
+ RoleName: roleName,
919
+ PolicyName: policyName,
920
+ }),
921
+ );
922
+ }
923
+ await fixture.aws.iam.send(new DeleteRoleCommand({ RoleName: roleName }));
924
+ } catch (e: any) {
925
+ if (e.message.indexOf('cannot be found') > -1) {
926
+ return;
927
+ }
928
+ throw e;
929
+ }
930
+ }
931
+ }),
932
+ );
933
+
934
+ // TODO add more testing that ensures the symmetry of the generated constructs to the resources.
935
+ ['typescript', 'python', 'csharp', 'java'].forEach((language) => {
936
+ integTest(
937
+ `cdk migrate ${language} deploys successfully`,
938
+ withCDKMigrateFixture(language, async (fixture) => {
939
+ if (language === 'python') {
940
+ await fixture.shell(['pip', 'install', '-r', 'requirements.txt']);
941
+ }
942
+
943
+ const stackArn = await fixture.cdkDeploy(
944
+ fixture.stackNamePrefix,
945
+ { neverRequireApproval: true, verbose: true, captureStderr: false },
946
+ true,
947
+ );
948
+ const response = await fixture.aws.cloudFormation.send(
949
+ new DescribeStacksCommand({
950
+ StackName: stackArn,
951
+ }),
952
+ );
953
+
954
+ expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
955
+ await fixture.cdkDestroy(fixture.stackNamePrefix);
956
+ }),
957
+ );
958
+ });
959
+
960
+ integTest(
961
+ 'cdk migrate generates migrate.json',
962
+ withCDKMigrateFixture('typescript', async (fixture) => {
963
+ const migrateFile = await fs.readFile(path.join(fixture.integTestDir, 'migrate.json'), 'utf8');
964
+ const expectedFile = `{
965
+ \"//\": \"This file is generated by cdk migrate. It will be automatically deleted after the first successful deployment of this app to the environment of the original resources.\",
966
+ \"Source\": \"localfile\"
967
+ }`;
968
+ expect(JSON.parse(migrateFile)).toEqual(JSON.parse(expectedFile));
969
+ await fixture.cdkDestroy(fixture.stackNamePrefix);
970
+ }),
971
+ );
972
+
973
+ // integTest('cdk migrate --from-scan with AND/OR filters correctly filters resources', withExtendedTimeoutFixture(async (fixture) => {
974
+ // const stackName = `cdk-migrate-integ-${fixture.randomString}`;
975
+
976
+ // await fixture.cdkDeploy('migrate-stack', {
977
+ // modEnv: { SAMPLE_RESOURCES: '1' },
978
+ // });
979
+ // await fixture.cdk(
980
+ // ['migrate', '--stack-name', stackName, '--from-scan', 'new', '--filter', 'type=AWS::SNS::Topic,tag-key=tag1', 'type=AWS::SQS::Queue,tag-key=tag3'],
981
+ // { modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
982
+ // );
983
+
984
+ // try {
985
+ // const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', {
986
+ // GeneratedTemplateName: stackName,
987
+ // });
988
+ // const resourceNames = [];
989
+ // for (const resource of response.Resources || []) {
990
+ // if (resource.LogicalResourceId) {
991
+ // resourceNames.push(resource.LogicalResourceId);
992
+ // }
993
+ // }
994
+ // fixture.log(`Resources: ${resourceNames}`);
995
+ // expect(resourceNames.some(ele => ele && ele.includes('migratetopic1'))).toBeTruthy();
996
+ // expect(resourceNames.some(ele => ele && ele.includes('migratequeue1'))).toBeTruthy();
997
+ // } finally {
998
+ // await fixture.cdkDestroy('migrate-stack');
999
+ // await fixture.aws.cloudFormation('deleteGeneratedTemplate', {
1000
+ // GeneratedTemplateName: stackName,
1001
+ // });
1002
+ // }
1003
+ // }));
1004
+
1005
+ // integTest('cdk migrate --from-scan for resources with Write Only Properties generates warnings', withExtendedTimeoutFixture(async (fixture) => {
1006
+ // const stackName = `cdk-migrate-integ-${fixture.randomString}`;
1007
+
1008
+ // await fixture.cdkDeploy('migrate-stack', {
1009
+ // modEnv: {
1010
+ // LAMBDA_RESOURCES: '1',
1011
+ // },
1012
+ // });
1013
+ // await fixture.cdk(
1014
+ // ['migrate', '--stack-name', stackName, '--from-scan', 'new', '--filter', 'type=AWS::Lambda::Function,tag-key=lambda-tag'],
1015
+ // { modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false },
1016
+ // );
1017
+
1018
+ // try {
1019
+
1020
+ // const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', {
1021
+ // GeneratedTemplateName: stackName,
1022
+ // });
1023
+ // const resourceNames = [];
1024
+ // for (const resource of response.Resources || []) {
1025
+ // if (resource.LogicalResourceId && resource.ResourceType === 'AWS::Lambda::Function') {
1026
+ // resourceNames.push(resource.LogicalResourceId);
1027
+ // }
1028
+ // }
1029
+ // fixture.log(`Resources: ${resourceNames}`);
1030
+ // const readmePath = path.join(fixture.integTestDir, stackName, 'README.md');
1031
+ // const readme = await fs.readFile(readmePath, 'utf8');
1032
+ // expect(readme).toContain('## Warnings');
1033
+ // for (const resourceName of resourceNames) {
1034
+ // expect(readme).toContain(`### ${resourceName}`);
1035
+ // }
1036
+ // } finally {
1037
+ // await fixture.cdkDestroy('migrate-stack');
1038
+ // await fixture.aws.cloudFormation('deleteGeneratedTemplate', {
1039
+ // GeneratedTemplateName: stackName,
1040
+ // });
1041
+ // }
1042
+ // }));
1043
+
1044
+ ['typescript', 'python', 'csharp', 'java'].forEach((language) => {
1045
+ integTest(
1046
+ `cdk migrate --from-stack creates deployable ${language} app`,
1047
+ withExtendedTimeoutFixture(async (fixture) => {
1048
+ const migrateStackName = fixture.fullStackName('migrate-stack');
1049
+ await fixture.aws.cloudFormation.send(
1050
+ new CreateStackCommand({
1051
+ StackName: migrateStackName,
1052
+ TemplateBody: await fs.readFile(
1053
+ path.join(__dirname, '..', '..', 'resources', 'templates', 'sqs-template.json'),
1054
+ 'utf8',
1055
+ ),
1056
+ }),
1057
+ );
1058
+ try {
1059
+ let stackStatus = 'CREATE_IN_PROGRESS';
1060
+ while (stackStatus === 'CREATE_IN_PROGRESS') {
1061
+ stackStatus = await (
1062
+ await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: migrateStackName }))
1063
+ ).Stacks?.[0].StackStatus!;
1064
+ await sleep(1000);
1065
+ }
1066
+ await fixture.cdk(['migrate', '--stack-name', migrateStackName, '--from-stack'], {
1067
+ modEnv: { MIGRATE_INTEG_TEST: '1' },
1068
+ neverRequireApproval: true,
1069
+ verbose: true,
1070
+ captureStderr: false,
1071
+ });
1072
+ await fixture.shell(['cd', path.join(fixture.integTestDir, migrateStackName)]);
1073
+ await fixture.cdk(['deploy', migrateStackName], {
1074
+ neverRequireApproval: true,
1075
+ verbose: true,
1076
+ captureStderr: false,
1077
+ });
1078
+ const response = await fixture.aws.cloudFormation.send(
1079
+ new DescribeStacksCommand({
1080
+ StackName: migrateStackName,
1081
+ }),
1082
+ );
1083
+
1084
+ expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE');
1085
+ } finally {
1086
+ await fixture.cdkDestroy('migrate-stack');
1087
+ }
1088
+ }),
1089
+ );
1090
+ });
1091
+
1092
+ integTest(
1093
+ 'cdk diff',
1094
+ withDefaultFixture(async (fixture) => {
1095
+ const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]);
1096
+ expect(diff1).toContain('AWS::SNS::Topic');
1097
+
1098
+ const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]);
1099
+ expect(diff2).toContain('AWS::SNS::Topic');
1100
+
1101
+ // We can make it fail by passing --fail
1102
+ await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])).rejects.toThrow('exited with error');
1103
+ }),
1104
+ );
1105
+
1106
+ integTest(
1107
+ 'enableDiffNoFail',
1108
+ withDefaultFixture(async (fixture) => {
1109
+ await diffShouldSucceedWith({ fail: false, enableDiffNoFail: false });
1110
+ await diffShouldSucceedWith({ fail: false, enableDiffNoFail: true });
1111
+ await diffShouldFailWith({ fail: true, enableDiffNoFail: false });
1112
+ await diffShouldFailWith({ fail: true, enableDiffNoFail: true });
1113
+ await diffShouldFailWith({ fail: undefined, enableDiffNoFail: false });
1114
+ await diffShouldSucceedWith({ fail: undefined, enableDiffNoFail: true });
1115
+
1116
+ async function diffShouldSucceedWith(props: DiffParameters) {
1117
+ await expect(diff(props)).resolves.not.toThrow();
1118
+ }
1119
+
1120
+ async function diffShouldFailWith(props: DiffParameters) {
1121
+ await expect(diff(props)).rejects.toThrow('exited with error');
1122
+ }
1123
+
1124
+ async function diff(props: DiffParameters): Promise<string> {
1125
+ await updateContext(props.enableDiffNoFail);
1126
+ const flag = props.fail != null ? (props.fail ? '--fail' : '--no-fail') : '';
1127
+
1128
+ return fixture.cdk(['diff', flag, fixture.fullStackName('test-1')]);
1129
+ }
1130
+
1131
+ async function updateContext(enableDiffNoFail: boolean) {
1132
+ const cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8'));
1133
+ cdkJson.context = {
1134
+ ...cdkJson.context,
1135
+ 'aws-cdk:enableDiffNoFail': enableDiffNoFail,
1136
+ };
1137
+ await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson));
1138
+ }
1139
+
1140
+ type DiffParameters = { fail?: boolean; enableDiffNoFail: boolean };
1141
+ }),
1142
+ );
1143
+
1144
+ integTest(
1145
+ 'cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff',
1146
+ withDefaultFixture(async (fixture) => {
1147
+ // GIVEN
1148
+ const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]);
1149
+ expect(diff1).toContain('AWS::SNS::Topic');
1150
+
1151
+ await fixture.cdkDeploy('test-2');
1152
+ const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]);
1153
+ expect(diff2).toContain('There were no differences');
1154
+
1155
+ // WHEN / THEN
1156
+ await expect(
1157
+ fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')]),
1158
+ ).rejects.toThrow('exited with error');
1159
+ }),
1160
+ );
1161
+
1162
+ integTest(
1163
+ 'cdk diff --fail with multiple stack exits with if any of the stacks contains a diff',
1164
+ withDefaultFixture(async (fixture) => {
1165
+ // GIVEN
1166
+ await fixture.cdkDeploy('test-1');
1167
+ const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]);
1168
+ expect(diff1).toContain('There were no differences');
1169
+
1170
+ const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]);
1171
+ expect(diff2).toContain('AWS::SNS::Topic');
1172
+
1173
+ // WHEN / THEN
1174
+ await expect(
1175
+ fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')]),
1176
+ ).rejects.toThrow('exited with error');
1177
+ }),
1178
+ );
1179
+
1180
+ integTest(
1181
+ 'cdk diff with large changeset does not fail',
1182
+ withDefaultFixture(async (fixture) => {
1183
+ // GIVEN - small initial stack with only one IAM role
1184
+ await fixture.cdkDeploy('iam-roles', {
1185
+ modEnv: {
1186
+ NUMBER_OF_ROLES: '1',
1187
+ },
1188
+ });
1189
+
1190
+ // WHEN - adding an additional role with a ton of metadata to create a large diff
1191
+ const diff = await fixture.cdk(['diff', fixture.fullStackName('iam-roles')], {
1192
+ verbose: true,
1193
+ modEnv: {
1194
+ NUMBER_OF_ROLES: '2',
1195
+ },
1196
+ });
1197
+
1198
+ // Assert that the CLI assumes the file publishing role:
1199
+ expect(diff).toMatch(/Assuming role .*file-publishing-role/);
1200
+ expect(diff).toContain('success: Published');
1201
+ }),
1202
+ );
1203
+
1204
+ integTest(
1205
+ 'cdk diff doesnt show resource metadata changes',
1206
+ withDefaultFixture(async (fixture) => {
1207
+
1208
+ // GIVEN - small initial stack with default resource metadata
1209
+ await fixture.cdkDeploy('metadata');
1210
+
1211
+ // WHEN - changing resource metadata value
1212
+ const diff = await fixture.cdk(['diff', fixture.fullStackName('metadata')], {
1213
+ verbose: true,
1214
+ modEnv: {
1215
+ INTEG_METADATA_VALUE: 'custom',
1216
+ },
1217
+ });
1218
+
1219
+ // Assert there are no changes
1220
+ expect(diff).toContain('There were no differences');
1221
+ }),
1222
+ );
1223
+
1224
+ integTest(
1225
+ 'cdk diff shows resource metadata changes with --no-change-set',
1226
+ withDefaultFixture(async (fixture) => {
1227
+
1228
+ // GIVEN - small initial stack with default resource metadata
1229
+ await fixture.cdkDeploy('metadata');
1230
+
1231
+ // WHEN - changing resource metadata value
1232
+ const diff = await fixture.cdk(['diff --no-change-set', fixture.fullStackName('metadata')], {
1233
+ verbose: true,
1234
+ modEnv: {
1235
+ INTEG_METADATA_VALUE: 'custom',
1236
+ },
1237
+ });
1238
+
1239
+ // Assert there are changes
1240
+ expect(diff).not.toContain('There were no differences');
1241
+ }),
1242
+ );
1243
+
1244
+ integTest('cdk diff with large changeset and custom toolkit stack name and qualifier does not fail', withoutBootstrap(async (fixture) => {
1245
+ // Bootstrapping with custom toolkit stack name and qualifier
1246
+ const qualifier = 'abc1111';
1247
+ const toolkitStackName = 'custom-stack2';
1248
+ await fixture.cdkBootstrapModern({
1249
+ verbose: true,
1250
+ toolkitStackName: toolkitStackName,
1251
+ qualifier: qualifier,
1252
+ });
1253
+
1254
+ // Deploying small initial stack with only one IAM role
1255
+ await fixture.cdkDeploy('iam-roles', {
1256
+ modEnv: {
1257
+ NUMBER_OF_ROLES: '1',
1258
+ },
1259
+ options: [
1260
+ '--toolkit-stack-name', toolkitStackName,
1261
+ '--context', `@aws-cdk/core:bootstrapQualifier=${qualifier}`,
1262
+ ],
1263
+ });
1264
+
1265
+ // WHEN - adding a role with a ton of metadata to create a large diff
1266
+ const diff = await fixture.cdk(['diff', '--toolkit-stack-name', toolkitStackName, '--context', `@aws-cdk/core:bootstrapQualifier=${qualifier}`, fixture.fullStackName('iam-roles')], {
1267
+ verbose: true,
1268
+ modEnv: {
1269
+ NUMBER_OF_ROLES: '2',
1270
+ },
1271
+ });
1272
+
1273
+ // Assert that the CLI assumes the file publishing role:
1274
+ expect(diff).toMatch(/Assuming role .*file-publishing-role/);
1275
+ expect(diff).toContain('success: Published');
1276
+ }));
1277
+
1278
+ integTest(
1279
+ 'cdk diff --security-only successfully outputs sso-permission-set-without-managed-policy information',
1280
+ withDefaultFixture(async (fixture) => {
1281
+ const diff = await fixture.cdk([
1282
+ 'diff',
1283
+ '--security-only',
1284
+ fixture.fullStackName('sso-perm-set-without-managed-policy'),
1285
+ ]);
1286
+ `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────┬─────────────────────────────────┐
1287
+ │ │ Resource │ InstanceArn │ PermissionSet name │ PermissionsBoundary │ CustomerManagedPolicyReferences │
1288
+ ├───┼──────────────────────────────────────────┼──────────────────────────────────┼────────────────────┼───────────────────────────────────┼─────────────────────────────────┤
1289
+ │ + │\${permission-set-without-managed-policy} │ arn:aws:sso:::instance/testvalue │ testName │ CustomerManagedPolicyReference: { │ │
1290
+ │ │ │ │ │ Name: why, Path: /how/ │ │
1291
+ │ │ │ │ │ } │ │
1292
+ `;
1293
+ expect(diff).toContain('Resource');
1294
+ expect(diff).toContain('permission-set-without-managed-policy');
1295
+
1296
+ expect(diff).toContain('InstanceArn');
1297
+ expect(diff).toContain('arn:aws:sso:::instance/testvalue');
1298
+
1299
+ expect(diff).toContain('PermissionSet name');
1300
+ expect(diff).toContain('testName');
1301
+
1302
+ expect(diff).toContain('PermissionsBoundary');
1303
+ expect(diff).toContain('CustomerManagedPolicyReference: {');
1304
+ expect(diff).toContain('Name: why, Path: /how/');
1305
+ expect(diff).toContain('}');
1306
+
1307
+ expect(diff).toContain('CustomerManagedPolicyReferences');
1308
+ }),
1309
+ );
1310
+
1311
+ integTest(
1312
+ 'cdk diff --security-only successfully outputs sso-permission-set-with-managed-policy information',
1313
+ withDefaultFixture(async (fixture) => {
1314
+ const diff = await fixture.cdk([
1315
+ 'diff',
1316
+ '--security-only',
1317
+ fixture.fullStackName('sso-perm-set-with-managed-policy'),
1318
+ ]);
1319
+ `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────────────────────────────────┬─────────────────────────────────┐
1320
+ │ │ Resource │ InstanceArn │ PermissionSet name │ PermissionsBoundary │ CustomerManagedPolicyReferences │
1321
+ ├───┼──────────────────────────────────────────┼──────────────────────────────────┼────────────────────┼───────────────────────────────────────────────────────────────┼─────────────────────────────────┤
1322
+ │ + │\${permission-set-with-managed-policy} │ arn:aws:sso:::instance/testvalue │ niceWork │ ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess │ Name: forSSO, Path: │
1323
+ `;
1324
+
1325
+ expect(diff).toContain('Resource');
1326
+ expect(diff).toContain('permission-set-with-managed-policy');
1327
+
1328
+ expect(diff).toContain('InstanceArn');
1329
+ expect(diff).toContain('arn:aws:sso:::instance/testvalue');
1330
+
1331
+ expect(diff).toContain('PermissionSet name');
1332
+ expect(diff).toContain('niceWork');
1333
+
1334
+ expect(diff).toContain('PermissionsBoundary');
1335
+ expect(diff).toContain('ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess');
1336
+
1337
+ expect(diff).toContain('CustomerManagedPolicyReferences');
1338
+ expect(diff).toContain('Name: forSSO, Path:');
1339
+ }),
1340
+ );
1341
+
1342
+ integTest(
1343
+ 'cdk diff --security-only successfully outputs sso-assignment information',
1344
+ withDefaultFixture(async (fixture) => {
1345
+ const diff = await fixture.cdk(['diff', '--security-only', fixture.fullStackName('sso-assignment')]);
1346
+ `┌───┬───────────────┬──────────────────────────────────┬─────────────────────────┬──────────────────────────────┬───────────────┬──────────────┬─────────────┐
1347
+ │ │ Resource │ InstanceArn │ PermissionSetArn │ PrincipalId │ PrincipalType │ TargetId │ TargetType │
1348
+ ├───┼───────────────┼──────────────────────────────────┼─────────────────────────┼──────────────────────────────┼───────────────┼──────────────┼─────────────┤
1349
+ │ + │\${assignment} │ arn:aws:sso:::instance/testvalue │ arn:aws:sso:::testvalue │ 11111111-2222-3333-4444-test │ USER │ 111111111111 │ AWS_ACCOUNT │
1350
+ └───┴───────────────┴──────────────────────────────────┴─────────────────────────┴──────────────────────────────┴───────────────┴──────────────┴─────────────┘
1351
+ `;
1352
+ expect(diff).toContain('Resource');
1353
+ expect(diff).toContain('assignment');
1354
+
1355
+ expect(diff).toContain('InstanceArn');
1356
+ expect(diff).toContain('arn:aws:sso:::instance/testvalue');
1357
+
1358
+ expect(diff).toContain('PermissionSetArn');
1359
+ expect(diff).toContain('arn:aws:sso:::testvalue');
1360
+
1361
+ expect(diff).toContain('PrincipalId');
1362
+ expect(diff).toContain('11111111-2222-3333-4444-test');
1363
+
1364
+ expect(diff).toContain('PrincipalType');
1365
+ expect(diff).toContain('USER');
1366
+
1367
+ expect(diff).toContain('TargetId');
1368
+ expect(diff).toContain('111111111111');
1369
+
1370
+ expect(diff).toContain('TargetType');
1371
+ expect(diff).toContain('AWS_ACCOUNT');
1372
+ }),
1373
+ );
1374
+
1375
+ integTest(
1376
+ 'cdk diff --security-only successfully outputs sso-access-control information',
1377
+ withDefaultFixture(async (fixture) => {
1378
+ const diff = await fixture.cdk(['diff', '--security-only', fixture.fullStackName('sso-access-control')]);
1379
+ `┌───┬────────────────────────────────┬────────────────────────┬─────────────────────────────────┐
1380
+ │ │ Resource │ InstanceArn │ AccessControlAttributes │
1381
+ ├───┼────────────────────────────────┼────────────────────────┼─────────────────────────────────┤
1382
+ │ + │\${instanceAccessControlConfig} │ arn:aws:test:testvalue │ Key: first, Values: [a] │
1383
+ │ │ │ │ Key: second, Values: [b] │
1384
+ │ │ │ │ Key: third, Values: [c] │
1385
+ │ │ │ │ Key: fourth, Values: [d] │
1386
+ │ │ │ │ Key: fifth, Values: [e] │
1387
+ │ │ │ │ Key: sixth, Values: [f] │
1388
+ └───┴────────────────────────────────┴────────────────────────┴─────────────────────────────────┘
1389
+ `;
1390
+ expect(diff).toContain('Resource');
1391
+ expect(diff).toContain('instanceAccessControlConfig');
1392
+
1393
+ expect(diff).toContain('InstanceArn');
1394
+ expect(diff).toContain('arn:aws:sso:::instance/testvalue');
1395
+
1396
+ expect(diff).toContain('AccessControlAttributes');
1397
+ expect(diff).toContain('Key: first, Values: [a]');
1398
+ expect(diff).toContain('Key: second, Values: [b]');
1399
+ expect(diff).toContain('Key: third, Values: [c]');
1400
+ expect(diff).toContain('Key: fourth, Values: [d]');
1401
+ expect(diff).toContain('Key: fifth, Values: [e]');
1402
+ expect(diff).toContain('Key: sixth, Values: [f]');
1403
+ }),
1404
+ );
1405
+
1406
+ integTest(
1407
+ 'cdk diff --security-only --fail exits when security diff for sso access control config',
1408
+ withDefaultFixture(async (fixture) => {
1409
+ await expect(
1410
+ fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-access-control')]),
1411
+ ).rejects.toThrow('exited with error');
1412
+ }),
1413
+ );
1414
+
1415
+ integTest(
1416
+ 'cdk diff --security-only --fail exits when security diff for sso-perm-set-without-managed-policy',
1417
+ withDefaultFixture(async (fixture) => {
1418
+ await expect(
1419
+ fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-without-managed-policy')]),
1420
+ ).rejects.toThrow('exited with error');
1421
+ }),
1422
+ );
1423
+
1424
+ integTest(
1425
+ 'cdk diff --security-only --fail exits when security diff for sso-perm-set-with-managed-policy',
1426
+ withDefaultFixture(async (fixture) => {
1427
+ await expect(
1428
+ fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-with-managed-policy')]),
1429
+ ).rejects.toThrow('exited with error');
1430
+ }),
1431
+ );
1432
+
1433
+ integTest(
1434
+ 'cdk diff --security-only --fail exits when security diff for sso-assignment',
1435
+ withDefaultFixture(async (fixture) => {
1436
+ await expect(
1437
+ fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-assignment')]),
1438
+ ).rejects.toThrow('exited with error');
1439
+ }),
1440
+ );
1441
+
1442
+ integTest(
1443
+ 'cdk diff --security-only --fail exits when security changes are present',
1444
+ withDefaultFixture(async (fixture) => {
1445
+ const stackName = 'iam-test';
1446
+ await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow(
1447
+ 'exited with error',
1448
+ );
1449
+ }),
1450
+ );
1451
+
1452
+ integTest(
1453
+ "cdk diff --quiet does not print 'There were no differences' message for stacks which have no differences",
1454
+ withDefaultFixture(async (fixture) => {
1455
+ // GIVEN
1456
+ await fixture.cdkDeploy('test-1');
1457
+
1458
+ // WHEN
1459
+ const diff = await fixture.cdk(['diff', '--quiet', fixture.fullStackName('test-1')]);
1460
+
1461
+ // THEN
1462
+ expect(diff).not.toContain('Stack test-1');
1463
+ expect(diff).not.toContain('There were no differences');
1464
+ }),
1465
+ );
1466
+
1467
+ integTest(
1468
+ 'deploy stack with docker asset',
1469
+ withDefaultFixture(async (fixture) => {
1470
+ await fixture.cdkDeploy('docker');
1471
+ }),
1472
+ );
1473
+
1474
+ integTest(
1475
+ 'deploy and test stack with lambda asset',
1476
+ withDefaultFixture(async (fixture) => {
1477
+ const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false });
1478
+
1479
+ const response = await fixture.aws.cloudFormation.send(
1480
+ new DescribeStacksCommand({
1481
+ StackName: stackArn,
1482
+ }),
1483
+ );
1484
+ const lambdaArn = response.Stacks?.[0].Outputs?.[0].OutputValue;
1485
+ if (lambdaArn === undefined) {
1486
+ throw new Error('Stack did not have expected Lambda ARN output');
1487
+ }
1488
+
1489
+ const output = await fixture.aws.lambda.send(
1490
+ new InvokeCommand({
1491
+ FunctionName: lambdaArn,
1492
+ }),
1493
+ );
1494
+
1495
+ expect(JSON.stringify(output.Payload?.transformToString())).toContain('dear asset');
1496
+ }),
1497
+ );
1498
+
1499
+ integTest('deploy stack with Lambda Asset to Object Lock-enabled asset bucket', withoutBootstrap(async (fixture) => {
1500
+ // Bootstrapping with custom toolkit stack name and qualifier
1501
+ const qualifier = fixture.qualifier;
1502
+ const toolkitStackName = fixture.bootstrapStackName;
1503
+ await fixture.cdkBootstrapModern({
1504
+ verbose: true,
1505
+ toolkitStackName: toolkitStackName,
1506
+ qualifier: qualifier,
1507
+ });
1508
+
1509
+ const bucketName = `cdk-${qualifier}-assets-${await fixture.aws.account()}-${fixture.aws.region}`;
1510
+ await fixture.aws.s3.send(new PutObjectLockConfigurationCommand({
1511
+ Bucket: bucketName,
1512
+ ObjectLockConfiguration: {
1513
+ ObjectLockEnabled: 'Enabled',
1514
+ Rule: {
1515
+ DefaultRetention: {
1516
+ Days: 1,
1517
+ Mode: 'GOVERNANCE',
1518
+ },
1519
+ },
1520
+ },
1521
+ }));
1522
+
1523
+ // Deploy a stack that definitely contains a file asset
1524
+ await fixture.cdkDeploy('lambda', {
1525
+ options: [
1526
+ '--toolkit-stack-name', toolkitStackName,
1527
+ '--context', `@aws-cdk/core:bootstrapQualifier=${qualifier}`,
1528
+ ],
1529
+ });
1530
+
1531
+ // THEN - should not fail. Now clean the bucket with governance bypass: a regular delete
1532
+ // operation will fail.
1533
+ await fixture.aws.emptyBucket(bucketName, { bypassGovernance: true });
1534
+ }));
1535
+
1536
+ integTest(
1537
+ 'cdk ls',
1538
+ withDefaultFixture(async (fixture) => {
1539
+ const listing = await fixture.cdk(['ls'], { captureStderr: false });
1540
+
1541
+ const expectedStacks = [
1542
+ 'conditional-resource',
1543
+ 'docker',
1544
+ 'docker-with-custom-file',
1545
+ 'failed',
1546
+ 'iam-test',
1547
+ 'lambda',
1548
+ 'missing-ssm-parameter',
1549
+ 'order-providing',
1550
+ 'outputs-test-1',
1551
+ 'outputs-test-2',
1552
+ 'param-test-1',
1553
+ 'param-test-2',
1554
+ 'param-test-3',
1555
+ 'termination-protection',
1556
+ 'test-1',
1557
+ 'test-2',
1558
+ 'with-nested-stack',
1559
+ 'with-nested-stack-using-parameters',
1560
+ 'order-consuming',
1561
+ ];
1562
+
1563
+ for (const stack of expectedStacks) {
1564
+ expect(listing).toContain(fixture.fullStackName(stack));
1565
+ }
1566
+ }),
1567
+ );
1568
+
1569
+ /**
1570
+ * Type to store stack dependencies recursively
1571
+ */
1572
+ type DependencyDetails = {
1573
+ id: string;
1574
+ dependencies: DependencyDetails[];
1575
+ };
1576
+
1577
+ type StackDetails = {
1578
+ id: string;
1579
+ dependencies: DependencyDetails[];
1580
+ };
1581
+
1582
+ integTest(
1583
+ 'cdk ls --show-dependencies --json',
1584
+ withDefaultFixture(async (fixture) => {
1585
+ const listing = await fixture.cdk(['ls --show-dependencies --json'], { captureStderr: false });
1586
+
1587
+ const expectedStacks = [
1588
+ {
1589
+ id: 'test-1',
1590
+ dependencies: [],
1591
+ },
1592
+ {
1593
+ id: 'order-providing',
1594
+ dependencies: [],
1595
+ },
1596
+ {
1597
+ id: 'order-consuming',
1598
+ dependencies: [
1599
+ {
1600
+ id: 'order-providing',
1601
+ dependencies: [],
1602
+ },
1603
+ ],
1604
+ },
1605
+ {
1606
+ id: 'with-nested-stack',
1607
+ dependencies: [],
1608
+ },
1609
+ {
1610
+ id: 'list-stacks',
1611
+ dependencies: [
1612
+ {
1613
+ id: 'list-stacks/DependentStack',
1614
+ dependencies: [
1615
+ {
1616
+ id: 'list-stacks/DependentStack/InnerDependentStack',
1617
+ dependencies: [],
1618
+ },
1619
+ ],
1620
+ },
1621
+ ],
1622
+ },
1623
+ {
1624
+ id: 'list-multiple-dependent-stacks',
1625
+ dependencies: [
1626
+ {
1627
+ id: 'list-multiple-dependent-stacks/DependentStack1',
1628
+ dependencies: [],
1629
+ },
1630
+ {
1631
+ id: 'list-multiple-dependent-stacks/DependentStack2',
1632
+ dependencies: [],
1633
+ },
1634
+ ],
1635
+ },
1636
+ ];
1637
+
1638
+ function validateStackDependencies(stack: StackDetails) {
1639
+ expect(listing).toContain(stack.id);
1640
+
1641
+ function validateDependencies(dependencies: DependencyDetails[]) {
1642
+ for (const dependency of dependencies) {
1643
+ expect(listing).toContain(dependency.id);
1644
+ if (dependency.dependencies.length > 0) {
1645
+ validateDependencies(dependency.dependencies);
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ if (stack.dependencies.length > 0) {
1651
+ validateDependencies(stack.dependencies);
1652
+ }
1653
+ }
1654
+
1655
+ for (const stack of expectedStacks) {
1656
+ validateStackDependencies(stack);
1657
+ }
1658
+ }),
1659
+ );
1660
+
1661
+ integTest(
1662
+ 'cdk ls --show-dependencies --json --long',
1663
+ withDefaultFixture(async (fixture) => {
1664
+ const listing = await fixture.cdk(['ls --show-dependencies --json --long'], { captureStderr: false });
1665
+
1666
+ const expectedStacks = [
1667
+ {
1668
+ id: 'order-providing',
1669
+ name: 'order-providing',
1670
+ enviroment: {
1671
+ account: 'unknown-account',
1672
+ region: 'unknown-region',
1673
+ name: 'aws://unknown-account/unknown-region',
1674
+ },
1675
+ dependencies: [],
1676
+ },
1677
+ {
1678
+ id: 'order-consuming',
1679
+ name: 'order-consuming',
1680
+ enviroment: {
1681
+ account: 'unknown-account',
1682
+ region: 'unknown-region',
1683
+ name: 'aws://unknown-account/unknown-region',
1684
+ },
1685
+ dependencies: [
1686
+ {
1687
+ id: 'order-providing',
1688
+ dependencies: [],
1689
+ },
1690
+ ],
1691
+ },
1692
+ ];
1693
+
1694
+ for (const stack of expectedStacks) {
1695
+ expect(listing).toContain(fixture.fullStackName(stack.id));
1696
+ expect(listing).toContain(fixture.fullStackName(stack.name));
1697
+ expect(listing).toContain(stack.enviroment.account);
1698
+ expect(listing).toContain(stack.enviroment.name);
1699
+ expect(listing).toContain(stack.enviroment.region);
1700
+ for (const dependency of stack.dependencies) {
1701
+ expect(listing).toContain(fixture.fullStackName(dependency.id));
1702
+ }
1703
+ }
1704
+ }),
1705
+ );
1706
+
1707
+ integTest(
1708
+ 'synthing a stage with errors leads to failure',
1709
+ withDefaultFixture(async (fixture) => {
1710
+ const output = await fixture.cdk(['synth'], {
1711
+ allowErrExit: true,
1712
+ modEnv: {
1713
+ INTEG_STACK_SET: 'stage-with-errors',
1714
+ },
1715
+ });
1716
+
1717
+ expect(output).toContain('This is an error');
1718
+ }),
1719
+ );
1720
+
1721
+ integTest(
1722
+ 'synthing a stage with errors can be suppressed',
1723
+ withDefaultFixture(async (fixture) => {
1724
+ await fixture.cdk(['synth', '--no-validation'], {
1725
+ modEnv: {
1726
+ INTEG_STACK_SET: 'stage-with-errors',
1727
+ },
1728
+ });
1729
+ }),
1730
+ );
1731
+
1732
+ integTest(
1733
+ 'synth --quiet can be specified in cdk.json',
1734
+ withDefaultFixture(async (fixture) => {
1735
+ let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8'));
1736
+ cdkJson = {
1737
+ ...cdkJson,
1738
+ quiet: true,
1739
+ };
1740
+ await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson));
1741
+ const synthOutput = await fixture.cdk(['synth', fixture.fullStackName('test-2')]);
1742
+ expect(synthOutput).not.toContain('topic152D84A37');
1743
+ }),
1744
+ );
1745
+
1746
+ integTest(
1747
+ 'deploy stack without resource',
1748
+ withDefaultFixture(async (fixture) => {
1749
+ // Deploy the stack without resources
1750
+ await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } });
1751
+
1752
+ // This should have succeeded but not deployed the stack.
1753
+ await expect(
1754
+ fixture.aws.cloudFormation.send(
1755
+ new DescribeStacksCommand({ StackName: fixture.fullStackName('conditional-resource') }),
1756
+ ),
1757
+ ).rejects.toThrow('conditional-resource does not exist');
1758
+
1759
+ // Deploy the stack with resources
1760
+ await fixture.cdkDeploy('conditional-resource');
1761
+
1762
+ // Then again WITHOUT resources (this should destroy the stack)
1763
+ await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } });
1764
+
1765
+ await expect(
1766
+ fixture.aws.cloudFormation.send(
1767
+ new DescribeStacksCommand({ StackName: fixture.fullStackName('conditional-resource') }),
1768
+ ),
1769
+ ).rejects.toThrow('conditional-resource does not exist');
1770
+ }),
1771
+ );
1772
+
1773
+ integTest(
1774
+ 'deploy no stacks with --ignore-no-stacks',
1775
+ withDefaultFixture(async (fixture) => {
1776
+ // empty array for stack names
1777
+ await fixture.cdkDeploy([], {
1778
+ options: ['--ignore-no-stacks'],
1779
+ modEnv: {
1780
+ INTEG_STACK_SET: 'stage-with-no-stacks',
1781
+ },
1782
+ });
1783
+ }),
1784
+ );
1785
+
1786
+ integTest(
1787
+ 'deploy no stacks error',
1788
+ withDefaultFixture(async (fixture) => {
1789
+ // empty array for stack names
1790
+ await expect(
1791
+ fixture.cdkDeploy([], {
1792
+ modEnv: {
1793
+ INTEG_STACK_SET: 'stage-with-no-stacks',
1794
+ },
1795
+ }),
1796
+ ).rejects.toThrow('exited with error');
1797
+ }),
1798
+ );
1799
+
1800
+ integTest(
1801
+ 'IAM diff',
1802
+ withDefaultFixture(async (fixture) => {
1803
+ const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]);
1804
+
1805
+ // Roughly check for a table like this:
1806
+ //
1807
+ // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐
1808
+ // │ │ Resource │ Effect │ Action │ Principal │ Condition │
1809
+ // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤
1810
+ // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │
1811
+ // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘
1812
+
1813
+ expect(output).toContain('${SomeRole.Arn}');
1814
+ expect(output).toContain('sts:AssumeRole');
1815
+ expect(output).toContain('ec2.amazonaws.com');
1816
+ }),
1817
+ );
1818
+
1819
+ integTest(
1820
+ 'fast deploy',
1821
+ withDefaultFixture(async (fixture) => {
1822
+ // we are using a stack with a nested stack because CFN will always attempt to
1823
+ // update a nested stack, which will allow us to verify that updates are actually
1824
+ // skipped unless --force is specified.
1825
+ const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false });
1826
+ const changeSet1 = await getLatestChangeSet();
1827
+
1828
+ // Deploy the same stack again, there should be no new change set created
1829
+ await fixture.cdkDeploy('with-nested-stack');
1830
+ const changeSet2 = await getLatestChangeSet();
1831
+ expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId);
1832
+
1833
+ // Deploy the stack again with --force, now we should create a changeset
1834
+ await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] });
1835
+ const changeSet3 = await getLatestChangeSet();
1836
+ expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId);
1837
+
1838
+ // Deploy the stack again with tags, expected to create a new changeset
1839
+ // even though the resources didn't change.
1840
+ await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] });
1841
+ const changeSet4 = await getLatestChangeSet();
1842
+ expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId);
1843
+
1844
+ async function getLatestChangeSet() {
1845
+ const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackArn }));
1846
+ if (!response.Stacks?.[0]) {
1847
+ throw new Error('Did not get a ChangeSet at all');
1848
+ }
1849
+ fixture.log(`Found Change Set ${response.Stacks?.[0].ChangeSetId}`);
1850
+ return response.Stacks?.[0];
1851
+ }
1852
+ }),
1853
+ );
1854
+
1855
+ integTest(
1856
+ 'failed deploy does not hang',
1857
+ withDefaultFixture(async (fixture) => {
1858
+ // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again.
1859
+ await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error');
1860
+ }),
1861
+ );
1862
+
1863
+ integTest(
1864
+ 'can still load old assemblies',
1865
+ withDefaultFixture(async (fixture) => {
1866
+ const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx');
1867
+
1868
+ const testAssembliesDirectory = path.join(RESOURCES_DIR, 'cloud-assemblies');
1869
+ for (const asmdir of await listChildDirs(testAssembliesDirectory)) {
1870
+ fixture.log(`ASSEMBLY ${asmdir}`);
1871
+ await cloneDirectory(asmdir, cxAsmDir);
1872
+
1873
+ // Some files in the asm directory that have a .js extension are
1874
+ // actually treated as templates. Evaluate them using NodeJS.
1875
+ const templates = await listChildren(cxAsmDir, (fullPath) => Promise.resolve(fullPath.endsWith('.js')));
1876
+ for (const template of templates) {
1877
+ const targetName = template.replace(/.js$/, '');
1878
+ await shell([process.execPath, template, '>', targetName], {
1879
+ cwd: cxAsmDir,
1880
+ outputs: [fixture.output],
1881
+ modEnv: {
1882
+ TEST_ACCOUNT: await fixture.aws.account(),
1883
+ TEST_REGION: fixture.aws.region,
1884
+ },
1885
+ });
1886
+ }
1887
+
1888
+ // Use this directory as a Cloud Assembly
1889
+ const output = await fixture.cdk(['--app', cxAsmDir, '-v', 'synth']);
1890
+
1891
+ // Assert that there was no providerError in CDK's stderr
1892
+ // Because we rely on the app/framework to actually error in case the
1893
+ // provider fails, we inspect the logs here.
1894
+ expect(output).not.toContain('$providerError');
1895
+ }
1896
+ }),
1897
+ );
1898
+
1899
+ integTest(
1900
+ 'generating and loading assembly',
1901
+ withDefaultFixture(async (fixture) => {
1902
+ const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`;
1903
+ await fixture.shell(['rm', '-rf', asmOutputDir]);
1904
+
1905
+ // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory.
1906
+ await fixture.cdk(['synth']);
1907
+ await fixture.cdk(['synth', '--output', asmOutputDir]);
1908
+
1909
+ // cdk.out in the current directory and the indicated --output should be the same
1910
+ await fixture.shell(['diff', 'cdk.out', asmOutputDir]);
1911
+
1912
+ // Check that we can 'ls' the synthesized asm.
1913
+ // Change to some random directory to make sure we're not accidentally loading cdk.json
1914
+ const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() });
1915
+ // Same stacks we know are in the app
1916
+ expect(list).toContain(`${fixture.stackNamePrefix}-lambda`);
1917
+ expect(list).toContain(`${fixture.stackNamePrefix}-test-1`);
1918
+ expect(list).toContain(`${fixture.stackNamePrefix}-test-2`);
1919
+
1920
+ // Check that we can use '.' and just synth ,the generated asm
1921
+ const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], {
1922
+ cwd: asmOutputDir,
1923
+ });
1924
+ expect(stackTemplate).toContain('topic152D84A37');
1925
+
1926
+ // Deploy a Lambda from the copied asm
1927
+ await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir });
1928
+
1929
+ // Remove (rename) the original custom docker file that was used during synth.
1930
+ // this verifies that the assemly has a copy of it and that the manifest uses
1931
+ // relative paths to reference to it.
1932
+ const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom');
1933
+ await fs.rename(customDockerFile, `${customDockerFile}~`);
1934
+ try {
1935
+ // deploy a docker image with custom file without synth (uses assets)
1936
+ await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir });
1937
+ } finally {
1938
+ // Rename back to restore fixture to original state
1939
+ await fs.rename(`${customDockerFile}~`, customDockerFile);
1940
+ }
1941
+ }),
1942
+ );
1943
+
1944
+ integTest(
1945
+ 'templates on disk contain metadata resource, also in nested assemblies',
1946
+ withDefaultFixture(async (fixture) => {
1947
+ // Synth first, and switch on version reporting because cdk.json is disabling it
1948
+ await fixture.cdk(['synth', '--version-reporting=true']);
1949
+
1950
+ // Load template from disk from root assembly
1951
+ const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']);
1952
+
1953
+ expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy();
1954
+
1955
+ // Load template from nested assembly
1956
+ const nestedTemplateContents = await fixture.shell([
1957
+ 'cat',
1958
+ 'cdk.out/assembly-*-stage/*StackInStage*.template.json',
1959
+ ]);
1960
+
1961
+ expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy();
1962
+ }),
1963
+ );
1964
+
1965
+ integTest(
1966
+ 'CDK synth add the metadata properties expected by sam',
1967
+ withSamIntegrationFixture(async (fixture) => {
1968
+ // Synth first
1969
+ await fixture.cdkSynth();
1970
+
1971
+ const template = fixture.template('TestStack');
1972
+
1973
+ const expectedResources = [
1974
+ {
1975
+ // Python Layer Version
1976
+ id: 'PythonLayerVersion39495CEF',
1977
+ cdkId: 'PythonLayerVersion',
1978
+ isBundled: true,
1979
+ property: 'Content',
1980
+ },
1981
+ {
1982
+ // Layer Version
1983
+ id: 'LayerVersion3878DA3A',
1984
+ cdkId: 'LayerVersion',
1985
+ isBundled: false,
1986
+ property: 'Content',
1987
+ },
1988
+ {
1989
+ // Bundled layer version
1990
+ id: 'BundledLayerVersionPythonRuntime6BADBD6E',
1991
+ cdkId: 'BundledLayerVersionPythonRuntime',
1992
+ isBundled: true,
1993
+ property: 'Content',
1994
+ },
1995
+ {
1996
+ // Python Function
1997
+ id: 'PythonFunction0BCF77FD',
1998
+ cdkId: 'PythonFunction',
1999
+ isBundled: true,
2000
+ property: 'Code',
2001
+ },
2002
+ {
2003
+ // Log Retention Function
2004
+ id: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A',
2005
+ cdkId: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a',
2006
+ isBundled: false,
2007
+ property: 'Code',
2008
+ },
2009
+ {
2010
+ // Function
2011
+ id: 'FunctionPythonRuntime28CBDA05',
2012
+ cdkId: 'FunctionPythonRuntime',
2013
+ isBundled: false,
2014
+ property: 'Code',
2015
+ },
2016
+ {
2017
+ // Bundled Function
2018
+ id: 'BundledFunctionPythonRuntime4D9A0918',
2019
+ cdkId: 'BundledFunctionPythonRuntime',
2020
+ isBundled: true,
2021
+ property: 'Code',
2022
+ },
2023
+ {
2024
+ // NodeJs Function
2025
+ id: 'NodejsFunction09C1F20F',
2026
+ cdkId: 'NodejsFunction',
2027
+ isBundled: true,
2028
+ property: 'Code',
2029
+ },
2030
+ {
2031
+ // Go Function
2032
+ id: 'GoFunctionCA95FBAA',
2033
+ cdkId: 'GoFunction',
2034
+ isBundled: true,
2035
+ property: 'Code',
2036
+ },
2037
+ {
2038
+ // Docker Image Function
2039
+ id: 'DockerImageFunction28B773E6',
2040
+ cdkId: 'DockerImageFunction',
2041
+ dockerFilePath: 'Dockerfile',
2042
+ property: 'Code.ImageUri',
2043
+ },
2044
+ {
2045
+ // Spec Rest Api
2046
+ id: 'SpecRestAPI7D4B3A34',
2047
+ cdkId: 'SpecRestAPI',
2048
+ property: 'BodyS3Location',
2049
+ },
2050
+ ];
2051
+
2052
+ for (const resource of expectedResources) {
2053
+ fixture.output.write(`validate assets metadata for resource ${resource}`);
2054
+ expect(resource.id in template.Resources).toBeTruthy();
2055
+ expect(template.Resources[resource.id]).toEqual(
2056
+ expect.objectContaining({
2057
+ Metadata: {
2058
+ 'aws:cdk:path': `${fixture.fullStackName('TestStack')}/${resource.cdkId}/Resource`,
2059
+ 'aws:asset:path': expect.stringMatching(/asset\.[0-9a-zA-Z]{64}/),
2060
+ 'aws:asset:is-bundled': resource.isBundled,
2061
+ 'aws:asset:dockerfile-path': resource.dockerFilePath,
2062
+ 'aws:asset:property': resource.property,
2063
+ },
2064
+ }),
2065
+ );
2066
+ }
2067
+
2068
+ // Nested Stack
2069
+ fixture.output.write('validate assets metadata for nested stack resource');
2070
+ expect('NestedStackNestedStackNestedStackNestedStackResourceB70834FD' in template.Resources).toBeTruthy();
2071
+ expect(template.Resources.NestedStackNestedStackNestedStackNestedStackResourceB70834FD).toEqual(
2072
+ expect.objectContaining({
2073
+ Metadata: {
2074
+ 'aws:cdk:path': `${fixture.fullStackName(
2075
+ 'TestStack',
2076
+ )}/NestedStack.NestedStack/NestedStack.NestedStackResource`,
2077
+ 'aws:asset:path': expect.stringMatching(
2078
+ `${fixture.stackNamePrefix.replace(/-/, '')}TestStackNestedStack[0-9A-Z]{8}\.nested\.template\.json`,
2079
+ ),
2080
+ 'aws:asset:property': 'TemplateURL',
2081
+ },
2082
+ }),
2083
+ );
2084
+ }),
2085
+ );
2086
+
2087
+ integTest(
2088
+ 'CDK synth bundled functions as expected',
2089
+ withSamIntegrationFixture(async (fixture) => {
2090
+ // Synth first
2091
+ await fixture.cdkSynth();
2092
+
2093
+ const template = fixture.template('TestStack');
2094
+
2095
+ const expectedBundledAssets = [
2096
+ {
2097
+ // Python Layer Version
2098
+ id: 'PythonLayerVersion39495CEF',
2099
+ files: [
2100
+ 'python/layer_version_dependency.py',
2101
+ 'python/geonamescache/__init__.py',
2102
+ 'python/geonamescache-1.3.0.dist-info',
2103
+ ],
2104
+ },
2105
+ {
2106
+ // Layer Version
2107
+ id: 'LayerVersion3878DA3A',
2108
+ files: ['layer_version_dependency.py', 'requirements.txt'],
2109
+ },
2110
+ {
2111
+ // Bundled layer version
2112
+ id: 'BundledLayerVersionPythonRuntime6BADBD6E',
2113
+ files: [
2114
+ 'python/layer_version_dependency.py',
2115
+ 'python/geonamescache/__init__.py',
2116
+ 'python/geonamescache-1.3.0.dist-info',
2117
+ ],
2118
+ },
2119
+ {
2120
+ // Python Function
2121
+ id: 'PythonFunction0BCF77FD',
2122
+ files: ['app.py', 'geonamescache/__init__.py', 'geonamescache-1.3.0.dist-info'],
2123
+ },
2124
+ {
2125
+ // Function
2126
+ id: 'FunctionPythonRuntime28CBDA05',
2127
+ files: ['app.py', 'requirements.txt'],
2128
+ },
2129
+ {
2130
+ // Bundled Function
2131
+ id: 'BundledFunctionPythonRuntime4D9A0918',
2132
+ files: ['app.py', 'geonamescache/__init__.py', 'geonamescache-1.3.0.dist-info'],
2133
+ },
2134
+ {
2135
+ // NodeJs Function
2136
+ id: 'NodejsFunction09C1F20F',
2137
+ files: ['index.js'],
2138
+ },
2139
+ {
2140
+ // Go Function
2141
+ id: 'GoFunctionCA95FBAA',
2142
+ files: ['bootstrap'],
2143
+ },
2144
+ {
2145
+ // Docker Image Function
2146
+ id: 'DockerImageFunction28B773E6',
2147
+ files: ['app.js', 'Dockerfile', 'package.json'],
2148
+ },
2149
+ ];
2150
+
2151
+ for (const resource of expectedBundledAssets) {
2152
+ const assetPath = template.Resources[resource.id].Metadata['aws:asset:path'];
2153
+ for (const file of resource.files) {
2154
+ fixture.output.write(`validate Path ${file} for resource ${resource}`);
2155
+ expect(existsSync(path.join(fixture.integTestDir, 'cdk.out', assetPath, file))).toBeTruthy();
2156
+ }
2157
+ }
2158
+ }),
2159
+ );
2160
+
2161
+ integTest(
2162
+ 'sam can locally test the synthesized cdk application',
2163
+ withSamIntegrationFixture(async (fixture) => {
2164
+ // Synth first
2165
+ await fixture.cdkSynth();
2166
+
2167
+ const result = await fixture.samLocalStartApi(
2168
+ 'TestStack',
2169
+ false,
2170
+ randomInteger(30000, 40000),
2171
+ '/restapis/spec/pythonFunction',
2172
+ );
2173
+ expect(result.actionSucceeded).toBeTruthy();
2174
+ expect(result.actionOutput).toEqual(
2175
+ expect.objectContaining({
2176
+ message: 'Hello World',
2177
+ }),
2178
+ );
2179
+ }),
2180
+ );
2181
+
2182
+ integTest(
2183
+ 'skips notice refresh',
2184
+ withDefaultFixture(async (fixture) => {
2185
+ const output = await fixture.cdkSynth({
2186
+ options: ['--no-notices'],
2187
+ modEnv: {
2188
+ INTEG_STACK_SET: 'stage-using-context',
2189
+ },
2190
+ allowErrExit: true,
2191
+ });
2192
+
2193
+ // Neither succeeds nor fails, but skips the refresh
2194
+ await expect(output).not.toContain('Notices refreshed');
2195
+ await expect(output).not.toContain('Notices refresh failed');
2196
+ }),
2197
+ );
2198
+
2199
+ /**
2200
+ * Create a queue, orphan that queue, then import the queue.
2201
+ *
2202
+ * We want to test with a large template to make sure large templates can work with import.
2203
+ */
2204
+ integTest(
2205
+ 'test resource import',
2206
+ withDefaultFixture(async (fixture) => {
2207
+ // GIVEN
2208
+ const randomPrefix = randomString();
2209
+ const uniqueOutputsFileName = `${randomPrefix}Outputs.json`; // other tests use the outputs file. Make sure we don't collide.
2210
+ const outputsFile = path.join(fixture.integTestDir, 'outputs', uniqueOutputsFileName);
2211
+ await fs.mkdir(path.dirname(outputsFile), { recursive: true });
2212
+
2213
+ // First, create a stack that includes many queues, and one queue that will be removed from the stack but NOT deleted from AWS.
2214
+ await fixture.cdkDeploy('importable-stack', {
2215
+ modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '1' },
2216
+ options: ['--outputs-file', outputsFile],
2217
+ });
2218
+
2219
+ try {
2220
+ // Second, now the queue we will remove is in the stack and has a logicalId. We can now make the resource mapping file.
2221
+ // This resource mapping file will be used to tell the import operation what queue to bring into the stack.
2222
+ const fullStackName = fixture.fullStackName('importable-stack');
2223
+ const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString());
2224
+ const queueLogicalId = outputs[fullStackName].QueueLogicalId;
2225
+ const queueResourceMap = {
2226
+ [queueLogicalId]: { QueueUrl: outputs[fullStackName].QueueUrl },
2227
+ };
2228
+ const mappingFile = path.join(fixture.integTestDir, 'outputs', `${randomPrefix}Mapping.json`);
2229
+ await fs.writeFile(mappingFile, JSON.stringify(queueResourceMap), { encoding: 'utf-8' });
2230
+
2231
+ // Third, remove the queue from the stack, but don't delete the queue from AWS.
2232
+ await fixture.cdkDeploy('importable-stack', {
2233
+ modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '0', RETAIN_SINGLE_QUEUE: '0' },
2234
+ });
2235
+ const cfnTemplateBeforeImport = await fixture.aws.cloudFormation.send(
2236
+ new GetTemplateCommand({ StackName: fullStackName }),
2237
+ );
2238
+ expect(cfnTemplateBeforeImport.TemplateBody).not.toContain(queueLogicalId);
2239
+
2240
+ // WHEN
2241
+ await fixture.cdk(['import', '--resource-mapping', mappingFile, fixture.fullStackName('importable-stack')], {
2242
+ modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '0' },
2243
+ });
2244
+
2245
+ // THEN
2246
+ const describeStacksResponse = await fixture.aws.cloudFormation.send(
2247
+ new DescribeStacksCommand({ StackName: fullStackName }),
2248
+ );
2249
+ const cfnTemplateAfterImport = await fixture.aws.cloudFormation.send(
2250
+ new GetTemplateCommand({ StackName: fullStackName }),
2251
+ );
2252
+ expect(describeStacksResponse.Stacks![0].StackStatus).toEqual('IMPORT_COMPLETE');
2253
+ expect(cfnTemplateAfterImport.TemplateBody).toContain(queueLogicalId);
2254
+ } finally {
2255
+ // Clean up
2256
+ await fixture.cdkDestroy('importable-stack');
2257
+ }
2258
+ }),
2259
+ );
2260
+
2261
+ integTest(
2262
+ 'test migrate deployment for app with localfile source in migrate.json',
2263
+ withDefaultFixture(async (fixture) => {
2264
+ const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json');
2265
+ await fs.mkdir(path.dirname(outputsFile), { recursive: true });
2266
+
2267
+ // Initial deploy
2268
+ await fixture.cdkDeploy('migrate-stack', {
2269
+ modEnv: { ORPHAN_TOPIC: '1' },
2270
+ options: ['--outputs-file', outputsFile],
2271
+ });
2272
+
2273
+ const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString());
2274
+ const stackName = fixture.fullStackName('migrate-stack');
2275
+ const queueName = outputs[stackName].QueueName;
2276
+ const queueUrl = outputs[stackName].QueueUrl;
2277
+ const queueLogicalId = outputs[stackName].QueueLogicalId;
2278
+ fixture.log(`Created queue ${queueUrl} in stack ${fixture.fullStackName}`);
2279
+
2280
+ // Write the migrate file based on the ID from step one, then deploy the app with migrate
2281
+ const migrateFile = path.join(fixture.integTestDir, 'migrate.json');
2282
+ await fs.writeFile(
2283
+ migrateFile,
2284
+ JSON.stringify({
2285
+ Source: 'localfile',
2286
+ Resources: [
2287
+ {
2288
+ ResourceType: 'AWS::SQS::Queue',
2289
+ LogicalResourceId: queueLogicalId,
2290
+ ResourceIdentifier: { QueueUrl: queueUrl },
2291
+ },
2292
+ ],
2293
+ }),
2294
+ { encoding: 'utf-8' },
2295
+ );
2296
+
2297
+ await fixture.cdkDestroy('migrate-stack');
2298
+ fixture.log(`Deleted stack ${fixture.fullStackName}, orphaning ${queueName}`);
2299
+
2300
+ // Create new stack from existing queue
2301
+ try {
2302
+ fixture.log(`Deploying new stack ${fixture.fullStackName}, migrating ${queueName} into stack`);
2303
+ await fixture.cdkDeploy('migrate-stack');
2304
+ } finally {
2305
+ // Cleanup
2306
+ await fixture.cdkDestroy('migrate-stack');
2307
+ }
2308
+ }),
2309
+ );
2310
+
2311
+ integTest(
2312
+ "hotswap deployment supports Lambda function's description and environment variables",
2313
+ withDefaultFixture(async (fixture) => {
2314
+ // GIVEN
2315
+ const stackArn = await fixture.cdkDeploy('lambda-hotswap', {
2316
+ captureStderr: false,
2317
+ modEnv: {
2318
+ DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value',
2319
+ },
2320
+ });
2321
+
2322
+ // WHEN
2323
+ const deployOutput = await fixture.cdkDeploy('lambda-hotswap', {
2324
+ options: ['--hotswap'],
2325
+ captureStderr: true,
2326
+ onlyStderr: true,
2327
+ modEnv: {
2328
+ DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value',
2329
+ },
2330
+ });
2331
+
2332
+ const response = await fixture.aws.cloudFormation.send(
2333
+ new DescribeStacksCommand({
2334
+ StackName: stackArn,
2335
+ }),
2336
+ );
2337
+ const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue;
2338
+
2339
+ // THEN
2340
+ // The deployment should not trigger a full deployment, thus the stack's status must remains
2341
+ // "CREATE_COMPLETE"
2342
+ expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
2343
+ // The entire string fails locally due to formatting. Making this test less specific
2344
+ expect(deployOutput).toMatch(/hotswapped!/);
2345
+ expect(deployOutput).toContain(functionName);
2346
+ }),
2347
+ );
2348
+
2349
+ integTest(
2350
+ 'hotswap deployment supports Fn::ImportValue intrinsic',
2351
+ withDefaultFixture(async (fixture) => {
2352
+ // GIVEN
2353
+ try {
2354
+ await fixture.cdkDeploy('export-value-stack');
2355
+ const stackArn = await fixture.cdkDeploy('lambda-hotswap', {
2356
+ captureStderr: false,
2357
+ modEnv: {
2358
+ DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value',
2359
+ USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true',
2360
+ },
2361
+ });
2362
+
2363
+ // WHEN
2364
+ const deployOutput = await fixture.cdkDeploy('lambda-hotswap', {
2365
+ options: ['--hotswap'],
2366
+ captureStderr: true,
2367
+ onlyStderr: true,
2368
+ modEnv: {
2369
+ DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value',
2370
+ USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true',
2371
+ },
2372
+ });
2373
+
2374
+ const response = await fixture.aws.cloudFormation.send(
2375
+ new DescribeStacksCommand({
2376
+ StackName: stackArn,
2377
+ }),
2378
+ );
2379
+ const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue;
2380
+
2381
+ // THEN
2382
+
2383
+ // The deployment should not trigger a full deployment, thus the stack's status must remains
2384
+ // "CREATE_COMPLETE"
2385
+ expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
2386
+ // The entire string fails locally due to formatting. Making this test less specific
2387
+ expect(deployOutput).toMatch(/hotswapped!/);
2388
+ expect(deployOutput).toContain(functionName);
2389
+ } finally {
2390
+ // Ensure cleanup in reverse order due to use of import/export
2391
+ await fixture.cdkDestroy('lambda-hotswap');
2392
+ await fixture.cdkDestroy('export-value-stack');
2393
+ }
2394
+ }),
2395
+ );
2396
+
2397
+ integTest(
2398
+ 'hotswap deployment supports ecs service',
2399
+ withDefaultFixture(async (fixture) => {
2400
+ // GIVEN
2401
+ const stackArn = await fixture.cdkDeploy('ecs-hotswap', {
2402
+ captureStderr: false,
2403
+ });
2404
+
2405
+ // WHEN
2406
+ const deployOutput = await fixture.cdkDeploy('ecs-hotswap', {
2407
+ options: ['--hotswap'],
2408
+ captureStderr: true,
2409
+ onlyStderr: true,
2410
+ modEnv: {
2411
+ DYNAMIC_ECS_PROPERTY_VALUE: 'new value',
2412
+ },
2413
+ });
2414
+
2415
+ const response = await fixture.aws.cloudFormation.send(
2416
+ new DescribeStacksCommand({
2417
+ StackName: stackArn,
2418
+ }),
2419
+ );
2420
+ const serviceName = response.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ServiceName')?.OutputValue;
2421
+
2422
+ // THEN
2423
+
2424
+ // The deployment should not trigger a full deployment, thus the stack's status must remains
2425
+ // "CREATE_COMPLETE"
2426
+ expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
2427
+ // The entire string fails locally due to formatting. Making this test less specific
2428
+ expect(deployOutput).toMatch(/hotswapped!/);
2429
+ expect(deployOutput).toContain(serviceName);
2430
+ }),
2431
+ );
2432
+
2433
+ integTest(
2434
+ 'hotswap deployment for ecs service waits for deployment to complete',
2435
+ withDefaultFixture(async (fixture) => {
2436
+ // GIVEN
2437
+ const stackArn = await fixture.cdkDeploy('ecs-hotswap', {
2438
+ captureStderr: false,
2439
+ });
2440
+
2441
+ // WHEN
2442
+ const deployOutput = await fixture.cdkDeploy('ecs-hotswap', {
2443
+ options: ['--hotswap'],
2444
+ modEnv: {
2445
+ DYNAMIC_ECS_PROPERTY_VALUE: 'new value',
2446
+ },
2447
+ });
2448
+
2449
+ const describeStacksResponse = await fixture.aws.cloudFormation.send(
2450
+ new DescribeStacksCommand({
2451
+ StackName: stackArn,
2452
+ }),
2453
+ );
2454
+ const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ClusterName')
2455
+ ?.OutputValue!;
2456
+ const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ServiceName')
2457
+ ?.OutputValue!;
2458
+
2459
+ // THEN
2460
+
2461
+ const describeServicesResponse = await fixture.aws.ecs.send(
2462
+ new DescribeServicesCommand({
2463
+ cluster: clusterName,
2464
+ services: [serviceName],
2465
+ }),
2466
+ );
2467
+ expect(describeServicesResponse.services?.[0].deployments).toHaveLength(1); // only one deployment present
2468
+ expect(deployOutput).toMatch(/hotswapped!/);
2469
+ }),
2470
+ );
2471
+
2472
+ integTest(
2473
+ 'hotswap deployment for ecs service detects failed deployment and errors',
2474
+ withExtendedTimeoutFixture(async (fixture) => {
2475
+ // GIVEN
2476
+ await fixture.cdkDeploy('ecs-hotswap', { verbose: true });
2477
+
2478
+ // WHEN
2479
+ const deployOutput = await fixture.cdkDeploy('ecs-hotswap', {
2480
+ options: ['--hotswap'],
2481
+ modEnv: {
2482
+ USE_INVALID_ECS_HOTSWAP_IMAGE: 'true',
2483
+ },
2484
+ allowErrExit: true,
2485
+ verbose: true,
2486
+ });
2487
+
2488
+ // THEN
2489
+ const expectedSubstring = 'Resource is not in the expected state due to waiter status: TIMEOUT';
2490
+ expect(deployOutput).toContain(expectedSubstring);
2491
+ expect(deployOutput).not.toContain('hotswapped!');
2492
+ }),
2493
+ );
2494
+
2495
+ integTest('hotswap deployment supports AppSync APIs with many functions',
2496
+ withDefaultFixture(async (fixture) => {
2497
+ // GIVEN
2498
+ const stackArn = await fixture.cdkDeploy('appsync-hotswap', {
2499
+ captureStderr: false,
2500
+ });
2501
+
2502
+ // WHEN
2503
+ const deployOutput = await fixture.cdkDeploy('appsync-hotswap', {
2504
+ options: ['--hotswap'],
2505
+ captureStderr: true,
2506
+ onlyStderr: true,
2507
+ modEnv: {
2508
+ DYNAMIC_APPSYNC_PROPERTY_VALUE: '$util.qr($ctx.stash.put("newTemplate", []))\n$util.toJson({})',
2509
+ },
2510
+ });
2511
+
2512
+ const response = await fixture.aws.cloudFormation.send(
2513
+ new DescribeStacksCommand({
2514
+ StackName: stackArn,
2515
+ }),
2516
+ );
2517
+
2518
+ expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
2519
+ // assert all 50 functions were hotswapped
2520
+ for (const i of Array(50).keys()) {
2521
+ expect(deployOutput).toContain(`AWS::AppSync::FunctionConfiguration 'appsync_function${i}' hotswapped!`);
2522
+ }
2523
+ }),
2524
+ );
2525
+
2526
+ integTest('hotswap ECS deployment respects properties override', withDefaultFixture(async (fixture) => {
2527
+ // Update the CDK context with the new ECS properties
2528
+ let ecsMinimumHealthyPercent = 100;
2529
+ let ecsMaximumHealthyPercent = 200;
2530
+ let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8'));
2531
+ cdkJson = {
2532
+ ...cdkJson,
2533
+ hotswap: {
2534
+ ecs: {
2535
+ minimumHealthyPercent: ecsMinimumHealthyPercent,
2536
+ maximumHealthyPercent: ecsMaximumHealthyPercent,
2537
+ },
2538
+ },
2539
+ };
2540
+
2541
+ await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson));
2542
+
2543
+ // GIVEN
2544
+ const stackArn = await fixture.cdkDeploy('ecs-hotswap', {
2545
+ captureStderr: false,
2546
+ });
2547
+
2548
+ // WHEN
2549
+ await fixture.cdkDeploy('ecs-hotswap', {
2550
+ options: [
2551
+ '--hotswap',
2552
+ ],
2553
+ modEnv: {
2554
+ DYNAMIC_ECS_PROPERTY_VALUE: 'new value',
2555
+ },
2556
+ });
2557
+
2558
+ const describeStacksResponse = await fixture.aws.cloudFormation.send(
2559
+ new DescribeStacksCommand({
2560
+ StackName: stackArn,
2561
+ }),
2562
+ );
2563
+
2564
+ const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ClusterName')?.OutputValue!;
2565
+ const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ServiceName')?.OutputValue!;
2566
+
2567
+ // THEN
2568
+ const describeServicesResponse = await fixture.aws.ecs.send(
2569
+ new DescribeServicesCommand({
2570
+ cluster: clusterName,
2571
+ services: [serviceName],
2572
+ }),
2573
+ );
2574
+ expect(describeServicesResponse.services?.[0].deploymentConfiguration?.minimumHealthyPercent).toEqual(ecsMinimumHealthyPercent);
2575
+ expect(describeServicesResponse.services?.[0].deploymentConfiguration?.maximumPercent).toEqual(ecsMaximumHealthyPercent);
2576
+ }));
2577
+
2578
+ async function listChildren(parent: string, pred: (x: string) => Promise<boolean>) {
2579
+ const ret = new Array<string>();
2580
+ for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) {
2581
+ const fullPath = path.join(parent, child.toString());
2582
+ if (await pred(fullPath)) {
2583
+ ret.push(fullPath);
2584
+ }
2585
+ }
2586
+ return ret;
2587
+ }
2588
+
2589
+ async function listChildDirs(parent: string) {
2590
+ return listChildren(parent, async (fullPath: string) => (await fs.stat(fullPath)).isDirectory());
2591
+ }
2592
+
2593
+ integTest(
2594
+ 'cdk notices with --unacknowledged',
2595
+ withDefaultFixture(async (fixture) => {
2596
+ const noticesUnacknowledged = await fixture.cdk(['notices', '--unacknowledged'], { verbose: false });
2597
+ const noticesUnacknowledgedAlias = await fixture.cdk(['notices', '-u'], { verbose: false });
2598
+ expect(noticesUnacknowledged).toEqual(expect.stringMatching(/There are \d{1,} unacknowledged notice\(s\)./));
2599
+ expect(noticesUnacknowledged).toEqual(noticesUnacknowledgedAlias);
2600
+ }),
2601
+ );
2602
+
2603
+ integTest(
2604
+ 'test cdk rollback',
2605
+ withSpecificFixture('rollback-test-app', async (fixture) => {
2606
+ let phase = '1';
2607
+
2608
+ // Should succeed
2609
+ await fixture.cdkDeploy('test-rollback', {
2610
+ options: ['--no-rollback'],
2611
+ modEnv: { PHASE: phase },
2612
+ verbose: false,
2613
+ });
2614
+ try {
2615
+ phase = '2a';
2616
+
2617
+ // Should fail
2618
+ const deployOutput = await fixture.cdkDeploy('test-rollback', {
2619
+ options: ['--no-rollback'],
2620
+ modEnv: { PHASE: phase },
2621
+ verbose: false,
2622
+ allowErrExit: true,
2623
+ });
2624
+ expect(deployOutput).toContain('UPDATE_FAILED');
2625
+
2626
+ // Rollback
2627
+ await fixture.cdk(['rollback'], {
2628
+ modEnv: { PHASE: phase },
2629
+ verbose: false,
2630
+ });
2631
+ } finally {
2632
+ await fixture.cdkDestroy('test-rollback');
2633
+ }
2634
+ }),
2635
+ );
2636
+
2637
+ integTest(
2638
+ 'automatic rollback if paused and change contains a replacement',
2639
+ withSpecificFixture('rollback-test-app', async (fixture) => {
2640
+ let phase = '1';
2641
+
2642
+ // Should succeed
2643
+ await fixture.cdkDeploy('test-rollback', {
2644
+ options: ['--no-rollback'],
2645
+ modEnv: { PHASE: phase },
2646
+ verbose: false,
2647
+ });
2648
+ try {
2649
+ phase = '2a';
2650
+
2651
+ // Should fail
2652
+ const deployOutput = await fixture.cdkDeploy('test-rollback', {
2653
+ options: ['--no-rollback'],
2654
+ modEnv: { PHASE: phase },
2655
+ verbose: false,
2656
+ allowErrExit: true,
2657
+ });
2658
+ expect(deployOutput).toContain('UPDATE_FAILED');
2659
+
2660
+ // Do a deployment with a replacement and --force: this will roll back first and then deploy normally
2661
+ phase = '3';
2662
+ await fixture.cdkDeploy('test-rollback', {
2663
+ options: ['--no-rollback', '--force'],
2664
+ modEnv: { PHASE: phase },
2665
+ verbose: false,
2666
+ });
2667
+ } finally {
2668
+ await fixture.cdkDestroy('test-rollback');
2669
+ }
2670
+ }),
2671
+ );
2672
+
2673
+ integTest(
2674
+ 'automatic rollback if paused and --no-rollback is removed from flags',
2675
+ withSpecificFixture('rollback-test-app', async (fixture) => {
2676
+ let phase = '1';
2677
+
2678
+ // Should succeed
2679
+ await fixture.cdkDeploy('test-rollback', {
2680
+ options: ['--no-rollback'],
2681
+ modEnv: { PHASE: phase },
2682
+ verbose: false,
2683
+ });
2684
+ try {
2685
+ phase = '2a';
2686
+
2687
+ // Should fail
2688
+ const deployOutput = await fixture.cdkDeploy('test-rollback', {
2689
+ options: ['--no-rollback'],
2690
+ modEnv: { PHASE: phase },
2691
+ verbose: false,
2692
+ allowErrExit: true,
2693
+ });
2694
+ expect(deployOutput).toContain('UPDATE_FAILED');
2695
+
2696
+ // Do a deployment removing --no-rollback: this will roll back first and then deploy normally
2697
+ phase = '1';
2698
+ await fixture.cdkDeploy('test-rollback', {
2699
+ options: ['--force'],
2700
+ modEnv: { PHASE: phase },
2701
+ verbose: false,
2702
+ });
2703
+ } finally {
2704
+ await fixture.cdkDestroy('test-rollback');
2705
+ }
2706
+ }),
2707
+ );
2708
+
2709
+ integTest(
2710
+ 'automatic rollback if replacement and --no-rollback is removed from flags',
2711
+ withSpecificFixture('rollback-test-app', async (fixture) => {
2712
+ let phase = '1';
2713
+
2714
+ // Should succeed
2715
+ await fixture.cdkDeploy('test-rollback', {
2716
+ options: ['--no-rollback'],
2717
+ modEnv: { PHASE: phase },
2718
+ verbose: false,
2719
+ });
2720
+ try {
2721
+ // Do a deployment with a replacement and removing --no-rollback: this will do a regular rollback deploy
2722
+ phase = '3';
2723
+ await fixture.cdkDeploy('test-rollback', {
2724
+ options: ['--force'],
2725
+ modEnv: { PHASE: phase },
2726
+ verbose: false,
2727
+ });
2728
+ } finally {
2729
+ await fixture.cdkDestroy('test-rollback');
2730
+ }
2731
+ }),
2732
+ );
2733
+
2734
+ integTest(
2735
+ 'test cdk rollback --force',
2736
+ withSpecificFixture('rollback-test-app', async (fixture) => {
2737
+ let phase = '1';
2738
+
2739
+ // Should succeed
2740
+ await fixture.cdkDeploy('test-rollback', {
2741
+ options: ['--no-rollback'],
2742
+ modEnv: { PHASE: phase },
2743
+ verbose: false,
2744
+ });
2745
+ try {
2746
+ phase = '2b'; // Fail update and also fail rollback
2747
+
2748
+ // Should fail
2749
+ const deployOutput = await fixture.cdkDeploy('test-rollback', {
2750
+ options: ['--no-rollback'],
2751
+ modEnv: { PHASE: phase },
2752
+ verbose: false,
2753
+ allowErrExit: true,
2754
+ });
2755
+
2756
+ expect(deployOutput).toContain('UPDATE_FAILED');
2757
+
2758
+ // Should still fail
2759
+ const rollbackOutput = await fixture.cdk(['rollback'], {
2760
+ modEnv: { PHASE: phase },
2761
+ verbose: false,
2762
+ allowErrExit: true,
2763
+ });
2764
+
2765
+ expect(rollbackOutput).toContain('Failing rollback');
2766
+
2767
+ // Rollback and force cleanup
2768
+ await fixture.cdk(['rollback', '--force'], {
2769
+ modEnv: { PHASE: phase },
2770
+ verbose: false,
2771
+ });
2772
+ } finally {
2773
+ await fixture.cdkDestroy('test-rollback');
2774
+ }
2775
+ }),
2776
+ );
2777
+
2778
+ integTest('cdk notices are displayed correctly', withDefaultFixture(async (fixture) => {
2779
+
2780
+ const cache = {
2781
+ expiration: 4125963264000, // year 2100 so we never overwrite the cache
2782
+ notices: [
2783
+ {
2784
+ title: 'Bootstrap 1999 Notice',
2785
+ issueNumber: 4444,
2786
+ overview: 'Overview for Bootstrap 1999 Notice. AffectedEnvironments:<{resolve:ENVIRONMENTS}>',
2787
+ components: [
2788
+ {
2789
+ name: 'bootstrap',
2790
+ version: '<1999', // so we include all possible environments
2791
+ },
2792
+ ],
2793
+ schemaVersion: '1',
2794
+ },
2795
+ ],
2796
+ };
2797
+
2798
+ const cdkCacheDir = path.join(fixture.integTestDir, 'cache');
2799
+ await fs.mkdir(cdkCacheDir);
2800
+ await fs.writeFile(path.join(cdkCacheDir, 'notices.json'), JSON.stringify(cache));
2801
+
2802
+ const output = await fixture.cdkDeploy('notices', {
2803
+ verbose: false,
2804
+ modEnv: {
2805
+ CDK_HOME: fixture.integTestDir,
2806
+ },
2807
+ });
2808
+
2809
+ expect(output).toContain('Overview for Bootstrap 1999 Notice');
2810
+
2811
+ // assert dynamic environments are resolved
2812
+ expect(output).toContain(`AffectedEnvironments:<aws://${await fixture.aws.account()}/${fixture.aws.region}>`);
2813
+
2814
+ }));
2815
+
2816
+ integTest('requests go through a proxy when configured',
2817
+ withDefaultFixture(async (fixture) => {
2818
+ // Set up key and certificate
2819
+ const { key, cert } = await mockttp.generateCACertificate();
2820
+ const certDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-'));
2821
+ const certPath = path.join(certDir, 'cert.pem');
2822
+ const keyPath = path.join(certDir, 'key.pem');
2823
+ await fs.writeFile(keyPath, key);
2824
+ await fs.writeFile(certPath, cert);
2825
+
2826
+ const proxyServer = mockttp.getLocal({
2827
+ https: { keyPath, certPath },
2828
+ });
2829
+
2830
+ // We don't need to modify any request, so the proxy
2831
+ // passes through all requests to the target host.
2832
+ const endpoint = await proxyServer
2833
+ .forAnyRequest()
2834
+ .thenPassThrough();
2835
+
2836
+ proxyServer.enableDebug();
2837
+ await proxyServer.start();
2838
+
2839
+ // The proxy is now ready to intercept requests
2840
+
2841
+ try {
2842
+ await fixture.cdkDeploy('test-2', {
2843
+ captureStderr: true,
2844
+ options: [
2845
+ '--proxy', proxyServer.url,
2846
+ '--ca-bundle-path', certPath,
2847
+ ],
2848
+ modEnv: {
2849
+ CDK_HOME: fixture.integTestDir,
2850
+ },
2851
+ });
2852
+ } finally {
2853
+ await fs.rm(certDir, { recursive: true, force: true });
2854
+ await proxyServer.stop();
2855
+ }
2856
+
2857
+ const requests = await endpoint.getSeenRequests();
2858
+
2859
+ expect(requests.map(req => req.url))
2860
+ .toContain('https://cli.cdk.dev-tools.aws.dev/notices.json');
2861
+
2862
+ const actionsUsed = actions(requests);
2863
+ expect(actionsUsed).toContain('AssumeRole');
2864
+ expect(actionsUsed).toContain('CreateChangeSet');
2865
+ }),
2866
+ );
2867
+
2868
+ function actions(requests: CompletedRequest[]): string[] {
2869
+ return [...new Set(requests
2870
+ .map(req => req.body.buffer.toString('utf-8'))
2871
+ .map(body => querystring.decode(body))
2872
+ .map(x => x.Action as string)
2873
+ .filter(action => action != null))];
2874
+ }