Skip to content

Commit 21dba21

Browse files
authored
fix(cli): diff with changeset fails if deploy role cannot be assumed (#29718)
Closes #29650 ### Description of changes This addresses the issue in two ways: 1. If the describeStacks call errors out, we now catch it and default to classic diff behavior. 2. The describeStacks call now tries to use the lookup role rather than the deploy role. ### Description of how you validated changes Manual testing with a user that could only assume lookup roles. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 615ee2d commit 21dba21

File tree

3 files changed

+158
-13
lines changed

3 files changed

+158
-13
lines changed

packages/aws-cdk/lib/api/deployments.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export interface DestroyStackOptions {
265265
export interface StackExistsOptions {
266266
stack: cxapi.CloudFormationStackArtifact;
267267
deployName?: string;
268+
tryLookupRole?: boolean;
268269
}
269270

270271
export interface DeploymentsProps {
@@ -430,7 +431,12 @@ export class Deployments {
430431
}
431432

432433
public async stackExists(options: StackExistsOptions): Promise<boolean> {
433-
const { stackSdk } = await this.prepareSdkFor(options.stack, undefined, Mode.ForReading);
434+
let stackSdk;
435+
if (options.tryLookupRole) {
436+
stackSdk = (await this.prepareSdkWithLookupOrDeployRole(options.stack)).stackSdk;
437+
} else {
438+
stackSdk = (await this.prepareSdkFor(options.stack, undefined, Mode.ForReading)).stackSdk;
439+
}
434440
const stack = await CloudFormationStack.lookup(stackSdk.cloudFormation(), options.deployName ?? options.stack.stackName);
435441
return stack.exists;
436442
}

packages/aws-cdk/lib/cdk-toolkit.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,19 @@ export class CdkToolkit {
139139
let changeSet = undefined;
140140

141141
if (options.changeSet) {
142-
const stackExists = await this.props.deployments.stackExists({
143-
stack: stacks.firstStack,
144-
deployName: stacks.firstStack.stackName,
145-
});
142+
let stackExists = false;
143+
try {
144+
stackExists = await this.props.deployments.stackExists({
145+
stack: stacks.firstStack,
146+
deployName: stacks.firstStack.stackName,
147+
tryLookupRole: true,
148+
});
149+
} catch (e: any) {
150+
debug(e.message);
151+
stream.write('Checking if the stack exists before creating the changeset has failed, will base the diff on template differences (run again with -v to see the reason)\n');
152+
stackExists = false;
153+
}
154+
146155
if (stackExists) {
147156
changeSet = await createDiffChangeSet({
148157
stack: stacks.firstStack,
@@ -154,7 +163,7 @@ export class CdkToolkit {
154163
stream,
155164
});
156165
} else {
157-
debug(`the stack '${stacks.firstStack.stackName}' has not been deployed to CloudFormation, skipping changeset creation.`);
166+
debug(`the stack '${stacks.firstStack.stackName}' has not been deployed to CloudFormation or describeStacks call failed, skipping changeset creation.`);
158167
}
159168
}
160169

@@ -183,11 +192,20 @@ export class CdkToolkit {
183192
let changeSet = undefined;
184193

185194
if (options.changeSet) {
186-
// only perform this check if we're going to make a changeset. This check requires permissions that --no-changeset users might not have.
187-
const stackExists = await this.props.deployments.stackExists({
188-
stack: stack,
189-
deployName: stack.stackName,
190-
});
195+
196+
let stackExists = false;
197+
try {
198+
stackExists = await this.props.deployments.stackExists({
199+
stack,
200+
deployName: stack.stackName,
201+
tryLookupRole: true,
202+
});
203+
} catch (e: any) {
204+
debug(e.message);
205+
stream.write('Checking if the stack exists before creating the changeset has failed, will base the diff on template differences (run again with -v to see the reason)\n');
206+
stackExists = false;
207+
}
208+
191209
if (stackExists) {
192210
changeSet = await createDiffChangeSet({
193211
stack,
@@ -200,7 +218,7 @@ export class CdkToolkit {
200218
stream,
201219
});
202220
} else {
203-
debug(`the stack '${stack.stackName}' has not been deployed to CloudFormation, skipping changeset creation.`);
221+
debug(`the stack '${stack.stackName}' has not been deployed to CloudFormation or describeStacks call failed, skipping changeset creation.`);
204222
}
205223
}
206224

packages/aws-cdk/test/diff.test.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,86 @@ describe('non-nested stacks', () => {
335335
expect(buffer.data.trim()).not.toContain('There were no differences');
336336
expect(exitCode).toBe(0);
337337
});
338+
});
339+
340+
describe('stack exists checks', () => {
341+
beforeEach(() => {
342+
343+
jest.resetAllMocks();
344+
345+
cloudExecutable = new MockCloudExecutable({
346+
stacks: [{
347+
stackName: 'A',
348+
template: { resource: 'A' },
349+
},
350+
{
351+
stackName: 'B',
352+
depends: ['A'],
353+
template: { resource: 'B' },
354+
},
355+
{
356+
stackName: 'C',
357+
depends: ['A'],
358+
template: { resource: 'C' },
359+
metadata: {
360+
'/resource': [
361+
{
362+
type: cxschema.ArtifactMetadataEntryType.ERROR,
363+
data: 'this is an error',
364+
},
365+
],
366+
},
367+
},
368+
{
369+
stackName: 'D',
370+
template: { resource: 'D' },
371+
}],
372+
});
373+
374+
cloudFormation = instanceMockFrom(Deployments);
375+
376+
toolkit = new CdkToolkit({
377+
cloudExecutable,
378+
deployments: cloudFormation,
379+
configuration: cloudExecutable.configuration,
380+
sdkProvider: cloudExecutable.sdkProvider,
381+
});
338382

339-
test('diff does not check for stack existence when --no-changeset is passed', async () => {
383+
// Default implementations
384+
cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((stackArtifact: CloudFormationStackArtifact) => {
385+
if (stackArtifact.stackName === 'D') {
386+
return Promise.resolve({
387+
deployedRootTemplate: { resource: 'D' },
388+
nestedStacks: {},
389+
});
390+
}
391+
return Promise.resolve({
392+
deployedRootTemplate: {},
393+
nestedStacks: {},
394+
});
395+
});
396+
cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({
397+
noOp: true,
398+
outputs: {},
399+
stackArn: '',
400+
stackArtifact: options.stack,
401+
}));
402+
403+
jest.spyOn(cfn, 'createDiffChangeSet').mockImplementationOnce(async () => {
404+
return {
405+
Changes: [
406+
{
407+
ResourceChange: {
408+
Action: 'Dummy',
409+
LogicalResourceId: 'Object',
410+
},
411+
},
412+
],
413+
};
414+
});
415+
});
416+
417+
test('diff does not check for stack existence when --no-change-set is passed', async () => {
340418
// GIVEN
341419
const buffer = new StringWritable();
342420

@@ -353,6 +431,49 @@ describe('non-nested stacks', () => {
353431
expect(exitCode).toBe(0);
354432
expect(cloudFormation.stackExists).not.toHaveBeenCalled();
355433
});
434+
435+
test('diff falls back to classic diff when stack does not exist', async () => {
436+
// GIVEN
437+
const buffer = new StringWritable();
438+
cloudFormation.stackExists = jest.fn().mockReturnValue(Promise.resolve(false));
439+
440+
// WHEN
441+
const exitCode = await toolkit.diff({
442+
stackNames: ['A', 'A'],
443+
stream: buffer,
444+
fail: false,
445+
quiet: true,
446+
changeSet: true,
447+
});
448+
449+
// THEN
450+
expect(exitCode).toBe(0);
451+
expect(cloudFormation.stackExists).toHaveBeenCalled();
452+
expect(cfn.createDiffChangeSet).not.toHaveBeenCalled();
453+
});
454+
455+
test('diff falls back to classic diff when stackExists call fails', async () => {
456+
// GIVEN
457+
const buffer = new StringWritable();
458+
459+
cloudFormation.stackExists.mockImplementation(() => {
460+
throw new Error('Fail fail fail');
461+
});
462+
463+
// WHEN
464+
const exitCode = await toolkit.diff({
465+
stackNames: ['A', 'A'],
466+
stream: buffer,
467+
fail: false,
468+
quiet: true,
469+
changeSet: true,
470+
});
471+
472+
// THEN
473+
expect(exitCode).toBe(0);
474+
expect(cloudFormation.stackExists).toHaveBeenCalled();
475+
expect(cfn.createDiffChangeSet).not.toHaveBeenCalled();
476+
});
356477
});
357478

358479
describe('nested stacks', () => {

0 commit comments

Comments
 (0)