Skip to content

Commit

Permalink
feat(cli): support for matching notices with arbitrary module names (#…
Browse files Browse the repository at this point in the history
…19088)

This change allows us to publish notices to match any arbitrary module or
construct. For example, a notice that targets the component

```json
{
    "name": "@aws-cdk/aws-apigatewayv2-alpha",
    "version": ">=2.10.0"
}
```

will be displayed to the user if they have in their cloud assembly a resource of
type `@aws-cdk/aws-apigatewayv2-alpha.HttpStage` or
`@aws-cdk/aws-apigatewayv2-alpha.HttpApi` or any other construct from the
`apigatewayv2` module, as long as it was built using some version of the
module above or equal to 2.10.0.

We can also target a specific construct, instead:

```json
{
    "name": "@aws-cdk/aws-apigatewayv2-alpha.HttpStage",
    "version": ">=2.10.0"
}
```


----

*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 d325004 commit a87dee7
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 26 deletions.
13 changes: 9 additions & 4 deletions packages/aws-cdk/README.md
Expand Up @@ -507,8 +507,6 @@ $ cdk doctor

## 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,

Expand All @@ -530,6 +528,7 @@ NOTICES

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
Expand All @@ -540,6 +539,7 @@ NOTICES

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".
```

Expand All @@ -553,8 +553,9 @@ You can suppress warnings in a variety of ways:

```json
{
"notices": false,
"context": {
"notices": false
...
}
}
```
Expand Down Expand Up @@ -587,12 +588,16 @@ 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.
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".
```

Expand Down
116 changes: 100 additions & 16 deletions packages/aws-cdk/lib/notices.ts
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';
import * as fs from 'fs-extra';
import * as semver from 'semver';
import { debug, print } from './logging';
import { flatMap } from './util';
import { cdkCacheDir } from './util/directories';
import { versionNumber } from './version';

Expand Down Expand Up @@ -75,8 +76,8 @@ export interface FilterNoticeOptions {
export function filterNotices(data: Notice[], options: FilterNoticeOptions): Notice[] {
const filter = new NoticeFilter({
cliVersion: options.cliVersion ?? versionNumber(),
frameworkVersion: options.frameworkVersion ?? frameworkVersion(options.outdir ?? 'cdk.out'),
acknowledgedIssueNumbers: options.acknowledgedIssueNumbers ?? new Set(),
tree: loadTree(options.outdir ?? 'cdk.out').tree,
});
return data.filter(notice => filter.apply(notice));
}
Expand Down Expand Up @@ -188,8 +189,8 @@ export class CachedDataSource implements NoticeDataSource {

export interface NoticeFilterProps {
cliVersion: string,
frameworkVersion: string | undefined,
acknowledgedIssueNumbers: Set<number>,
tree: ConstructTreeNode,
}

export class NoticeFilter {
Expand All @@ -206,8 +207,9 @@ export class NoticeFilter {
if (this.acknowledgedIssueNumbers.has(notice.issueNumber)) {
return false;
}

return this.applyVersion(notice, 'cli', this.props.cliVersion) ||
this.applyVersion(notice, 'framework', this.props.frameworkVersion);
match(resolveAliases(notice.components), this.props.tree);
}

/**
Expand All @@ -222,6 +224,32 @@ export class NoticeFilter {
}
}

/**
* Some component names are aliases to actual component names. For example "framework"
* is an alias for either the core library (v1) or the whole CDK library (v2).
*
* This function converts all aliases to their actual counterpart names, to be used to
* match against the construct tree.
*
* @param components a list of components. Components whose name is an alias will be
* transformed and all others will be left intact.
*/
function resolveAliases(components: Component[]): Component[] {
return flatMap(components, component => {
if (component.name === 'framework') {
return [{
name: '@aws-cdk/core.',
version: component.version,
}, {
name: 'aws-cdk-lib.',
version: component.version,
}];
} else {
return [component];
}
});
}

function formatNotice(notice: Notice): string {
const componentsValue = notice.components.map(c => `${c.name}: ${c.version}`).join(', ');
return [
Expand All @@ -244,21 +272,77 @@ function formatOverview(text: string) {
return '\t' + heading + content;
}

function frameworkVersion(outdir: string): string | undefined {
const tree = loadTree().tree;
/**
* Whether any component in the tree matches any component in the query.
* A match happens when:
*
* 1. The version of the node matches the version in the query, interpreted
* as a semver range.
*
* 2. The name in the query is a prefix of the node name when the query ends in '.',
* or the two names are exactly the same, otherwise.
*/
function match(query: Component[], tree: ConstructTreeNode): boolean {
return some(tree, node => {
return query.some(component =>
compareNames(component.name, node.constructInfo?.fqn) &&
compareVersions(component.version, node.constructInfo?.version));
});

if (tree?.constructInfo?.fqn.startsWith('aws-cdk-lib')
|| tree?.constructInfo?.fqn.startsWith('@aws-cdk/core')) {
return tree.constructInfo.version;
function compareNames(pattern: string, target: string | undefined): boolean {
if (target == null) { return false; }
return pattern.endsWith('.') ? target.startsWith(pattern) : pattern === target;
}
return undefined;

function loadTree() {
try {
return fs.readJSONSync(path.join(outdir, 'tree.json'));
} catch (e) {
debug(`Failed to get tree.json file: ${e}`);
return {};
function compareVersions(pattern: string, target: string | undefined): boolean {
return semver.satisfies(target ?? '', pattern);
}
}

function loadTree(outdir: string) {
try {
return fs.readJSONSync(path.join(outdir, 'tree.json'));
} catch (e) {
debug(`Failed to get tree.json file: ${e}`);
return {};
}
}

/**
* Source information on a construct (class fqn and version)
*/
interface ConstructInfo {
readonly fqn: string;
readonly version: string;
}

/**
* A node in the construct tree.
* @internal
*/
interface ConstructTreeNode {
readonly id: string;
readonly path: string;
readonly children?: { [key: string]: ConstructTreeNode };
readonly attributes?: { [key: string]: any };

/**
* Information on the construct class that led to this node, if available
*/
readonly constructInfo?: ConstructInfo;
}

function some(node: ConstructTreeNode, predicate: (n: ConstructTreeNode) => boolean): boolean {
return node != null && (predicate(node) || findInChildren());

function findInChildren(): boolean {
if (node.children == null) { return false; }

for (const name in node.children) {
if (some(node.children[name], predicate)) {
return true;
}
}
return false;
}
}
}
@@ -0,0 +1,110 @@
{
"version": "tree-0.1",
"tree": {
"id": "App",
"path": "",
"children": {
"Tree": {
"id": "Tree",
"path": "Tree",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.0.66"
}
},
"SimulationStack": {
"id": "SimulationStack",
"path": "SimulationStack",
"children": {
"HttpApi": {
"id": "HttpApi",
"path": "SimulationStack/HttpApi",
"children": {
"Resource": {
"id": "Resource",
"path": "SimulationStack/HttpApi/Resource",
"attributes": {
"aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api",
"aws:cdk:cloudformation:props": {
"name": "HttpApi",
"protocolType": "HTTP"
}
},
"constructInfo": {
"fqn": "aws-cdk-lib.aws_apigatewayv2.CfnApi",
"version": "2.8.0"
}
},
"DefaultStage": {
"id": "DefaultStage",
"path": "SimulationStack/HttpApi/DefaultStage",
"children": {
"Resource": {
"id": "Resource",
"path": "SimulationStack/HttpApi/DefaultStage/Resource",
"attributes": {
"aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage",
"aws:cdk:cloudformation:props": {
"apiId": {
"Ref": "HttpApiF5A9A8A7"
},
"stageName": "$default",
"autoDeploy": true
}
},
"constructInfo": {
"fqn": "aws-cdk-lib.aws_apigatewayv2.CfnStage",
"version": "2.8.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-apigatewayv2-alpha.HttpStage",
"version": "2.13.0-alpha.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-apigatewayv2-alpha.HttpApi",
"version": "2.13.0-alpha.0"
}
},
"CDKMetadata": {
"id": "CDKMetadata",
"path": "SimulationStack/CDKMetadata",
"children": {
"Default": {
"id": "Default",
"path": "SimulationStack/CDKMetadata/Default",
"constructInfo": {
"fqn": "aws-cdk-lib.CfnResource",
"version": "2.8.0"
}
},
"Condition": {
"id": "Condition",
"path": "SimulationStack/CDKMetadata/Condition",
"constructInfo": {
"fqn": "aws-cdk-lib.CfnCondition",
"version": "2.8.0"
}
}
},
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.0.66"
}
}
},
"constructInfo": {
"fqn": "aws-cdk-lib.Stack",
"version": "2.8.0"
}
}
},
"constructInfo": {
"fqn": "aws-cdk-lib.App",
"version": "2.8.0"
}
}
}

0 comments on commit a87dee7

Please sign in to comment.