Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/_generated/integ-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -2542,6 +2542,7 @@
"cc-api-fallback-transitions",
"cloudfront-function-url",
"composite-stack",
"custom-resource-getatt-data",
"custom-resource-provider",
"data-pipeline",
"deep-getatt-chains",
Expand Down Expand Up @@ -2624,6 +2625,9 @@
"l2",
"literal"
],
"custom-resource-getatt-data": [
"l2"
],
"custom-resource-provider": [
"l2"
],
Expand Down Expand Up @@ -3465,6 +3469,7 @@
"context-test",
"cross-region-state-bucket",
"cross-stack-references",
"custom-resource-getatt-data",
"deep-getatt-chains",
"deletion-policy-retain",
"deployment-events",
Expand Down Expand Up @@ -3515,6 +3520,9 @@
"cross-stack-references": [
"l1"
],
"custom-resource-getatt-data": [
"l2"
],
"deep-getatt-chains": [
"l1"
],
Expand Down Expand Up @@ -3727,6 +3735,7 @@
"AWS::ApplicationAutoScaling::ScalableTarget",
"AWS::Athena::NamedQuery",
"AWS::Athena::WorkGroup",
"AWS::CloudFormation::CustomResource",
"AWS::CloudFront::OriginAccessControl",
"AWS::CloudWatch::CompositeAlarm",
"AWS::CodePipeline::Pipeline",
Expand Down
18 changes: 18 additions & 0 deletions docs/_generated/scenario-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
"tag": "custom-resource-async-poll",
"description": "Custom Resource backed by Lambda + cfn-response via S3 pre-signed URL polling."
},
{
"tag": "custom-resource-getatt-data",
"description": "Custom Resource response `Data` consumed via `Fn::GetAtt(CR, 'Data.<key>')` / `Fn::GetAtt(CR, '<key>')` into ANOTHER resource's property (e.g. an SSM Parameter Value) — the fragile CR response-Data attribute path (#756 / #804: CR attributes only exist after the CR Lambda runs). Asserts the dependent's on-AWS value equals the value the CR handler returned, across multiple Data keys + an explicit dependent->CR dependency."
},
{
"tag": "deep-getatt-chain-resolution",
"description": "Long GetAtt chain where each resource POST-CREATE attribute (ARN / generated name only known after the AWS create call) feeds the next resource property, spanning a SDK + CC-API type mix. A wrong / late attribute resolution on either path (SDK `attributes` write or CC-API stored attributes + `constructAttribute` fallback) is pinpointed by the failing link. Critical hop: an unregistered CC-API type (`AWS::CloudWatch::CompositeAlarm`) whose `Arn` feeds downstream SDK-resource properties (issue: deep-getatt-chains fixture)."
Expand Down Expand Up @@ -389,6 +393,13 @@
"multi-stack-getstackoutput"
]
},
{
"name": "custom-resource-getatt-data",
"annotated": true,
"scenarios": [
"custom-resource-getatt-data"
]
},
{
"name": "custom-resource-provider",
"annotated": true,
Expand Down Expand Up @@ -1197,6 +1208,13 @@
"vpc-lambda-cr-race"
]
},
{
"scenario": "custom-resource-getatt-data",
"description": "Custom Resource response `Data` consumed via `Fn::GetAtt(CR, 'Data.<key>')` / `Fn::GetAtt(CR, '<key>')` into ANOTHER resource's property (e.g. an SSM Parameter Value) — the fragile CR response-Data attribute path (#756 / #804: CR attributes only exist after the CR Lambda runs). Asserts the dependent's on-AWS value equals the value the CR handler returned, across multiple Data keys + an explicit dependent->CR dependency.",
"fixtures": [
"custom-resource-getatt-data"
]
},
{
"scenario": "deep-getatt-chain-resolution",
"description": "Long GetAtt chain where each resource POST-CREATE attribute (ARN / generated name only known after the AWS create call) feeds the next resource property, spanning a SDK + CC-API type mix. A wrong / late attribute resolution on either path (SDK `attributes` write or CC-API stored attributes + `constructAttribute` fallback) is pinpointed by the failing link. Critical hop: an unregistered CC-API type (`AWS::CloudWatch::CompositeAlarm`) whose `Arn` feeds downstream SDK-resource properties (issue: deep-getatt-chains fixture).",
Expand Down
1 change: 1 addition & 0 deletions docs/changelog-cdkd.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions docs/integ-coverage.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions docs/scenario-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Run `vp run scenario-coverage` to regenerate.

**58 / 58 canonical scenarios** have at least one integ fixture exercising them. **125 / 144 integ fixtures** carry a `.scenarios.json` sidecar (with 0+ tags); the rest are un-annotated and contributor-reviewed below.
**59 / 59 canonical scenarios** have at least one integ fixture exercising them. **126 / 145 integ fixtures** carry a `.scenarios.json` sidecar (with 0+ tags); the rest are un-annotated and contributor-reviewed below.

## How this is computed

Expand All @@ -26,7 +26,7 @@ This report is a visibility tool, not a commit-time gate. Many cdkd fixtures leg

_None._ Every canonical scenario has at least one integ fixture tagged with it.

## Per-scenario coverage (58 scenarios)
## Per-scenario coverage (59 scenarios)

| Scenario | Description | Integ Fixture(s) |
|---|---|---|
Expand All @@ -38,6 +38,7 @@ _None._ Every canonical scenario has at least one integ fixture tagged with it.
| `conditions-update-semantics` | Harder CloudFormation-Conditions-on-UPDATE semantics beyond the simple flip in `conditions-and-if` (which surfaced #840). A CDK-context phase flip (-c phase=a|b) redeploys the SAME stack in place and asserts: a resource that MOVES gating conditions (IsPhaseA-gated -> condition-false -> DELETED) and its reverse (IsPhaseB-gated absent -> CREATED); `Fn::If` -> `AWS::NoValue` REMOVING a nested property block (SQS RedrivePolicy) on an in-place UPDATE (same physical id, not a replacement); a condition-gated OUTPUT present vs absent in cdkd state outputs; a `DependsOn` to a condition-EXCLUDED resource being dropped (the depender still deploys); and a `Ref` to a condition-excluded resource living inside another condition-excluded resource (both pruned together, no dangling-ref crash). | [`conditions-update-2`](../tests/integration/conditions-update-2/) |
| `cross-cutting-deploy-destroy` | Broad real-AWS regression set (39+ resource VPC+NAT+CF+Lambda+SQS or comparable breadth). Refreshes the integ-broad gate. | [`bench-ccapi`](../tests/integration/bench-ccapi/)<br>[`bench-cdk-sample`](../tests/integration/bench-cdk-sample/)<br>[`bench-sdk`](../tests/integration/bench-sdk/)<br>[`full-stack-demo`](../tests/integration/full-stack-demo/)<br>[`lambda`](../tests/integration/lambda/)<br>[`microservices`](../tests/integration/microservices/)<br>[`multi-resource`](../tests/integration/multi-resource/) |
| `custom-resource-async-poll` | Custom Resource backed by Lambda + cfn-response via S3 pre-signed URL polling. | [`cloudfront-function-url`](../tests/integration/cloudfront-function-url/)<br>[`custom-resource-provider`](../tests/integration/custom-resource-provider/)<br>[`destroy-interrupt`](../tests/integration/destroy-interrupt/)<br>[`vpc-lambda-cr-race`](../tests/integration/vpc-lambda-cr-race/) |
| `custom-resource-getatt-data` | Custom Resource response `Data` consumed via `Fn::GetAtt(CR, 'Data.<key>')` / `Fn::GetAtt(CR, '<key>')` into ANOTHER resource's property (e.g. an SSM Parameter Value) — the fragile CR response-Data attribute path (#756 / #804: CR attributes only exist after the CR Lambda runs). Asserts the dependent's on-AWS value equals the value the CR handler returned, across multiple Data keys + an explicit dependent->CR dependency. | [`custom-resource-getatt-data`](../tests/integration/custom-resource-getatt-data/) |
| `deep-getatt-chain-resolution` | Long GetAtt chain where each resource POST-CREATE attribute (ARN / generated name only known after the AWS create call) feeds the next resource property, spanning a SDK + CC-API type mix. A wrong / late attribute resolution on either path (SDK `attributes` write or CC-API stored attributes + `constructAttribute` fallback) is pinpointed by the failing link. Critical hop: an unregistered CC-API type (`AWS::CloudWatch::CompositeAlarm`) whose `Arn` feeds downstream SDK-resource properties (issue: deep-getatt-chains fixture). | [`deep-getatt-chains`](../tests/integration/deep-getatt-chains/) |
| `deletion-policy-retain` | DeletionPolicy: Retain skip on destroy (schema v5 recorded value wins over template). | [`deletion-policy-retain`](../tests/integration/deletion-policy-retain/) |
| `deployment-events` | Structured deployment events to S3 + `cdkd events` command (issue #808): per-run `deployments/{runId}.jsonl` + `index.json` (separate key family from state.json, no schema bump), events survive `cdkd destroy`, and carry error + metadata ONLY (no resource properties / secrets). | [`deployment-events`](../tests/integration/deployment-events/) |
Expand Down
2 changes: 2 additions & 0 deletions scripts/build-scenario-coverage-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ const KNOWN_SCENARIOS: Record<string, string> = {
'Custom Resource backed by Lambda + cfn-response via S3 pre-signed URL polling.',
'vpc-lambda-cr-race':
'Custom Resource invocation against a VPC Lambda mid-deploy (ENI-attach race window).',
'custom-resource-getatt-data':
"Custom Resource response `Data` consumed via `Fn::GetAtt(CR, 'Data.<key>')` / `Fn::GetAtt(CR, '<key>')` into ANOTHER resource's property (e.g. an SSM Parameter Value) — the fragile CR response-Data attribute path (#756 / #804: CR attributes only exist after the CR Lambda runs). Asserts the dependent's on-AWS value equals the value the CR handler returned, across multiple Data keys + an explicit dependent->CR dependency.",
'destroy-interrupt':
'Graceful SIGINT on destroy (#816 — first Ctrl-C drains in-flight deletes, flushes trimmed state, releases the lock, exits non-zero; no 30m stranded lock) + Custom Resource replay fail-fast on re-run (#804 — the CR delete does NOT stall ~10 minutes invoking GetFunction against the already-deleted backing Lambda; the re-run resumes cleanly and quickly).',

Expand Down
5 changes: 5 additions & 0 deletions tests/integration/custom-resource-getatt-data/.scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"scenarios": [
"custom-resource-getatt-data"
]
}
51 changes: 51 additions & 0 deletions tests/integration/custom-resource-getatt-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Custom Resource `Data` GetAtt -> dependent property (failure-seeking)

Surfaces bugs where a Custom Resource's response `Data` — consumed by
`Fn::GetAtt(CustomResource, '<key>')` (a.k.a. `Data.<key>`) — must flow into
ANOTHER resource's property. The CR response-`Data` attribute path is fragile
(issues [#756](https://github.com/go-to-k/cdkd/issues/756) /
[#804](https://github.com/go-to-k/cdkd/issues/804)) because CR attributes only
exist AFTER the CR's backing Lambda runs and returns a SUCCESS response.

## What it tests

- An inline Lambda-backed `AWS::CloudFormation::CustomResource` whose handler
returns `Data: { ComputedValue, Another, NumericValue }` directly in its
response payload.
- Three `AWS::SSM::Parameter` dependents whose `Value` is
`Fn::GetAtt(MyCustomResource, '<key>')` — one per Data key. Multiple keys
catch a resolver that only wires the first attribute; the stringified-number
key catches a resolver that mishandles non-text Data.
- An explicit `addDependency(cr)` on one parameter, so the DAG ordering
(CR must complete and have its `attributes` populated BEFORE the dependent
is provisioned) is exercised.

No VPC. No Provider framework (the simple synchronous direct-payload-return
path — the cheapest way to surface a GetAtt-of-CR-Data resolution bug).

## Architecture

```
MyCustomResource (inline Lambda; returns Data: {ComputedValue, Another, NumericValue})
|
+--> SSM Parameter .../computed Value = Fn::GetAtt(CR, 'ComputedValue')
+--> SSM Parameter .../another Value = Fn::GetAtt(CR, 'Another')
+--> SSM Parameter .../numeric Value = Fn::GetAtt(CR, 'NumericValue')
```

## verify.sh

Deploys, then reads each SSM parameter back from AWS with
`aws ssm get-parameter` and asserts its `Value` equals the value the CR
handler returned (`computed-integ` / `another-<region>` / `42`). This proves
the CR `Data` attribute resolved THROUGH the intrinsic resolver INTO the
dependent resource's property — a blank / wrong value would otherwise pass
unnoticed because nothing else reads it. Then destroys and asserts the state
file, both SSM parameters, and the backing Lambda are gone.

## Deploy / Destroy

```bash
cdkd deploy CdkdCrGetAttDataExample
cdkd destroy CdkdCrGetAttDataExample
```
11 changes: 11 additions & 0 deletions tests/integration/custom-resource-getatt-data/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CrGetAttDataStack } from '../lib/cr-getatt-data-stack.ts';

const app = new cdk.App();
new CrGetAttDataStack(app, 'CdkdCrGetAttDataExample', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
8 changes: 8 additions & 0 deletions tests/integration/custom-resource-getatt-data/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"app": "node bin/app.ts",
"context": {
"cdkd": {
"stateBucket": "your-cdkd-state-bucket"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ssm from 'aws-cdk-lib/aws-ssm';

/**
* Failure-seeking integ for Custom Resource response `Data` consumed via
* `Fn::GetAtt(CustomResource, '<key>')` (a.k.a. `Data.<key>`) into ANOTHER
* resource's property.
*
* Why this is fragile (issues #756 / #804): a Custom Resource's `Data`
* attributes only exist AFTER its backing Lambda handler runs and returns a
* SUCCESS response carrying `Data: { ... }`. cdkd's `CustomResourceProvider`
* captures that `Data` map into `ResourceCreateResult.attributes`, and the
* intrinsic-function resolver must then make `Fn::GetAtt(<CR>, '<key>')`
* resolve to `attributes['<key>']` so a DEPENDENT resource created later in
* the DAG receives the concrete value. If the resolver returns the wrong /
* empty value, or the DAG runs the dependent BEFORE the CR's attributes are
* populated, the dependent gets a blank / wrong property and the bug is
* silent unless the consuming resource's value is asserted on AWS.
*
* Shape under test:
* MyCustomResource (AWS::CloudFormation::CustomResource, inline Lambda)
* handler returns Data: {
* ComputedValue: "<echo of an input + a fixed marker>",
* Another: "another-<region>",
* NumericValue: "<a stringified number>",
* }
* |
* +--> SSM Parameter "ComputedParam" Value = Fn::GetAtt(CR, 'ComputedValue')
* +--> SSM Parameter "AnotherParam" Value = Fn::GetAtt(CR, 'Another')
* +--> SSM Parameter "NumericParam" Value = Fn::GetAtt(CR, 'NumericValue')
*
* The SSM parameters carry explicit Names so verify.sh can read each one back
* with `aws ssm get-parameter --name <name>` and assert the value equals the
* value the CR's handler returned — proving the CR `Data` attribute resolved
* THROUGH the intrinsic resolver INTO the dependent resource's property.
*
* No VPC. No Provider framework (deliberately the simple synchronous
* direct-payload-return path, which is the cheapest way to surface a
* GetAtt-of-CR-Data resolution bug). Multiple Data keys + a dependent that
* `addDependency`s the CR make this a fan-out regression net.
*/
export class CrGetAttDataStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const namePrefix = `/cdkd-integ/cr-getatt-data/${id}`;

// An inline Lambda that returns `Data` DIRECTLY in its response payload.
// cdkd's CustomResourceProvider parses a direct payload carrying `Data`
// (no ResponseURL round-trip needed for this simple synchronous shape).
// The handler echoes one input property plus fixed markers so the
// assertion proves the value came from THIS handler run, not a constant
// baked into the template.
const handler = new lambda.Function(this, 'CrHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
timeout: cdk.Duration.seconds(30),
code: lambda.Code.fromInline(`
exports.handler = async (event) => {
console.log('CR event:', JSON.stringify(event));
const requestType = event.RequestType;
const props = event.ResourceProperties || {};
// Delete must succeed (return SUCCESS) so destroy is clean.
if (requestType === 'Delete') {
return {
Status: 'SUCCESS',
PhysicalResourceId: event.PhysicalResourceId || 'cr-getatt-data',
};
}
const seed = props.Seed || 'noseed';
const region = props.Region || 'noregion';
// The Data map consumed via Fn::GetAtt by the SSM parameters below.
return {
PhysicalResourceId: 'cr-getatt-data-' + seed,
Data: {
ComputedValue: 'computed-' + seed,
Another: 'another-' + region,
NumericValue: '42',
},
};
};
`),
});

// The Custom Resource. Using new cdk.CustomResource(...) WITHOUT a
// resourceType makes CDK emit AWS::CloudFormation::CustomResource (the
// Lambda-backed type cdkd's CustomResourceProvider drives).
const cr = new cdk.CustomResource(this, 'MyCustomResource', {
serviceToken: handler.functionArn,
properties: {
// A unique-ish seed so re-deploys produce a distinct echoed value.
Seed: 'integ',
Region: this.region,
},
});

// Dependent #1: Value = Fn::GetAtt(CR, 'ComputedValue').
const computedParam = new ssm.StringParameter(this, 'ComputedParam', {
parameterName: `${namePrefix}/computed`,
stringValue: cr.getAttString('ComputedValue'),
});

// Dependent #2: Value = Fn::GetAtt(CR, 'Another') — second Data key, so a
// resolver that only wires the first attribute would fail HERE.
new ssm.StringParameter(this, 'AnotherParam', {
parameterName: `${namePrefix}/another`,
stringValue: cr.getAttString('Another'),
});

// Dependent #3: Value = Fn::GetAtt(CR, 'NumericValue') — a stringified
// number, to catch a resolver that mishandles non-text Data values.
new ssm.StringParameter(this, 'NumericParam', {
parameterName: `${namePrefix}/numeric`,
stringValue: cr.getAttString('NumericValue'),
});

// Make the CR->dependent ordering explicit (CDK already adds the Ref edge
// via getAttString, but an explicit addDependency documents intent and
// guards against a future refactor dropping the implicit edge).
computedParam.node.addDependency(cr);

// Surface the resolved values as outputs too, so verify.sh has a
// belt-and-suspenders cross-check (state.outputs) alongside the on-AWS
// SSM read.
new cdk.CfnOutput(this, 'ComputedParamName', {
value: computedParam.parameterName,
description: 'SSM parameter name whose Value is Fn::GetAtt(CR, ComputedValue)',
});
new cdk.CfnOutput(this, 'ComputedValueResolved', {
value: cr.getAttString('ComputedValue'),
description: 'The CR ComputedValue Data attr resolved at synth/deploy time',
});
}
}
16 changes: 16 additions & 0 deletions tests/integration/custom-resource-getatt-data/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "custom-resource-getatt-data-example",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc",
"watch": "tsc -w"
},
"devDependencies": {
"@types/node": "^20.0.0",
"aws-cdk-lib": "^2.172.0",
"constructs": "^10.0.0",
"typescript": "^5.7.2"
},
"type": "module"
}
37 changes: 37 additions & 0 deletions tests/integration/custom-resource-getatt-data/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"lib": [
"ES2023"
],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"typeRoots": [
"./node_modules/@types"
],
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"moduleResolution": "NodeNext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
},
"exclude": [
"node_modules",
"cdk.out"
]
}
Loading