From 2a57bc9560d9a78ef01e8151a749e09e623a0913 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Tue, 25 Feb 2025 13:29:31 +0000 Subject: [PATCH 01/12] docs(toolkit-lib): include `VERSION` file (#131) The docs build requires this file to contain the current version number. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 17 +++++++++-------- .../@aws-cdk/toolkit-lib/.projen/tasks.json | 5 ++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.projenrc.ts b/.projenrc.ts index d3d286a2..bc947298 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1217,18 +1217,19 @@ for (const tsconfig of [toolkitLib.tsconfigDev]) { } } +// Ad a command for the docs const toolkitLibDocs = toolkitLib.addTask('docs', { exec: 'typedoc lib/index.ts', receiveArgs: true, }); -toolkitLib.packageTask.spawn(toolkitLibDocs, { - // the nested directory is important - // the zip file needs to have this structure when created - args: ['--out dist/docs/cdk/api/toolkit-lib'], -}); -toolkitLib.packageTask.exec('zip -r ../docs.zip cdk ', { - cwd: 'dist/docs', -}); + +// When packaging, output the docs into a specific nested directory +// This is required because the zip file needs to have this structure when created +toolkitLib.packageTask.spawn(toolkitLibDocs, { args: ['--out dist/docs/cdk/api/toolkit-lib'] }); +// The docs build needs the version in a specific file at the nested root +toolkitLib.packageTask.exec('(cat dist/version.txt || echo "latest") > dist/docs/cdk/api/toolkit-lib/VERSION'); +// Zip the whole thing up, again paths are important here to get the desired folder structure +toolkitLib.packageTask.exec('zip -r ../docs.zip cdk', { cwd: 'dist/docs' }); toolkitLib.addTask('publish-local', { exec: './build-tools/package.sh', diff --git a/packages/@aws-cdk/toolkit-lib/.projen/tasks.json b/packages/@aws-cdk/toolkit-lib/.projen/tasks.json index c526c4e9..63fd8048 100644 --- a/packages/@aws-cdk/toolkit-lib/.projen/tasks.json +++ b/packages/@aws-cdk/toolkit-lib/.projen/tasks.json @@ -158,7 +158,10 @@ ] }, { - "exec": "zip -r ../docs.zip cdk ", + "exec": "(cat dist/version.txt || echo \"latest\") > dist/docs/cdk/api/toolkit-lib/VERSION" + }, + { + "exec": "zip -r ../docs.zip cdk", "cwd": "dist/docs" } ] From 757ae69e80931a83ac20ef32fe52e503961e2212 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Tue, 25 Feb 2025 15:40:12 +0100 Subject: [PATCH 02/12] feat(cli): can match notices against Node version (#128) This allows publishing notices that match against the version of Node we're executing on. Implemented by generalizing the code that matches against CLI version and bootstrap versions: it can now match against arbitrary named components. Also implement more complex matching rules: currently we match the pattern if one of them matches: ```ts // Matches if one of A, B or C matches components: [A, B, C] ``` Instead, generalize to Disjunctive Normal Form and treat the current case as a special case of DNF where every conjunction has one element: ```ts // The above gets interpreted as components: [[A], [B], [C]] // More complex rules: A and B together, or C components: [[A, B], [C]] ``` This way we can write rules to say that "component X on Node version Y" should get a notice. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- packages/aws-cdk/lib/notices.ts | 276 +++++++++++++++++--------- packages/aws-cdk/lib/tree.ts | 12 +- packages/aws-cdk/test/notices.test.ts | 120 ++++++++++- packages/aws-cdk/test/tree.test.ts | 4 +- 4 files changed, 312 insertions(+), 100 deletions(-) diff --git a/packages/aws-cdk/lib/notices.ts b/packages/aws-cdk/lib/notices.ts index ebeec4d0..8976d34d 100644 --- a/packages/aws-cdk/lib/notices.ts +++ b/packages/aws-cdk/lib/notices.ts @@ -11,8 +11,7 @@ import type { Context } from './api/context'; import { versionNumber } from './cli/version'; import { debug, info, warning, error } from './logging'; import { ToolkitError } from './toolkit/error'; -import { loadTreeFromDir, some } from './tree'; -import { flatMap } from './util'; +import { ConstructTreeNode, loadTreeFromDir } from './tree'; import { cdkCacheDir } from './util/directories'; import { formatErrorMessage } from './util/format-error'; @@ -84,116 +83,189 @@ export interface NoticesFilterFilterOptions { readonly bootstrappedEnvironments: BootstrappedEnvironment[]; } -export class NoticesFilter { +export abstract class NoticesFilter { public static filter(options: NoticesFilterFilterOptions): FilteredNotice[] { - return [ - ...this.findForCliVersion(options.data, options.cliVersion), - ...this.findForFrameworkVersion(options.data, options.outDir), - ...this.findForBootstrapVersion(options.data, options.bootstrappedEnvironments), + const components = [ + ...NoticesFilter.constructTreeComponents(options.outDir), + ...NoticesFilter.otherComponents(options), ]; + + return NoticesFilter.findForNamedComponents(options.data, components); } - private static findForCliVersion(data: Notice[], cliVersion: string): FilteredNotice[] { - return flatMap(data, notice => { - const affectedComponent = notice.components.find(component => component.name === 'cli'); - const affectedRange = affectedComponent?.version; + /** + * From a set of input options, return the notices components we are searching for + */ + private static otherComponents(options: NoticesFilterFilterOptions): ActualComponent[] { + return [ + // CLI + { + name: 'cli', + version: options.cliVersion, + }, + + // Node version + { + name: 'node', + version: process.version.replace(/^v/, ''), // remove the 'v' prefix. + dynamicName: 'node', + }, + + // Bootstrap environments + ...options.bootstrappedEnvironments.flatMap(env => { + const semverBootstrapVersion = semver.coerce(env.bootstrapStackVersion); + if (!semverBootstrapVersion) { + // we don't throw because notices should never crash the cli. + warning(`While filtering notices, could not coerce bootstrap version '${env.bootstrapStackVersion}' into semver`); + return []; + } - if (affectedRange == null) { - return []; - } + return [{ + name: 'bootstrap', + version: `${semverBootstrapVersion}`, + dynamicName: 'ENVIRONMENTS', + dynamicValue: env.environment.name, + }]; + }), + ]; + } - if (!semver.satisfies(cliVersion, affectedRange)) { - return []; + /** + * Based on a set of component names, find all notices that match one of the given components + */ + private static findForNamedComponents(data: Notice[], actualComponents: ActualComponent[]): FilteredNotice[] { + return data.flatMap(notice => { + const ors = this.resolveAliases(normalizeComponents(notice.components)); + + // Find the first set of the disjunctions of which all components match against the actual components. + // Return the actual components we found so that we can inject their dynamic values. A single filter + // component can match more than one actual component + for (const ands of ors) { + const matched = ands.map(affected => actualComponents.filter(actual => + NoticesFilter.componentNameMatches(affected, actual) && semver.satisfies(actual.version, affected.version))); + + // For every clause in the filter we matched one or more components + if (matched.every(xs => xs.length > 0)) { + const ret = new FilteredNotice(notice); + NoticesFilter.addDynamicValues(matched.flatMap(x => x), ret); + return [ret]; + } } - return [new FilteredNotice(notice)]; + return []; }); } - private static findForFrameworkVersion(data: Notice[], outDir: string): FilteredNotice[] { - const tree = loadTreeFromDir(outDir); - return flatMap(data, notice => { - // A match happens when: - // - // 1. The version of the node matches the version in the notice, interpreted - // as a semver range. - // - // AND - // - // 2. The name in the notice is a prefix of the node name when the query ends in '.', - // or the two names are exactly the same, otherwise. - - const matched = some(tree, node => { - return this.resolveAliases(notice.components).some(component => - compareNames(component.name, node.constructInfo?.fqn) && - compareVersions(component.version, node.constructInfo?.version)); - }); - - if (!matched) { - return []; - } - - return [new FilteredNotice(notice)]; + /** + * Whether the given "affected component" name applies to the given actual component name. + * + * The name matches if the name is exactly the same, or the name in the notice + * is a prefix of the node name when the query ends in '.'. + */ + private static componentNameMatches(pattern: Component, actual: ActualComponent): boolean { + return pattern.name.endsWith('.') ? actual.name.startsWith(pattern.name) : pattern.name === actual.name; + } - function compareNames(pattern: string, target: string | undefined): boolean { - if (target == null) { - return false; - } - return pattern.endsWith('.') ? target.startsWith(pattern) : pattern === target; + /** + * Adds dynamic values from the given ActualComponents + * + * If there are multiple components with the same dynamic name, they are joined + * by a comma. + */ + private static addDynamicValues(comps: ActualComponent[], notice: FilteredNotice) { + const dynamicValues: Record = {}; + for (const comp of comps) { + if (comp.dynamicName) { + dynamicValues[comp.dynamicName] = dynamicValues[comp.dynamicName] ?? []; + dynamicValues[comp.dynamicName].push(comp.dynamicValue ?? comp.version); } + } + for (const [key, values] of Object.entries(dynamicValues)) { + notice.addDynamicValue(key, values.join(',')); + } + } - function compareVersions(pattern: string, target: string | undefined): boolean { - return semver.satisfies(target ?? '', pattern); + /** + * Treat 'framework' as an alias for either `aws-cdk-lib.` or `@aws-cdk/core.`. + * + * Because it's EITHER `aws-cdk-lib` or `@aws-cdk/core`, we need to add multiple + * arrays at the top level. + */ + private static resolveAliases(ors: Component[][]): Component[][] { + return ors.flatMap(ands => { + const hasFramework = ands.find(c => c.name === 'framework'); + if (!hasFramework) { + return [ands]; } + + return [ + ands.map(c => c.name === 'framework' ? { ...c, name: '@aws-cdk/core.' } : c), + ands.map(c => c.name === 'framework' ? { ...c, name: 'aws-cdk-lib.' } : c), + ]; }); } - private static findForBootstrapVersion(data: Notice[], bootstrappedEnvironments: BootstrappedEnvironment[]): FilteredNotice[] { - return flatMap(data, notice => { - const affectedComponent = notice.components.find(component => component.name === 'bootstrap'); - const affectedRange = affectedComponent?.version; + /** + * Load the construct tree from the given directory and return its components + */ + private static constructTreeComponents(manifestDir: string): ActualComponent[] { + const tree = loadTreeFromDir(manifestDir); + if (!tree) { + return []; + } - if (affectedRange == null) { - return []; - } + const ret: ActualComponent[] = []; + recurse(tree); + return ret; - const affected = bootstrappedEnvironments.filter(i => { - const semverBootstrapVersion = semver.coerce(i.bootstrapStackVersion); - if (!semverBootstrapVersion) { - // we don't throw because notices should never crash the cli. - warning(`While filtering notices, could not coerce bootstrap version '${i.bootstrapStackVersion}' into semver`); - return false; - } - - return semver.satisfies(semverBootstrapVersion, affectedRange); - }); + function recurse(x: ConstructTreeNode) { + if (x.constructInfo?.fqn && x.constructInfo?.version) { + ret.push({ + name: x.constructInfo?.fqn, + version: x.constructInfo?.version, + }); + } - if (affected.length === 0) { - return []; + for (const child of Object.values(x.children ?? {})) { + recurse(child); } + } + } +} - const filtered = new FilteredNotice(notice); - filtered.addDynamicValue('ENVIRONMENTS', affected.map(s => s.environment.name).join(',')); +interface ActualComponent { + /** + * Name of the component + */ + readonly name: string; - return [filtered]; - }); - } + /** + * Version of the component + */ + readonly version: string; - private static 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]; - } - }); - } + /** + * If matched, under what name should it be added to the set of dynamic values + * + * These will be used to substitute placeholders in the message string, where + * placeholders look like `{resolve:XYZ}`. + * + * If there is more than one component with the same dynamic name, they are + * joined by ','. + * + * @default - Don't add to the set of dynamic values. + */ + readonly dynamicName?: string; + + /** + * If matched, what we should put in the set of dynamic values insstead of the version. + * + * Only used if `dynamicName` is set; by default we will add the actual version + * of the component. + * + * @default - The version. + */ + readonly dynamicValue?: string; } /** @@ -327,6 +399,10 @@ export class Notices { export interface Component { name: string; + + /** + * The range of affected versions + */ version: string; } @@ -334,11 +410,33 @@ export interface Notice { title: string; issueNumber: number; overview: string; - components: Component[]; + /** + * A set of affected components + * + * The canonical form of a list of components is in Disjunctive Normal Form + * (i.e., an OR of ANDs). This is the form when the list of components is a + * doubly nested array: the notice matches if all components of at least one + * of the top-level array matches. + * + * If the `components` is a single-level array, it is evaluated as an OR; it + * matches if any of the components matches. + */ + components: Array; schemaVersion: string; severity?: string; } +/** + * Normalizes the given components structure into DNF form + */ +function normalizeComponents(xs: Array): Component[][] { + return xs.map(x => Array.isArray(x) ? x : [x]); +} + +function renderConjunction(xs: Component[]): string { + return xs.map(c => `${c.name}: ${c.version}`).join(' AND '); +} + /** * Notice after passing the filter. A filter can augment a notice with * dynamic values as it has access to the dynamic matching data. @@ -354,7 +452,7 @@ export class FilteredNotice { } public format(): string { - const componentsValue = this.notice.components.map(c => `${c.name}: ${c.version}`).join(', '); + const componentsValue = normalizeComponents(this.notice.components).map(renderConjunction).join(', '); return this.resolveDynamicValues([ `${this.notice.issueNumber}\t${this.notice.title}`, this.formatOverview(), diff --git a/packages/aws-cdk/lib/tree.ts b/packages/aws-cdk/lib/tree.ts index 57ae5685..5368e9a1 100644 --- a/packages/aws-cdk/lib/tree.ts +++ b/packages/aws-cdk/lib/tree.ts @@ -29,30 +29,30 @@ export interface ConstructTreeNode { /** * Whether the provided predicate is true for at least one element in the construct (sub-)tree. */ -export function some(node: ConstructTreeNode, predicate: (n: ConstructTreeNode) => boolean): boolean { +export function some(node: ConstructTreeNode | undefined, predicate: (n: ConstructTreeNode) => boolean): boolean { return node != null && (predicate(node) || findInChildren()); function findInChildren(): boolean { - return Object.values(node.children ?? {}).some(child => some(child, predicate)); + return Object.values(node?.children ?? {}).some(child => some(child, predicate)); } } -export function loadTree(assembly: CloudAssembly) { +export function loadTree(assembly: CloudAssembly): ConstructTreeNode | undefined { try { const outdir = assembly.directory; const fileName = assembly.tree()?.file; return fileName ? fs.readJSONSync(path.join(outdir, fileName)).tree : {}; } catch (e) { trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); - return {}; + return undefined; } } -export function loadTreeFromDir(outdir: string) { +export function loadTreeFromDir(outdir: string): ConstructTreeNode | undefined { try { return fs.readJSONSync(path.join(outdir, 'tree.json')).tree; } catch (e) { trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); - return {}; + return undefined; } } diff --git a/packages/aws-cdk/test/notices.test.ts b/packages/aws-cdk/test/notices.test.ts index 76397318..cf031844 100644 --- a/packages/aws-cdk/test/notices.test.ts +++ b/packages/aws-cdk/test/notices.test.ts @@ -13,6 +13,7 @@ import { FilteredNotice, WebsiteNoticeDataSource, BootstrappedEnvironment, + Component, } from '../lib/notices'; import * as version from '../lib/cli/version'; import { Settings } from '../lib/api/settings'; @@ -21,7 +22,7 @@ import { Context } from '../lib/api/context'; const BASIC_BOOTSTRAP_NOTICE = { title: 'Exccessive permissions on file asset publishing role', issueNumber: 16600, - overview: 'FilePublishingRoleDefaultPolicy has too many permissions', + overview: 'FilePublishingRoleDefaultPolicy has too many permissions in {resolve:ENVIRONMENTS}', components: [{ name: 'bootstrap', version: '<25', @@ -281,12 +282,14 @@ describe(NoticesFilter, () => { }, ]; - expect(NoticesFilter.filter({ + const filtered = NoticesFilter.filter({ data: [BASIC_BOOTSTRAP_NOTICE], cliVersion, outDir, bootstrappedEnvironments: bootstrappedEnvironments, - }).map(f => f.notice)).toEqual([BASIC_BOOTSTRAP_NOTICE]); + }); + expect(filtered.map(f => f.notice)).toEqual([BASIC_BOOTSTRAP_NOTICE]); + expect(filtered.map(f => f.format()).join('\n')).toContain('env1,env2'); }); test('ignores invalid bootstrap versions', () => { @@ -301,9 +304,120 @@ describe(NoticesFilter, () => { bootstrappedEnvironments: [{ bootstrapStackVersion: NaN, environment: { account: 'account', region: 'region', name: 'env' } }], }).map(f => f.notice)).toEqual([]); }); + + test('node version', () => { + // can match node version + const outDir = path.join(__dirname, 'cloud-assembly-trees', 'built-with-2_12_0'); + const cliVersion = '1.0.0'; + + const filtered = NoticesFilter.filter({ + data: [ + { + title: 'matchme', + overview: 'You are running {resolve:node}', + issueNumber: 1, + schemaVersion: '1', + components: [ + { + name: 'node', + version: '>= 14.x', + }, + ] + }, + { + title: 'dontmatchme', + overview: 'dontmatchme', + issueNumber: 2, + schemaVersion: '1', + components: [ + { + name: 'node', + version: '>= 999.x', + }, + ] + }, + ] satisfies Notice[], + cliVersion, + outDir, + bootstrappedEnvironments: [], + }); + + expect(filtered.map(f => f.notice.title)).toEqual(['matchme']); + const nodeVersion = process.version.replace(/^v/, ''); + expect(filtered.map(f => f.format()).join('\n')).toContain(`You are running ${nodeVersion}`); + }); + + test.each([ + // No components => doesnt match + [ + [], + false, + ], + // Multiple single-level components => treated as an OR, one of them is fine + [ + [['cli 1.0.0'], ['node >=999.x']], + true, + ], + // OR of ANDS, all must match + [ + [['cli 1.0.0', 'node >=999.x']], + false, + ], + [ + [['cli 1.0.0', 'node >=14.x']], + true, + ], + [ + [['cli 1.0.0', 'node >=14.x'], ['cli >999.0.0']], + true, + ], + // Can combine matching against a construct and e.g. node version in the same query + [ + [['aws-cdk-lib.App ^2', 'node >=14.x']], + true, + ], + ])('disjunctive normal form: %j => %p', (components: string[][], shouldMatch) => { + // can match node version + const outDir = path.join(__dirname, 'cloud-assembly-trees', 'built-with-2_12_0'); + const cliVersion = '1.0.0'; + + // WHEN + const filtered = NoticesFilter.filter({ + data: [ + { + title: 'match', + overview: 'match', + issueNumber: 1, + schemaVersion: '1', + components: components.map(ands => ands.map(parseTestComponent)), + }, + ] satisfies Notice[], + cliVersion, + outDir, + bootstrappedEnvironments: [], + }); + + // THEN + expect(filtered.map(f => f.notice.title)).toEqual(shouldMatch ? ['match'] : []); + }); }); }); +/** + * Parse a test component from a string into a Component object. Just because this is easier to read in tests. + */ +function parseTestComponent(x: string): Component { + const parts = x.split(' '); + if (parts.length !== 2) { + throw new Error(`Invalid test component: ${x} (must use exactly 1 space)`); + } + return { + name: parts[0], + version: parts[1], + }; +} + + describe(WebsiteNoticeDataSource, () => { const dataSource = new WebsiteNoticeDataSource(); diff --git a/packages/aws-cdk/test/tree.test.ts b/packages/aws-cdk/test/tree.test.ts index 06a7e324..e47b621a 100644 --- a/packages/aws-cdk/test/tree.test.ts +++ b/packages/aws-cdk/test/tree.test.ts @@ -105,11 +105,11 @@ describe('some', () => { describe('loadTreeFromDir', () => { test('can find tree', () => { const tree = loadTreeFromDir(path.join(__dirname, 'cloud-assembly-trees', 'built-with-1_144_0')); - expect(tree.id).toEqual('App'); + expect(tree?.id).toEqual('App'); }); test('cannot find tree', () => { const tree = loadTreeFromDir(path.join(__dirname, 'cloud-assembly-trees', 'foo')); - expect(tree).toEqual({}); + expect(tree).toEqual(undefined); }); }); From fdf54d0fc1c878f85a72ec632f0dc7ab9d4bdc70 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:03:17 -0500 Subject: [PATCH 03/12] docs(toolkit-lib): CODE_REGISTRY.md tracks valid codes and their documentation (#44) closes https://github.com/aws/aws-cdk/issues/33434 generates `CODE_REGISTRY.md` using information gathered in `codes.ts`. this pr: - updates `codes.ts` to have additional information stored - introduces a script that generates a markdown table from the `CODES` object - updates projen to build `CODE_REGISTRY.md` as a post-build step --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Momo Kornher --- .projenrc.ts | 1 + .../@aws-cdk/toolkit-lib/.projen/tasks.json | 3 + .../@aws-cdk/toolkit-lib/CODE_REGISTRY.md | 31 +++ .../private/context-aware-source.ts | 6 +- .../cloud-assembly/private/prepare-source.ts | 4 +- .../cloud-assembly/private/source-builder.ts | 10 +- .../toolkit-lib/lib/api/io/private/codes.ts | 208 +++++++++++++++--- .../lib/api/io/private/messages.ts | 59 ++--- .../toolkit-lib/lib/api/io/private/timer.ts | 12 +- .../@aws-cdk/toolkit-lib/lib/toolkit/index.ts | 1 + .../toolkit-lib/lib/toolkit/toolkit.ts | 37 ++-- .../@aws-cdk/toolkit-lib/lib/toolkit/types.ts | 78 +++++++ .../toolkit-lib/scripts/gen-code-registry.ts | 29 +++ 13 files changed, 385 insertions(+), 94 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md create mode 100644 packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts create mode 100644 packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts diff --git a/.projenrc.ts b/.projenrc.ts index bc947298..31f05466 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1174,6 +1174,7 @@ toolkitLib.package.addField('exports', { './package.json': './package.json', }); +toolkitLib.postCompileTask.exec('ts-node scripts/gen-code-registry.ts'); toolkitLib.postCompileTask.exec('node build-tools/bundle.mjs'); // Smoke test built JS files toolkitLib.postCompileTask.exec('node ./lib/index.js >/dev/null 2>/dev/null { switch (type) { case 'data_stdout': - await services.ioHost.notify(info(line, 'CDK_ASSEMBLY_I1001')); + await services.ioHost.notify(info(line, CODES.CDK_ASSEMBLY_I1001)); break; case 'data_stderr': - await services.ioHost.notify(error(line, 'CDK_ASSEMBLY_E1002')); + await services.ioHost.notify(error(line, CODES.CDK_ASSEMBLY_E1002)); break; } }, diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts index df36c116..3c35848f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts @@ -1,4 +1,39 @@ -import { IoMessageCode } from '../io-message'; +import { IoMessageCode, IoMessageLevel } from '../io-message'; + +/** + * Information for each IO Message Code. + */ +export interface CodeInfo { + /** + * The message code. + */ + code: IoMessageCode; + + /** + * A brief description of the meaning of this IO Message. + */ + description: string; + + /** + * The message level + */ + level: IoMessageLevel; + + /** + * The name of the payload interface, if applicable. + * Some Io Messages include a payload, with a specific interface. The name of + * the interface is specified here so that it can be linked with the message + * when documentation is generated. + * + * The interface _must_ be exposed directly from toolkit-lib, so that it will + * have a documentation page generated (that can be linked to). + */ + interface?: string; +} + +function codeInfo(info: CodeInfo): CodeInfo { + return info; +} /** * We have a rough system by which we assign message codes: @@ -8,55 +43,162 @@ import { IoMessageCode } from '../io-message'; */ export const CODES = { // 1: Synth - CDK_TOOLKIT_I1000: 'Provides synthesis times', - CDK_TOOLKIT_I1901: 'Provides stack data', - CDK_TOOLKIT_I1902: 'Successfully deployed stacks', + CDK_TOOLKIT_I1000: codeInfo({ + code: 'CDK_TOOLKIT_I1000', + description: 'Provides synthesis times.', + level: 'info', + }), + CDK_TOOLKIT_I1901: codeInfo({ + code: 'CDK_TOOLKIT_I1901', + description: 'Provides stack data', + level: 'result', + interface: 'StackData', + }), + CDK_TOOLKIT_I1902: codeInfo({ + code: 'CDK_TOOLKIT_I1902', + description: 'Successfully deployed stacks', + level: 'result', + interface: 'AssemblyData', + }), // 2: List - CDK_TOOLKIT_I2901: 'Provides details on the selected stacks and their dependencies', + CDK_TOOLKIT_I2901: codeInfo({ + code: 'CDK_TOOLKIT_I2901', + description: 'Provides details on the selected stacks and their dependencies', + level: 'result', + }), // 3: Import & Migrate - CDK_TOOLKIT_E3900: 'Resource import failed', + CDK_TOOLKIT_E3900: codeInfo({ + code: 'CDK_TOOLKIT_E3900', + description: 'Resource import failed', + level: 'error', + }), // 4: Diff // 5: Deploy & Watch - CDK_TOOLKIT_I5000: 'Provides deployment times', - CDK_TOOLKIT_I5001: 'Provides total time in deploy action, including synth and rollback', - CDK_TOOLKIT_I5002: 'Provides time for resource migration', - CDK_TOOLKIT_I5031: 'Informs about any log groups that are traced as part of the deployment', - CDK_TOOLKIT_I5050: 'Confirm rollback during deployment', - CDK_TOOLKIT_I5060: 'Confirm deploy security sensitive changes', - CDK_TOOLKIT_I5900: 'Deployment results on success', + CDK_TOOLKIT_I5000: codeInfo({ + code: 'CDK_TOOLKIT_I5000', + description: 'Provides deployment times', + level: 'info', + }), + CDK_TOOLKIT_I5001: codeInfo({ + code: 'CDK_TOOLKIT_I5001', + description: 'Provides total time in deploy action, including synth and rollback', + level: 'info', + interface: 'Duration', + }), + CDK_TOOLKIT_I5002: codeInfo({ + code: 'CDK_TOOLKIT_I5002', + description: 'Provides time for resource migration', + level: 'info', + }), + CDK_TOOLKIT_I5031: codeInfo({ + code: 'CDK_TOOLKIT_I5031', + description: 'Informs about any log groups that are traced as part of the deployment', + level: 'info', + }), + CDK_TOOLKIT_I5050: codeInfo({ + code: 'CDK_TOOLKIT_I5050', + description: 'Confirm rollback during deployment', + level: 'info', + }), + CDK_TOOLKIT_I5060: codeInfo({ + code: 'CDK_TOOLKIT_I5060', + description: 'Confirm deploy security sensitive changes', + level: 'info', + }), + CDK_TOOLKIT_I5900: codeInfo({ + code: 'CDK_TOOLKIT_I5900', + description: 'Deployment results on success', + level: 'result', + interface: 'SuccessfulDeployStackResult', + }), - CDK_TOOLKIT_E5001: 'No stacks found', + CDK_TOOLKIT_E5001: codeInfo({ + code: 'CDK_TOOLKIT_E5001', + description: 'No stacks found', + level: 'error', + }), // 6: Rollback - CDK_TOOLKIT_I6000: 'Provides rollback times', + CDK_TOOLKIT_I6000: codeInfo({ + code: 'CDK_TOOLKIT_I6000', + description: 'Provides rollback times', + level: 'info', + }), - CDK_TOOLKIT_E6001: 'No stacks found', - CDK_TOOLKIT_E6900: 'Rollback failed', + CDK_TOOLKIT_E6001: codeInfo({ + code: 'CDK_TOOLKIT_E6001', + description: 'No stacks found', + level: 'error', + }), + CDK_TOOLKIT_E6900: codeInfo({ + code: 'CDK_TOOLKIT_E6900', + description: 'Rollback failed', + level: 'error', + }), // 7: Destroy - CDK_TOOLKIT_I7000: 'Provides destroy times', - CDK_TOOLKIT_I7010: 'Confirm destroy stacks', + CDK_TOOLKIT_I7000: codeInfo({ + code: 'CDK_TOOLKIT_I7000', + description: 'Provides destroy times', + level: 'info', + }), + CDK_TOOLKIT_I7010: codeInfo({ + code: 'CDK_TOOLKIT_I7010', + description: 'Confirm destroy stacks', + level: 'info', + }), - CDK_TOOLKIT_E7010: 'Action was aborted due to negative confirmation of request', - CDK_TOOLKIT_E7900: 'Stack deletion failed', + CDK_TOOLKIT_E7010: codeInfo({ + code: 'CDK_TOOLKIT_E7010', + description: 'Action was aborted due to negative confirmation of request', + level: 'error', + }), + CDK_TOOLKIT_E7900: codeInfo({ + code: 'CDK_TOOLKIT_E7900', + description: 'Stack deletion failed', + level: 'error', + }), // 9: Bootstrap // Assembly codes - CDK_ASSEMBLY_I0042: 'Writing updated context', - CDK_ASSEMBLY_I0241: 'Fetching missing context', - CDK_ASSEMBLY_I1000: 'Cloud assembly output starts', - CDK_ASSEMBLY_I1001: 'Output lines emitted by the cloud assembly to stdout', - CDK_ASSEMBLY_E1002: 'Output lines emitted by the cloud assembly to stderr', - CDK_ASSEMBLY_I1003: 'Cloud assembly output finished', - CDK_ASSEMBLY_E1111: 'Incompatible CDK CLI version. Upgrade needed.', + CDK_ASSEMBLY_I0042: codeInfo({ + code: 'CDK_ASSEMBLY_I0042', + description: 'Writing updated context', + level: 'debug', + }), + CDK_ASSEMBLY_I0241: codeInfo({ + code: 'CDK_ASSEMBLY_I0241', + description: 'Fetching missing context', + level: 'debug', + }), + CDK_ASSEMBLY_I1000: codeInfo({ + code: 'CDK_ASSEMBLY_I1000', + description: 'Cloud assembly output starts', + level: 'debug', + }), + CDK_ASSEMBLY_I1001: codeInfo({ + code: 'CDK_ASSEMBLY_I1001', + description: 'Output lines emitted by the cloud assembly to stdout', + level: 'info', + }), + CDK_ASSEMBLY_E1002: codeInfo({ + code: 'CDK_ASSEMBLY_E1002', + description: 'Output lines emitted by the cloud assembly to stderr', + level: 'error', + }), + CDK_ASSEMBLY_I1003: codeInfo({ + code: 'CDK_ASSEMBLY_I1003', + description: 'Cloud assembly output finished', + level: 'info', + }), + CDK_ASSEMBLY_E1111: codeInfo({ + code: 'CDK_ASSEMBLY_E1111', + description: 'Incompatible CDK CLI version. Upgrade needed.', + level: 'error', + }), }; - -// If we give CODES a type with key: IoMessageCode, -// this dynamically generated type will generalize to allow all IoMessageCodes. -// Instead, we will validate that VALID_CODE must be IoMessageCode with the '&'. -export type VALID_CODE = keyof typeof CODES & IoMessageCode; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts index 5c4d04cd..56aeac3b 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts @@ -1,6 +1,6 @@ import * as chalk from 'chalk'; -import type { IoMessageLevel } from '../io-message'; -import { type VALID_CODE } from './codes'; +import type { IoMessageCode, IoMessageLevel } from '../io-message'; +import { CodeInfo } from './codes'; import type { ActionLessMessage, ActionLessRequest, IoMessageCodeCategory, Optional, SimplifiedMessage } from './types'; /** @@ -8,11 +8,11 @@ import type { ActionLessMessage, ActionLessRequest, IoMessageCodeCategory, Optio * Handles string interpolation, format strings, and object parameter styles. * Applies optional styling and prepares the final message for logging. */ -export function formatMessage(msg: Optional, 'code'>, category: IoMessageCodeCategory = 'TOOLKIT'): ActionLessMessage { +function formatMessage(msg: Optional, 'code'>, category: IoMessageCodeCategory = 'TOOLKIT'): ActionLessMessage { return { time: new Date(), level: msg.level, - code: msg.code ?? defaultMessageCode(msg.level, category), + code: msg.code ?? defaultMessageCode(msg.level, category).code, message: msg.message, data: msg.data, }; @@ -22,18 +22,23 @@ export function formatMessage(msg: Optional, 'code'>, ca * Build a message code from level and category. The code must be valid for this function to pass. * Otherwise it returns a ToolkitError. */ -export function defaultMessageCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT'): VALID_CODE { +export function defaultMessageCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT'): CodeInfo { const levelIndicator = level === 'error' ? 'E' : level === 'warn' ? 'W' : 'I'; - return `CDK_${category}_${levelIndicator}0000` as VALID_CODE; + const code = `CDK_${category}_${levelIndicator}0000` as IoMessageCode; + return { + code, + description: `Generic ${level} message for CDK_${category}`, + level, + }; } /** * Requests a yes/no confirmation from the IoHost. */ export const confirm = ( - code: VALID_CODE, + code: CodeInfo, question: string, motivation: string, defaultResponse: boolean, @@ -49,14 +54,14 @@ export const confirm = ( }; /** - * Prompt for a a response from the IoHost. + * Prompt for a response from the IoHost. */ -export const prompt = (code: VALID_CODE, message: string, defaultResponse: U, payload?: T): ActionLessRequest => { +export const prompt = (code: CodeInfo, message: string, defaultResponse: U, payload?: T): ActionLessRequest => { return { defaultResponse, ...formatMessage({ - level: 'info', - code, + level: code.level, + code: code.code, message, data: payload, }), @@ -67,10 +72,10 @@ export const prompt = (code: VALID_CODE, message: string, defaultResponse: * Creates an error level message. * Errors must always have a unique code. */ -export const error = (message: string, code: VALID_CODE, payload?: T) => { +export const error = (message: string, code: CodeInfo, payload?: T) => { return formatMessage({ level: 'error', - code, + code: code.code, message, data: payload, }); @@ -83,10 +88,10 @@ export const error = (message: string, code: VALID_CODE, payload?: T) => { * However actions that operate on Cloud Assemblies might include a result per Stack. * Unlike other messages, results must always have a code and a payload. */ -export const result = (message: string, code: VALID_CODE, payload: T) => { +export const result = (message: string, code: CodeInfo, payload: T) => { return formatMessage({ level: 'result', - code, + code: code.code, message, data: payload, }); @@ -95,10 +100,10 @@ export const result = (message: string, code: VALID_CODE, payload: T) => { /** * Creates a warning level message. */ -export const warn = (message: string, code?: VALID_CODE, payload?: T) => { +export const warn = (message: string, code?: CodeInfo, payload?: T) => { return formatMessage({ level: 'warn', - code, + code: code?.code, message, data: payload, }); @@ -107,10 +112,10 @@ export const warn = (message: string, code?: VALID_CODE, payload?: T) => { /** * Creates an info level message. */ -export const info = (message: string, code?: VALID_CODE, payload?: T) => { +export const info = (message: string, code?: CodeInfo, payload?: T) => { return formatMessage({ level: 'info', - code, + code: code?.code, message, data: payload, }); @@ -119,10 +124,10 @@ export const info = (message: string, code?: VALID_CODE, payload?: T) => { /** * Creates a debug level message. */ -export const debug = (message: string, code?: VALID_CODE, payload?: T) => { +export const debug = (message: string, code?: CodeInfo, payload?: T) => { return formatMessage({ level: 'debug', - code, + code: code?.code, message, data: payload, }); @@ -131,10 +136,10 @@ export const debug = (message: string, code?: VALID_CODE, payload?: T) => { /** * Creates a trace level message. */ -export const trace = (message: string, code?: VALID_CODE, payload?: T) => { +export const trace = (message: string, code?: CodeInfo, payload?: T) => { return formatMessage({ level: 'trace', - code, + code: code?.code, message, data: payload, }); @@ -144,10 +149,10 @@ export const trace = (message: string, code?: VALID_CODE, payload?: T) => { * Creates an info level success message in green text. * @deprecated */ -export const success = (message: string, code?: VALID_CODE, payload?: T) => { +export const success = (message: string, code?: CodeInfo, payload?: T) => { return formatMessage({ level: 'info', - code, + code: code?.code, message: chalk.green(message), data: payload, }); @@ -157,10 +162,10 @@ export const success = (message: string, code?: VALID_CODE, payload?: T) => { * Creates an info level message in bold text. * @deprecated */ -export const highlight = (message: string, code?: VALID_CODE, payload?: T) => { +export const highlight = (message: string, code?: CodeInfo, payload?: T) => { return formatMessage({ level: 'info', - code, + code: code?.code, message: chalk.bold(message), data: payload, }); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts index 2bfdafff..03ac403a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts @@ -1,4 +1,4 @@ -import { VALID_CODE } from './codes'; +import { CodeInfo, CODES } from './codes'; import { info } from './messages'; import { ActionAwareIoHost } from './types'; import { formatTime } from '../../aws-cdk'; @@ -50,13 +50,13 @@ export class Timer { } function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy'): { - code: VALID_CODE; + code: CodeInfo; text: string; } { switch (type) { - case 'synth': return { code: 'CDK_TOOLKIT_I1000', text: 'Synthesis' }; - case 'deploy': return { code: 'CDK_TOOLKIT_I5000', text: 'Deployment' }; - case 'rollback': return { code: 'CDK_TOOLKIT_I6000', text: 'Rollback' }; - case 'destroy': return { code: 'CDK_TOOLKIT_I7000', text: 'Destroy' }; + case 'synth': return { code: CODES.CDK_TOOLKIT_I1000, text: 'Synthesis' }; + case 'deploy': return { code: CODES.CDK_TOOLKIT_I5000, text: 'Deployment' }; + case 'rollback': return { code: CODES.CDK_TOOLKIT_I6000, text: 'Rollback' }; + case 'destroy': return { code: CODES.CDK_TOOLKIT_I7000, text: 'Destroy' }; } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts index 01ee9c2c..952bc9a3 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/index.ts @@ -1 +1,2 @@ export * from './toolkit'; +export * from './types'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index ddc9186f..d314eb70 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -4,6 +4,7 @@ import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import { ToolkitServices } from './private'; +import { AssemblyData, StackAndAssemblyData } from './types'; import { AssetBuildTime, type DeployOptions, RequireApproval } from '../actions/deploy'; import { type ExtendedDeployOptions, buildParameterMap, createHotswapPropertyOverrides, removePublishedAssets } from '../actions/deploy/private'; import { type DestroyOptions } from '../actions/destroy'; @@ -19,7 +20,7 @@ import { ICloudAssemblySource, StackSelectionStrategy } from '../api/cloud-assem import { ALL_STACKS, CachedCloudAssemblySource, CloudAssemblySourceBuilder, IdentityCloudAssemblySource, StackAssembly } from '../api/cloud-assembly/private'; import { ToolkitError } from '../api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io'; -import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private'; +import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace, CODES } from '../api/io/private'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -167,7 +168,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab // if we have a single stack, print it to STDOUT const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; - const assemblyData = { + const assemblyData: AssemblyData = { assemblyDirectory: stacks.assembly.directory, stacksCount: stacks.stackCount, stackIds: stacks.hierarchicalIds, @@ -177,7 +178,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const firstStack = stacks.firstStack!; const template = firstStack.template; const obscuredTemplate = obscureTemplate(template); - await ioHost.notify(result(message, 'CDK_TOOLKIT_I1901', { + await ioHost.notify(result(message, CODES.CDK_TOOLKIT_I1901, { ...assemblyData, stack: { stackName: firstStack.stackName, @@ -186,10 +187,10 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab stringifiedJson: serializeStructure(obscuredTemplate, true), stringifiedYaml: serializeStructure(obscuredTemplate, false), }, - })); + } as StackAndAssemblyData)); } else { // not outputting template to stdout, let's explain things to the user a little bit... - await ioHost.notify(result(chalk.green(message), 'CDK_TOOLKIT_I1902', assemblyData)); + await ioHost.notify(result(chalk.green(message), CODES.CDK_TOOLKIT_I1902, assemblyData)); await ioHost.notify(info(`Supply a stack id (${stacks.stackArtifacts.map((s) => chalk.green(s.hierarchicalId)).join(', ')}) to display its template.`)); } @@ -211,7 +212,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const stacks = stackCollection.withDependencies(); const message = stacks.map(s => s.id).join('\n'); - await ioHost.notify(result(message, 'CDK_TOOLKIT_I2901', { stacks })); + await ioHost.notify(result(message, CODES.CDK_TOOLKIT_I2901, { stacks })); return stacks; } @@ -236,7 +237,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const synthDuration = await synthTimer.endAs(ioHost, 'synth'); if (stackCollection.stackCount === 0) { - await ioHost.notify(error('This app contains no stacks', 'CDK_TOOLKIT_E5001')); + await ioHost.notify(error('This app contains no stacks', CODES.CDK_TOOLKIT_E5001)); return; } @@ -318,7 +319,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (diffRequiresApproval(currentTemplate, stack, requireApproval)) { const motivation = '"--require-approval" is enabled and stack includes security-sensitive updates.'; const question = `${motivation}\nDo you wish to deploy these changes`; - const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I5060', question, motivation, true, concurrency)); + const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I5060, question, motivation, true, concurrency)); if (!confirmed) { throw new ToolkitError('Aborted by user'); } @@ -397,7 +398,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (options.force) { await ioHost.notify(warn(`${motivation}. Rolling back first (--force).`)); } else { - const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I5050', question, motivation, true, concurrency)); + const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I5050, question, motivation, true, concurrency)); if (!confirmed) { throw new ToolkitError('Aborted by user'); } @@ -422,7 +423,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab if (options.force) { await ioHost.notify(warn(`${motivation}. Proceeding with regular deployment (--force).`)); } else { - const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I5050', question, motivation, true, concurrency)); + const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I5050, question, motivation, true, concurrency)); if (!confirmed) { throw new ToolkitError('Aborted by user'); } @@ -442,7 +443,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab ? ` ✅ ${stack.displayName} (no changes)` : ` ✅ ${stack.displayName}`; - await ioHost.notify(result(chalk.green('\n' + message), 'CDK_TOOLKIT_I5900', deployResult)); + await ioHost.notify(result(chalk.green('\n' + message), CODES.CDK_TOOLKIT_I5900, deployResult)); deployDuration = await deployTimer.endAs(ioHost, 'deploy'); if (Object.keys(deployResult.outputs).length > 0) { @@ -472,7 +473,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab foundLogGroupsResult.sdk, foundLogGroupsResult.logGroupNames, ); - await ioHost.notify(info(`The following log groups are added: ${foundLogGroupsResult.logGroupNames}`, 'CDK_TOOLKIT_I5031')); + await ioHost.notify(info(`The following log groups are added: ${foundLogGroupsResult.logGroupNames}`, CODES.CDK_TOOLKIT_I5031)); } // If an outputs file has been specified, create the file path and write stack outputs to it once. @@ -487,7 +488,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } } const duration = synthDuration.asMs + (deployDuration?.asMs ?? 0); - await ioHost.notify(info(`\n✨ Total time: ${formatTime(duration)}s\n`, 'CDK_TOOLKIT_I5001', { duration })); + await ioHost.notify(info(`\n✨ Total time: ${formatTime(duration)}s\n`, CODES.CDK_TOOLKIT_I5001, { duration })); }; const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY; @@ -646,7 +647,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab await synthTimer.endAs(ioHost, 'synth'); if (stacks.stackCount === 0) { - await ioHost.notify(error('No stacks selected', 'CDK_TOOLKIT_E6001')); + await ioHost.notify(error('No stacks selected', CODES.CDK_TOOLKIT_E6001)); return; } @@ -670,7 +671,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab } await rollbackTimer.endAs(ioHost, 'rollback'); } catch (e: any) { - await ioHost.notify(error(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, 'CDK_TOOLKIT_E6900')); + await ioHost.notify(error(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, CODES.CDK_TOOLKIT_E6900)); throw new ToolkitError('Rollback failed (use --force to orphan failing resources)'); } } @@ -701,9 +702,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const motivation = 'Destroying stacks is an irreversible action'; const question = `Are you sure you want to delete: ${chalk.red(stacks.hierarchicalIds.join(', '))}`; - const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I7010', question, motivation, true)); + const confirmed = await ioHost.requestResponse(confirm(CODES.CDK_TOOLKIT_I7010, question, motivation, true)); if (!confirmed) { - return ioHost.notify(error('Aborted by user', 'CDK_TOOLKIT_E7010')); + return ioHost.notify(error('Aborted by user', CODES.CDK_TOOLKIT_E7010)); } const destroyTimer = Timer.start(); @@ -720,7 +721,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab }); await ioHost.notify(success(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`)); } catch (e) { - await ioHost.notify(error(`\n ❌ ${chalk.blue(stack.displayName)}: ${action} failed ${e}`, 'CDK_TOOLKIT_E7900')); + await ioHost.notify(error(`\n ❌ ${chalk.blue(stack.displayName)}: ${action} failed ${e}`, CODES.CDK_TOOLKIT_E7900)); throw e; } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts new file mode 100644 index 00000000..73ebf35d --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -0,0 +1,78 @@ +import { SuccessfulDeployStackResult as _SuccessfulDeployStackResult } from '../api/aws-cdk'; + +/** + * Assembly data returned in the payload of an IO Message. + */ +export interface AssemblyData { + /** + * The path to the assembly directory + */ + readonly assemblyDirectory: string; + + /** + * The number of stacks actioned on + */ + readonly stacksCount: number; + + /** + * The stack IDs + */ + readonly stackIds: string[]; +} + +/** + * A successful deploy stack result. Intentionally exposed in toolkit-lib so documentation + * can be generated from this interface. + */ +export interface SuccessfulDeployStackResult extends _SuccessfulDeployStackResult { +} + +/** + * Stack data returned in the payload of an IO Message. + */ +export interface StackData { + /** + * The stack name + */ + readonly stackName: string; + + /** + * The stack ID + */ + readonly hierarchicalId: string; + + /** + * The stack template + */ + readonly template: any; + + /** + * The stack template converted to JSON format + */ + readonly stringifiedJson: string; + + /** + * The stack template converted to YAML format + */ + readonly stringifiedYaml: string; +} + +/** + * Stack data returned in the payload of an IO Message. + */ +export interface StackAndAssemblyData extends AssemblyData { + /** + * Stack Data + */ + readonly stack: StackData; +} + +/** + * Duration information returned in the payload of an IO Message. + */ +export interface Duration { + /** + * The duration of the action. + */ + readonly duration: number; +} diff --git a/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts new file mode 100644 index 00000000..935efee8 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts @@ -0,0 +1,29 @@ +import * as fs from 'fs'; +import { CODES, CodeInfo } from '../lib/api/io/private/codes'; + +function codesToMarkdownTable(codes: Record, mdPrefix?: string, mdPostfix?: string) { + let table = '| Code | Description | Level | Data Interface |\n'; + table += '|------|-------------|-------|----------------|\n'; + + Object.entries(codes).forEach(([key, code]) => { + if (key !== code.code) { + throw new Error(`Code key ${key} does not match code.code ${code.code}. This is probably a typo.`); + } + table += `| ${code.code} | ${code.description} | ${code.level} | ${code.interface ? linkInterface(code.interface) : 'n/a'} |\n`; + }); + + const prefix = mdPrefix ? `${mdPrefix}\n\n` : ''; + const postfix = mdPostfix ? `\n\n${mdPostfix}\n` : ''; + + return prefix + table + postfix; +} + +function linkInterface(interfaceName: string) { + const docSite = 'docs/interfaces/'; + return `[${interfaceName}](${docSite}${interfaceName}.html)`; +} + +fs.writeFileSync('CODE_REGISTRY.md', codesToMarkdownTable( + CODES, + '## Toolkit Code Registry', +)); From 3b7115df801d96d9eac2ff25112ffc15fc596bdf Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Wed, 26 Feb 2025 04:17:00 -0500 Subject: [PATCH 04/12] chore: github issue templates (#153) Grrr, no projen support: https://github.com/projen/projen/issues/3567. I'm lazy so I just copied and pasted a few from the aws-cdk repo. This is somewhat necessary because it adds the `needs-triage` label automatically, that my other PR will need: https://github.com/aws/aws-cdk-cli/pull/118 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .github/ISSUE_TEMPLATES/bug-report.yml | 144 ++++++++++++++++++++ .github/ISSUE_TEMPLATES/feature-request.yml | 59 ++++++++ .github/ISSUE_TEMPLATES/notice.yml | 58 ++++++++ 3 files changed, 261 insertions(+) create mode 100644 .github/ISSUE_TEMPLATES/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATES/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATES/notice.yml diff --git a/.github/ISSUE_TEMPLATES/bug-report.yml b/.github/ISSUE_TEMPLATES/bug-report.yml new file mode 100644 index 00000000..7512a919 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/bug-report.yml @@ -0,0 +1,144 @@ +--- +name: "🐛 Bug Report" +description: Report a bug +title: "(module name): (short issue description)" +labels: [bug, needs-triage] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + **⚠️ Please read this before filling out the form below:** + If the bug you are reporting is a security-related issue or a security vulnerability, + please report it via [Report a security vulnerability](https://github.com/aws/aws-cdk/security/advisories/new) instead of this template. + - type: textarea + id: description + attributes: + label: Describe the bug + description: What is the problem? A clear and concise description of the bug. + validations: + required: true + - type: checkboxes + id: regression + attributes: + label: Regression Issue + description: What is a regression? If it worked in a previous version but doesn’t in the latest version, it’s considered a regression. In this case, please provide specific version number in the report. + options: + - label: Select this option if this issue appears to be a regression. + required: false + - type: input + id: working-version + attributes: + label: Last Known Working CDK Version + description: Specify the last known CDK version where this code was functioning as expected (if applicable). + validations: + required: false + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: | + What did you expect to happen? + validations: + required: true + - type: textarea + id: current + attributes: + label: Current Behavior + description: | + What actually happened? + + Please include full errors, uncaught exceptions, stack traces, and relevant logs. + If service responses are relevant, please include wire logs. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction Steps + description: | + Provide a self-contained, concise snippet of code that can be used to reproduce the issue. + For more complex issues provide a repo with the smallest sample that reproduces the bug. + + Avoid including business logic or unrelated code, it makes diagnosis more difficult. + The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Possible Solution + description: | + Suggest a fix/reason for the bug + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional Information/Context + description: | + Anything else that might be relevant for troubleshooting this bug. Providing context helps us come up with a solution that is most useful in the real world. + validations: + required: false + + - type: input + id: cdk-version + attributes: + label: CDK CLI Version + description: Output of `cdk version` + validations: + required: true + + - type: input + id: framework-version + attributes: + label: Framework Version + validations: + required: false + + - type: input + id: node-version + attributes: + label: Node.js Version + validations: + required: true + + - type: input + id: operating-system + attributes: + label: OS + validations: + required: true + + - type: dropdown + id: language + attributes: + label: Language + multiple: true + options: + - TypeScript + - Python + - .NET + - Java + - Go + validations: + required: true + + - type: input + id: language-version + attributes: + label: Language Version + description: E.g. TypeScript (3.8.3) | Java (8) | Python (3.7.3) + validations: + required: false + + - type: textarea + id: other + attributes: + label: Other information + description: | + e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. associated pull-request, stackoverflow, slack, etc + validations: + required: false diff --git a/.github/ISSUE_TEMPLATES/feature-request.yml b/.github/ISSUE_TEMPLATES/feature-request.yml new file mode 100644 index 00000000..3747a5aa --- /dev/null +++ b/.github/ISSUE_TEMPLATES/feature-request.yml @@ -0,0 +1,59 @@ +--- +name: 🚀 Feature Request +description: Suggest an idea for this project +title: "(module name): (short issue description)" +labels: [feature-request, needs-triage] +assignees: [] +body: + - type: textarea + id: description + attributes: + label: Describe the feature + description: A clear and concise description of the feature you are proposing. + validations: + required: true + - type: textarea + id: use-case + attributes: + label: Use Case + description: | + Why do you need this feature? For example: "I'm always frustrated when..." + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: | + Suggest how to implement the addition or change. Please include prototype/workaround/sketch/reference implementation. + validations: + required: false + - type: textarea + id: other + attributes: + label: Other Information + description: | + Any alternative solutions or features you considered, a more detailed explanation, stack traces, related issues, links for context, etc. + validations: + required: false + - type: checkboxes + id: ack + attributes: + label: Acknowledgements + options: + - label: I may be able to implement this feature request + required: false + - label: This feature might incur a breaking change + required: false + - type: input + id: sdk-version + attributes: + label: CDK version used + validations: + required: true + - type: input + id: environment + attributes: + label: Environment details (OS name and version, etc.) + validations: + required: true diff --git a/.github/ISSUE_TEMPLATES/notice.yml b/.github/ISSUE_TEMPLATES/notice.yml new file mode 100644 index 00000000..f90f63de --- /dev/null +++ b/.github/ISSUE_TEMPLATES/notice.yml @@ -0,0 +1,58 @@ +--- +name: "❗ Notice" +description: Post a notice for a high impact issue. Internal CDK team use only. +title: "❗ NOTICE (module name): (short notice description)" +labels: [needs-triage, management/tracking] +body: + - type: dropdown + attributes: + label: Status + description: What is the current status of this issue? + options: + - Investigating (Default) + - In-Progress + - Resolved + validations: + required: true + - type: textarea + attributes: + label: What is the issue? + description: A clear and concise description of the issue you want customers to be aware of + validations: + required: true + - type: textarea + attributes: + label: Error message + description: If available, paste the error message users are seeing (no need to backticks) + render: console + - type: textarea + attributes: + label: What is the impact? + description: | + What can occur if this issue isn't addressed? + validations: + required: true + - type: textarea + attributes: + label: Workaround + description: | + Please provide a detailed workaround outlining all steps required for implementation. If none exist yet, leave blank + - type: textarea + attributes: + label: Who is affected? + description: | + What segment of customers are affected? Think about specific construct usage, version, feature toggles, etc... + validations: + required: true + - type: textarea + attributes: + label: How do I resolve this? + description: | + What actions should customers take to resolve the issue. Also elaborate on any code changes the customer may need to do. If unknown yet, say TBD + validations: + required: true + - type: textarea + attributes: + label: Related issues + description: | + List all related issues here. If none related, leave blank From 88e2bdf7450a737140d2e5c88817774414fe32af Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Wed, 26 Feb 2025 11:46:18 +0000 Subject: [PATCH 05/12] chore: fix issue templates not working (#156) They were in the wrong path. See https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .github/{ISSUE_TEMPLATES => ISSUE_TEMPLATE}/bug-report.yml | 0 .github/{ISSUE_TEMPLATES => ISSUE_TEMPLATE}/feature-request.yml | 0 .github/{ISSUE_TEMPLATES => ISSUE_TEMPLATE}/notice.yml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename .github/{ISSUE_TEMPLATES => ISSUE_TEMPLATE}/bug-report.yml (100%) rename .github/{ISSUE_TEMPLATES => ISSUE_TEMPLATE}/feature-request.yml (100%) rename .github/{ISSUE_TEMPLATES => ISSUE_TEMPLATE}/notice.yml (100%) diff --git a/.github/ISSUE_TEMPLATES/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml similarity index 100% rename from .github/ISSUE_TEMPLATES/bug-report.yml rename to .github/ISSUE_TEMPLATE/bug-report.yml diff --git a/.github/ISSUE_TEMPLATES/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml similarity index 100% rename from .github/ISSUE_TEMPLATES/feature-request.yml rename to .github/ISSUE_TEMPLATE/feature-request.yml diff --git a/.github/ISSUE_TEMPLATES/notice.yml b/.github/ISSUE_TEMPLATE/notice.yml similarity index 100% rename from .github/ISSUE_TEMPLATES/notice.yml rename to .github/ISSUE_TEMPLATE/notice.yml From 130445d9521af976295e51b193c0d433311e87c0 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Wed, 26 Feb 2025 17:13:30 +0100 Subject: [PATCH 06/12] feat(cli): build your own `fromLookup()` imports with the new context provider for CloudControl API (#138) A generic Context Provider for CloudControl API. See https://github.com/aws/aws-cdk/pull/33258 for an example how to implement a `fromLookup()` method using the new context provider. (Extracted from https://github.com/aws/aws-cdk/pull/33258) --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions Co-authored-by: github-actions Co-authored-by: Momo Kornher --- .projenrc.ts | 2 + .../cli-lib-alpha/THIRD_PARTY_LICENSES | 206 ++++++++++++++++ .../@aws-cdk/toolkit-lib/.projen/deps.json | 5 + packages/@aws-cdk/toolkit-lib/package.json | 1 + packages/aws-cdk/.projen/deps.json | 5 + packages/aws-cdk/THIRD_PARTY_LICENSES | 206 ++++++++++++++++ packages/aws-cdk/lib/api/aws-auth/sdk.ts | 24 ++ .../lib/context-providers/cc-api-provider.ts | 127 ++++++++++ .../aws-cdk/lib/context-providers/index.ts | 2 + packages/aws-cdk/lib/util/json.ts | 64 +++++ packages/aws-cdk/package.json | 1 + .../aws-cdk/test/commands/migrate.test.ts | 2 + .../context-providers/cc-api-provider.test.ts | 227 ++++++++++++++++++ packages/aws-cdk/test/util/json.test.ts | 50 ++++ packages/aws-cdk/test/util/mock-sdk.ts | 3 + yarn.lock | 48 ++++ 16 files changed, 973 insertions(+) create mode 100644 packages/aws-cdk/lib/context-providers/cc-api-provider.ts create mode 100644 packages/aws-cdk/lib/util/json.ts create mode 100644 packages/aws-cdk/test/context-providers/cc-api-provider.test.ts create mode 100644 packages/aws-cdk/test/util/json.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index 31f05466..c8c58c46 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -711,6 +711,7 @@ const cli = configureProject( `@aws-sdk/client-appsync@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-cloudformation@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-cloudwatch-logs@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-cloudcontrol@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-codebuild@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-ec2@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-ecr@${CLI_SDK_V3_RANGE}`, @@ -1049,6 +1050,7 @@ const toolkitLib = configureProject( `@aws-sdk/client-appsync@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-cloudformation@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-cloudwatch-logs@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-cloudcontrol@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-codebuild@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-ec2@${CLI_SDK_V3_RANGE}`, `@aws-sdk/client-ecr@${CLI_SDK_V3_RANGE}`, diff --git a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES index f0f8da5c..e51aef8a 100644 --- a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES @@ -822,6 +822,212 @@ The @aws-cdk/cli-lib-alpha package includes the following third-party software/l limitations under the License. +---------------- + +** @aws-sdk/client-cloudcontrol@3.741.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudcontrol/v/3.741.0 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ---------------- ** @aws-sdk/client-cloudformation@3.741.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudformation/v/3.741.0 | Apache-2.0 diff --git a/packages/@aws-cdk/toolkit-lib/.projen/deps.json b/packages/@aws-cdk/toolkit-lib/.projen/deps.json index 4e5910d9..91cc3e33 100644 --- a/packages/@aws-cdk/toolkit-lib/.projen/deps.json +++ b/packages/@aws-cdk/toolkit-lib/.projen/deps.json @@ -154,6 +154,11 @@ "version": "3.741", "type": "runtime" }, + { + "name": "@aws-sdk/client-cloudcontrol", + "version": "3.741", + "type": "runtime" + }, { "name": "@aws-sdk/client-cloudformation", "version": "3.741", diff --git a/packages/@aws-cdk/toolkit-lib/package.json b/packages/@aws-cdk/toolkit-lib/package.json index 0009969e..885e9203 100644 --- a/packages/@aws-cdk/toolkit-lib/package.json +++ b/packages/@aws-cdk/toolkit-lib/package.json @@ -71,6 +71,7 @@ "@aws-cdk/cx-api": "^2.180.0", "@aws-cdk/region-info": "^2.180.0", "@aws-sdk/client-appsync": "3.741", + "@aws-sdk/client-cloudcontrol": "3.741", "@aws-sdk/client-cloudformation": "3.741", "@aws-sdk/client-cloudwatch-logs": "3.741", "@aws-sdk/client-codebuild": "3.741", diff --git a/packages/aws-cdk/.projen/deps.json b/packages/aws-cdk/.projen/deps.json index 4730d65b..f904a0ce 100644 --- a/packages/aws-cdk/.projen/deps.json +++ b/packages/aws-cdk/.projen/deps.json @@ -228,6 +228,11 @@ "version": "3.741", "type": "runtime" }, + { + "name": "@aws-sdk/client-cloudcontrol", + "version": "3.741", + "type": "runtime" + }, { "name": "@aws-sdk/client-cloudformation", "version": "3.741", diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index 478003ae..05288a4e 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -822,6 +822,212 @@ The aws-cdk package includes the following third-party software/licensing: limitations under the License. +---------------- + +** @aws-sdk/client-cloudcontrol@3.741.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudcontrol/v/3.741.0 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ---------------- ** @aws-sdk/client-cloudformation@3.741.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudformation/v/3.741.0 | Apache-2.0 diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 5c6c3302..3ede58e6 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -19,6 +19,15 @@ import { type UpdateResolverCommandInput, type UpdateResolverCommandOutput, } from '@aws-sdk/client-appsync'; +import { + CloudControlClient, + GetResourceCommand, + GetResourceCommandInput, + GetResourceCommandOutput, + ListResourcesCommand, + ListResourcesCommandInput, + ListResourcesCommandOutput, +} from '@aws-sdk/client-cloudcontrol'; import { CloudFormationClient, ContinueUpdateRollbackCommand, @@ -371,6 +380,11 @@ export interface IAppSyncClient { listFunctions(input: ListFunctionsCommandInput): Promise; } +export interface ICloudControlClient{ + listResources(input: ListResourcesCommandInput): Promise; + getResource(input: GetResourceCommandInput): Promise; +} + export interface ICloudFormationClient { continueUpdateRollback(input: ContinueUpdateRollbackCommandInput): Promise; createChangeSet(input: CreateChangeSetCommandInput): Promise; @@ -603,6 +617,16 @@ export class SDK { }; } + public cloudControl(): ICloudControlClient { + const client = new CloudControlClient(this.config); + return { + listResources: (input: ListResourcesCommandInput): Promise => + client.send(new ListResourcesCommand(input)), + getResource: (input: GetResourceCommandInput): Promise => + client.send(new GetResourceCommand(input)), + }; + } + public cloudFormation(): ICloudFormationClient { const client = new CloudFormationClient({ ...this.config, diff --git a/packages/aws-cdk/lib/context-providers/cc-api-provider.ts b/packages/aws-cdk/lib/context-providers/cc-api-provider.ts new file mode 100644 index 00000000..38486d3b --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/cc-api-provider.ts @@ -0,0 +1,127 @@ +import type { CcApiContextQuery } from '@aws-cdk/cloud-assembly-schema'; +import { ICloudControlClient } from '../api'; +import { type SdkProvider, initContextProviderSdk } from '../api/aws-auth/sdk-provider'; +import { ContextProviderPlugin } from '../api/plugin'; +import { ContextProviderError } from '../toolkit/error'; +import { findJsonValue, getResultObj } from '../util/json'; + +export class CcApiContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + /** + * This returns a data object with the value from CloudControl API result. + * args.typeName - see https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html + * args.exactIdentifier - use CC API getResource. + * args.propertyMatch - use CCP API listResources to get resources and propertyMatch to search through the list. + * args.propertiesToReturn - Properties from CC API to return. + */ + public async getValue(args: CcApiContextQuery) { + const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl(); + + const result = await this.findResources(cloudControl, args); + return result; + } + + private async findResources(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any} []> { + if (args.exactIdentifier && args.propertyMatch) { + throw new ContextProviderError(`Specify either exactIdentifier or propertyMatch, but not both. Failed to find resources using CC API for type ${args.typeName}.`); + } + if (!args.exactIdentifier && !args.propertyMatch) { + throw new ContextProviderError(`Neither exactIdentifier nor propertyMatch is specified. Failed to find resources using CC API for type ${args.typeName}.`); + } + + if (args.exactIdentifier) { + // use getResource to get the exact indentifier + return this.getResource(cc, args.typeName, args.exactIdentifier, args.propertiesToReturn); + } else { + // use listResource + return this.listResources(cc, args.typeName, args.propertyMatch!, args.propertiesToReturn); + } + } + + /** + * Calls getResource from CC API to get the resource. + * See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/get-resource.html + * + * If the exactIdentifier is not found, then an empty map is returned. + * If the resource is found, then a map of the identifier to a map of property values is returned. + */ + private async getResource( + cc: ICloudControlClient, + typeName: string, + exactIdentifier: string, + propertiesToReturn: string[], + ): Promise<{[key: string]: any}[]> { + const resultObjs: {[key: string]: any}[] = []; + try { + const result = await cc.getResource({ + TypeName: typeName, + Identifier: exactIdentifier, + }); + const id = result.ResourceDescription?.Identifier ?? ''; + if (id !== '') { + const propsObject = JSON.parse(result.ResourceDescription?.Properties ?? ''); + const propsObj = getResultObj(propsObject, result.ResourceDescription?.Identifier!, propertiesToReturn); + resultObjs.push(propsObj); + } else { + throw new ContextProviderError(`Could not get resource ${exactIdentifier}.`); + } + } catch (err) { + throw new ContextProviderError(`Encountered CC API error while getting resource ${exactIdentifier}. Error: ${err}`); + } + return resultObjs; + } + + /** + * Calls listResources from CC API to get the resources and apply args.propertyMatch to find the resources. + * See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/list-resources.html + * + * Since exactIdentifier is not specified, propertyMatch must be specified. + * This returns an object where the ids are object keys and values are objects with keys of args.propertiesToReturn. + */ + private async listResources( + cc: ICloudControlClient, + typeName: string, + propertyMatch: Record, + propertiesToReturn: string[], + ): Promise<{[key: string]: any}[]> { + const resultObjs: {[key: string]: any}[] = []; + + try { + const result = await cc.listResources({ + TypeName: typeName, + }); + result.ResourceDescriptions?.forEach((resource) => { + const id = resource.Identifier ?? ''; + if (id !== '') { + const propsObject = JSON.parse(resource.Properties ?? ''); + + const filters = Object.entries(propertyMatch); + let match = false; + if (filters) { + match = filters.every((record, _index, _arr) => { + const key = record[0]; + const expected = record[1]; + const actual = findJsonValue(propsObject, key); + return propertyMatchesFilter(actual, expected); + }); + + function propertyMatchesFilter(actual: any, expected: unknown) { + // For now we just check for strict equality, but we can implement pattern matching and fuzzy matching here later + return expected === actual; + } + } + + if (match) { + const propsObj = getResultObj(propsObject, resource.Identifier!, propertiesToReturn); + resultObjs.push(propsObj); + } + } + }); + } catch (err) { + throw new ContextProviderError(`Could not get resources ${propertyMatch}. Error: ${err}`); + } + return resultObjs; + } +} diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index ed5ae723..4f377676 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -2,6 +2,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { AmiContextProviderPlugin } from './ami'; import { AZContextProviderPlugin } from './availability-zones'; +import { CcApiContextProviderPlugin } from './cc-api-provider'; import { EndpointServiceAZContextProviderPlugin } from './endpoint-service-availability-zones'; import { HostedZoneContextProviderPlugin } from './hosted-zones'; import { KeyContextProviderPlugin } from './keys'; @@ -118,4 +119,5 @@ const availableContextProviders: ProviderMap = { [cxschema.ContextProvider.LOAD_BALANCER_PROVIDER]: (s) => new LoadBalancerContextProviderPlugin(s), [cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER]: (s) => new LoadBalancerListenerContextProviderPlugin(s), [cxschema.ContextProvider.KEY_PROVIDER]: (s) => new KeyContextProviderPlugin(s), + [cxschema.ContextProvider.CC_API_PROVIDER]: (s) => new CcApiContextProviderPlugin(s), }; diff --git a/packages/aws-cdk/lib/util/json.ts b/packages/aws-cdk/lib/util/json.ts new file mode 100644 index 00000000..76f1c25b --- /dev/null +++ b/packages/aws-cdk/lib/util/json.ts @@ -0,0 +1,64 @@ +/** + * This gets the values of the jsonObject at the paths specified in propertiesToReturn. + * + * For example, jsonObject = { + * key1: 'abc', + * key2: { + * foo: 'qwerty', + * bar: 'data', + * } + * } + * + * propertiesToReturn = ['key1', 'key2.foo']; + * + * The returned object is: + * + * ``` + * { + * key1: 'abc', + * 'key2.foo': 'qwerty', + * Identifier: identifier + * } + * ``` + */ +export function getResultObj(jsonObject: any, identifier: string, propertiesToReturn: string[]): {[key: string]: any} { + const propsObj = {}; + propertiesToReturn.forEach((propName) => { + Object.assign(propsObj, { [propName]: findJsonValue(jsonObject, propName) }); + }); + Object.assign(propsObj, { ['Identifier']: identifier }); + return propsObj; +} + +/** + * This finds the value of the jsonObject at the path. Path is delimited by '.'. + * + * For example, jsonObject = { + * key1: 'abc', + * key2: { + * foo: 'qwerty', + * bar: 'data', + * } + * } + * + * If path is 'key1', then it will return 'abc'. + * If path is 'key2.foo', then it will return 'qwerty'. + * If path is 'key2', then it will return the object: + * { + * foo: 'qwerty', + * bar: 'data', + * } + * + * If the path is not found, an Error will be thrown stating which token is missing. + */ +export function findJsonValue(jsonObject: any, path: string): any { + const paths = path.split('.'); + let obj = jsonObject; + paths.forEach(p => { + obj = obj[p]; + if (obj === undefined) { + throw new TypeError(`Cannot read field ${path}. ${p} is not found.`); + } + }); + return obj; +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index f744c4b2..367494fb 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -90,6 +90,7 @@ "@aws-cdk/cx-api": "^2.180.0", "@aws-cdk/region-info": "^2.180.0", "@aws-sdk/client-appsync": "3.741", + "@aws-sdk/client-cloudcontrol": "3.741", "@aws-sdk/client-cloudformation": "3.741", "@aws-sdk/client-cloudwatch-logs": "3.741", "@aws-sdk/client-codebuild": "3.741", diff --git a/packages/aws-cdk/test/commands/migrate.test.ts b/packages/aws-cdk/test/commands/migrate.test.ts index 071cd2b3..3cff68a9 100644 --- a/packages/aws-cdk/test/commands/migrate.test.ts +++ b/packages/aws-cdk/test/commands/migrate.test.ts @@ -31,6 +31,8 @@ import { } from '../../lib/commands/migrate'; import { MockSdkProvider, mockCloudFormationClient, restoreSdkMocksToDefault } from '../util/mock-sdk'; +jest.setTimeout(120_000); + const exec = promisify(_exec); describe('Migrate Function Tests', () => { diff --git a/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts b/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts new file mode 100644 index 00000000..9f66b512 --- /dev/null +++ b/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts @@ -0,0 +1,227 @@ +import { GetResourceCommand, ListResourcesCommand } from '@aws-sdk/client-cloudcontrol'; +import { CcApiContextProviderPlugin } from '../../lib/context-providers/cc-api-provider'; +import { mockCloudControlClient, MockSdkProvider, restoreSdkMocksToDefault } from '../util/mock-sdk'; + +let provider: CcApiContextProviderPlugin; + +beforeEach(() => { + provider = new CcApiContextProviderPlugin(new MockSdkProvider()); + restoreSdkMocksToDefault(); +}); + +/* eslint-disable */ +test('looks up RDS instance using CC API getResource', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + TypeName: 'AWS::RDS::DBInstance', + ResourceDescription: { + Identifier: 'my-db-instance-1', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true"}', + }, + }); + + // WHEN + const results = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'my-db-instance-1', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }); + + // THEN + const propsObj = results[0]; + expect(propsObj).toEqual(expect.objectContaining({ + DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:test-instance-1', + StorageEncrypted: 'true', + Identifier: 'my-db-instance-1', + })); +}); + +// In theory, this should never happen. We ask for my-db-instance-1 but CC API returns ''. +// Included this to test the code path. +test('looks up RDS instance using CC API getResource - wrong match', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + TypeName: 'AWS::RDS::DBInstance', + ResourceDescription: { + Identifier: '', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true"}', + }, + }); + + await expect( + // WHEN + provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'my-db-instance-1', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }), + ).rejects.toThrow('Encountered CC API error while getting resource my-db-instance-1.'); // THEN +}); + +test('looks up RDS instance using CC API getResource - empty response', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + }); + + await expect( + // WHEN + provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'bad-identifier', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }), + ).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.'); // THEN +}); + +test('looks up RDS instance using CC API getResource - error in CC API', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects('No data found'); + + await expect( + // WHEN + provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'bad-identifier', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }), + ).rejects.toThrow('Encountered CC API error while getting resource bad-identifier.'); // THEN +}); + +test('looks up RDS instance using CC API listResources', async () => { + // GIVEN + mockCloudControlClient.on(ListResourcesCommand).resolves({ + ResourceDescriptions: [ + { + Identifier: 'my-db-instance-1', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true","Endpoint":{"Address":"address1.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-2', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-2","StorageEncrypted":"false","Endpoint":{"Address":"address2.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-3', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-3","StorageEncrypted":"true","Endpoint":{"Address":"address3.amazonaws.com","Port":"6000"}}', + }, + ], + }); + + // WHEN + const results = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + propertyMatch: { + StorageEncrypted: 'true', + }, + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted', 'Endpoint.Port'], + }); + + // THEN + let propsObj = results[0]; + expect(propsObj).toEqual(expect.objectContaining({ + DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:test-instance-1', + StorageEncrypted: 'true', + 'Endpoint.Port': '5432', + Identifier: 'my-db-instance-1', + })); + + propsObj = results[1]; + expect(propsObj).toEqual(expect.objectContaining({ + DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:test-instance-3', + StorageEncrypted: 'true', + 'Endpoint.Port': '6000', + Identifier: 'my-db-instance-3', + })); + + expect(results.length).toEqual(2); +}); + +test('looks up RDS instance using CC API listResources - nested prop', async () => { + // GIVEN + mockCloudControlClient.on(ListResourcesCommand).resolves({ + ResourceDescriptions: [ + { + Identifier: 'my-db-instance-1', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true","Endpoint":{"Address":"address1.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-2', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-2","StorageEncrypted":"false","Endpoint":{"Address":"address2.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-3', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-3","StorageEncrypted":"true","Endpoint":{"Address":"address3.amazonaws.com","Port":"6000"}}', + }, + ], + }); + + // WHEN + const results = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + propertyMatch: { + 'StorageEncrypted': 'true', + 'Endpoint.Port': '5432', + }, + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted', 'Endpoint.Port'], + }); + + // THEN + let propsObj = results[0]; + expect(propsObj).toEqual(expect.objectContaining({ + DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:test-instance-1', + StorageEncrypted: 'true', + 'Endpoint.Port': '5432', + Identifier: 'my-db-instance-1', + })); + + expect(results.length).toEqual(1); +}); + +test('error by specifying both exactIdentifier and propertyMatch', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + }); + + await expect( + // WHEN + provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'bad-identifier', + propertyMatch: { + 'StorageEncrypted': 'true', + 'Endpoint.Port': '5432', + }, + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }), + ).rejects.toThrow('Specify either exactIdentifier or propertyMatch, but not both. Failed to find resources using CC API for type AWS::RDS::DBInstance.'); // THEN +}); + +test('error by specifying neither exactIdentifier or propertyMatch', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + }); + + await expect( + // WHEN + provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }), + ).rejects.toThrow('Neither exactIdentifier nor propertyMatch is specified. Failed to find resources using CC API for type AWS::RDS::DBInstance.'); // THEN +}); +/* eslint-enable */ diff --git a/packages/aws-cdk/test/util/json.test.ts b/packages/aws-cdk/test/util/json.test.ts new file mode 100644 index 00000000..21d600a7 --- /dev/null +++ b/packages/aws-cdk/test/util/json.test.ts @@ -0,0 +1,50 @@ +import { findJsonValue, getResultObj } from '../../lib/util/json'; + +const jsonObj = { + DBInstanceArn: 'arn:aws:rds:us-east-1:123456789012:db:test-instance-1', + StorageEncrypted: 'true', + Endpoint: { + Address: 'address1.amazonaws.com', + Port: '5432', + }, +}; + +test('findJsonValue for paths', async () => { + expect(findJsonValue(jsonObj, 'DBInstanceArn')).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(findJsonValue(jsonObj, 'Endpoint.Address')).toEqual('address1.amazonaws.com'); + + const answer = { + Address: 'address1.amazonaws.com', + Port: '5432', + }; + expect(findJsonValue(jsonObj, 'Endpoint')).toEqual(answer); +}); + +test('findJsonValue for nonexisting paths', async () => { + expect(() => findJsonValue(jsonObj, 'Blah')).toThrow('Cannot read field Blah. Blah is not found.'); + + expect(() => findJsonValue(jsonObj, 'Endpoint.Blah')).toThrow('Cannot read field Endpoint.Blah. Blah is not found.'); + + expect(() => findJsonValue(jsonObj, 'Endpoint.Address.Blah')).toThrow('Cannot read field Endpoint.Address.Blah. Blah is not found.'); +}); + +test('getResultObj returns correct objects', async () => { + const propertiesToReturn = ['DBInstanceArn', 'Endpoint.Port', 'Endpoint']; + + const result = getResultObj(jsonObj, '12345', propertiesToReturn); + expect(result.DBInstanceArn).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(result['Endpoint.Port']).toEqual('5432'); + expect(result.Identifier).toEqual('12345'); + + const answer = { + Address: 'address1.amazonaws.com', + Port: '5432', + }; + expect(result.Endpoint).toEqual(answer); +}); + +test('getResultObj throws error for missing property', async () => { + const propertiesToReturn = ['DBInstanceArn', 'NoSuchProp']; + + expect(() => getResultObj(jsonObj, '12345', propertiesToReturn)).toThrow('Cannot read field NoSuchProp. NoSuchProp is not found.'); +}); diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index 04dca429..9666a6e0 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -1,6 +1,7 @@ import 'aws-sdk-client-mock-jest'; import { Environment } from '@aws-cdk/cx-api'; import { AppSyncClient } from '@aws-sdk/client-appsync'; +import { CloudControlClient } from '@aws-sdk/client-cloudcontrol'; import { CloudFormationClient, Stack, StackStatus } from '@aws-sdk/client-cloudformation'; import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs'; import { CodeBuildClient } from '@aws-sdk/client-codebuild'; @@ -34,6 +35,7 @@ export const FAKE_CREDENTIAL_CHAIN = createCredentialChain(() => Promise.resolve // Default implementations export const mockAppSyncClient = mockClient(AppSyncClient); +export const mockCloudControlClient = mockClient(CloudControlClient); export const mockCloudFormationClient = mockClient(CloudFormationClient); export const mockCloudWatchClient = mockClient(CloudWatchLogsClient); export const mockCodeBuildClient = mockClient(CodeBuildClient); @@ -64,6 +66,7 @@ export const restoreSdkMocksToDefault = () => { applyToAllMocks('reset'); mockAppSyncClient.onAnyCommand().resolves({}); + mockCloudControlClient.onAnyCommand().resolves({}); mockCloudFormationClient.onAnyCommand().resolves({}); mockCloudWatchClient.onAnyCommand().resolves({}); mockCodeBuildClient.onAnyCommand().resolves({}); diff --git a/yarn.lock b/yarn.lock index cda6e179..3cef79fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -187,6 +187,54 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@aws-sdk/client-cloudcontrol@3.741": + version "3.741.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudcontrol/-/client-cloudcontrol-3.741.0.tgz#20c6f91ac99c44c8d3ad3aff6315a8484593c2cf" + integrity sha512-naskjecQk44T3LzJ9FITbpwkN9QDKy97V49ponbVB89PlBHWgj6gjAL5seuu445tKir9drrHVkIO29icsA9yKw== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.734.0" + "@aws-sdk/credential-provider-node" "3.741.0" + "@aws-sdk/middleware-host-header" "3.734.0" + "@aws-sdk/middleware-logger" "3.734.0" + "@aws-sdk/middleware-recursion-detection" "3.734.0" + "@aws-sdk/middleware-user-agent" "3.734.0" + "@aws-sdk/region-config-resolver" "3.734.0" + "@aws-sdk/types" "3.734.0" + "@aws-sdk/util-endpoints" "3.734.0" + "@aws-sdk/util-user-agent-browser" "3.734.0" + "@aws-sdk/util-user-agent-node" "3.734.0" + "@smithy/config-resolver" "^4.0.1" + "@smithy/core" "^3.1.1" + "@smithy/fetch-http-handler" "^5.0.1" + "@smithy/hash-node" "^4.0.1" + "@smithy/invalid-dependency" "^4.0.1" + "@smithy/middleware-content-length" "^4.0.1" + "@smithy/middleware-endpoint" "^4.0.2" + "@smithy/middleware-retry" "^4.0.3" + "@smithy/middleware-serde" "^4.0.1" + "@smithy/middleware-stack" "^4.0.1" + "@smithy/node-config-provider" "^4.0.1" + "@smithy/node-http-handler" "^4.0.2" + "@smithy/protocol-http" "^5.0.1" + "@smithy/smithy-client" "^4.1.2" + "@smithy/types" "^4.1.0" + "@smithy/url-parser" "^4.0.1" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.3" + "@smithy/util-defaults-mode-node" "^4.0.3" + "@smithy/util-endpoints" "^3.0.1" + "@smithy/util-middleware" "^4.0.1" + "@smithy/util-retry" "^4.0.1" + "@smithy/util-utf8" "^4.0.0" + "@smithy/util-waiter" "^4.0.2" + "@types/uuid" "^9.0.1" + tslib "^2.6.2" + uuid "^9.0.1" + "@aws-sdk/client-cloudformation@3.741": version "3.741.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudformation/-/client-cloudformation-3.741.0.tgz#38ce1a13c41203256b0b0bfce43f229e645ee746" From 9873ff2a55a997f5ca03d0983f4f6a6789a35935 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Wed, 26 Feb 2025 18:10:01 +0100 Subject: [PATCH 07/12] chore: add idToken permissions for OIDC authentication (#125) The release steps don't work without these permissions. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license Co-authored-by: Momo Kornher --- .github/workflows/release.yml | 2 ++ projenrc/adc-publishing.ts | 1 + projenrc/record-publishing-timestamp.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd45848a..b64886f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -986,6 +986,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write environment: releasing if: ${{ needs.release.outputs.latest_commit == github.sha }} steps: @@ -1018,6 +1019,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write environment: releasing if: ${{ needs.release.outputs.latest_commit == github.sha }} steps: diff --git a/projenrc/adc-publishing.ts b/projenrc/adc-publishing.ts index 3b8de404..d9914734 100644 --- a/projenrc/adc-publishing.ts +++ b/projenrc/adc-publishing.ts @@ -35,6 +35,7 @@ export class AdcPublishing extends Component { runsOn: ['ubuntu-latest'], permissions: { contents: JobPermission.WRITE, + idToken: JobPermission.WRITE, }, if: '${{ needs.release.outputs.latest_commit == github.sha }}', steps: [ diff --git a/projenrc/record-publishing-timestamp.ts b/projenrc/record-publishing-timestamp.ts index 3c92c103..6b10a774 100644 --- a/projenrc/record-publishing-timestamp.ts +++ b/projenrc/record-publishing-timestamp.ts @@ -25,6 +25,7 @@ export class RecordPublishingTimestamp extends Component { runsOn: ['ubuntu-latest'], permissions: { contents: JobPermission.WRITE, + idToken: JobPermission.WRITE, }, if: '${{ needs.release.outputs.latest_commit == github.sha }}', steps: [ From f9e65cb0fb5ba0fc5718406dc5dab943750e86b8 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 27 Feb 2025 03:52:34 +0000 Subject: [PATCH 08/12] chore(toolkit-lib): role duration and session tags don't work (#162) Fixes another few issues with various publishing jobs: - Role chaining doesn't actually work with a [session duration over one hour.](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#:~:text=However%2C%20if%20you%20assume%20a%20role%20using%20role%20chaining%20and%20provide%20a%20DurationSeconds%20parameter%20value%20greater%20than%20one%20hour%2C%20the%20operation%20fails.) - Also I realized none of these job actually take that long. This was a copy-and-paste fail from the integ-tests (which do take longer). So let's go back to a shorter duration for these jobs - Use different session names for all jobs - Consistently use `mask-aws-account-id` everywhere. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .github/workflows/release.yml | 15 ++++++--------- projenrc/adc-publishing.ts | 3 +-- projenrc/record-publishing-timestamp.ts | 4 +--- projenrc/s3-docs-publishing.ts | 8 ++++---- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b64886f9..498cfd3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1003,9 +1003,8 @@ jobs: uses: aws-actions/configure-aws-credentials@v4 with: aws-region: us-east-1 - role-duration-seconds: 14400 role-to-assume: ${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }} - role-session-name: releasing@aws-cdk-cli + role-session-name: standalone-release@aws-cdk-cli output-credentials: true mask-aws-account-id: true - name: Publish artifacts @@ -1036,10 +1035,8 @@ jobs: uses: aws-actions/configure-aws-credentials@v4 with: aws-region: us-east-1 - role-duration-seconds: 14400 role-to-assume: ${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }} - role-session-name: releasing@aws-cdk-cli - output-credentials: true + role-session-name: publish-timestamps@aws-cdk-cli mask-aws-account-id: true - name: Publish artifacts run: |- @@ -1064,17 +1061,17 @@ jobs: uses: aws-actions/configure-aws-credentials@v4 with: aws-region: us-east-1 - role-duration-seconds: 14400 role-to-assume: ${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }} - role-session-name: releasing@aws-cdk-cli + role-session-name: s3-docs-publishing@aws-cdk-cli + mask-aws-account-id: true - name: Assume the publishing role id: publishing-creds uses: aws-actions/configure-aws-credentials@v4 with: aws-region: us-east-1 - role-duration-seconds: 14400 role-to-assume: ${{ vars.PUBLISH_TOOLKIT_LIB_DOCS_ROLE_ARN }} - role-session-name: s3publishing@aws-cdk-cli + role-session-name: s3-docs-publishing@aws-cdk-cli + mask-aws-account-id: true role-chaining: true - name: Publish docs env: diff --git a/projenrc/adc-publishing.ts b/projenrc/adc-publishing.ts index d9914734..9949941e 100644 --- a/projenrc/adc-publishing.ts +++ b/projenrc/adc-publishing.ts @@ -59,9 +59,8 @@ export class AdcPublishing extends Component { uses: 'aws-actions/configure-aws-credentials@v4', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': 14400, 'role-to-assume': '${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }}', - 'role-session-name': 'releasing@aws-cdk-cli', + 'role-session-name': 'standalone-release@aws-cdk-cli', 'output-credentials': true, 'mask-aws-account-id': true, }, diff --git a/projenrc/record-publishing-timestamp.ts b/projenrc/record-publishing-timestamp.ts index 6b10a774..28f9d075 100644 --- a/projenrc/record-publishing-timestamp.ts +++ b/projenrc/record-publishing-timestamp.ts @@ -48,10 +48,8 @@ export class RecordPublishingTimestamp extends Component { uses: 'aws-actions/configure-aws-credentials@v4', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': 14400, 'role-to-assume': '${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }}', - 'role-session-name': 'releasing@aws-cdk-cli', - 'output-credentials': true, + 'role-session-name': 'publish-timestamps@aws-cdk-cli', 'mask-aws-account-id': true, }, }, diff --git a/projenrc/s3-docs-publishing.ts b/projenrc/s3-docs-publishing.ts index d6755bbb..f8c23846 100644 --- a/projenrc/s3-docs-publishing.ts +++ b/projenrc/s3-docs-publishing.ts @@ -71,9 +71,9 @@ export class S3DocsPublishing extends Component { uses: 'aws-actions/configure-aws-credentials@v4', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': 14400, 'role-to-assume': '${{ vars.AWS_ROLE_TO_ASSUME_FOR_ACCOUNT }}', - 'role-session-name': 'releasing@aws-cdk-cli', + 'role-session-name': 's3-docs-publishing@aws-cdk-cli', + 'mask-aws-account-id': true, }, }, { @@ -82,9 +82,9 @@ export class S3DocsPublishing extends Component { uses: 'aws-actions/configure-aws-credentials@v4', with: { 'aws-region': 'us-east-1', - 'role-duration-seconds': 14400, 'role-to-assume': this.props.roleToAssume, - 'role-session-name': 's3publishing@aws-cdk-cli', + 'role-session-name': 's3-docs-publishing@aws-cdk-cli', + 'mask-aws-account-id': true, 'role-chaining': true, }, }, From 04ac50c8940c5a8f4e2d107f6480c7cd07544795 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Thu, 27 Feb 2025 03:32:15 -0500 Subject: [PATCH 09/12] docs(toolkit-lib): code registry links to public doc page (#161) and `IoMessage` doc page links to code registry --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions Co-authored-by: github-actions --- packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md | 8 ++++---- packages/@aws-cdk/toolkit-lib/lib/api/io/io-message.ts | 2 +- .../@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md b/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md index c5c65d2c..220144b0 100644 --- a/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md +++ b/packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md @@ -3,17 +3,17 @@ | Code | Description | Level | Data Interface | |------|-------------|-------|----------------| | CDK_TOOLKIT_I1000 | Provides synthesis times. | info | n/a | -| CDK_TOOLKIT_I1901 | Provides stack data | result | [StackData](docs/interfaces/StackData.html) | -| CDK_TOOLKIT_I1902 | Successfully deployed stacks | result | [AssemblyData](docs/interfaces/AssemblyData.html) | +| CDK_TOOLKIT_I1901 | Provides stack data | result | [StackData](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/StackData.html) | +| CDK_TOOLKIT_I1902 | Successfully deployed stacks | result | [AssemblyData](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/AssemblyData.html) | | CDK_TOOLKIT_I2901 | Provides details on the selected stacks and their dependencies | result | n/a | | CDK_TOOLKIT_E3900 | Resource import failed | error | n/a | | CDK_TOOLKIT_I5000 | Provides deployment times | info | n/a | -| CDK_TOOLKIT_I5001 | Provides total time in deploy action, including synth and rollback | info | [Duration](docs/interfaces/Duration.html) | +| CDK_TOOLKIT_I5001 | Provides total time in deploy action, including synth and rollback | info | [Duration](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/Duration.html) | | CDK_TOOLKIT_I5002 | Provides time for resource migration | info | n/a | | CDK_TOOLKIT_I5031 | Informs about any log groups that are traced as part of the deployment | info | n/a | | CDK_TOOLKIT_I5050 | Confirm rollback during deployment | info | n/a | | CDK_TOOLKIT_I5060 | Confirm deploy security sensitive changes | info | n/a | -| CDK_TOOLKIT_I5900 | Deployment results on success | result | [SuccessfulDeployStackResult](docs/interfaces/SuccessfulDeployStackResult.html) | +| CDK_TOOLKIT_I5900 | Deployment results on success | result | [SuccessfulDeployStackResult](https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/SuccessfulDeployStackResult.html) | | CDK_TOOLKIT_E5001 | No stacks found | error | n/a | | CDK_TOOLKIT_I6000 | Provides rollback times | info | n/a | | CDK_TOOLKIT_E6001 | No stacks found | error | n/a | diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/io-message.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/io-message.ts index fcfef497..b4d70939 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/io-message.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/io-message.ts @@ -7,7 +7,7 @@ import { ToolkitAction } from '../../toolkit'; export type IoMessageLevel = 'error'| 'result' | 'warn' | 'info' | 'debug' | 'trace'; /** - * A valid message code + * A valid message code. See https://github.com/aws/aws-cdk-cli/blob/main/packages/%40aws-cdk/toolkit-lib/CODE_REGISTRY.md */ export type IoMessageCode = `CDK_${string}_${'E' | 'W' | 'I'}${number}${number}${number}${number}`; diff --git a/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts index 935efee8..c461fc62 100644 --- a/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts +++ b/packages/@aws-cdk/toolkit-lib/scripts/gen-code-registry.ts @@ -19,7 +19,7 @@ function codesToMarkdownTable(codes: Record, mdPrefix?: string } function linkInterface(interfaceName: string) { - const docSite = 'docs/interfaces/'; + const docSite = 'https://docs.aws.amazon.com/cdk/api/toolkit-lib/interfaces/'; return `[${interfaceName}](${docSite}${interfaceName}.html)`; } From ce77923bd19814d698563e7ea9e7537262d3cff5 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Thu, 27 Feb 2025 03:34:14 -0500 Subject: [PATCH 10/12] chore: add git-secrets-scan to repository (#122) Will also need to be added to aws-cdk-cli-testing i believe. `git-secrets-scan.sh` is copied directly from the old repo; i didn't want to also change it to a typescript file. it runs now before `npx projen build`. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions Co-authored-by: github-actions --- .gitallowed | 42 ++++++++++++++++++++++++++++++++++++ .gitignore | 1 + .projen/tasks.json | 11 ++++++++++ .projenrc.ts | 15 ++++++++++++- package.json | 1 + projenrc/git-secrets-scan.sh | 30 ++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 .gitallowed create mode 100644 projenrc/git-secrets-scan.sh diff --git a/.gitallowed b/.gitallowed new file mode 100644 index 00000000..1ba82386 --- /dev/null +++ b/.gitallowed @@ -0,0 +1,42 @@ +# The only AWS account number allowed to be used in tests (used by git-secrets) +account = '123456789012'; +# account used for cross-environment tests in addition to the one above +account: '234567890123' +# Account patterns used in the README +account: '000000000000' +account: '111111111111' +account: '222222222222' +account: '333333333333' + +# used in physical names tests in @aws-cdk/core +account: '012345678912' +account: '012345678913' + +# Account patterns used in the CHANGELOG +account: '123456789012' + +111111111111 +222222222222 +123456789012 +333333333333 + +# The account ID's of public facing ECR images for App Mesh Envoy +# https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html +account: '772975370895' +account: '856666278305' +account: '840364872350' +account: '422531588944' +account: '924023996002' +account: '919366029133' #cn-north-1 +account: '919830735681' #cn-northwest-1 +account: '909464085924' #ap-southeast-3 +account: '564877687649' #il-central-1 + +# The account IDs of password rotation applications of Serverless Application Repository +# https://docs.aws.amazon.com/secretsmanager/latest/userguide/enable-rotation-rds.html +# partition aws +account: '297356227824' +# partition aws-cn +account: '193023089310' +# partition aws-us-gov +account: '023102451235' diff --git a/.gitignore b/.gitignore index 5b69bb86..2ecaf4ae 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ jspm_packages/ .yarn-integrity .cache .DS_Store +.tools !/.github/workflows/build.yml !/.github/workflows/upgrade.yml !/.github/pull_request_template.md diff --git a/.projen/tasks.json b/.projen/tasks.json index cd8caa4d..50040601 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -12,6 +12,9 @@ { "spawn": "eslint" }, + { + "spawn": "git-secrets-scan" + }, { "exec": "tsx projenrc/build-standalone-zip.task.ts" }, @@ -83,6 +86,14 @@ } ] }, + "git-secrets-scan": { + "name": "git-secrets-scan", + "steps": [ + { + "exec": "/bin/bash ./projenrc/git-secrets-scan.sh" + } + ] + }, "install": { "name": "install", "description": "Install project dependencies and update lockfile (non-frozen)", diff --git a/.projenrc.ts b/.projenrc.ts index c8c58c46..55a37808 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -183,7 +183,7 @@ const repoProject = new yarn.Monorepo({ workflowNodeVersion: 'lts/*', workflowRunsOn, - gitignore: ['.DS_Store'], + gitignore: ['.DS_Store', '.tools'], autoApproveUpgrades: true, autoApproveOptions: { @@ -215,6 +215,7 @@ const repoProject = new yarn.Monorepo({ }, }, }, + buildWorkflowOptions: { preBuildSteps: [ // Need this for the init tests @@ -241,11 +242,23 @@ repoProject.eslint = new pj.javascript.Eslint(repoProject, { fileExtensions: ['.ts', '.tsx'], lintProjenRc: false, }); + // always lint projen files as part of the build if (repoProject.eslint?.eslintTask) { repoProject.tasks.tryFind('build')?.spawn(repoProject.eslint?.eslintTask); } +// always scan for git secrets before building +const gitSecretsScan = repoProject.addTask('git-secrets-scan', { + steps: [ + { + exec: '/bin/bash ./projenrc/git-secrets-scan.sh', + }, + ], +}); + +repoProject.tasks.tryFind('build')!.spawn(gitSecretsScan); + new AdcPublishing(repoProject); const repo = configureProject(repoProject); diff --git a/package.json b/package.json index 64337051..8f7df4a3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "compile": "npx projen compile", "default": "npx projen default", "eslint": "npx projen eslint", + "git-secrets-scan": "npx projen git-secrets-scan", "package": "npx projen package", "post-upgrade": "npx projen post-upgrade", "release": "npx projen release", diff --git a/projenrc/git-secrets-scan.sh b/projenrc/git-secrets-scan.sh new file mode 100644 index 00000000..95c2425a --- /dev/null +++ b/projenrc/git-secrets-scan.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +mkdir -p .tools +[[ ! -d .tools/git-secrets ]] && { + echo "=============================================================================================" + echo "Downloading git-secrets..." + (cd .tools && git clone --depth 1 https://github.com/awslabs/git-secrets.git) +} + +# As the name implies, git-secrets heavily depends on git: +# +# a) the config is stored and fetched using 'git config'. +# b) the search is performed using 'git grep' (other search methods don't work +# properly, see https://github.com/awslabs/git-secrets/issues/66) +# +# When we run in a CodeBuild build, we don't have a git repo, unfortunately. So +# when that's the case, 'git init' one on the spot, add all files to it (which +# because of the .gitignore will exclude dependencies and generated files) and +# then call 'git-secrets' as usual. +git rev-parse --git-dir > /dev/null 2>&1 || { + git init --quiet + git add -A . +} + +# AWS config needs to be added to this repository's config +.tools/git-secrets/git-secrets --register-aws + +.tools/git-secrets/git-secrets --scan +echo "git-secrets scan ok" From 47beb66ff701748a75f0dd83e945e72064c0ab30 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Thu, 27 Feb 2025 10:28:59 +0100 Subject: [PATCH 11/12] chore: add checkout step for ADC publishing (#164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This tries to run a script from the source repo, so it should checkout the source otherwise the script isn't going to be there 😭. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .github/workflows/release.yml | 2 ++ projenrc/adc-publishing.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 498cfd3b..e01c7fdb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -990,6 +990,8 @@ jobs: environment: releasing if: ${{ needs.release.outputs.latest_commit == github.sha }} steps: + - name: Checkout + uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: lts/* diff --git a/projenrc/adc-publishing.ts b/projenrc/adc-publishing.ts index 9949941e..d8be4047 100644 --- a/projenrc/adc-publishing.ts +++ b/projenrc/adc-publishing.ts @@ -39,6 +39,7 @@ export class AdcPublishing extends Component { }, if: '${{ needs.release.outputs.latest_commit == github.sha }}', steps: [ + github.WorkflowSteps.checkout(), { uses: 'actions/setup-node@v4', with: { From 5b3afbef8b0859dcdad0ab579dec0ce0dd15b723 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Thu, 27 Feb 2025 05:44:51 -0500 Subject: [PATCH 12/12] docs(cli): unstable flag documentation (#159) document `--unstable` flag outside of `cdk gc`, since it is a global option available to additional use cases. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license Co-authored-by: Rico Hermans --- packages/aws-cdk/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index d9ab0750..0a580ed6 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -39,6 +39,21 @@ The AWS CDK Toolkit provides the `cdk` command-line interface that can be used t This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +## Global Options + +### `unstable` + +The `--unstable` flag indicates that the scope and API of a feature might still change. +Otherwise the feature is generally production ready and fully supported. For example, +`cdk gc` is gated behind an `--unstable` flag: + +```bash +cdk gc --unstable=gc +``` + +The command will fail if `--unstable=gc` is not passed in, which acknowledges that the user +is aware of the caveats in place for the feature. + ## Commands ### `cdk docs`