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

feat(cli): support for notices #18936

Merged
merged 26 commits into from Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
otaviomacedo marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have put the command here, but I guess you're not doing that on purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if we put the command here, it will show the notices again at the end, unless we have some way of signaling that notices were already shown and there is no need to show it again. I don't like either approach, but this one seems a bit cleaner.

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