diff --git a/packages/aws-cdk-lib/core/lib/private/feature-flag-report.ts b/packages/aws-cdk-lib/core/lib/private/feature-flag-report.ts new file mode 100644 index 0000000000000..40066e140d4cc --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/private/feature-flag-report.ts @@ -0,0 +1,39 @@ +import { ArtifactType, FeatureFlag } from '@aws-cdk/cloud-assembly-schema'; +import { IConstruct } from 'constructs'; +import { CloudAssemblyBuilder } from '../../../cx-api'; +import * as feats from '../../../cx-api/lib/features'; +import { FlagInfo } from '../../../cx-api/lib/private/flag-modeling'; + +/** + * Creates a FeatureFlag object based on flag information given. + */ +function parseFeatureFlagInfo(flagName: string, info: FlagInfo, root: IConstruct): FeatureFlag { + const userValue = root.node.tryGetContext(flagName) ?? undefined; + + let parsedFlag: FeatureFlag = { + userValue: userValue, + recommendedValue: info.recommendedValue, + explanation: info.summary, + }; + + return parsedFlag; +} + +/** + * Iterate through all feature flags, retrieve the user's context, + * and create a Feature Flag report. + */ +export function generateFeatureFlagReport(builder: CloudAssemblyBuilder, root: IConstruct): void { + const featureFlags: Record = {}; + for (const [flagName, flagInfo] of Object.entries(feats.FLAGS)) { + featureFlags[flagName] = parseFeatureFlagInfo(flagName, flagInfo, root); + } + + builder.addArtifact('aws-cdk-lib/feature-flag-report', { + type: ArtifactType.FEATURE_FLAG_REPORT, + properties: { + module: 'aws-cdk-lib', + flags: featureFlags, + }, + }); +} diff --git a/packages/aws-cdk-lib/core/lib/private/synthesis.ts b/packages/aws-cdk-lib/core/lib/private/synthesis.ts index 6a5c72facd265..5bae10a8c8189 100644 --- a/packages/aws-cdk-lib/core/lib/private/synthesis.ts +++ b/packages/aws-cdk-lib/core/lib/private/synthesis.ts @@ -15,6 +15,7 @@ import { Stack } from '../stack'; import { ISynthesisSession } from '../stack-synthesizers/types'; import { Stage, StageSynthesisOptions } from '../stage'; import { IPolicyValidationPluginBeta1 } from '../validation'; +import { generateFeatureFlagReport } from './feature-flag-report'; import { ConstructTree } from '../validation/private/construct-tree'; import { PolicyValidationReportFormatter, NamedValidationPluginReport } from '../validation/private/report'; @@ -64,6 +65,8 @@ export function synthesize(root: IConstruct, options: SynthesisOptions = { }): c // stacks to add themselves to the synthesized cloud assembly. synthesizeTree(root, builder, options.validateOnSynthesis); + generateFeatureFlagReport(builder, root); + const assembly = builder.buildAssembly(); invokeValidationPlugins(root, builder.outdir, assembly); diff --git a/packages/aws-cdk-lib/core/test/feature-flag-report.test.ts b/packages/aws-cdk-lib/core/test/feature-flag-report.test.ts new file mode 100644 index 0000000000000..d83a5a05155b6 --- /dev/null +++ b/packages/aws-cdk-lib/core/test/feature-flag-report.test.ts @@ -0,0 +1,50 @@ +import { FeatureFlagReportProperties } from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '../../cx-api'; +import { App } from '../lib'; +import { generateFeatureFlagReport } from '../lib/private/feature-flag-report'; + +describe('generate feature flag report', () => { + test('feature flag report can be retrieved from CloudAssembly using its artifact ID', () => { + const app = new App(); + const assembly = app.synth(); + expect(assembly.manifest.artifacts?.['aws-cdk-lib/feature-flag-report']).toBeDefined(); + }); + test('report contains context values that represent the feature flags', () => { + const app = new App({ + context: { + '@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault': true, + '@aws-cdk/core:aspectStabilization': false, + }, + }); + const assembly = app.synth(); + const report = assembly.manifest.artifacts?.['aws-cdk-lib/feature-flag-report']; + expect(report).toEqual(expect.objectContaining({ + type: 'cdk:feature-flag-report', + properties: expect.objectContaining({ + module: 'aws-cdk-lib', + flags: expect.objectContaining({ + '@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault': expect.objectContaining({ + userValue: true, + }), + '@aws-cdk/core:aspectStabilization': expect.objectContaining({ + userValue: false, + }), + }), + }), + })); + }); + test('defaults userValue to undefined when not set in context', () => { + const app = new App(); + const builder = new cxapi.CloudAssemblyBuilder('/tmp/test'); + const spy = jest.spyOn(builder, 'addArtifact'); + + generateFeatureFlagReport(builder, app); + + const flags = (spy.mock.calls[0][1].properties as FeatureFlagReportProperties).flags; + expect(flags).toEqual(expect.objectContaining({ + '@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault': expect.objectContaining({ + userValue: undefined, + }), + })); + }); +}); diff --git a/packages/aws-cdk-lib/core/test/stage.test.ts b/packages/aws-cdk-lib/core/test/stage.test.ts index 4c113c96e1e69..70e56cf7fe798 100644 --- a/packages/aws-cdk-lib/core/test/stage.test.ts +++ b/packages/aws-cdk-lib/core/test/stage.test.ts @@ -252,7 +252,7 @@ describe('stage', () => { const rootAssembly = app.synth(); // THEN - expect(rootAssembly.manifest.artifacts).toEqual({ + expect(rootAssembly.manifest.artifacts).toMatchObject({ 'assembly-StageLevel1': { type: 'cdk:cloud-assembly', properties: { @@ -263,7 +263,7 @@ describe('stage', () => { }); const assemblyLevel1 = rootAssembly.getNestedAssembly('assembly-StageLevel1'); - expect(assemblyLevel1.manifest.artifacts).toEqual({ + expect(assemblyLevel1.manifest.artifacts).toMatchObject({ 'assembly-StageLevel1-StageLevel2': { type: 'cdk:cloud-assembly', properties: { @@ -274,7 +274,7 @@ describe('stage', () => { }); const assemblyLevel2 = assemblyLevel1.getNestedAssembly('assembly-StageLevel1-StageLevel2'); - expect(assemblyLevel2.manifest.artifacts).toEqual({ + expect(assemblyLevel2.manifest.artifacts).toMatchObject({ 'assembly-StageLevel1-StageLevel2-StageLevel3': { type: 'cdk:cloud-assembly', properties: { diff --git a/packages/aws-cdk-lib/core/test/synthesis.test.ts b/packages/aws-cdk-lib/core/test/synthesis.test.ts index 65e9a6df24012..f92bde7131030 100644 --- a/packages/aws-cdk-lib/core/test/synthesis.test.ts +++ b/packages/aws-cdk-lib/core/test/synthesis.test.ts @@ -24,7 +24,7 @@ describe('synthesis', () => { // THEN expect(app.synth()).toEqual(session); // same session if we synth() again expect(list(session.directory)).toEqual(['cdk.out', 'manifest.json', 'tree.json']); - expect(readJson(session.directory, 'manifest.json').artifacts).toEqual({ + expect(readJson(session.directory, 'manifest.json').artifacts).toMatchObject({ Tree: { type: 'cdk:tree', properties: { file: 'tree.json' },