Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli): report errors from resource failures in nested stacks #27318

Merged
merged 6 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,15 @@ export class StackActivityMonitor {
* see a next page and the last event in the page is new to us (and within the time window).
* haven't seen the final event
*/
private async readNewEvents(): Promise<void> {
private async readNewEvents(stackName?: string): Promise<void> {
const stackToPollForEvents = stackName ?? this.stackName;
const events: StackActivity[] = [];

const CFN_SUCCESS_STATUS = ['UPDATE_COMPLETE', 'CREATE_COMPLETE', 'DELETE_COMPLETE', 'DELETE_SKIPPED'];
try {
let nextToken: string | undefined;
let finished = false;
while (!finished) {
const response = await this.cfn.describeStackEvents({ StackName: this.stackName, NextToken: nextToken }).promise();
const response = await this.cfn.describeStackEvents({ StackName: stackToPollForEvents, NextToken: nextToken }).promise();
const eventPage = response?.StackEvents ?? [];

for (const event of eventPage) {
Expand All @@ -249,6 +250,13 @@ export class StackActivityMonitor {
event: event,
metadata: this.findMetadataFor(event.LogicalResourceId),
});

if (event.ResourceType === 'AWS::CloudFormation::Stack' && !CFN_SUCCESS_STATUS.includes(event.ResourceStatus ?? '')) {
// If the event is not for `this` stack, recursively call for events in the nested stack
if (event.PhysicalResourceId !== stackToPollForEvents) {
await this.readNewEvents(event.PhysicalResourceId);
}
}
}

// We're also done if there's nothing left to read
Expand All @@ -258,7 +266,7 @@ export class StackActivityMonitor {
}
}
} catch (e: any) {
if (e.code === 'ValidationError' && e.message === `Stack [${this.stackName}] does not exist`) {
if (e.code === 'ValidationError' && e.message === `Stack [${stackToPollForEvents}] does not exist`) {
return;
}
throw e;
Expand Down Expand Up @@ -475,7 +483,7 @@ abstract class ActivityPrinterBase implements IActivityPrinter {
this.resourcesPrevCompleteState[activity.event.LogicalResourceId] = status;
}

if (hookStatus!== undefined && hookStatus.endsWith('_COMPLETE_FAILED') && activity.event.LogicalResourceId !== undefined && hookType !== undefined) {
if (hookStatus !== undefined && hookStatus.endsWith('_COMPLETE_FAILED') && activity.event.LogicalResourceId !== undefined && hookType !== undefined) {

if (this.hookFailureMap.has(activity.event.LogicalResourceId)) {
this.hookFailureMap.get(activity.event.LogicalResourceId)?.set(hookType, activity.event.HookStatusReason ?? '');
Expand Down Expand Up @@ -803,4 +811,3 @@ function shorten(maxWidth: number, p: string) {

const TIMESTAMP_WIDTH = 12;
const STATUS_WIDTH = 20;

267 changes: 181 additions & 86 deletions packages/aws-cdk/test/util/stack-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,95 +10,171 @@ beforeEach(() => {
printer = new FakePrinter();
});

test('continue to the next page if it exists', async () => {
await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(102)],
NextToken: 'some-token',
};
},
(request) => {
expect(request.NextToken).toBe('some-token');
return {
StackEvents: [event(101)],
};
},
]);

// Printer sees them in chronological order
expect(printer.eventIds).toEqual(['101', '102']);
});
describe('stack monitor event ordering and pagination', () => {
test('continue to the next page if it exists', async () => {
await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(102)],
NextToken: 'some-token',
};
},
(request) => {
expect(request.NextToken).toBe('some-token');
return {
StackEvents: [event(101)],
};
},
]);

test('do not page further if we already saw the last event', async () => {
await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(101)],
};
},
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(102), event(101)],
NextToken: 'some-token',
};
},
(request) => {
// Did not use the token
expect(request.NextToken).toBeUndefined();
return {};
},
]);

// Seen in chronological order
expect(printer.eventIds).toEqual(['101', '102']);
});
// Printer sees them in chronological order
expect(printer.eventIds).toEqual(['101', '102']);
});

test('do not page further if we already saw the last event', async () => {
await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(101)],
};
},
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(102), event(101)],
NextToken: 'some-token',
};
},
(request) => {
// Did not use the token
expect(request.NextToken).toBeUndefined();
return {};
},
]);

// Seen in chronological order
expect(printer.eventIds).toEqual(['101', '102']);
});

test('do not page further if the last event is too old', async () => {
await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(101), event(95)],
NextToken: 'some-token',
};
},
(request) => {
// Start again from the top
expect(request.NextToken).toBeUndefined();
return {};
},
]);

// Seen only the new one
expect(printer.eventIds).toEqual(['101']);
});

test('do a final request after the monitor is stopped', async () => {
await testMonitorWithEventCalls([
// Before stop
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(101)],
};
},
],
// After stop
[
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(102), event(101)],
};
},
]);

test('do not page further if the last event is too old', async () => {
await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(101), event(95)],
NextToken: 'some-token',
};
},
(request) => {
// Start again from the top
expect(request.NextToken).toBeUndefined();
return {};
},
]);

// Seen only the new one
expect(printer.eventIds).toEqual(['101']);
// Seen both
expect(printer.eventIds).toEqual(['101', '102']);
});
});

test('do a final request after the monitor is stopped', async () => {
await testMonitorWithEventCalls([
// Before stop
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(101)],
};
},
],
// After stop
[
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [event(102), event(101)],
};
},
]);

// Seen both
expect(printer.eventIds).toEqual(['101', '102']);
describe('stack monitor, collecting errors from events', () => {
test('return errors from the root stack', async () => {
const monitor = await testMonitorWithEventCalls([
(request) => {
expect(request.NextToken).toBeUndefined();
return {
StackEvents: [addErrorToStackEvent(event(100))],
};
},
]);

expect(monitor.errors).toStrictEqual(['Test Error']);
});

test('return errors from the nested stack', async () => {
const monitor = await testMonitorWithEventCalls([
(request) => {
expect(request.StackName).toStrictEqual('StackName');
return {
StackEvents: [
addErrorToStackEvent(
event(100), {
logicalResourceId: 'nestedStackLogicalResourceId',
physicalResourceId: 'nestedStackPhysicalResourceId',
resourceType: 'AWS::CloudFormation::Stack',
resourceStatusReason: 'nested stack failed',
},
),
],
};
},
(request) => {
expect(request.StackName).toStrictEqual('nestedStackPhysicalResourceId');
return {
StackEvents: [
addErrorToStackEvent(
event(101), {
logicalResourceId: 'nestedResource',
resourceType: 'Some::Nested::Resource',
resourceStatusReason: 'actual failure error message',
},
),
],
};
},
]);

expect(monitor.errors).toStrictEqual(['actual failure error message', 'nested stack failed']);
});

test('does not check for nested stacks that have already completed successfully', async () => {
const monitor = await testMonitorWithEventCalls([
(request) => {
expect(request.StackName).toStrictEqual('StackName');
return {
StackEvents: [
addErrorToStackEvent(
event(100), {
logicalResourceId: 'nestedStackLogicalResourceId',
physicalResourceId: 'nestedStackPhysicalResourceId',
resourceType: 'AWS::CloudFormation::Stack',
resourceStatusReason: 'nested stack status reason',
resourceStatus: 'CREATE_COMPLETE',
},
),
],
};
},
]);

expect(monitor.errors).toStrictEqual([]);
});
});

const T0 = 1597837230504;
Expand All @@ -115,10 +191,28 @@ function event(nr: number): AWS.CloudFormation.StackEvent {
};
}

function addErrorToStackEvent(
eventToUpdate: AWS.CloudFormation.StackEvent,
props: {
resourceStatus?: string,
resourceType?: string,
resourceStatusReason?: string,
logicalResourceId?: string,
physicalResourceId?: string,
} = {},
): AWS.CloudFormation.StackEvent {
eventToUpdate.ResourceStatus = props.resourceStatus ?? 'UPDATE_FAILED';
eventToUpdate.ResourceType = props.resourceType ?? 'Test::Resource::Type';
eventToUpdate.ResourceStatusReason = props.resourceStatusReason ?? 'Test Error';
eventToUpdate.LogicalResourceId = props.logicalResourceId ?? 'testLogicalId';
eventToUpdate.PhysicalResourceId = props.physicalResourceId ?? 'testPhysicalResourceId';
return eventToUpdate;
}

async function testMonitorWithEventCalls(
beforeStopInvocations: Array<(x: AWS.CloudFormation.DescribeStackEventsInput) => AWS.CloudFormation.DescribeStackEventsOutput>,
afterStopInvocations: Array<(x: AWS.CloudFormation.DescribeStackEventsInput) => AWS.CloudFormation.DescribeStackEventsOutput> = [],
) {
): Promise<StackActivityMonitor> {
let describeStackEvents = (jest.fn() as jest.Mock<AWS.CloudFormation.DescribeStackEventsOutput, [AWS.CloudFormation.DescribeStackEventsInput]>);

let finished = false;
Expand All @@ -144,6 +238,7 @@ async function testMonitorWithEventCalls(
const monitor = new StackActivityMonitor(sdk.cloudFormation(), 'StackName', printer, undefined, new Date(T100)).start();
await waitForCondition(() => finished);
await monitor.stop();
return monitor;
}

class FakePrinter implements IActivityPrinter {
Expand Down
Loading