Skip to content

Commit

Permalink
feat(cli): support for notices (#18936)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
otaviomacedo committed Feb 22, 2022
1 parent c7394c9 commit d37fbbb
Show file tree
Hide file tree
Showing 9 changed files with 772 additions and 61 deletions.
117 changes: 105 additions & 12 deletions packages/aws-cdk/README.md
Expand Up @@ -11,18 +11,20 @@

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

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

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

Expand Down Expand Up @@ -503,6 +505,97 @@ $ cdk doctor
- AWS_SDK_LOAD_CONFIG = 1
```

## Notices

> This feature exists on CDK CLI version 2.14.0 and up.
CDK Notices are important messages regarding security vulnerabilities, regressions, and usage of unsupported
versions. Relevant notices appear on every command by default. For example,

```console
$ cdk deploy

... # Normal output of the command

NOTICES

16603 Toggling off auto_delete_objects for Bucket empties the bucket

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.
Affected versions: <1.126.0.

More information at: https://github.com/aws/aws-cdk/issues/16603

17061 Error when building EKS cluster with monocdk import

Overview: When using monocdk/aws-eks to build a stack containing
an EKS cluster, error is thrown about missing
lambda-layer-node-proxy-agent/layer/package.json.
Affected versions: >=1.126.0 <=1.130.0.

More information at: https://github.com/aws/aws-cdk/issues/17061

If you don’t want to see an notice anymore, use "cdk acknowledge ID". For example, "cdk acknowledge 16603".
```

You can suppress warnings in a variety of ways:

- per individual execution:

`cdk deploy --no-notices`

- disable all notices indefinitely through context in `cdk.json`:

```json
{
"context": {
"notices": false
}
}
```

- acknowleding individual notices via `cdk acknowledge` (see below).

### `cdk acknowledge`

To hide a particular notice that has been addressed or does not apply, call `cdk acknowledge` with the ID of
the notice:

```console
$cdk acknowledge 16603
```

> Please note that the acknowledgements are made project by project. If you acknowledge an notice in one CDK
> project, it will still appear on other projects when you run any CDK commands, unless you have suppressed
> or disabled notices.

### `cdk notices`

List the notices that are relevant to the current CDK repository, regardless of context flags or notices that
have been acknowledged:

```console
$ cdk notices

NOTICES

16603 Toggling off auto_delete_objects for Bucket empties the bucket

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.

Affected versions: framework: <=2.15.0 >=2.10.0

More information at: https://github.com/aws/aws-cdk/issues/16603

If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge 16603".
```

### Bundling

By default asset bundling is skipped for `cdk list` and `cdk destroy`. For `cdk deploy`, `cdk diff`
Expand Down
24 changes: 16 additions & 8 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Expand Up @@ -16,7 +16,7 @@ import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
import { data, debug, error, highlight, print, success, warning } from './logging';
import { deserializeStructure } from './serialize';
import { deserializeStructure, serializeStructure } from './serialize';
import { Configuration, PROJECT_CONFIG } from './settings';
import { numberFromBool, partition } from './util';

Expand Down Expand Up @@ -74,9 +74,16 @@ export class CdkToolkit {
constructor(private readonly props: CdkToolkitProps) {
}

public async metadata(stackName: string) {
public async metadata(stackName: string, json: boolean) {
const stacks = await this.selectSingleStackByName(stackName);
return stacks.firstStack.manifest.metadata ?? {};
data(serializeStructure(stacks.firstStack.manifest.metadata ?? {}, json));
}

public async acknowledge(noticeId: string) {
const acks = this.props.configuration.context.get('acknowledged-issue-numbers') ?? [];
acks.push(Number(noticeId));
this.props.configuration.context.set('acknowledged-issue-numbers', acks);
await this.props.configuration.saveContext();
}

public async diff(options: DiffOptions): Promise<number> {
Expand Down Expand Up @@ -384,7 +391,7 @@ export class CdkToolkit {
}
}

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

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

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

// if we have a single stack, print it to STDOUT
if (stacks.stackCount === 1) {
if (!quiet) {
return stacks.firstStack.template;
data(serializeStructure(stacks.firstStack.template, json ?? false));
}
return undefined;
}
Expand All @@ -437,7 +445,7 @@ export class CdkToolkit {
// behind an environment variable.
const isIntegMode = process.env.CDK_INTEG_MODE === '1';
if (isIntegMode) {
return stacks.stackArtifacts.map(s => s.template);
data(serializeStructure(stacks.stackArtifacts.map(s => s.template), json ?? false));
}

// not outputting template to stdout, let's explain things to the user a little bit...
Expand Down
88 changes: 50 additions & 38 deletions packages/aws-cdk/lib/cli.ts
Expand Up @@ -19,8 +19,8 @@ import { realHandler as doctor } from '../lib/commands/doctor';
import { RequireApproval } from '../lib/diff';
import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init';
import { data, debug, error, print, setLogLevel } from '../lib/logging';
import { displayNotices, refreshNotices } from '../lib/notices';
import { PluginHost } from '../lib/plugin';
import { serializeStructure } from '../lib/serialize';
import { Command, Configuration, Settings } from '../lib/settings';
import * as version from '../lib/version';

Expand Down Expand Up @@ -71,6 +71,7 @@ async function parseCommandLineArguments() {
.option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true })
.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 })
.option('output', { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true })
.option('notices', { type: 'boolean', desc: 'Show relevant notices' })
.option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false })
.command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', yargs => yargs
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }),
Expand Down Expand Up @@ -193,6 +194,8 @@ async function parseCommandLineArguments() {
.option('security-only', { type: 'boolean', desc: 'Only diff for broadened security changes', default: false })
.option('fail', { type: 'boolean', desc: 'Fail with exit code 1 in case of diff', default: false }))
.command('metadata [STACK]', 'Returns all metadata associated with this stack')
.command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore')
.command('notices', 'Returns a list of relevant notices')
.command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', yargs => yargs
.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 })
.option('list', { type: 'boolean', desc: 'List the available templates' })
Expand Down Expand Up @@ -227,6 +230,10 @@ if (!process.stdout.isTTY) {
}

async function initCommandLine() {
void refreshNotices()
.then(_ => debug('Notices refreshed'))
.catch(e => debug(`Notices refresh failed: ${e}`));

const argv = await parseCommandLineArguments();
if (argv.verbose) {
setLogLevel(argv.verbose);
Expand Down Expand Up @@ -295,37 +302,32 @@ async function initCommandLine() {
const commandOptions = { args: argv, configuration, aws: sdkProvider };

try {
return await main(cmd, argv);
} finally {
await version.displayVersionMessage();

let returnValue = undefined;

switch (cmd) {
case 'context':
returnValue = await context(commandOptions);
break;
case 'docs':
returnValue = await docs(commandOptions);
break;
case 'doctor':
returnValue = await doctor(commandOptions);
break;
}

if (returnValue === undefined) {
returnValue = await main(cmd, argv);
if (shouldDisplayNotices()) {
if (cmd === 'notices') {
await displayNotices({
outdir: configuration.settings.get(['output']) ?? 'cdk.out',
acknowledgedIssueNumbers: [],
ignoreCache: true,
});
} else {
await displayNotices({
outdir: configuration.settings.get(['output']) ?? 'cdk.out',
acknowledgedIssueNumbers: configuration.context.get('acknowledged-issue-numbers') ?? [],
ignoreCache: false,
});
}
}

if (typeof returnValue === 'object') {
return toJsonOrYaml(returnValue);
} else if (typeof returnValue === 'string') {
return returnValue;
} else {
return returnValue;
function shouldDisplayNotices(): boolean {
return configuration.settings.get(['notices']) ?? true;
}
} finally {
await version.displayVersionMessage();
}

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

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

switch (command) {
case 'context':
return context(commandOptions);

case 'docs':
return docs(commandOptions);

case 'doctor':
return doctor(commandOptions);

case 'ls':
case 'list':
return cli.list(args.STACKS, { long: args.long });
return cli.list(args.STACKS, { long: args.long, json: argv.json });

case 'diff':
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL);
Expand Down Expand Up @@ -458,14 +469,21 @@ async function initCommandLine() {
case 'synthesize':
case 'synth':
if (args.exclusively) {
return cli.synth(args.STACKS, args.exclusively, args.quiet, args.validation);
return cli.synth(args.STACKS, args.exclusively, args.quiet, args.validation, argv.json);
} else {
return cli.synth(args.STACKS, true, args.quiet, args.validation);
return cli.synth(args.STACKS, true, args.quiet, args.validation, argv.json);
}

case 'notices':
// This is a valid command, but we're postponing its execution
return;

case 'metadata':
return cli.metadata(args.STACK);
return cli.metadata(args.STACK, argv.json);

case 'acknowledge':
case 'ack':
return cli.acknowledge(args.ID);

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

function toJsonOrYaml(object: any): string {
return serializeStructure(object, argv.json);
}
}

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

export function cli() {
initCommandLine()
.then(value => {
if (value == null) { return; }
if (typeof value === 'string') {
data(value);
} else if (typeof value === 'number') {
.then(async (value) => {
if (typeof value === 'number') {
process.exitCode = value;
}
})
Expand Down

0 comments on commit d37fbbb

Please sign in to comment.