Skip to content

Commit d37fbbb

Browse files
authored
feat(cli): support for notices (#18936)
Features - [x] notices show up on every `cdk` command - [x] `cdk acknowledge` will acknowledge an issue by id, scoped to individual cdk apps - [x] `cdk notices` _always_ returns relevant notices - [x] context flag `'notices' = false` will hide notices always - [x] notices are filtered by cli version - [x] notices are filtered by v2 framework version - [x] notices are filtered by v1 framework version - [x] `--no-notices` option - [ ] think about versioning for v2 alpha modules -- this will be left for a separate PR - [ ] `--fail-on-notices` option -- this will be left for a separate PR Example: <img width="964" alt="Screenshot 2022-02-21 at 20 22 24" src="https://user-images.githubusercontent.com/288203/155021996-e4f72dec-5f1d-4940-85fb-0abdd3939c8b.png"> ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent c7394c9 commit d37fbbb

File tree

9 files changed

+772
-61
lines changed

9 files changed

+772
-61
lines changed

packages/aws-cdk/README.md

+105-12
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@
1111

1212
The AWS CDK Toolkit provides the `cdk` command-line interface that can be used to work with AWS CDK applications.
1313

14-
Command | Description
15-
----------------------------------|-------------------------------------------------------------------------------------
16-
[`cdk docs`](#cdk-docs) | Access the online documentation
17-
[`cdk init`](#cdk-init) | Start a new CDK project (app or library)
18-
[`cdk list`](#cdk-list) | List stacks in an application
19-
[`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s)
20-
[`cdk diff`](#cdk-diff) | Diff stacks against current state
21-
[`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account
22-
[`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes
23-
[`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account
24-
[`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts
25-
[`cdk doctor`](#cdk-doctor) | Inspect the environment and produce information useful for troubleshooting
14+
Command | Description
15+
--------------------------------------|---------------------------------------------------------------------------------
16+
[`cdk docs`](#cdk-docs) | Access the online documentation
17+
[`cdk init`](#cdk-init) | Start a new CDK project (app or library)
18+
[`cdk list`](#cdk-list) | List stacks in an application
19+
[`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s)
20+
[`cdk diff`](#cdk-diff) | Diff stacks against current state
21+
[`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account
22+
[`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes
23+
[`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account
24+
[`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts
25+
[`cdk doctor`](#cdk-doctor) | Inspect the environment and produce information useful for troubleshooting
26+
[`cdk acknowledge`](#cdk-acknowledge) | Acknowledge (and hide) a notice by issue number
27+
[`cdk notices`](#cdk-notices) | List all relevant notices for the application
2628

2729
This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.
2830

@@ -503,6 +505,97 @@ $ cdk doctor
503505
- AWS_SDK_LOAD_CONFIG = 1
504506
```
505507

508+
## Notices
509+
510+
> This feature exists on CDK CLI version 2.14.0 and up.
511+
512+
CDK Notices are important messages regarding security vulnerabilities, regressions, and usage of unsupported
513+
versions. Relevant notices appear on every command by default. For example,
514+
515+
```console
516+
$ cdk deploy
517+
518+
... # Normal output of the command
519+
520+
NOTICES
521+
522+
16603 Toggling off auto_delete_objects for Bucket empties the bucket
523+
524+
Overview: If a stack is deployed with an S3 bucket with
525+
auto_delete_objects=True, and then re-deployed with
526+
auto_delete_objects=False, all the objects in the bucket
527+
will be deleted.
528+
529+
Affected versions: <1.126.0.
530+
531+
More information at: https://github.com/aws/aws-cdk/issues/16603
532+
533+
17061 Error when building EKS cluster with monocdk import
534+
535+
Overview: When using monocdk/aws-eks to build a stack containing
536+
an EKS cluster, error is thrown about missing
537+
lambda-layer-node-proxy-agent/layer/package.json.
538+
539+
Affected versions: >=1.126.0 <=1.130.0.
540+
541+
More information at: https://github.com/aws/aws-cdk/issues/17061
542+
543+
If you don’t want to see an notice anymore, use "cdk acknowledge ID". For example, "cdk acknowledge 16603".
544+
```
545+
546+
You can suppress warnings in a variety of ways:
547+
548+
- per individual execution:
549+
550+
`cdk deploy --no-notices`
551+
552+
- disable all notices indefinitely through context in `cdk.json`:
553+
554+
```json
555+
{
556+
"context": {
557+
"notices": false
558+
}
559+
}
560+
```
561+
562+
- acknowleding individual notices via `cdk acknowledge` (see below).
563+
564+
### `cdk acknowledge`
565+
566+
To hide a particular notice that has been addressed or does not apply, call `cdk acknowledge` with the ID of
567+
the notice:
568+
569+
```console
570+
$cdk acknowledge 16603
571+
```
572+
573+
> Please note that the acknowledgements are made project by project. If you acknowledge an notice in one CDK
574+
> project, it will still appear on other projects when you run any CDK commands, unless you have suppressed
575+
> or disabled notices.
576+
577+
578+
### `cdk notices`
579+
580+
List the notices that are relevant to the current CDK repository, regardless of context flags or notices that
581+
have been acknowledged:
582+
583+
```console
584+
$ cdk notices
585+
586+
NOTICES
587+
588+
16603 Toggling off auto_delete_objects for Bucket empties the bucket
589+
590+
Overview: if a stack is deployed with an S3 bucket with auto_delete_objects=True, and then re-deployed with auto_delete_objects=False, all the objects in the bucket will be deleted.
591+
592+
Affected versions: framework: <=2.15.0 >=2.10.0
593+
594+
More information at: https://github.com/aws/aws-cdk/issues/16603
595+
596+
If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge 16603".
597+
```
598+
506599
### Bundling
507600

508601
By default asset bundling is skipped for `cdk list` and `cdk destroy`. For `cdk deploy`, `cdk diff`

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

+16-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
1616
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
1717
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
1818
import { data, debug, error, highlight, print, success, warning } from './logging';
19-
import { deserializeStructure } from './serialize';
19+
import { deserializeStructure, serializeStructure } from './serialize';
2020
import { Configuration, PROJECT_CONFIG } from './settings';
2121
import { numberFromBool, partition } from './util';
2222

@@ -74,9 +74,16 @@ export class CdkToolkit {
7474
constructor(private readonly props: CdkToolkitProps) {
7575
}
7676

77-
public async metadata(stackName: string) {
77+
public async metadata(stackName: string, json: boolean) {
7878
const stacks = await this.selectSingleStackByName(stackName);
79-
return stacks.firstStack.manifest.metadata ?? {};
79+
data(serializeStructure(stacks.firstStack.manifest.metadata ?? {}, json));
80+
}
81+
82+
public async acknowledge(noticeId: string) {
83+
const acks = this.props.configuration.context.get('acknowledged-issue-numbers') ?? [];
84+
acks.push(Number(noticeId));
85+
this.props.configuration.context.set('acknowledged-issue-numbers', acks);
86+
await this.props.configuration.saveContext();
8087
}
8188

8289
public async diff(options: DiffOptions): Promise<number> {
@@ -384,7 +391,7 @@ export class CdkToolkit {
384391
}
385392
}
386393

387-
public async list(selectors: string[], options: { long?: boolean } = { }) {
394+
public async list(selectors: string[], options: { long?: boolean, json?: boolean } = { }): Promise<number> {
388395
const stacks = await this.selectStacksForList(selectors);
389396

390397
// if we are in "long" mode, emit the array as-is (JSON/YAML)
@@ -397,7 +404,8 @@ export class CdkToolkit {
397404
environment: stack.environment,
398405
});
399406
}
400-
return long; // will be YAML formatted output
407+
data(serializeStructure(long, options.json ?? false));
408+
return 0;
401409
}
402410

403411
// just print stack IDs
@@ -417,13 +425,13 @@ export class CdkToolkit {
417425
* OUTPUT: If more than one stack ends up being selected, an output directory
418426
* should be supplied, where the templates will be written.
419427
*/
420-
public async synth(stackNames: string[], exclusively: boolean, quiet: boolean, autoValidate?: boolean): Promise<any> {
428+
public async synth(stackNames: string[], exclusively: boolean, quiet: boolean, autoValidate?: boolean, json?: boolean): Promise<any> {
421429
const stacks = await this.selectStacksForDiff(stackNames, exclusively, autoValidate);
422430

423431
// if we have a single stack, print it to STDOUT
424432
if (stacks.stackCount === 1) {
425433
if (!quiet) {
426-
return stacks.firstStack.template;
434+
data(serializeStructure(stacks.firstStack.template, json ?? false));
427435
}
428436
return undefined;
429437
}
@@ -437,7 +445,7 @@ export class CdkToolkit {
437445
// behind an environment variable.
438446
const isIntegMode = process.env.CDK_INTEG_MODE === '1';
439447
if (isIntegMode) {
440-
return stacks.stackArtifacts.map(s => s.template);
448+
data(serializeStructure(stacks.stackArtifacts.map(s => s.template), json ?? false));
441449
}
442450

443451
// not outputting template to stdout, let's explain things to the user a little bit...

packages/aws-cdk/lib/cli.ts

+50-38
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { realHandler as doctor } from '../lib/commands/doctor';
1919
import { RequireApproval } from '../lib/diff';
2020
import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init';
2121
import { data, debug, error, print, setLogLevel } from '../lib/logging';
22+
import { displayNotices, refreshNotices } from '../lib/notices';
2223
import { PluginHost } from '../lib/plugin';
23-
import { serializeStructure } from '../lib/serialize';
2424
import { Command, Configuration, Settings } from '../lib/settings';
2525
import * as version from '../lib/version';
2626

@@ -71,6 +71,7 @@ async function parseCommandLineArguments() {
7171
.option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true })
7272
.option('staging', { type: 'boolean', desc: 'Copy assets to the output directory (use --no-staging to disable, needed for local debugging the source files with SAM CLI)', default: true })
7373
.option('output', { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true })
74+
.option('notices', { type: 'boolean', desc: 'Show relevant notices' })
7475
.option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false })
7576
.command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', yargs => yargs
7677
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }),
@@ -193,6 +194,8 @@ async function parseCommandLineArguments() {
193194
.option('security-only', { type: 'boolean', desc: 'Only diff for broadened security changes', default: false })
194195
.option('fail', { type: 'boolean', desc: 'Fail with exit code 1 in case of diff', default: false }))
195196
.command('metadata [STACK]', 'Returns all metadata associated with this stack')
197+
.command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore')
198+
.command('notices', 'Returns a list of relevant notices')
196199
.command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', yargs => yargs
197200
.option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanguages })
198201
.option('list', { type: 'boolean', desc: 'List the available templates' })
@@ -227,6 +230,10 @@ if (!process.stdout.isTTY) {
227230
}
228231

229232
async function initCommandLine() {
233+
void refreshNotices()
234+
.then(_ => debug('Notices refreshed'))
235+
.catch(e => debug(`Notices refresh failed: ${e}`));
236+
230237
const argv = await parseCommandLineArguments();
231238
if (argv.verbose) {
232239
setLogLevel(argv.verbose);
@@ -295,37 +302,32 @@ async function initCommandLine() {
295302
const commandOptions = { args: argv, configuration, aws: sdkProvider };
296303

297304
try {
305+
return await main(cmd, argv);
306+
} finally {
307+
await version.displayVersionMessage();
298308

299-
let returnValue = undefined;
300-
301-
switch (cmd) {
302-
case 'context':
303-
returnValue = await context(commandOptions);
304-
break;
305-
case 'docs':
306-
returnValue = await docs(commandOptions);
307-
break;
308-
case 'doctor':
309-
returnValue = await doctor(commandOptions);
310-
break;
311-
}
312-
313-
if (returnValue === undefined) {
314-
returnValue = await main(cmd, argv);
309+
if (shouldDisplayNotices()) {
310+
if (cmd === 'notices') {
311+
await displayNotices({
312+
outdir: configuration.settings.get(['output']) ?? 'cdk.out',
313+
acknowledgedIssueNumbers: [],
314+
ignoreCache: true,
315+
});
316+
} else {
317+
await displayNotices({
318+
outdir: configuration.settings.get(['output']) ?? 'cdk.out',
319+
acknowledgedIssueNumbers: configuration.context.get('acknowledged-issue-numbers') ?? [],
320+
ignoreCache: false,
321+
});
322+
}
315323
}
316324

317-
if (typeof returnValue === 'object') {
318-
return toJsonOrYaml(returnValue);
319-
} else if (typeof returnValue === 'string') {
320-
return returnValue;
321-
} else {
322-
return returnValue;
325+
function shouldDisplayNotices(): boolean {
326+
return configuration.settings.get(['notices']) ?? true;
323327
}
324-
} finally {
325-
await version.displayVersionMessage();
326328
}
327329

328-
async function main(command: string, args: any): Promise<number | string | {} | void> {
330+
async function main(command: string, args: any): Promise<number | void> {
329331
const toolkitStackName: string = ToolkitInfo.determineName(configuration.settings.get(['toolkitStackName']));
330332
debug(`Toolkit stack: ${chalk.bold(toolkitStackName)}`);
331333

@@ -352,9 +354,18 @@ async function initCommandLine() {
352354
});
353355

354356
switch (command) {
357+
case 'context':
358+
return context(commandOptions);
359+
360+
case 'docs':
361+
return docs(commandOptions);
362+
363+
case 'doctor':
364+
return doctor(commandOptions);
365+
355366
case 'ls':
356367
case 'list':
357-
return cli.list(args.STACKS, { long: args.long });
368+
return cli.list(args.STACKS, { long: args.long, json: argv.json });
358369

359370
case 'diff':
360371
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL);
@@ -458,14 +469,21 @@ async function initCommandLine() {
458469
case 'synthesize':
459470
case 'synth':
460471
if (args.exclusively) {
461-
return cli.synth(args.STACKS, args.exclusively, args.quiet, args.validation);
472+
return cli.synth(args.STACKS, args.exclusively, args.quiet, args.validation, argv.json);
462473
} else {
463-
return cli.synth(args.STACKS, true, args.quiet, args.validation);
474+
return cli.synth(args.STACKS, true, args.quiet, args.validation, argv.json);
464475
}
465476

477+
case 'notices':
478+
// This is a valid command, but we're postponing its execution
479+
return;
466480

467481
case 'metadata':
468-
return cli.metadata(args.STACK);
482+
return cli.metadata(args.STACK, argv.json);
483+
484+
case 'acknowledge':
485+
case 'ack':
486+
return cli.acknowledge(args.ID);
469487

470488
case 'init':
471489
const language = configuration.settings.get(['language']);
@@ -482,9 +500,6 @@ async function initCommandLine() {
482500
}
483501
}
484502

485-
function toJsonOrYaml(object: any): string {
486-
return serializeStructure(object, argv.json);
487-
}
488503
}
489504

490505
/**
@@ -558,11 +573,8 @@ function yargsNegativeAlias<T extends { [x in S | L ]: boolean | undefined }, S
558573

559574
export function cli() {
560575
initCommandLine()
561-
.then(value => {
562-
if (value == null) { return; }
563-
if (typeof value === 'string') {
564-
data(value);
565-
} else if (typeof value === 'number') {
576+
.then(async (value) => {
577+
if (typeof value === 'number') {
566578
process.exitCode = value;
567579
}
568580
})

0 commit comments

Comments
 (0)