Skip to content

Commit 2eb1b84

Browse files
authored
fix(toolkit-lib)!: diff message payloads are incomplete (#546)
Fixes #494 The messages emitted as part of diff were incomplete when multiple stacks are diffed. Previously the code would overwrite the formatted output with the last processed stack. We are also removing the already deprecated `securityOnly` option. To solve this properly, the diff messages have been restructured: For each stack we emit a new message with code `CDK_TOOLKIT_I4002` that contains all formatted diffs for the stack (including any nested stacks). The collated result message `CDK_TOOLKIT_I4001` does not contain formatted diffs anymore, but instead has a count for stacks with changes and the structured diff as returned by the action. BREAKING CHANGE: The data emitted by message `CDK_TOOLKIT_I4001` has changed. Instead of formatted diffs, it now contains a stack count and the template diff as returned by the action. The formatted output is now available on a new message `CDK_TOOLKIT_I4002`. Deprecated option `securityOnly` removed from `DiffOptions` used by `Toolkit.diff()`. Instead the action always prints and returns a security diff. Use a custom `IoHost` implementation to customize the exact diff output. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 4bd0289 commit 2eb1b84

File tree

8 files changed

+107
-71
lines changed

8 files changed

+107
-71
lines changed

packages/@aws-cdk/toolkit-lib/docs/message-registry.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issu
7171
| `CDK_TOOLKIT_I3110` | Additional information is needed to identify a resource | `info` | {@link ResourceIdentificationRequest} |
7272
| `CDK_TOOLKIT_E3900` | Resource import failed | `error` | {@link ErrorPayload} |
7373
| `CDK_TOOLKIT_I4000` | Diff stacks is starting | `trace` | {@link StackSelectionDetails} |
74-
| `CDK_TOOLKIT_I4001` | Output of the diff command | `info` | {@link DiffResult} |
74+
| `CDK_TOOLKIT_I4001` | Output of the diff command | `result` | {@link DiffResult} |
75+
| `CDK_TOOLKIT_I4002` | The diff for a single stack | `result` | {@link StackDiff} |
7576
| `CDK_TOOLKIT_I4590` | Results of the drift command | `result` | {@link DriftResultPayload} |
7677
| `CDK_TOOLKIT_I4591` | Missing drift result fort a stack. | `warn` | {@link SingleStack} |
7778
| `CDK_TOOLKIT_I5000` | Provides deployment times | `info` | {@link Duration} |

packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class DiffMethod {
8989
}
9090

9191
/**
92-
* Optins for the diff method
92+
* Options for the diff method
9393
*/
9494
export interface DiffOptions {
9595
/**
@@ -129,13 +129,4 @@ export interface DiffOptions {
129129
* @default 3
130130
*/
131131
readonly contextLines?: number;
132-
133-
/**
134-
* Only include broadened security changes in the diff
135-
*
136-
* @default false
137-
*
138-
* @deprecated implement in IoHost
139-
*/
140-
readonly securityOnly?: boolean;
141132
}

packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ interface FormatStackDiffOptions {
7171
*
7272
* @default 3
7373
*/
74-
readonly context?: number;
74+
readonly contextLines?: number;
7575

7676
/**
7777
* silences \'There were no differences\' messages
@@ -157,7 +157,7 @@ export class DiffFormatter {
157157
/**
158158
* Get or creates the diff of a stack.
159159
* If it creates the diff, it stores the result in a map for
160-
* easier retreval later.
160+
* easier retrieval later.
161161
*/
162162
private diff(stackName?: string, oldTemplate?: any) {
163163
const realStackName = stackName ?? this.stackName;
@@ -178,8 +178,8 @@ export class DiffFormatter {
178178
*
179179
* If no stackName is given, then the root stack name is used.
180180
*/
181-
private permissionType(stackName?: string): PermissionChangeType {
182-
const diff = this.diff(stackName);
181+
private permissionType(): PermissionChangeType {
182+
const diff = this.diff();
183183

184184
if (diff.permissionsBroadened) {
185185
return PermissionChangeType.BROADENING;
@@ -211,7 +211,7 @@ export class DiffFormatter {
211211
let diff = this.diff(stackName, oldTemplate);
212212

213213
// The stack diff is formatted via `Formatter`, which takes in a stream
214-
// and sends its output directly to that stream. To faciliate use of the
214+
// and sends its output directly to that stream. To facilitate use of the
215215
// global CliIoHost, we create our own stream to capture the output of
216216
// `Formatter` and return the output as a string for the consumer of
217217
// `formatStackDiff` to decide what to do with it.
@@ -253,7 +253,7 @@ export class DiffFormatter {
253253
formatDifferences(stream, diff, {
254254
...logicalIdMapFromTemplate(this.oldTemplate),
255255
...buildLogicalToPathMap(this.newTemplate),
256-
}, options.context);
256+
}, options.contextLines);
257257
} else if (!options.quiet) {
258258
stream.write(chalk.green('There were no differences\n'));
259259
}

packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type * as cxapi from '@aws-cdk/cx-api';
22
import * as make from './message-maker';
33
import type { SpanDefinition } from './span';
4-
import type { DiffResult } from '../../../payloads';
4+
import type { StackDiff, DiffResult } from '../../../payloads';
55
import type { BootstrapEnvironmentProgress } from '../../../payloads/bootstrap-environment-progress';
66
import type { MissingContext, UpdatedContext } from '../../../payloads/context';
77
import type { BuildAsset, DeployConfirmationRequest, PublishAsset, StackDeployProgress, SuccessfulDeployStackResult } from '../../../payloads/deploy';
@@ -85,11 +85,16 @@ export const IO = {
8585
description: 'Diff stacks is starting',
8686
interface: 'StackSelectionDetails',
8787
}),
88-
CDK_TOOLKIT_I4001: make.info<DiffResult>({
88+
CDK_TOOLKIT_I4001: make.result<DiffResult>({
8989
code: 'CDK_TOOLKIT_I4001',
9090
description: 'Output of the diff command',
9191
interface: 'DiffResult',
9292
}),
93+
CDK_TOOLKIT_I4002: make.result<StackDiff>({
94+
code: 'CDK_TOOLKIT_I4002',
95+
description: 'The diff for a single stack',
96+
interface: 'StackDiff',
97+
}),
9398

9499
// 4: Drift (45xx - 49xx)
95100
CDK_TOOLKIT_I4590: make.result<DriftResultPayload>({

packages/@aws-cdk/toolkit-lib/lib/payloads/diff.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Duration } from './types';
1+
import type { ITemplateDiff } from '@aws-cdk/cloudformation-diff';
2+
import type { Duration, SingleStack } from './types';
23

34
/**
45
* Different types of permission related changes in a diff
@@ -20,17 +21,57 @@ export enum PermissionChangeType {
2021
NON_BROADENING = 'non-broadening',
2122
}
2223

24+
/**
25+
* The diff formatted as different types of output
26+
*/
27+
export interface FormattedDiff {
28+
/**
29+
* The stack diff formatted as a string
30+
*/
31+
readonly diff: string;
32+
/**
33+
* The security diff formatted as a string, if any
34+
*/
35+
readonly security?: string;
36+
}
37+
38+
/**
39+
* Diff information for a single stack
40+
*/
41+
export interface StackDiff extends SingleStack {
42+
/**
43+
* Total number of stacks that have changes
44+
* Can be higher than `1` if the stack has nested stacks.
45+
*/
46+
readonly numStacksWithChanges: number;
47+
48+
/**
49+
* Structural diff of the stack
50+
* Can include more than a single diff if the stack has nested stacks.
51+
*/
52+
readonly diffs: { [name: string]: ITemplateDiff };
53+
54+
/**
55+
* The formatted diff
56+
*/
57+
readonly formattedDiff: FormattedDiff;
58+
59+
/**
60+
* Does the diff contain changes to permissions and what kind
61+
*/
62+
readonly permissionChanges: PermissionChangeType;
63+
}
64+
2365
/**
2466
* Output of the diff command
2567
*/
2668
export interface DiffResult extends Duration {
2769
/**
28-
* Stack diff formatted as a string
70+
* Total number of stacks that have changes
2971
*/
30-
readonly formattedStackDiff: string;
31-
72+
readonly numStacksWithChanges: number;
3273
/**
33-
* Security diff formatted as a string
74+
* Structural diff of all selected stacks
3475
*/
35-
readonly formattedSecurityDiff: string;
76+
readonly diffs: { [name: string]: ITemplateDiff };
3677
}

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

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -339,39 +339,41 @@ export class Toolkit extends CloudAssemblySourceBuilder {
339339
const contextLines = options.contextLines || 3;
340340

341341
let diffs = 0;
342-
let formattedSecurityDiff = '';
343-
let formattedStackDiff = '';
344342

345343
const templateInfos = await prepareDiff(ioHelper, stacks, deployments, await this.sdkProvider('diff'), options);
346344
const templateDiffs: { [name: string]: TemplateDiff } = {};
347345
for (const templateInfo of templateInfos) {
348-
const formatter = new DiffFormatter({
349-
templateInfo,
350-
});
346+
const formatter = new DiffFormatter({ templateInfo });
347+
const stackDiff = formatter.formatStackDiff({ strict, contextLines });
351348

352-
if (options.securityOnly) {
353-
const securityDiff = formatter.formatSecurityDiff();
354-
// In Diff, we only care about BROADENING security diffs
355-
if (securityDiff.permissionChangeType == PermissionChangeType.BROADENING) {
356-
const warningMessage = 'This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n';
357-
await ioHelper.defaults.warn(warningMessage);
358-
formattedSecurityDiff = securityDiff.formattedDiff;
359-
diffs = securityDiff.formattedDiff ? diffs + 1 : diffs;
360-
}
361-
} else {
362-
const diff = formatter.formatStackDiff({
363-
strict,
364-
context: contextLines,
365-
});
366-
formattedStackDiff = diff.formattedDiff;
367-
diffs = diff.numStacksWithChanges;
349+
// Security Diff
350+
const securityDiff = formatter.formatSecurityDiff();
351+
const formattedSecurityDiff = securityDiff.permissionChangeType !== PermissionChangeType.NONE ? stackDiff.formattedDiff : undefined;
352+
// We only warn about BROADENING changes
353+
if (securityDiff.permissionChangeType == PermissionChangeType.BROADENING) {
354+
const warningMessage = 'This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n';
355+
await diffSpan.notifyDefault('warn', warningMessage);
356+
await diffSpan.notifyDefault('info', securityDiff.formattedDiff);
368357
}
358+
359+
// Stack Diff
360+
diffs += stackDiff.numStacksWithChanges;
369361
appendObject(templateDiffs, formatter.diffs);
362+
await diffSpan.notify(IO.CDK_TOOLKIT_I4002.msg(stackDiff.formattedDiff, {
363+
stack: templateInfo.newTemplate,
364+
diffs: formatter.diffs,
365+
numStacksWithChanges: stackDiff.numStacksWithChanges,
366+
permissionChanges: securityDiff.permissionChangeType,
367+
formattedDiff: {
368+
diff: stackDiff.formattedDiff,
369+
security: formattedSecurityDiff,
370+
},
371+
}));
370372
}
371373

372374
await diffSpan.end(`✨ Number of stacks with differences: ${diffs}`, {
373-
formattedSecurityDiff,
374-
formattedStackDiff,
375+
numStacksWithChanges: diffs,
376+
diffs: templateDiffs,
375377
});
376378

377379
return templateDiffs;

packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,14 @@ describe('diff', () => {
5151
// THEN
5252
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
5353
action: 'diff',
54-
level: 'info',
54+
level: 'result',
5555
code: 'CDK_TOOLKIT_I4001',
5656
message: expect.stringContaining('✨ Number of stacks with differences: 1'),
5757
data: expect.objectContaining({
58-
formattedStackDiff: expect.stringContaining((chalk.bold('Stack1'))),
58+
numStacksWithChanges: 1,
59+
diffs: expect.objectContaining({
60+
Stack1: expect.anything(),
61+
}),
5962
}),
6063
}));
6164
});
@@ -131,12 +134,11 @@ describe('diff', () => {
131134
}));
132135
});
133136

134-
test('only security diff', async () => {
137+
test('security diff', async () => {
135138
// WHEN
136139
const cx = await builderFixture(toolkit, 'stack-with-role');
137140
const result = await toolkit.diff(cx, {
138141
stacks: { strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE, patterns: ['Stack1'] },
139-
securityOnly: true,
140142
method: DiffMethod.TemplateOnly({ compareAgainstProcessedTemplate: true }),
141143
});
142144

@@ -148,11 +150,12 @@ describe('diff', () => {
148150
}));
149151
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
150152
action: 'diff',
151-
level: 'info',
152-
code: 'CDK_TOOLKIT_I4001',
153-
message: expect.stringContaining('✨ Number of stacks with differences: 1'),
153+
level: 'result',
154+
code: 'CDK_TOOLKIT_I4002',
154155
data: expect.objectContaining({
155-
formattedSecurityDiff: expect.stringContaining((chalk.underline(chalk.bold('IAM Statement Changes')))),
156+
formattedDiff: expect.objectContaining({
157+
security: expect.stringContaining((chalk.underline(chalk.bold('IAM Statement Changes')))),
158+
}),
156159
}),
157160
}));
158161

@@ -182,17 +185,17 @@ describe('diff', () => {
182185
const cx = await builderFixture(toolkit, 'two-empty-stacks');
183186
await toolkit.diff(cx, {
184187
stacks: { strategy: StackSelectionStrategy.ALL_STACKS },
185-
securityOnly: true,
186188
});
187189

188190
// THEN
189191
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
190192
action: 'diff',
191-
level: 'info',
192-
code: 'CDK_TOOLKIT_I4001',
193-
message: expect.stringContaining('✨ Number of stacks with differences: 0'),
193+
level: 'result',
194+
code: 'CDK_TOOLKIT_I4002',
194195
data: expect.objectContaining({
195-
formattedSecurityDiff: '',
196+
formattedDiff: expect.objectContaining({
197+
security: undefined,
198+
}),
196199
}),
197200
}));
198201
});
@@ -207,19 +210,13 @@ describe('diff', () => {
207210

208211
// THEN
209212
expect(ioHost.notifySpy).not.toHaveBeenCalledWith(expect.objectContaining({
210-
action: 'diff',
211-
level: 'info',
212-
code: 'CDK_TOOLKIT_I0000',
213213
message: expect.stringContaining('Could not create a change set'),
214214
}));
215215
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
216216
action: 'diff',
217-
level: 'info',
217+
level: 'result',
218218
code: 'CDK_TOOLKIT_I4001',
219219
message: expect.stringContaining('✨ Number of stacks with differences: 1'),
220-
data: expect.objectContaining({
221-
formattedStackDiff: expect.stringContaining(chalk.bold('Stack1')),
222-
}),
223220
}));
224221
});
225222

@@ -344,7 +341,6 @@ describe('diff', () => {
344341
const cx = await builderFixture(toolkit, 'stack-with-role');
345342
const result = await toolkit.diff(cx, {
346343
stacks: { strategy: StackSelectionStrategy.ALL_STACKS },
347-
securityOnly: true,
348344
method: DiffMethod.LocalFile(path.join(__dirname, '..', '_fixtures', 'two-empty-stacks', 'cdk.out', 'Stack1.template.json')),
349345
});
350346

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export class CdkToolkit {
241241
} else {
242242
const diff = formatter.formatStackDiff({
243243
strict,
244-
context: contextLines,
244+
contextLines,
245245
quiet,
246246
});
247247
diffs = diff.numStacksWithChanges;
@@ -325,7 +325,7 @@ export class CdkToolkit {
325325
} else {
326326
const diff = formatter.formatStackDiff({
327327
strict,
328-
context: contextLines,
328+
contextLines,
329329
quiet,
330330
});
331331
info(diff.formattedDiff);

0 commit comments

Comments
 (0)