From e4615f32627aed48b919304f905838adbb80f4e2 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:31:22 -0400 Subject: [PATCH 1/5] feat(toolkit-lib): unstable evaluate action to resolve a stack's template against its deployed stack toolkit.evaluate(cx, { stacks }) (unstable feature 'evaluate') returns each selected stack's template with every expression in Resources and Outputs that can be evaluated against the deployed stack replaced by its value, and the expressions that could not be evaluated with the reason. - reads the deployed stack's parameters, physical ids and nested stack templates - evaluates CDK-managed nested stacks recursively, with the parameter values their parent passes - NoEcho parameters (in the evaluated or deployed template) are reported, never replaced by a default - skips stacks that are not deployed, or whose creation failed or never ran, and lists them in notDeployed - warns when the deployed template has different resources than the evaluated one - one shared limit on resource lookups for the whole run --- .../toolkit-lib/lib/actions/evaluate/index.ts | 98 ++++ .../evaluate/private/evaluate-stack.ts | 142 +++++ .../lib/actions/evaluate/private/index.ts | 1 + .../@aws-cdk/toolkit-lib/lib/actions/index.ts | 1 + .../evaluate-template-best-effort.ts | 21 +- .../toolkit-lib/lib/api/io/toolkit-action.ts | 3 +- .../toolkit-lib/lib/toolkit/toolkit.ts | 74 ++- .../cdk.out/Stack1.assets.json | 36 ++ .../cdk.out/Stack1.metadata.json | 92 ++++ .../cdk.out/Stack1.template.json | 143 +++++ .../Stack1Nested36ADE314.nested.template.json | 30 + .../cdk.out/manifest.json | 512 ++++++++++++++++++ .../stack-with-references/cdk.out/tree.json | 1 + .../_fixtures/stack-with-references/index.ts | 32 ++ .../toolkit-lib/test/actions/evaluate.test.ts | 277 ++++++++++ packages/aws-cdk/lib/cli/cdk-toolkit.ts | 1 + 16 files changed, 1441 insertions(+), 23 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.assets.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.metadata.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.template.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1Nested36ADE314.nested.template.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/manifest.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/tree.json create mode 100644 packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts new file mode 100644 index 000000000..4a7a4d9c6 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts @@ -0,0 +1,98 @@ +import type { StackSelector } from '../../api/cloud-assembly'; + +export interface EvaluateOptions { + /** + * Select the stacks to evaluate + * + * @default - All stacks + */ + readonly stacks?: StackSelector; + + /** + * Name of the toolkit stack, used to access the stacks' environments + * + * @default "CDKToolkit" + */ + readonly toolkitStackName?: string; + + /** + * Maximum number of stacks to evaluate in parallel + * + * @default 10 + */ + readonly concurrency?: number; +} + +/** + * An expression in a stack template that could not be evaluated against the deployed stack + */ +export interface UnresolvedExpression { + /** + * Location of the expression in the template, e.g. `Resources.MyBucket.Properties.BucketName` + */ + readonly path: string; + + /** + * The intrinsic function that could not be evaluated, e.g. `Fn::GetAtt` + */ + readonly intrinsic: string; + + /** + * Why the expression could not be evaluated + */ + readonly reason: string; +} + +/** + * A stack template evaluated against the deployed stack + */ +export interface EvaluatedStack { + /** + * The name of the stack + */ + readonly stackName: string; + + /** + * The hierarchical id of the stack in the cloud assembly + * + * For a nested stack, the hierarchical id of its parent followed by the nested stack's logical id. + */ + readonly hierarchicalId: string; + + /** + * The stack's template, with every expression in `Resources` and `Outputs` that could be + * evaluated against the deployed stack replaced by its value + * + * Expressions that could not be evaluated are left in place, and listed in `unresolved`. + */ + readonly template: { [key: string]: any }; + + /** + * Expressions that could not be evaluated + */ + readonly unresolved: UnresolvedExpression[]; + + /** + * The stack's deployed CDK-managed nested stacks, evaluated the same way, by logical id + * + * A nested stack gets the parameter values its parent passes, as evaluated in the parent. + * + * @default - the stack has no nested stacks + */ + readonly nestedStacks?: { [logicalId: string]: EvaluatedStack }; +} + +/** + * Result of the evaluate action + */ +export interface EvaluateResult { + /** + * The evaluated stacks + */ + readonly stacks: EvaluatedStack[]; + + /** + * Names of the selected stacks that are not deployed (including stacks whose creation failed or never ran), and so were not evaluated + */ + readonly notDeployed: string[]; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts new file mode 100644 index 000000000..53ba78cab --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts @@ -0,0 +1,142 @@ +import type { CloudFormationStackArtifact } from '@aws-cdk/cloud-assembly-api'; +import type { SDK } from '../../../api/aws-auth/private'; +import type { NestedStackTemplates, SharedLookups, Template } from '../../../api/cloudformation'; +import { EvaluateCloudFormationTemplate, evaluateTemplateBestEffort, containsExpression, loadCurrentTemplateWithNestedStacks } from '../../../api/cloudformation'; +import { parallelPromises } from '../../../util'; +import type { EvaluatedStack } from '../index'; + +/** Nested stacks of one stack to evaluate at the same time */ +const NESTED_STACK_CONCURRENCY = 4; + +export interface EvaluateStackProps { + readonly stackArtifact: CloudFormationStackArtifact; + /** The deployed stack's parameter values, as DescribeStacks returns them (NoEcho values are masked) */ + readonly parameters: { [name: string]: string }; + /** Lookups shared by all evaluators of the run */ + readonly lookups: SharedLookups; + readonly stackId: string; + readonly account: string; + readonly region: string; + readonly partition: string; + readonly sdk: SDK; + /** Receives warnings, e.g. when the deployed template differs from the one being evaluated */ + readonly warn: (message: string) => Promise; +} + +/** + * Evaluates a deployed stack's template and, recursively, the templates of its CDK-managed nested stacks. + * + * A nested stack is evaluated with the parameter values its parent passes, as evaluated in the parent; + * values the parent could not evaluate are unavailable in the nested stack. + */ +export async function evaluateStackBestEffort(props: EvaluateStackProps): Promise { + const { stackArtifact, sdk } = props; + const currentTemplate = await loadCurrentTemplateWithNestedStacks(stackArtifact, sdk); + const common = { + stackArtifact, + account: props.account, + region: props.region, + partition: props.partition, + sdk, + // shared by all the stacks' evaluators, so each stack is listed at most once + stackResourcesByStackName: new Map(), + lookups: props.lookups, + }; + + async function evaluateStack( + stackName: string, + hierarchicalId: string, + template: Template, + parameters: { [name: string]: string }, + unavailableParameters: { [name: string]: string }, + stackId: string, + nestedStacks: { [logicalId: string]: NestedStackTemplates }, + ): Promise { + const evaluator = new EvaluateCloudFormationTemplate({ + ...common, + stackName, + template, + // pseudo parameters the evaluator does not provide itself + parameters: { ...parameters, 'AWS::StackName': stackName, 'AWS::StackId': stackId }, + nestedStacks, + // CloudFormation semantics: empty strings are values; unavailable parameters never fall back to their Default + strictValues: true, + unavailableParameters: Object.keys(unavailableParameters), + }); + const evaluation = await evaluateTemplateBestEffort(evaluator, template, { unavailableParameters }); + + const evaluatedNestedStacks: { [logicalId: string]: EvaluatedStack } = {}; + const evaluateNested = async (logicalId: string, nested: NestedStackTemplates) => { + const resource = evaluation.template.Resources?.[logicalId]; + if (!nested.physicalName || !resource) { + return; // not deployed, or not created because of its condition + } + await warnOnDrift(nested.physicalName, nested.generatedTemplate, nested.deployedTemplate); + const nestedParameters: { [name: string]: string } = {}; + const nestedUnavailable: { [name: string]: string } = {}; + for (const [name, value] of Object.entries(resource.Properties?.Parameters ?? {})) { + if (containsExpression(value)) { + nestedUnavailable[name] = `the value passed by stack '${stackName}' could not be evaluated`; + } else { + nestedParameters[name] = Array.isArray(value) ? value.join(',') : String(value); + } + } + const nestedStackId = await evaluator.evaluateCfnExpression({ Ref: logicalId }); + evaluatedNestedStacks[logicalId] = await evaluateStack( + nested.physicalName, + `${hierarchicalId}/${logicalId}`, + nested.generatedTemplate, + nestedParameters, + nestedUnavailable, + nestedStackId, + nested.nestedStackTemplates, + ); + }; + await parallelPromises(NESTED_STACK_CONCURRENCY, + Object.entries(nestedStacks).map(([logicalId, nested]) => () => evaluateNested(logicalId, nested))); + + // in template order, whichever finished first + const ordered = Object.fromEntries(Object.keys(nestedStacks) + .filter((id) => id in evaluatedNestedStacks) + .map((id) => [id, evaluatedNestedStacks[id]])); + return { + stackName, + hierarchicalId, + template: evaluation.template, + unresolved: evaluation.unresolved, + ...(Object.keys(ordered).length > 0 ? { nestedStacks: ordered } : {}), + }; + } + + // The template being evaluated should be the one that is deployed. Compare structure rather than text: + // GetTemplate doesn't always return the template's characters exactly (e.g. non-ASCII punctuation). + async function warnOnDrift(stackName: string, evaluated: Template, deployed: Template) { + const types = (t: Template) => Object.entries(t.Resources ?? {}).map(([id, r]) => `${id} (${r?.Type})`); + const deployedTypes = new Set(types(deployed)); + const evaluatedTypes = new Set(types(evaluated)); + const onlyEvaluated = [...evaluatedTypes].filter((x) => !deployedTypes.has(x)); + const onlyDeployed = [...deployedTypes].filter((x) => !evaluatedTypes.has(x)); + if (onlyEvaluated.length || onlyDeployed.length) { + await props.warn(`The template of stack '${stackName}' differs from its deployed template, so evaluated values may not match ` + + `the deployment. Not deployed: ${onlyEvaluated.join(', ') || 'none'}. Only in the deployed template: ${onlyDeployed.join(', ') || 'none'}`); + } + } + await warnOnDrift(stackArtifact.stackName, stackArtifact.template, currentTemplate.deployedRootTemplate); + + // NoEcho values come back masked; expressions that use them are reported as unresolved instead. A parameter + // counts as NoEcho if either the evaluated or the deployed template says so. + const noEcho = new Set([stackArtifact.template, currentTemplate.deployedRootTemplate] + .flatMap((t) => Object.entries(t.Parameters ?? {}).filter(([, p]) => String(p?.NoEcho).toLowerCase() === 'true').map(([name]) => name))); + const unavailableParameters = Object.fromEntries([...noEcho].map((name) => [name, 'NoEcho parameter, whose deployed value can\'t be read'])); + const parameters = Object.fromEntries(Object.entries(props.parameters).filter(([name]) => !noEcho.has(name))); + + return evaluateStack( + stackArtifact.stackName, + stackArtifact.hierarchicalId, + stackArtifact.template, + parameters, + unavailableParameters, + props.stackId, + currentTemplate.nestedStacks, + ); +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/index.ts new file mode 100644 index 000000000..a3a3966ba --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/index.ts @@ -0,0 +1 @@ +export * from './evaluate-stack'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts index ee126522f..e849ce735 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/index.ts @@ -12,3 +12,4 @@ export * from './synth'; export * from './watch'; export * from './diagnose'; export * from './validate'; +export * from './evaluate'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloudformation/evaluate-template-best-effort.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloudformation/evaluate-template-best-effort.ts index 6c73410ae..444741b1e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloudformation/evaluate-template-best-effort.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloudformation/evaluate-template-best-effort.ts @@ -1,28 +1,9 @@ import type { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template'; import { CfnEvaluationException, CfnUnsupportedFunctionException, parseIntrinsic } from './evaluate-cloudformation-template'; import type { Template } from './stack-helpers'; +import type { UnresolvedExpression } from '../../actions/evaluate'; import { formatErrorMessage, parallelPromises } from '../../util'; -/** - * An expression in a template that could not be evaluated - */ -export interface UnresolvedExpression { - /** - * Location of the expression in the template, e.g. `Resources.MyBucket.Properties.BucketName` - */ - readonly path: string; - - /** - * The intrinsic function that could not be evaluated, e.g. `Fn::GetAtt` - */ - readonly intrinsic: string; - - /** - * Why the expression could not be evaluated - */ - readonly reason: string; -} - /** * A template with as many expressions as possible replaced by their values */ diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts index aa25e06e7..e40568da3 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/toolkit-action.ts @@ -23,4 +23,5 @@ export type ToolkitAction = | 'diagnose' | 'orphan' | 'flags' -| 'validate'; +| 'validate' +| 'evaluate'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 8cb119854..b31598871 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -52,6 +52,8 @@ import type { DiagnosedStack, DiagnoseOptions, DiagnoseResult } from '../actions import type { DiffOptions } from '../actions/diff'; import { appendObject, prepareDiff } from '../actions/diff/private'; import type { DriftOptions, DriftResult } from '../actions/drift'; +import type { EvaluatedStack, EvaluateOptions, EvaluateResult } from '../actions/evaluate'; +import { evaluateStackBestEffort } from '../actions/evaluate/private'; import { type ListOptions } from '../actions/list'; import type { OrphanOptions } from '../actions/orphan'; import type { PublishAssetsOptions, PublishAssetsResult } from '../actions/publish-assets'; @@ -79,6 +81,7 @@ import { ALL_STACKS } from '../api/cloud-assembly/private'; import { AsyncDisposableBox } from '../api/cloud-assembly/private/disposable-box'; import { CloudAssemblySourceBuilder } from '../api/cloud-assembly/source-builder'; import { combineConclusions, type StackCollection } from '../api/cloud-assembly/stack-collection'; +import { CloudFormationStack as DeployedStack, SharedLookups } from '../api/cloudformation'; import { Deployments } from '../api/deployments'; import { createValidationChangeSet } from '../api/deployments/cfn-api'; import { hostMessageFromDiagnosis } from '../api/diagnosing/diagnosis-formatting'; @@ -112,7 +115,7 @@ import type { AssetBuildNode, AssetPublishNode, Concurrency, StackNode } from '. import { WorkGraph, WorkGraphBuilder, buildDestroyWorkGraph } from '../api/work-graph'; import type { AssemblyData, RefactorResult, StackDetails, SuccessfulDeployStackResult } from '../payloads'; import { PermissionChangeType } from '../payloads'; -import { formatErrorMessage, formatExpressStabilizationWarning, formatTime, obscureTemplate, serializeStructure, toCloudFormationRollbackConfiguration, validateSnsTopicArn } from '../util'; +import { formatErrorMessage, formatExpressStabilizationWarning, formatTime, obscureTemplate, parallelPromises, serializeStructure, toCloudFormationRollbackConfiguration, validateSnsTopicArn } from '../util'; import { pLimit } from '../util/concurrency'; import { createIgnoreMatcher } from '../util/glob-matcher'; import { promiseWithResolvers } from '../util/promises'; @@ -182,7 +185,7 @@ export interface ToolkitOptions { * Names of toolkit features that are still under development, and may change in * the future. */ -export type UnstableFeature = 'refactor' | 'orphan' | 'flags' | 'publish-assets' | 'diagnose' | 'validate'; +export type UnstableFeature = 'refactor' | 'orphan' | 'flags' | 'publish-assets' | 'diagnose' | 'validate' | 'evaluate'; /** * The AWS CDK Programmatic Toolkit @@ -680,6 +683,73 @@ export class Toolkit extends CloudAssemblySourceBuilder { return { stacks }; } + /** + * Evaluate Action + * + * Evaluates the templates of the selected stacks against their deployed stacks: references, attributes, + * parameters, pseudo parameters and the intrinsic functions supported by the hotswap evaluator are replaced + * by their deployed values. This gives the concrete values CloudFormation used for each resource, for + * example to compare them with the live state of the resources. + * + * Expressions that cannot be evaluated are left in place and reported with the reason, so one unresolvable + * expression does not prevent the rest. CDK-managed nested stacks are evaluated with their parent. + * + * Selected stacks that are not deployed (including stacks whose creation failed or never ran) are skipped with a + * warning, and listed in `notDeployed`. A warning is also given when a stack's deployed template has different + * resources than the one being evaluated. + */ + public async evaluate(cx: ICloudAssemblySource, options: EvaluateOptions = {}): Promise { + this.requireUnstableFeature('evaluate'); + + const concurrency = options.concurrency ?? 10; + if (!Number.isInteger(concurrency) || concurrency < 1) { + throw new ToolkitError('InvalidConcurrency', `concurrency must be a positive integer, got ${concurrency}`); + } + + const ioHelper = asIoHelper(this.ioHost, 'evaluate'); + const selectStacks = stacksOpt(options); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); + const stackCollection = await assembly.selectStacks(selectStacks); + const envs = new EnvironmentAccess(await this.sdkProvider('evaluate'), options.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME, ioHelper); + const artifacts = stackCollection.stackArtifacts; + // one limit on resource lookups for the whole run, however many stacks and nested stacks run in parallel + const lookups = new SharedLookups(10); + const results: Array = new Array(artifacts.length); + const evaluateStack = async (index: number) => { + const stack = artifacts[index]; + const stackEnv = await envs.accessStackForLookupBestEffort(stack); + const sdk = stackEnv.sdk; + + const deployed = await DeployedStack.lookup(sdk.cloudFormation(), stack.stackName); + const status = deployed.stackStatus; + // a stack that exists but whose resources were never (or are no longer) created + if (status.isNotFound || status.isReviewInProgress || status.isCreationFailure || status.isDeleted + || ['CREATE_IN_PROGRESS', 'CREATE_FAILED', 'ROLLBACK_IN_PROGRESS'].includes(status.name)) { + const state = status.isNotFound ? 'is not deployed' : `is ${status.name}`; + await ioHelper.defaults.warn(`Stack '${stack.stackName}' ${state} in ${stackEnv.resolvedEnvironment.name}, skipping it`); + return; + } + results[index] = await evaluateStackBestEffort({ + stackArtifact: stack, + parameters: deployed.parameters, + lookups, + stackId: deployed.stackId, + account: stackEnv.resolvedEnvironment.account, + region: stackEnv.resolvedEnvironment.region, + partition: (await sdk.currentAccount()).partition, + sdk, + warn: (message) => ioHelper.defaults.warn(message), + }); + }; + + await parallelPromises(concurrency, artifacts.map((_, index) => () => evaluateStack(index))); + + return { + stacks: results.filter((r): r is EvaluatedStack => r !== undefined), + notDeployed: artifacts.filter((_, i) => results[i] === undefined).map((a) => a.stackName), + }; + } + /** * Validate Action * diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.assets.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.assets.json new file mode 100644 index 000000000..269d256d6 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.assets.json @@ -0,0 +1,36 @@ +{ + "version": "54.0.0", + "files": { + "79bf57b1b3bbe8f179f2b75327d6f95f059a31631484c17e6ee3aaceea8a0264": { + "displayName": "Nested Nested Stack Template", + "source": { + "path": "Stack1Nested36ADE314.nested.template.json", + "packaging": "file" + }, + "destinations": { + "123456789012-us-east-1-1498841e": { + "bucketName": "cdk-hnb659fds-assets-123456789012-us-east-1", + "objectKey": "79bf57b1b3bbe8f179f2b75327d6f95f059a31631484c17e6ee3aaceea8a0264.json", + "region": "us-east-1", + "assumeRoleArn": "arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-file-publishing-role-123456789012-us-east-1" + } + } + }, + "91b722a85f337ba12178b933113aa50e118a4bb71559f94ab1a70ff2f01aef35": { + "displayName": "Stack1 Template", + "source": { + "path": "Stack1.template.json", + "packaging": "file" + }, + "destinations": { + "123456789012-us-east-1-b947283a": { + "bucketName": "cdk-hnb659fds-assets-123456789012-us-east-1", + "objectKey": "91b722a85f337ba12178b933113aa50e118a4bb71559f94ab1a70ff2f01aef35.json", + "region": "us-east-1", + "assumeRoleArn": "arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-file-publishing-role-123456789012-us-east-1" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.metadata.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.metadata.json new file mode 100644 index 000000000..4b3a2071b --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.metadata.json @@ -0,0 +1,92 @@ +{ + "/Stack1/BucketArn": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketArn" + } + ], + "/Stack1/BucketDomain": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketDomain" + } + ], + "/Stack1/Name": [ + { + "type": "aws:cdk:logicalId", + "data": "Name" + } + ], + "/Stack1/Joined": [ + { + "type": "aws:cdk:logicalId", + "data": "Joined" + } + ], + "/Stack1/Secret": [ + { + "type": "aws:cdk:logicalId", + "data": "Secret" + } + ], + "/Stack1/SecretValue": [ + { + "type": "aws:cdk:logicalId", + "data": "SecretValue" + } + ], + "/Stack1/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Stack1/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ], + "/Stack1/MyBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyBucketF68F3FF0" + } + ], + "/Stack1/BucketName/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketName742CD8B0" + } + ], + "/Stack1/Nested/reference-to-Stack1MyBucket3E4D6F6CRef": [ + { + "type": "aws:cdk:logicalId", + "data": "referencetoStack1MyBucket3E4D6F6CRef" + } + ], + "/Stack1/Nested.NestedStack/Nested.NestedStackResource": [ + { + "type": "aws:cdk:logicalId", + "data": "NestedNestedStackNestedNestedStackResourceDEFDAA4D" + } + ], + "/Stack1/CDKMetadata/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadata" + } + ], + "/Stack1/Nested/NestedBucketName/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "NestedBucketName7CD077DC" + } + ], + "/Stack1/Nested/CDKMetadata/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadata" + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.template.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.template.json new file mode 100644 index 000000000..a2d3b44fc --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1.template.json @@ -0,0 +1,143 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "aws:cdk:path": "Stack1/MyBucket/Resource" + } + }, + "BucketName742CD8B0": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Ref": "MyBucketF68F3FF0" + } + }, + "Metadata": { + "aws:cdk:path": "Stack1/BucketName/Resource" + } + }, + "NestedNestedStackNestedNestedStackResourceDEFDAA4D": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "Parameters": { + "referencetoStack1MyBucket3E4D6F6CRef": { + "Ref": "MyBucketF68F3FF0" + } + }, + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.us-east-1.", + { + "Ref": "AWS::URLSuffix" + }, + "/cdk-hnb659fds-assets-123456789012-us-east-1/79bf57b1b3bbe8f179f2b75327d6f95f059a31631484c17e6ee3aaceea8a0264.json" + ] + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "aws:cdk:path": "Stack1/Nested.NestedStack/Nested.NestedStackResource", + "aws:asset:path": "Stack1Nested36ADE314.nested.template.json", + "aws:asset:property": "TemplateURL" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/1WMQQ7CIBBFz9I9jIQ2ca8H0OgBzEjRUAQbZrALwt0bbGLi6v+X9/M16H0PqsOFpBm9fLk7lCuj8QIXuhXqoRyy8ZbF8RG3VgVRaKvk4vOMCYNlm5r/Qa0NT5nnzH+iwfe+ivgeLUy0++gBtALVTeScTDmyCxYuW64tJoN+oAAAAA==" + }, + "Metadata": { + "aws:cdk:path": "Stack1/CDKMetadata/Default" + } + } + }, + "Outputs": { + "BucketArn": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + } + }, + "BucketDomain": { + "Value": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "RegionalDomainName" + ] + } + }, + "Name": { + "Value": { + "Ref": "AWS::StackName" + } + }, + "Joined": { + "Value": { + "Fn::Join": [ + "-", + [ + { + "Ref": "MyBucketF68F3FF0" + }, + { + "Ref": "AWS::Region" + } + ] + ] + } + }, + "SecretValue": { + "Value": { + "Ref": "Secret" + } + } + }, + "Parameters": { + "Secret": { + "Type": "String", + "Default": "template-default" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1Nested36ADE314.nested.template.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1Nested36ADE314.nested.template.json new file mode 100644 index 000000000..b1f1dd5b5 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/Stack1Nested36ADE314.nested.template.json @@ -0,0 +1,30 @@ +{ + "Resources": { + "NestedBucketName7CD077DC": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Ref": "referencetoStack1MyBucket3E4D6F6CRef" + } + }, + "Metadata": { + "aws:cdk:path": "Stack1/Nested/NestedBucketName/Resource" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/z3HQQqDMBAAwLd4T9aQFnyAdyn6ANmaVVZNhOyqB/HvPRQ8DePBVy9wBZ5ih7DYlb9wNSRKoVMcFoOn9CIRrk4zp+mDGSMpZVOP6cl9m7QFglnKw7/BO3DFLMw270k5ErR/f0wpXLduAAAA" + }, + "Metadata": { + "aws:cdk:path": "Stack1/Nested/CDKMetadata/Default" + } + } + }, + "Parameters": { + "referencetoStack1MyBucket3E4D6F6CRef": { + "Type": "String" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/manifest.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/manifest.json new file mode 100644 index 000000000..eea1e5d64 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/manifest.json @@ -0,0 +1,512 @@ +{ + "version": "54.0.0", + "artifacts": { + "Stack1.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "Stack1.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "Stack1": { + "type": "aws:cloudformation:stack", + "environment": "aws://123456789012/us-east-1", + "properties": { + "templateFile": "Stack1.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-east-1", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-east-1", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-123456789012-us-east-1/91b722a85f337ba12178b933113aa50e118a4bb71559f94ab1a70ff2f01aef35.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "Stack1.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-lookup-role-123456789012-us-east-1", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "Stack1.assets" + ], + "additionalMetadataFile": "Stack1.metadata.json", + "displayName": "Stack1" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-lib/feature-flag-report": { + "type": "cdk:feature-flag-report", + "properties": { + "module": "aws-cdk-lib", + "flags": { + "@aws-cdk/aws-signer:signingProfileNamePassedToCfn": { + "recommendedValue": true, + "explanation": "Pass signingProfileName to CfnSigningProfile" + }, + "@aws-cdk/core:newStyleStackSynthesis": { + "recommendedValue": true, + "explanation": "Switch to new stack synthesis method which enables CI/CD", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:stackRelativeExports": { + "recommendedValue": true, + "explanation": "Name exports based on the construct paths relative to the stack, rather than the global construct path", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": { + "recommendedValue": true, + "explanation": "Disable implicit openListener when custom security groups are provided" + }, + "@aws-cdk/aws-rds:lowercaseDbIdentifier": { + "recommendedValue": true, + "explanation": "Force lowercasing of RDS Cluster names in CDK", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": { + "recommendedValue": true, + "explanation": "Allow adding/removing multiple UsagePlanKeys independently", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-lambda:recognizeVersionProps": { + "recommendedValue": true, + "explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-lambda:recognizeLayerVersion": { + "recommendedValue": true, + "explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`." + }, + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": { + "recommendedValue": true, + "explanation": "Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:checkSecretUsage": { + "recommendedValue": true, + "explanation": "Enable this flag to make it impossible to accidentally use SecretValues in unsafe locations" + }, + "@aws-cdk/core:target-partitions": { + "recommendedValue": [ + "aws", + "aws-cn" + ], + "explanation": "What regions to include in lookup tables of environment agnostic stacks" + }, + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": { + "recommendedValue": true, + "explanation": "ECS extensions will automatically add an `awslogs` driver if no logging is specified" + }, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": { + "recommendedValue": true, + "explanation": "Enable this feature flag to have Launch Templates generated by the `InstanceRequireImdsv2Aspect` use unique names." + }, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": { + "recommendedValue": true, + "explanation": "ARN format used by ECS. In the new ARN format, the cluster name is part of the resource ID." + }, + "@aws-cdk/aws-iam:minimizePolicies": { + "recommendedValue": true, + "explanation": "Minimize IAM policies by combining Statements" + }, + "@aws-cdk/core:validateSnapshotRemovalPolicy": { + "recommendedValue": true, + "explanation": "Error on snapshot removal policies on resources that do not support it." + }, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": { + "recommendedValue": true, + "explanation": "Generate key aliases that include the stack name" + }, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": { + "recommendedValue": true, + "explanation": "Enable this feature flag to create an S3 bucket policy by default in cases where an AWS service would automatically create the Policy if one does not exist." + }, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": { + "recommendedValue": true, + "explanation": "Restrict KMS key policy for encrypted Queues a bit more" + }, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": { + "recommendedValue": true, + "explanation": "Make default CloudWatch Role behavior safe for multiple API Gateways in one environment" + }, + "@aws-cdk/core:enablePartitionLiterals": { + "recommendedValue": true, + "explanation": "Make ARNs concrete if AWS partition is known" + }, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": { + "recommendedValue": true, + "explanation": "Event Rules may only push to encrypted SQS queues in the same account" + }, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": { + "recommendedValue": true, + "explanation": "Avoid setting the \"ECS\" deployment controller when adding a circuit breaker" + }, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": { + "recommendedValue": true, + "explanation": "Enable this feature to create default policy names for imported roles that depend on the stack the role is in." + }, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": { + "recommendedValue": true, + "explanation": "Use S3 Bucket Policy instead of ACLs for Server Access Logging" + }, + "@aws-cdk/aws-route53-patters:useCertificate": { + "recommendedValue": true, + "explanation": "Use the official `Certificate` resource instead of `DnsValidatedCertificate`" + }, + "@aws-cdk/customresources:installLatestAwsSdkDefault": { + "recommendedValue": false, + "explanation": "Whether to install the latest SDK by default in AwsCustomResource" + }, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": { + "recommendedValue": true, + "explanation": "Use unique resource name for Database Proxy" + }, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": { + "recommendedValue": true, + "explanation": "Remove CloudWatch alarms from deployment group" + }, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": { + "recommendedValue": true, + "explanation": "Include authorizer configuration in the calculation of the API deployment logical ID." + }, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": { + "recommendedValue": true, + "explanation": "Define user data for a launch template by default when a machine image is provided." + }, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": { + "recommendedValue": true, + "explanation": "SecretTargetAttachments uses the ResourcePolicy of the attached Secret." + }, + "@aws-cdk/aws-redshift:columnId": { + "recommendedValue": true, + "explanation": "Whether to use an ID to track Redshift column changes" + }, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": { + "recommendedValue": true, + "explanation": "Enable AmazonEMRServicePolicy_v2 managed policies" + }, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": { + "recommendedValue": true, + "explanation": "Restrict access to the VPC default security group" + }, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": { + "recommendedValue": true, + "explanation": "Generate a unique id for each RequestValidator added to a method" + }, + "@aws-cdk/aws-kms:aliasNameRef": { + "recommendedValue": true, + "explanation": "KMS Alias name and keyArn will have implicit reference to KMS Key" + }, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": { + "recommendedValue": true, + "explanation": "Enable grant methods on Aliases imported by name to use kms:ResourceAliases condition" + }, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": { + "recommendedValue": true, + "explanation": "Generate a launch template when creating an AutoScalingGroup" + }, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": { + "recommendedValue": true, + "explanation": "Include the stack prefix in the stack name generation process" + }, + "@aws-cdk/aws-efs:denyAnonymousAccess": { + "recommendedValue": true, + "explanation": "EFS denies anonymous clients accesses" + }, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": { + "recommendedValue": true, + "explanation": "Enables support for Multi-AZ with Standby deployment for opensearch domains" + }, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": { + "recommendedValue": true, + "explanation": "Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default" + }, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": { + "recommendedValue": true, + "explanation": "When enabled, mount targets will have a stable logicalId that is linked to the associated subnet." + }, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": { + "recommendedValue": true, + "explanation": "When enabled, a scope of InstanceParameterGroup for AuroraClusterInstance with each parameters will change." + }, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": { + "recommendedValue": true, + "explanation": "When enabled, will always use the arn for identifiers for CfnSourceApiAssociation in the GraphqlApi construct rather than id." + }, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": { + "recommendedValue": true, + "explanation": "When enabled, creating an RDS database cluster from a snapshot will only render credentials for snapshot credentials." + }, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": { + "recommendedValue": true, + "explanation": "When enabled, the CodeCommit source action is using the default branch name 'main'." + }, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": { + "recommendedValue": true, + "explanation": "When enabled, the logical ID of a Lambda permission for a Lambda action includes an alarm ID." + }, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": { + "recommendedValue": true, + "explanation": "Enables Pipeline to set the default value for crossAccountKeys to false." + }, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": { + "recommendedValue": true, + "explanation": "Enables Pipeline to set the default pipeline type to V2." + }, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": { + "recommendedValue": true, + "explanation": "When enabled, IAM Policy created from KMS key grant will reduce the resource scope to this key only." + }, + "@aws-cdk/pipelines:reduceAssetRoleTrustScope": { + "recommendedValue": true, + "explanation": "Remove the root account principal from PipelineAssetsFileRole trust policy", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-eks:nodegroupNameAttribute": { + "recommendedValue": true, + "explanation": "When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix." + }, + "@aws-cdk/aws-eks:useNativeOidcProvider": { + "recommendedValue": true, + "explanation": "When enabled, EKS V2 clusters will use the native OIDC provider resource AWS::IAM::OIDCProvider instead of creating the OIDCProvider with a custom resource (iam.OpenIDConnectProvider)." + }, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": { + "recommendedValue": true, + "explanation": "When enabled, the default volume type of the EBS volume will be GP3" + }, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": { + "recommendedValue": true, + "explanation": "When enabled, remove default deployment alarm settings" + }, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": { + "recommendedValue": false, + "explanation": "When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default" + }, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": { + "recommendedValue": false, + "explanation": "When enabled, Adding notifications to a bucket in the current stack will not remove notification from imported stack." + }, + "@aws-cdk/aws-stepfunctions-tasks:useNewS3UriParametersForBedrockInvokeModelTask": { + "recommendedValue": true, + "explanation": "When enabled, use new props for S3 URI field in task definition of state machine for bedrock invoke model.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/core:explicitStackTags": { + "recommendedValue": true, + "explanation": "When enabled, stack tags need to be assigned explicitly on a Stack." + }, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": { + "recommendedValue": true, + "explanation": "When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration" + }, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": { + "recommendedValue": true, + "explanation": "When enabled will allow you to specify a resource policy per replica, and not copy the source table policy to all replicas" + }, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": { + "recommendedValue": true, + "explanation": "When enabled, initOptions.timeout and resourceSignalTimeout values will be summed together." + }, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": { + "recommendedValue": true, + "explanation": "When enabled, a Lambda authorizer Permission created when using GraphqlApi will be properly scoped with a SourceArn." + }, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": { + "recommendedValue": true, + "explanation": "When enabled, the value of property `instanceResourceId` in construct `DatabaseInstanceReadReplica` will be set to the correct value which is `DbiResourceId` instead of currently `DbInstanceArn`" + }, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": { + "recommendedValue": true, + "explanation": "When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values." + }, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": { + "recommendedValue": true, + "explanation": "When enabled, both `@aws-sdk` and `@smithy` packages will be excluded from the Lambda Node.js 18.x runtime to prevent version mismatches in bundled applications." + }, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": { + "recommendedValue": true, + "explanation": "When enabled, the resource of IAM Run Ecs policy generated by SFN EcsRunTask will reference the definition, instead of constructing ARN." + }, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": { + "recommendedValue": true, + "explanation": "When enabled, the BastionHost construct will use the latest Amazon Linux 2023 AMI, instead of Amazon Linux 2." + }, + "@aws-cdk/core:aspectStabilization": { + "recommendedValue": true, + "explanation": "When enabled, a stabilization loop will be run when invoking Aspects during synthesis.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": { + "recommendedValue": true, + "explanation": "When enabled, use a new method for DNS Name of user pool domain target without creating a custom resource." + }, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": { + "recommendedValue": true, + "explanation": "When enabled, the default security group ingress rules will allow IPv6 ingress from anywhere" + }, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": { + "recommendedValue": true, + "explanation": "When enabled, the default behaviour of OIDC provider will reject unauthorized connections" + }, + "@aws-cdk/core:enableAdditionalMetadataCollection": { + "recommendedValue": true, + "explanation": "When enabled, CDK will expand the scope of usage data collected to better inform CDK development and improve communication for security concerns and emerging issues." + }, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": { + "recommendedValue": false, + "explanation": "[Deprecated] When enabled, Lambda will create new inline policies with AddToRolePolicy instead of adding to the Default Policy Statement" + }, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": { + "recommendedValue": true, + "explanation": "When enabled, CDK will automatically generate a unique role name that is used for s3 object replication." + }, + "@aws-cdk/pipelines:reduceStageRoleTrustScope": { + "recommendedValue": true, + "explanation": "Remove the root account principal from Stage addActions trust policy", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-events:requireEventBusPolicySid": { + "recommendedValue": true, + "explanation": "When enabled, grantPutEventsTo() will use resource policies with Statement IDs for service principals." + }, + "@aws-cdk/core:aspectPrioritiesMutating": { + "recommendedValue": true, + "explanation": "When set to true, Aspects added by the construct library on your behalf will be given a priority of MUTATING." + }, + "@aws-cdk/aws-dynamodb:retainTableReplica": { + "recommendedValue": true, + "explanation": "When enabled, table replica will be default to the removal policy of source table unless specified otherwise." + }, + "@aws-cdk/cognito:logUserPoolClientSecretValue": { + "recommendedValue": false, + "explanation": "When disabled, the value of the user pool client secret will not be logged in the custom resource lambda function logs." + }, + "@aws-cdk/pipelines:reduceCrossAccountActionRoleTrustScope": { + "recommendedValue": true, + "explanation": "When enabled, scopes down the trust policy for the cross-account action role", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": { + "recommendedValue": true, + "explanation": "When enabled, the resultWriterV2 property of DistributedMap will be used insted of resultWriter" + }, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": { + "recommendedValue": true, + "explanation": "Add an S3 trust policy to a KMS key resource policy for SNS subscriptions." + }, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": { + "recommendedValue": true, + "explanation": "When enabled, the EgressOnlyGateway resource is only created if private subnets are defined in the dual-stack VPC." + }, + "@aws-cdk/aws-ec2-alpha:useResourceIdForVpcV2Migration": { + "recommendedValue": false, + "explanation": "When enabled, use resource IDs for VPC V2 migration" + }, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": { + "recommendedValue": true, + "explanation": "When enabled, setting any combination of options for BlockPublicAccess will automatically set true for any options not defined." + }, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": { + "recommendedValue": true, + "explanation": "When enabled, CDK creates and manages loggroup for the lambda function" + }, + "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": { + "recommendedValue": true, + "explanation": "When enabled, Network Load Balancer will be created with a security group by default." + }, + "@aws-cdk/aws-stepfunctions-tasks:httpInvokeDynamicJsonPathEndpoint": { + "recommendedValue": true, + "explanation": "When enabled, allows using a dynamic apiEndpoint with JSONPath format in HttpInvoke tasks.", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": { + "recommendedValue": true, + "explanation": "When enabled, ECS patterns will generate unique target group IDs to prevent conflicts during load balancer replacement" + }, + "@aws-cdk/aws-route53-patterns:useDistribution": { + "recommendedValue": true, + "explanation": "Use the `Distribution` resource instead of `CloudFrontWebDistribution`" + }, + "@aws-cdk/aws-cloudfront:defaultFunctionRuntimeV2_0": { + "recommendedValue": true, + "explanation": "Use cloudfront-js-2.0 as the default runtime for CloudFront Functions" + }, + "@aws-cdk/aws-elasticloadbalancingv2:usePostQuantumTlsPolicy": { + "recommendedValue": true, + "explanation": "When enabled, HTTPS/TLS listeners use post-quantum TLS policy by default" + }, + "@aws-cdk/core:automaticL1Traits": { + "recommendedValue": true, + "explanation": "Automatically use the default L1 traits for L1 constructs`", + "unconfiguredBehavesLike": { + "v2": true + } + }, + "@aws-cdk/aws-batch:defaultToAL2023": { + "recommendedValue": true, + "explanation": "Use AL2023 as the default imageType for EC2 Batch compute environments instead of the deprecated AL2" + }, + "@aws-cdk/aws-eks:defaultToAL2023": { + "recommendedValue": true, + "explanation": "Use AL2023 as the default AMI type for EKS managed node groups using non-GPU instance types instead of the deprecated AL2" + }, + "@aws-cdk/core:annotationsInValidationReport": { + "recommendedValue": true, + "explanation": "Include construct annotations (warnings and errors) in the policy validation report" + }, + "@aws-cdk/core:defaultCrossStackReferences": { + "recommendedValue": "weak", + "explanation": "Controls whether cross-stack references are strong, weak, or both", + "unconfiguredBehavesLike": { + "v2": "strong" + } + }, + "@aws-cdk/core:validateAgainstDefaultRules": { + "recommendedValue": true, + "explanation": "Treat CloudFormation Validate findings as errors" + }, + "@aws-cdk/aws-ecs:removeEmptyLoadBalancers": { + "recommendedValue": true, + "explanation": "Render an empty `LoadBalancers` array on an ECS service that has no target groups" + }, + "@aws-cdk/aws-cloudfront:defaultViewerProtocolPolicyRedirectToHttps": { + "recommendedValue": true, + "explanation": "Default Distribution cache behaviors to redirect HTTP viewer requests to HTTPS" + } + } + } + } + }, + "minimumCliVersion": "2.1143.0" +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/tree.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/tree.json new file mode 100644 index 000000000..168885b55 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/cdk.out/tree.json @@ -0,0 +1 @@ +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.273.0"},"children":{"Stack1":{"id":"Stack1","path":"Stack1","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.273.0"},"children":{"MyBucket":{"id":"MyBucket","path":"Stack1/MyBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.Bucket","version":"2.273.0"},"children":{"Resource":{"id":"Resource","path":"Stack1/MyBucket/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.CfnBucket","version":"2.273.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::S3::Bucket","aws:cdk:cloudformation:logicalId":"MyBucketF68F3FF0","aws:cdk:cloudformation:props":{}}}}},"BucketName":{"id":"BucketName","path":"Stack1/BucketName","constructInfo":{"fqn":"aws-cdk-lib.aws_ssm.StringParameter","version":"2.273.0"},"children":{"Resource":{"id":"Resource","path":"Stack1/BucketName/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ssm.CfnParameter","version":"2.273.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SSM::Parameter","aws:cdk:cloudformation:logicalId":"BucketName742CD8B0","aws:cdk:cloudformation:props":{"type":"String","value":{"Ref":"MyBucketF68F3FF0"}}}}}},"BucketArn":{"id":"BucketArn","path":"Stack1/BucketArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.273.0"}},"BucketDomain":{"id":"BucketDomain","path":"Stack1/BucketDomain","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.273.0"}},"Name":{"id":"Name","path":"Stack1/Name","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.273.0"}},"Joined":{"id":"Joined","path":"Stack1/Joined","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.273.0"}},"Secret":{"id":"Secret","path":"Stack1/Secret","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.273.0"}},"SecretValue":{"id":"SecretValue","path":"Stack1/SecretValue","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.273.0"}},"Nested":{"id":"Nested","path":"Stack1/Nested","constructInfo":{"fqn":"aws-cdk-lib.NestedStack","version":"2.273.0"},"children":{"NestedBucketName":{"id":"NestedBucketName","path":"Stack1/Nested/NestedBucketName","constructInfo":{"fqn":"aws-cdk-lib.aws_ssm.StringParameter","version":"2.273.0"},"children":{"Resource":{"id":"Resource","path":"Stack1/Nested/NestedBucketName/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ssm.CfnParameter","version":"2.273.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SSM::Parameter","aws:cdk:cloudformation:logicalId":"NestedBucketName7CD077DC","aws:cdk:cloudformation:props":{"type":"String","value":{"Ref":"referencetoStack1MyBucket3E4D6F6CRef"}}}}}},"CDKMetadata":{"id":"CDKMetadata","path":"Stack1/Nested/CDKMetadata","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Default":{"id":"Default","path":"Stack1/Nested/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.273.0"}}}},"reference-to-Stack1MyBucket3E4D6F6CRef":{"id":"reference-to-Stack1MyBucket3E4D6F6CRef","path":"Stack1/Nested/reference-to-Stack1MyBucket3E4D6F6CRef","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.273.0"}}}},"Nested.NestedStack":{"id":"Nested.NestedStack","path":"Stack1/Nested.NestedStack","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Nested.NestedStackResource":{"id":"Nested.NestedStackResource","path":"Stack1/Nested.NestedStack/Nested.NestedStackResource","constructInfo":{"fqn":"aws-cdk-lib.CfnStack","version":"2.273.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::CloudFormation::Stack","aws:cdk:cloudformation:logicalId":"NestedNestedStackNestedNestedStackResourceDEFDAA4D","aws:cdk:cloudformation:props":{"parameters":{"referencetoStack1MyBucket3E4D6F6CRef":{"Ref":"MyBucketF68F3FF0"}},"templateUrl":{"Fn::Join":["",["https://s3.us-east-1.",{"Ref":"AWS::URLSuffix"},"/cdk-hnb659fds-assets-123456789012-us-east-1/79bf57b1b3bbe8f179f2b75327d6f95f059a31631484c17e6ee3aaceea8a0264.json"]]}}}}}},"CDKMetadata":{"id":"CDKMetadata","path":"Stack1/CDKMetadata","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Default":{"id":"Default","path":"Stack1/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.273.0"}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"Stack1/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.273.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"Stack1/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.273.0"}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/index.ts b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/index.ts new file mode 100644 index 000000000..247ae9756 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-references/index.ts @@ -0,0 +1,32 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import * as core from 'aws-cdk-lib/core'; + +export default async () => { + const app = new core.App({ + autoSynth: false, + // keep local paths out of the checked-in metadata + context: { 'aws:cdk:disable-creation-stack-traces': true }, + }); + const stack = new core.Stack(app, 'Stack1', { + env: { + account: '123456789012', + region: 'us-east-1', + }, + }); + const bucket = new s3.Bucket(stack, 'MyBucket'); + // Ref to another resource + new ssm.StringParameter(stack, 'BucketName', { stringValue: bucket.bucketName }); + // Fn::GetAtt the evaluator formats itself, one it has to look up, a pseudo parameter and a Fn::Join + new core.CfnOutput(stack, 'BucketArn', { value: bucket.bucketArn }); + new core.CfnOutput(stack, 'BucketDomain', { value: bucket.bucketRegionalDomainName }); + new core.CfnOutput(stack, 'Name', { value: core.Aws.STACK_NAME }); + new core.CfnOutput(stack, 'Joined', { value: core.Fn.join('-', [bucket.bucketName, core.Aws.REGION]) }); + // A parameter, to test parameters that are NoEcho in the deployed stack only + const secret = new core.CfnParameter(stack, 'Secret', { default: 'template-default' }); + new core.CfnOutput(stack, 'SecretValue', { value: secret.valueAsString }); + // A nested stack that gets the bucket name as a parameter + const nested = new core.NestedStack(stack, 'Nested'); + new ssm.StringParameter(nested, 'NestedBucketName', { stringValue: bucket.bucketName }); + return app.synth(); +}; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts new file mode 100644 index 000000000..f5284977a --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts @@ -0,0 +1,277 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { GetResourceCommand } from '@aws-sdk/client-cloudcontrol'; +import { DescribeStacksCommand, GetTemplateCommand, ListStackResourcesCommand, StackStatus } from '@aws-sdk/client-cloudformation'; +import { StackSelectionStrategy } from '../../lib/api/cloud-assembly'; +import { Toolkit } from '../../lib/toolkit'; +import { cdkOutFixture, TestIoHost } from '../_helpers'; +import { + mockCloudControlClient, + mockCloudFormationClient, + mockSdkProvider, + restoreSdkMocksToDefault, + setDefaultSTSMocks, +} from '../_helpers/mock-sdk'; + +const FIXTURE = 'stack-with-references'; +const template = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '_fixtures', FIXTURE, 'cdk.out', 'Stack1.template.json'), 'utf-8')); +const nestedTemplate = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '_fixtures', FIXTURE, 'cdk.out', 'Stack1Nested36ADE314.nested.template.json'), 'utf-8')); +const STACK_ID = 'arn:aws:cloudformation:us-east-1:123456789012:stack/Stack1/f1b0a1b0-0000-0000-0000-000000000000'; +const NESTED_LOGICAL_ID = 'NestedNestedStackNestedNestedStackResourceDEFDAA4D'; +const NESTED_STACK_NAME = 'Stack1-NestedNestedStack-ABC123'; +const NESTED_STACK_ID = `arn:aws:cloudformation:us-east-1:123456789012:stack/${NESTED_STACK_NAME}/f1b0a1b0-0000-0000-0000-000000000001`; + +let ioHost: TestIoHost; +let toolkit: Toolkit; + +beforeEach(() => { + jest.restoreAllMocks(); + restoreSdkMocksToDefault(); + setDefaultSTSMocks(); + mockSdkProvider(); + ioHost = new TestIoHost('info', true); + toolkit = new Toolkit({ ioHost, unstableFeatures: ['evaluate'] }); +}); + +function givenDeployedStack() { + mockCloudFormationClient.on(DescribeStacksCommand).resolves({ + Stacks: [{ + StackName: 'Stack1', + StackId: STACK_ID, + StackStatus: StackStatus.CREATE_COMPLETE, + CreationTime: new Date(), + Parameters: [{ ParameterKey: 'BootstrapVersion', ParameterValue: '/cdk-bootstrap/hnb659fds/version', ResolvedValue: '30' }], + }], + }); + mockCloudFormationClient.on(DescribeStacksCommand, { StackName: NESTED_STACK_NAME }).resolves({ + Stacks: [{ StackName: NESTED_STACK_NAME, StackId: NESTED_STACK_ID, StackStatus: StackStatus.CREATE_COMPLETE, CreationTime: new Date() }], + }); + mockCloudFormationClient.on(GetTemplateCommand).resolves({ TemplateBody: JSON.stringify(template) }); + mockCloudFormationClient.on(GetTemplateCommand, { StackName: NESTED_STACK_NAME }).resolves({ TemplateBody: JSON.stringify(nestedTemplate) }); + mockCloudFormationClient.on(ListStackResourcesCommand, { StackName: NESTED_STACK_NAME }).resolves({ + StackResourceSummaries: [{ + LogicalResourceId: 'NestedBucketName7CD077DC', + PhysicalResourceId: 'CFN-NestedBucketName7CD077DC-def', + ResourceType: 'AWS::SSM::Parameter', + ResourceStatus: 'CREATE_COMPLETE', + LastUpdatedTimestamp: new Date(), + }], + }); + mockCloudFormationClient.on(ListStackResourcesCommand, { StackName: 'Stack1' }).resolves({ + StackResourceSummaries: [ + { + LogicalResourceId: NESTED_LOGICAL_ID, + PhysicalResourceId: NESTED_STACK_ID, + ResourceType: 'AWS::CloudFormation::Stack', + ResourceStatus: 'CREATE_COMPLETE', + LastUpdatedTimestamp: new Date(), + }, + { + LogicalResourceId: 'MyBucketF68F3FF0', + PhysicalResourceId: 'stack1-mybucket-1a2b3c', + ResourceType: 'AWS::S3::Bucket', + ResourceStatus: 'CREATE_COMPLETE', + LastUpdatedTimestamp: new Date(), + }, + { + LogicalResourceId: 'BucketName742CD8B0', + PhysicalResourceId: 'CFN-BucketName742CD8B0-abc', + ResourceType: 'AWS::SSM::Parameter', + ResourceStatus: 'CREATE_COMPLETE', + LastUpdatedTimestamp: new Date(), + }, + ], + }); +} + +describe('evaluate', () => { + test('requires the evaluate unstable feature', async () => { + const stableToolkit = new Toolkit({ ioHost }); + const cx = await cdkOutFixture(stableToolkit, FIXTURE); + + await expect(stableToolkit.evaluate(cx)).rejects.toThrow(/Unstable feature 'evaluate' is not enabled/); + }); + + test('replaces references, attributes and pseudo parameters with their deployed values', async () => { + // GIVEN + givenDeployedStack(); + mockCloudControlClient.on(GetResourceCommand).rejects(Object.assign(new Error('Resource not found'), { name: 'ResourceNotFoundException' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + expect(result.stacks).toHaveLength(1); + const stack = result.stacks[0]; + expect(stack.stackName).toEqual('Stack1'); + expect(stack.template.Resources.BucketName742CD8B0.Properties.Value).toEqual('stack1-mybucket-1a2b3c'); + expect(stack.template.Outputs.BucketArn.Value).toEqual('arn:aws:s3:::stack1-mybucket-1a2b3c'); + expect(stack.template.Outputs.Name.Value).toEqual('Stack1'); + expect(stack.template.Outputs.Joined.Value).toEqual('stack1-mybucket-1a2b3c-us-east-1'); + + expect(result.notDeployed).toEqual([]); + + // the attribute that needs a Cloud Control lookup is kept and reported + expect(stack.template.Outputs.BucketDomain.Value).toEqual({ 'Fn::GetAtt': ['MyBucketF68F3FF0', 'RegionalDomainName'] }); + expect(stack.unresolved).toEqual([expect.objectContaining({ + path: 'Outputs.BucketDomain.Value', + intrinsic: 'Fn::GetAtt', + })]); + }); + + test('looks up attributes through Cloud Control, and leaves the other template sections as they are', async () => { + // GIVEN + givenDeployedStack(); + mockCloudControlClient.on(GetResourceCommand).resolves({ + ResourceDescription: { Properties: JSON.stringify({ RegionalDomainName: 'stack1-mybucket-1a2b3c.s3.us-east-1.amazonaws.com' }) }, + }); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + const stack = result.stacks[0]; + expect(stack.unresolved).toEqual([]); + expect(stack.template.Outputs.BucketDomain.Value).toEqual('stack1-mybucket-1a2b3c.s3.us-east-1.amazonaws.com'); + expect(stack.template.Rules.CheckBootstrapVersion).toEqual(template.Rules.CheckBootstrapVersion); + }); + + test('evaluates nested stacks with the parameter values their parent passes', async () => { + // GIVEN + givenDeployedStack(); + mockCloudControlClient.on(GetResourceCommand).rejects(Object.assign(new Error('Resource not found'), { name: 'ResourceNotFoundException' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + const nested = result.stacks[0].nestedStacks?.[NESTED_LOGICAL_ID]; + expect(nested).toEqual(expect.objectContaining({ + stackName: NESTED_STACK_NAME, + hierarchicalId: `Stack1/${NESTED_LOGICAL_ID}`, + unresolved: [], + })); + expect(nested?.template.Resources.NestedBucketName7CD077DC.Properties.Value).toEqual('stack1-mybucket-1a2b3c'); + }); + + test('reports expressions in nested stacks that use a parameter value the parent could not evaluate', async () => { + // GIVEN: the bucket is missing from the parent's resources, so its Ref can't be evaluated + givenDeployedStack(); + mockCloudFormationClient.on(ListStackResourcesCommand, { StackName: 'Stack1' }).resolves({ + StackResourceSummaries: [{ + LogicalResourceId: NESTED_LOGICAL_ID, + PhysicalResourceId: NESTED_STACK_ID, + ResourceType: 'AWS::CloudFormation::Stack', + ResourceStatus: 'CREATE_COMPLETE', + LastUpdatedTimestamp: new Date(), + }], + }); + mockCloudControlClient.on(GetResourceCommand).rejects(Object.assign(new Error('Resource not found'), { name: 'ResourceNotFoundException' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + const nested = result.stacks[0].nestedStacks?.[NESTED_LOGICAL_ID]; + expect(nested?.template.Resources.NestedBucketName7CD077DC.Properties.Value).toEqual({ Ref: 'referencetoStack1MyBucket3E4D6F6CRef' }); + expect(nested?.unresolved).toEqual([{ + path: 'Resources.NestedBucketName7CD077DC.Properties.Value', + intrinsic: 'Ref', + reason: "references parameter 'referencetoStack1MyBucket3E4D6F6CRef': the value passed by stack 'Stack1' could not be evaluated", + }]); + }); + + test('fails, rather than reporting expressions as unresolved, when AWS calls fail', async () => { + // GIVEN + givenDeployedStack(); + mockCloudControlClient.on(GetResourceCommand).rejects(Object.assign(new Error('Rate exceeded'), { name: 'ThrottlingException' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + + // THEN + await expect(toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } })) + .rejects.toThrow(/Could not find 'RegionalDomainName' attribute/); + }); + + test('skips stacks that are not deployed, and lists them', async () => { + // GIVEN + mockCloudFormationClient.on(DescribeStacksCommand).rejects(Object.assign(new Error('Stack with id Stack1 does not exist'), { name: 'ValidationError' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + expect(result).toEqual({ stacks: [], notDeployed: ['Stack1'] }); + ioHost.expectMessage({ level: 'warn', containing: "Stack 'Stack1' is not deployed in aws://123456789012/us-east-1, skipping it" }); + }); + + test.each(['REVIEW_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_COMPLETE', 'DELETE_IN_PROGRESS', 'CREATE_FAILED'])( + 'skips a stack that is %s, and lists it as not deployed', async (status) => { + // GIVEN + givenDeployedStack(); + mockCloudFormationClient.on(DescribeStacksCommand, { StackName: 'Stack1' }).resolves({ + Stacks: [{ StackName: 'Stack1', StackId: STACK_ID, StackStatus: status as StackStatus, CreationTime: new Date() }], + }); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + expect(result).toEqual({ stacks: [], notDeployed: ['Stack1'] }); + ioHost.expectMessage({ level: 'warn', containing: `Stack 'Stack1' is ${status} in aws://123456789012/us-east-1, skipping it` }); + }); + + test.each([true, 'True'])('treats a parameter that is NoEcho (%p) only in the deployed template as unavailable', async (noEcho) => { + // GIVEN: DescribeStacks masks the value, and the deployed template says NoEcho + givenDeployedStack(); + mockCloudFormationClient.on(DescribeStacksCommand, { StackName: 'Stack1' }).resolves({ + Stacks: [{ + StackName: 'Stack1', + StackId: STACK_ID, + StackStatus: StackStatus.CREATE_COMPLETE, + CreationTime: new Date(), + Parameters: [{ ParameterKey: 'Secret', ParameterValue: '****' }], + }], + }); + const deployed = { ...template, Parameters: { ...template.Parameters, Secret: { ...template.Parameters.Secret, NoEcho: noEcho } } }; + mockCloudFormationClient.on(GetTemplateCommand, { StackName: 'Stack1' }).resolves({ TemplateBody: JSON.stringify(deployed) }); + mockCloudControlClient.on(GetResourceCommand).rejects(Object.assign(new Error('Resource not found'), { name: 'ResourceNotFoundException' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + const result = await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN: neither the mask nor the template default is used + expect(result.stacks[0].template.Outputs.SecretValue.Value).toEqual({ Ref: 'Secret' }); + expect(result.stacks[0].unresolved).toContainEqual(expect.objectContaining({ path: 'Outputs.SecretValue.Value', intrinsic: 'Ref' })); + }); + + test('warns when the deployed template has different resources than the evaluated one', async () => { + // GIVEN + givenDeployedStack(); + const { BucketName742CD8B0: _removed, ...resources } = template.Resources; + mockCloudFormationClient.on(GetTemplateCommand, { StackName: 'Stack1' }).resolves({ TemplateBody: JSON.stringify({ ...template, Resources: resources }) }); + mockCloudControlClient.on(GetResourceCommand).rejects(Object.assign(new Error('Resource not found'), { name: 'ResourceNotFoundException' })); + + // WHEN + const cx = await cdkOutFixture(toolkit, FIXTURE); + await toolkit.evaluate(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } }); + + // THEN + ioHost.expectMessage({ level: 'warn', containing: "The template of stack 'Stack1' differs from its deployed template" }); + ioHost.expectMessage({ level: 'warn', containing: 'Not deployed: BucketName742CD8B0 (AWS::SSM::Parameter)' }); + }); + + test('rejects a concurrency that is not a positive integer', async () => { + const cx = await cdkOutFixture(toolkit, FIXTURE); + + await expect(toolkit.evaluate(cx, { concurrency: 0 })).rejects.toThrow('concurrency must be a positive integer, got 0'); + }); +}); diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index c9a353074..45bdf58a5 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -212,6 +212,7 @@ export class CdkToolkit { const iPromiseUnstablenessIsCheckedByTheCli: Record = { 'publish-assets': true, 'diagnose': true, + 'evaluate': true, 'flags': true, 'orphan': true, 'refactor': true, From aa49fb7c329884a00a77fc0b6af0992e4c51ba99 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:43:06 -0400 Subject: [PATCH 2/5] docs(toolkit-lib): describe the evaluate action without referring to hotswap --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index b31598871..118de7ee2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -687,7 +687,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { * Evaluate Action * * Evaluates the templates of the selected stacks against their deployed stacks: references, attributes, - * parameters, pseudo parameters and the intrinsic functions supported by the hotswap evaluator are replaced + * parameters, pseudo parameters and the supported intrinsic functions are replaced * by their deployed values. This gives the concrete values CloudFormation used for each resource, for * example to compare them with the live state of the resources. * From bfcd3b2907927141f99bf7be0d61699b20549b03 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:06:45 -0400 Subject: [PATCH 3/5] refactor(toolkit-lib): evaluate action no longer passes strictValues, which is now the default --- .../toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts index 53ba78cab..fe724a345 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts @@ -59,8 +59,7 @@ export async function evaluateStackBestEffort(props: EvaluateStackProps): Promis // pseudo parameters the evaluator does not provide itself parameters: { ...parameters, 'AWS::StackName': stackName, 'AWS::StackId': stackId }, nestedStacks, - // CloudFormation semantics: empty strings are values; unavailable parameters never fall back to their Default - strictValues: true, + // unavailable parameters never fall back to their Default unavailableParameters: Object.keys(unavailableParameters), }); const evaluation = await evaluateTemplateBestEffort(evaluator, template, { unavailableParameters }); From d591fd223ef675ea202ee7da9d864e1d3cb441e7 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:35:39 -0400 Subject: [PATCH 4/5] feat(toolkit-lib): evaluate returns each stack's id and physical resource ids --- .../@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts | 10 ++++++++++ .../lib/actions/evaluate/private/evaluate-stack.ts | 10 ++++++++-- .../@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts | 7 +++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts index 4a7a4d9c6..939bcdbdf 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts @@ -59,6 +59,16 @@ export interface EvaluatedStack { */ readonly hierarchicalId: string; + /** + * The deployed stack's id (its ARN) + */ + readonly stackId: string; + + /** + * The physical id of each deployed resource in the stack, by logical id + */ + readonly physicalResourceIds: { [logicalId: string]: string }; + /** * The stack's template, with every expression in `Resources` and `Outputs` that could be * evaluated against the deployed stack replaced by its value diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts index fe724a345..d8b8ea131 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/private/evaluate-stack.ts @@ -1,6 +1,6 @@ import type { CloudFormationStackArtifact } from '@aws-cdk/cloud-assembly-api'; import type { SDK } from '../../../api/aws-auth/private'; -import type { NestedStackTemplates, SharedLookups, Template } from '../../../api/cloudformation'; +import type { ListStackResources, NestedStackTemplates, SharedLookups, Template } from '../../../api/cloudformation'; import { EvaluateCloudFormationTemplate, evaluateTemplateBestEffort, containsExpression, loadCurrentTemplateWithNestedStacks } from '../../../api/cloudformation'; import { parallelPromises } from '../../../util'; import type { EvaluatedStack } from '../index'; @@ -39,7 +39,7 @@ export async function evaluateStackBestEffort(props: EvaluateStackProps): Promis partition: props.partition, sdk, // shared by all the stacks' evaluators, so each stack is listed at most once - stackResourcesByStackName: new Map(), + stackResourcesByStackName: new Map(), lookups: props.lookups, }; @@ -98,9 +98,15 @@ export async function evaluateStackBestEffort(props: EvaluateStackProps): Promis const ordered = Object.fromEntries(Object.keys(nestedStacks) .filter((id) => id in evaluatedNestedStacks) .map((id) => [id, evaluatedNestedStacks[id]])); + // the evaluator has listed the stack's resources already (or does so now, once) + const resources = await common.stackResourcesByStackName.get(stackName)?.listStackResources() ?? []; return { stackName, hierarchicalId, + stackId, + physicalResourceIds: Object.fromEntries(resources + .filter((r) => r.LogicalResourceId && r.PhysicalResourceId) + .map((r) => [r.LogicalResourceId!, r.PhysicalResourceId!])), template: evaluation.template, unresolved: evaluation.unresolved, ...(Object.keys(ordered).length > 0 ? { nestedStacks: ordered } : {}), diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts index f5284977a..fed13a071 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/evaluate.test.ts @@ -111,6 +111,11 @@ describe('evaluate', () => { expect(stack.template.Outputs.Joined.Value).toEqual('stack1-mybucket-1a2b3c-us-east-1'); expect(result.notDeployed).toEqual([]); + expect(stack.stackId).toEqual(STACK_ID); + expect(stack.physicalResourceIds).toEqual(expect.objectContaining({ + MyBucketF68F3FF0: 'stack1-mybucket-1a2b3c', + BucketName742CD8B0: 'CFN-BucketName742CD8B0-abc', + })); // the attribute that needs a Cloud Control lookup is kept and reported expect(stack.template.Outputs.BucketDomain.Value).toEqual({ 'Fn::GetAtt': ['MyBucketF68F3FF0', 'RegionalDomainName'] }); @@ -152,6 +157,8 @@ describe('evaluate', () => { expect(nested).toEqual(expect.objectContaining({ stackName: NESTED_STACK_NAME, hierarchicalId: `Stack1/${NESTED_LOGICAL_ID}`, + stackId: NESTED_STACK_ID, + physicalResourceIds: { NestedBucketName7CD077DC: 'CFN-NestedBucketName7CD077DC-def' }, unresolved: [], })); expect(nested?.template.Resources.NestedBucketName7CD077DC.Properties.Value).toEqual('stack1-mybucket-1a2b3c'); From 536a6ae862e6c148edd3267f68acb162046e299e Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:03:02 -0400 Subject: [PATCH 5/5] fix(toolkit-lib): evaluate accepts an abort signal, like the other actions --- .../@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts | 7 +++++++ packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts index 939bcdbdf..7c06e9c91 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/evaluate/index.ts @@ -21,6 +21,13 @@ export interface EvaluateOptions { * @default 10 */ readonly concurrency?: number; + + /** + * Cancel the synth operation + * + * @default - No ability to abort + */ + readonly abort?: AbortSignal; } /** diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 118de7ee2..d47d2e878 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -708,7 +708,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { const ioHelper = asIoHelper(this.ioHost, 'evaluate'); const selectStacks = stacksOpt(options); - await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks, options.abort); const stackCollection = await assembly.selectStacks(selectStacks); const envs = new EnvironmentAccess(await this.sdkProvider('evaluate'), options.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME, ioHelper); const artifacts = stackCollection.stackArtifacts;