Skip to content

Commit a87dee7

Browse files
authored
feat(cli): support for matching notices with arbitrary module names (#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*
1 parent d325004 commit a87dee7

File tree

4 files changed

+275
-26
lines changed

4 files changed

+275
-26
lines changed

packages/aws-cdk/README.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,6 @@ $ cdk doctor
507507

508508
## Notices
509509

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

@@ -530,6 +528,7 @@ NOTICES
530528

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

531+
533532
17061 Error when building EKS cluster with monocdk import
534533

535534
Overview: When using monocdk/aws-eks to build a stack containing
@@ -540,6 +539,7 @@ NOTICES
540539

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

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

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

554554
```json
555555
{
556+
"notices": false,
556557
"context": {
557-
"notices": false
558+
...
558559
}
559560
}
560561
```
@@ -587,12 +588,16 @@ NOTICES
587588

588589
16603 Toggling off auto_delete_objects for Bucket empties the bucket
589590

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+
Overview: if a stack is deployed with an S3 bucket with
592+
auto_delete_objects=True, and then re-deployed with
593+
auto_delete_objects=False, all the objects in the bucket
594+
will be deleted.
591595

592596
Affected versions: framework: <=2.15.0 >=2.10.0
593597

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

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

packages/aws-cdk/lib/notices.ts

+100-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import * as fs from 'fs-extra';
44
import * as semver from 'semver';
55
import { debug, print } from './logging';
6+
import { flatMap } from './util';
67
import { cdkCacheDir } from './util/directories';
78
import { versionNumber } from './version';
89

@@ -75,8 +76,8 @@ export interface FilterNoticeOptions {
7576
export function filterNotices(data: Notice[], options: FilterNoticeOptions): Notice[] {
7677
const filter = new NoticeFilter({
7778
cliVersion: options.cliVersion ?? versionNumber(),
78-
frameworkVersion: options.frameworkVersion ?? frameworkVersion(options.outdir ?? 'cdk.out'),
7979
acknowledgedIssueNumbers: options.acknowledgedIssueNumbers ?? new Set(),
80+
tree: loadTree(options.outdir ?? 'cdk.out').tree,
8081
});
8182
return data.filter(notice => filter.apply(notice));
8283
}
@@ -188,8 +189,8 @@ export class CachedDataSource implements NoticeDataSource {
188189

189190
export interface NoticeFilterProps {
190191
cliVersion: string,
191-
frameworkVersion: string | undefined,
192192
acknowledgedIssueNumbers: Set<number>,
193+
tree: ConstructTreeNode,
193194
}
194195

195196
export class NoticeFilter {
@@ -206,8 +207,9 @@ export class NoticeFilter {
206207
if (this.acknowledgedIssueNumbers.has(notice.issueNumber)) {
207208
return false;
208209
}
210+
209211
return this.applyVersion(notice, 'cli', this.props.cliVersion) ||
210-
this.applyVersion(notice, 'framework', this.props.frameworkVersion);
212+
match(resolveAliases(notice.components), this.props.tree);
211213
}
212214

213215
/**
@@ -222,6 +224,32 @@ export class NoticeFilter {
222224
}
223225
}
224226

227+
/**
228+
* Some component names are aliases to actual component names. For example "framework"
229+
* is an alias for either the core library (v1) or the whole CDK library (v2).
230+
*
231+
* This function converts all aliases to their actual counterpart names, to be used to
232+
* match against the construct tree.
233+
*
234+
* @param components a list of components. Components whose name is an alias will be
235+
* transformed and all others will be left intact.
236+
*/
237+
function resolveAliases(components: Component[]): Component[] {
238+
return flatMap(components, component => {
239+
if (component.name === 'framework') {
240+
return [{
241+
name: '@aws-cdk/core.',
242+
version: component.version,
243+
}, {
244+
name: 'aws-cdk-lib.',
245+
version: component.version,
246+
}];
247+
} else {
248+
return [component];
249+
}
250+
});
251+
}
252+
225253
function formatNotice(notice: Notice): string {
226254
const componentsValue = notice.components.map(c => `${c.name}: ${c.version}`).join(', ');
227255
return [
@@ -244,21 +272,77 @@ function formatOverview(text: string) {
244272
return '\t' + heading + content;
245273
}
246274

247-
function frameworkVersion(outdir: string): string | undefined {
248-
const tree = loadTree().tree;
275+
/**
276+
* Whether any component in the tree matches any component in the query.
277+
* A match happens when:
278+
*
279+
* 1. The version of the node matches the version in the query, interpreted
280+
* as a semver range.
281+
*
282+
* 2. The name in the query is a prefix of the node name when the query ends in '.',
283+
* or the two names are exactly the same, otherwise.
284+
*/
285+
function match(query: Component[], tree: ConstructTreeNode): boolean {
286+
return some(tree, node => {
287+
return query.some(component =>
288+
compareNames(component.name, node.constructInfo?.fqn) &&
289+
compareVersions(component.version, node.constructInfo?.version));
290+
});
249291

250-
if (tree?.constructInfo?.fqn.startsWith('aws-cdk-lib')
251-
|| tree?.constructInfo?.fqn.startsWith('@aws-cdk/core')) {
252-
return tree.constructInfo.version;
292+
function compareNames(pattern: string, target: string | undefined): boolean {
293+
if (target == null) { return false; }
294+
return pattern.endsWith('.') ? target.startsWith(pattern) : pattern === target;
253295
}
254-
return undefined;
255296

256-
function loadTree() {
257-
try {
258-
return fs.readJSONSync(path.join(outdir, 'tree.json'));
259-
} catch (e) {
260-
debug(`Failed to get tree.json file: ${e}`);
261-
return {};
297+
function compareVersions(pattern: string, target: string | undefined): boolean {
298+
return semver.satisfies(target ?? '', pattern);
299+
}
300+
}
301+
302+
function loadTree(outdir: string) {
303+
try {
304+
return fs.readJSONSync(path.join(outdir, 'tree.json'));
305+
} catch (e) {
306+
debug(`Failed to get tree.json file: ${e}`);
307+
return {};
308+
}
309+
}
310+
311+
/**
312+
* Source information on a construct (class fqn and version)
313+
*/
314+
interface ConstructInfo {
315+
readonly fqn: string;
316+
readonly version: string;
317+
}
318+
319+
/**
320+
* A node in the construct tree.
321+
* @internal
322+
*/
323+
interface ConstructTreeNode {
324+
readonly id: string;
325+
readonly path: string;
326+
readonly children?: { [key: string]: ConstructTreeNode };
327+
readonly attributes?: { [key: string]: any };
328+
329+
/**
330+
* Information on the construct class that led to this node, if available
331+
*/
332+
readonly constructInfo?: ConstructInfo;
333+
}
334+
335+
function some(node: ConstructTreeNode, predicate: (n: ConstructTreeNode) => boolean): boolean {
336+
return node != null && (predicate(node) || findInChildren());
337+
338+
function findInChildren(): boolean {
339+
if (node.children == null) { return false; }
340+
341+
for (const name in node.children) {
342+
if (some(node.children[name], predicate)) {
343+
return true;
344+
}
262345
}
346+
return false;
263347
}
264-
}
348+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"version": "tree-0.1",
3+
"tree": {
4+
"id": "App",
5+
"path": "",
6+
"children": {
7+
"Tree": {
8+
"id": "Tree",
9+
"path": "Tree",
10+
"constructInfo": {
11+
"fqn": "constructs.Construct",
12+
"version": "10.0.66"
13+
}
14+
},
15+
"SimulationStack": {
16+
"id": "SimulationStack",
17+
"path": "SimulationStack",
18+
"children": {
19+
"HttpApi": {
20+
"id": "HttpApi",
21+
"path": "SimulationStack/HttpApi",
22+
"children": {
23+
"Resource": {
24+
"id": "Resource",
25+
"path": "SimulationStack/HttpApi/Resource",
26+
"attributes": {
27+
"aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api",
28+
"aws:cdk:cloudformation:props": {
29+
"name": "HttpApi",
30+
"protocolType": "HTTP"
31+
}
32+
},
33+
"constructInfo": {
34+
"fqn": "aws-cdk-lib.aws_apigatewayv2.CfnApi",
35+
"version": "2.8.0"
36+
}
37+
},
38+
"DefaultStage": {
39+
"id": "DefaultStage",
40+
"path": "SimulationStack/HttpApi/DefaultStage",
41+
"children": {
42+
"Resource": {
43+
"id": "Resource",
44+
"path": "SimulationStack/HttpApi/DefaultStage/Resource",
45+
"attributes": {
46+
"aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage",
47+
"aws:cdk:cloudformation:props": {
48+
"apiId": {
49+
"Ref": "HttpApiF5A9A8A7"
50+
},
51+
"stageName": "$default",
52+
"autoDeploy": true
53+
}
54+
},
55+
"constructInfo": {
56+
"fqn": "aws-cdk-lib.aws_apigatewayv2.CfnStage",
57+
"version": "2.8.0"
58+
}
59+
}
60+
},
61+
"constructInfo": {
62+
"fqn": "@aws-cdk/aws-apigatewayv2-alpha.HttpStage",
63+
"version": "2.13.0-alpha.0"
64+
}
65+
}
66+
},
67+
"constructInfo": {
68+
"fqn": "@aws-cdk/aws-apigatewayv2-alpha.HttpApi",
69+
"version": "2.13.0-alpha.0"
70+
}
71+
},
72+
"CDKMetadata": {
73+
"id": "CDKMetadata",
74+
"path": "SimulationStack/CDKMetadata",
75+
"children": {
76+
"Default": {
77+
"id": "Default",
78+
"path": "SimulationStack/CDKMetadata/Default",
79+
"constructInfo": {
80+
"fqn": "aws-cdk-lib.CfnResource",
81+
"version": "2.8.0"
82+
}
83+
},
84+
"Condition": {
85+
"id": "Condition",
86+
"path": "SimulationStack/CDKMetadata/Condition",
87+
"constructInfo": {
88+
"fqn": "aws-cdk-lib.CfnCondition",
89+
"version": "2.8.0"
90+
}
91+
}
92+
},
93+
"constructInfo": {
94+
"fqn": "constructs.Construct",
95+
"version": "10.0.66"
96+
}
97+
}
98+
},
99+
"constructInfo": {
100+
"fqn": "aws-cdk-lib.Stack",
101+
"version": "2.8.0"
102+
}
103+
}
104+
},
105+
"constructInfo": {
106+
"fqn": "aws-cdk-lib.App",
107+
"version": "2.8.0"
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)