diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 3c05f296e2dc9..372be74d765b3 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -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, @@ -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 @@ -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". ``` @@ -553,8 +553,9 @@ You can suppress warnings in a variety of ways: ```json { + "notices": false, "context": { - "notices": false + ... } } ``` @@ -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 ". For example, "cdk acknowledge 16603". ``` diff --git a/packages/aws-cdk/lib/notices.ts b/packages/aws-cdk/lib/notices.ts index dee9eab05fa68..ba815414f02b2 100644 --- a/packages/aws-cdk/lib/notices.ts +++ b/packages/aws-cdk/lib/notices.ts @@ -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'; @@ -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)); } @@ -188,8 +189,8 @@ export class CachedDataSource implements NoticeDataSource { export interface NoticeFilterProps { cliVersion: string, - frameworkVersion: string | undefined, acknowledgedIssueNumbers: Set, + tree: ConstructTreeNode, } export class NoticeFilter { @@ -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); } /** @@ -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 [ @@ -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; } -} \ No newline at end of file +} diff --git a/packages/aws-cdk/test/cloud-assembly-trees/experimental-module/tree.json b/packages/aws-cdk/test/cloud-assembly-trees/experimental-module/tree.json new file mode 100644 index 0000000000000..bb2fa029b3aed --- /dev/null +++ b/packages/aws-cdk/test/cloud-assembly-trees/experimental-module/tree.json @@ -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" + } + } + } \ No newline at end of file diff --git a/packages/aws-cdk/test/notices.test.ts b/packages/aws-cdk/test/notices.test.ts index d0c6d41852d63..71819ff87ad48 100644 --- a/packages/aws-cdk/test/notices.test.ts +++ b/packages/aws-cdk/test/notices.test.ts @@ -45,6 +45,39 @@ const FRAMEWORK_2_1_0_AFFECTED_NOTICE = { schemaVersion: '1', }; +const NOTICE_FOR_APIGATEWAYV2 = { + title: 'Regression on module foobar', + issueNumber: 1234, + overview: 'Some bug description', + components: [{ + name: '@aws-cdk/aws-apigatewayv2-alpha.', + version: '<= 2.13.0-alpha.0', + }], + schemaVersion: '1', +}; + +const NOTICE_FOR_APIGATEWAY = { + title: 'Regression on module foobar', + issueNumber: 1234, + overview: 'Some bug description', + components: [{ + name: '@aws-cdk/aws-apigateway', + version: '<= 2.13.0-alpha.0', + }], + schemaVersion: '1', +}; + +const NOTICE_FOR_APIGATEWAYV2_CFN_STAGE = { + title: 'Regression on module foobar', + issueNumber: 1234, + overview: 'Some bug description', + components: [{ + name: 'aws-cdk-lib.aws_apigatewayv2.CfnStage', + version: '<= 2.13.0-alpha.0', + }], + schemaVersion: '1', +}; + describe('cli notices', () => { beforeAll(() => { jest @@ -111,21 +144,38 @@ describe('cli notices', () => { const notices = [FRAMEWORK_2_1_0_AFFECTED_NOTICE]; expect(filterNotices(notices, { - frameworkVersion: '2.0.0', + outdir: path.join(__dirname, 'cloud-assembly-trees/built-with-2_12_0'), + })).toEqual([]); + + expect(filterNotices(notices, { + outdir: path.join(__dirname, 'cloud-assembly-trees/built-with-1_144_0'), })).toEqual([FRAMEWORK_2_1_0_AFFECTED_NOTICE]); + }); + + test('correctly filter notices on arbitrary modules', () => { + const notices = [NOTICE_FOR_APIGATEWAYV2]; + // module-level match expect(filterNotices(notices, { - frameworkVersion: '2.2.0', - })).toEqual([]); + outdir: path.join(__dirname, 'cloud-assembly-trees/experimental-module'), + })).toEqual([NOTICE_FOR_APIGATEWAYV2]); + // no apigatewayv2 in the tree expect(filterNotices(notices, { outdir: path.join(__dirname, 'cloud-assembly-trees/built-with-2_12_0'), })).toEqual([]); - expect(filterNotices(notices, { - outdir: path.join(__dirname, 'cloud-assembly-trees/built-with-1_144_0'), - })).toEqual([FRAMEWORK_2_1_0_AFFECTED_NOTICE]); + // module name mismatch: apigateway != apigatewayv2 + expect(filterNotices([NOTICE_FOR_APIGATEWAY], { + outdir: path.join(__dirname, 'cloud-assembly-trees/experimental-module'), + })).toEqual([]); + + // construct-level match + expect(filterNotices([NOTICE_FOR_APIGATEWAYV2_CFN_STAGE], { + outdir: path.join(__dirname, 'cloud-assembly-trees/experimental-module'), + })).toEqual([NOTICE_FOR_APIGATEWAYV2_CFN_STAGE]); }); + }); describe(WebsiteNoticeDataSource, () => {