diff --git a/docs/_generated/integ-coverage.json b/docs/_generated/integ-coverage.json index 7b978e6ad..c91059cfe 100644 --- a/docs/_generated/integ-coverage.json +++ b/docs/_generated/integ-coverage.json @@ -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", @@ -2624,6 +2625,9 @@ "l2", "literal" ], + "custom-resource-getatt-data": [ + "l2" + ], "custom-resource-provider": [ "l2" ], @@ -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", @@ -3515,6 +3520,9 @@ "cross-stack-references": [ "l1" ], + "custom-resource-getatt-data": [ + "l2" + ], "deep-getatt-chains": [ "l1" ], @@ -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", diff --git a/docs/_generated/scenario-coverage.json b/docs/_generated/scenario-coverage.json index e9a30340b..ecc9b91d3 100644 --- a/docs/_generated/scenario-coverage.json +++ b/docs/_generated/scenario-coverage.json @@ -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.')` / `Fn::GetAtt(CR, '')` 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)." @@ -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, @@ -1197,6 +1208,13 @@ "vpc-lambda-cr-race" ] }, + { + "scenario": "custom-resource-getatt-data", + "description": "Custom Resource response `Data` consumed via `Fn::GetAtt(CR, 'Data.')` / `Fn::GetAtt(CR, '')` 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).", diff --git a/docs/changelog-cdkd.md b/docs/changelog-cdkd.md index df25282f7..f3a6744fc 100644 --- a/docs/changelog-cdkd.md +++ b/docs/changelog-cdkd.md @@ -32,6 +32,7 @@ The CLAUDE.md `## Known Limitations` section retains the load-bearing summary - ✅ **New `propagation-races-2` integ surfaces more fresh-principal / propagation-race edges (sibling to the original IAM-propagation stress integ that found [#839](https://github.com/go-to-k/cdkd/issues/839))** — `tests/integration/propagation-races-2/`. The original IAM-propagation stress integ exercised Lambda exec role / SFN role / EventBridge target / SQS+SNS resource policy and surfaced #839 (an SNS/SQS policy PUT not retried on a fresh-role `PrincipalNotFound`); many sibling AWS APIs share the same race. This fixture (`CdkdPropagationRaces2Example`, 20 synthesized resources) probes four DISTINCT race edges the prior integ did not cover, each a NEW consumer of a resource created moments earlier in the SAME deploy: (1) **IAM InstanceProfile -> EC2 Instance** — `RunInstances` validates the instance profile at launch (instance-profile propagation is the slowest IAM surface, often 5-10s+, so this is the highest-probability edge); the instance is a RAW `ec2.CfnInstance` (L1) emitting only cdkd-handled top-level props (`ImageId` / `InstanceType` / `SubnetId` / `SecurityGroupIds` / `IamInstanceProfile` / `Tags`) so it stays on the SDK provider path (an L2 instance emits `AvailabilityZone`, a silent-drop that flips it onto Cloud Control) in a minimal single-AZ no-NAT VPC; (2) **`AWS::Lambda::Permission` granting a fresh S3 bucket source** — `AddPermission` validates the just-created `SourceArn` + function in one call; (3) **`AWS::S3::BucketPolicy` referencing a fresh IAM role principal** — `PutBucketPolicy` validates the role principal (the classic `Invalid principal in policy` S3 race); (4) **`AWS::KMS::Key` key policy referencing a fresh IAM role principal** — `CreateKey` validates every principal in the key policy (this edge surfaced the KMS retry fix in `src/deployment/retryable-errors.ts` above). The PASS CONDITION is that `cdkd deploy` SUCCEEDS — the fixture is a RACE DETECTOR: if cdkd does not retry the fresh-principal propagation error for one of these edges, the deploy fails and `verify.sh` prints which resource failed + the AWS error + the `cdkd events --format json` `RESOURCE_FAILED` lines for triage. On success it asserts each resource actually works (instance running with the profile attached, Lambda invokable + resource policy grants the S3 source, bucket policy present + references the fresh role, KMS key Enabled + usable + policy references the fresh role), then destroys and asserts every NAMED resource is gone (EC2 instance terminated, instance profile / Lambda / both S3 buckets gone, KMS key PendingDeletion) by the fixture-owned `cdkd:integ-fixture=propagation-races-2` tag / state-resolved physical ids — NOT the AWS-reserved `aws:cdk:path` tag — plus a tag-scoped orphan-instance sweep that catches an orphan a state-resolved-id check would miss. The cleanup trap deletes in EC2-before-VPC order (`terminate-instances` + `instance-terminated` wait so a lingering ENI does not block the SG/subnet) and is BSD/macOS-portable (no `grep -P` / `date -d`, real exit codes captured, explicit `=== PASS ===` only on full success). New scenario tag `fresh-principal-consumer-race` in `KNOWN_SCENARIOS` (`scripts/build-scenario-coverage-matrix.ts`); the `.scenarios.json` also tags the existing `iam-policy-propagation-retry`; coverage matrices regenerated. (NOTE: needs `/run-integ propagation-races-2` against real AWS before merge; the deploy step is the bug-finder.) - ✅ **`Fn::GetAtt` of an unenriched computed attribute resolved to the raw physicalId instead of the real attribute, plus a new failure-seeking `deep-getatt-chains` integ for LONG `Fn::GetAtt` chains** — `src/deployment/intrinsic-function-resolver.ts` + `tests/integration/deep-getatt-chains/**`. The fixture surfaced a real cdkd bug shared by two resource classes: `intrinsic-function-resolver.ts`'s `constructAttribute` (the per-type fallback that synthesizes an attribute when the provider never populated it — e.g. on the Cloud-Control-API path, where attributes are not always captured) had no case for `AWS::CloudWatch::CompositeAlarm` (so `Fn::GetAtt(, 'Arn')` fell through to the physicalId default and resolved to the alarm NAME, not its ARN) and no case for `AWS::EC2::Instance` (so `Fn::GetAtt(, 'PrivateIp')` resolved to the instance id, not the IP — breaking an ELBv2 IP-target registration with `not a valid IPv4 address`). The fix adds both: **CompositeAlarm** `Arn` is constructed deterministically from the physicalId (`arn::cloudwatch:::alarm:` — the same `:alarm:` ARN shape as a metric alarm, so no AWS call is needed); **EC2 Instance** `PrivateIp` / `PublicIp` / `PrivateDnsName` / `PublicDnsName` / `AvailabilityZone` require a LIVE `DescribeInstances` lookup (the IP is not derivable from the id), cached per (physicalId, attribute) like the file's other live-lookup cases. The resolver-level fix works regardless of SDK-vs-CC-API routing. The new `CdkdDeepGetAttChainsExample` stack is a 5-deep chain (cheap; SNS / CloudWatch / SSM / IAM / Lambda only, no VPC, inline Lambda `ZipFile`): `A AWS::SNS::Topic` (SDK) `--TopicArn-->` `B AWS::CloudWatch::Alarm` (SDK, `AlarmActions[0]`) `--AlarmName (Ref)-->` `C AWS::CloudWatch::CompositeAlarm` (**CC-API**, no SDK provider registered; `AlarmRule`) `--Arn-->` `D AWS::SSM::Parameter` (SDK, `Value` = `Fn::Sub` joining C.Arn + B.Arn) `--Name (Ref)-->` `E AWS::Lambda::Function` (SDK, terminal multi-attribute `Fn::Sub` env pulling A.TopicArn + C.Arn + D(Ref) at once). The **critical link is C**: `AWS::CloudWatch::CompositeAlarm` is unregistered, so cdkd routes it via Cloud Control API and its `Arn` now comes from the new `constructAttribute` case rather than falling through to the physicalId default. `verify.sh` (BSD/macOS-portable — no `grep -P` / `date -d` — real rc capture via `PIPESTATUS`, explicit `[verify] PASS`) deploys, reads each upstream resource's REAL attribute back from AWS as ground truth, and asserts per-link: alarm B's `AlarmActions[0]` == A's real `TopicArn`; composite C's `AlarmRule` references B's real name; SSM param D's `Value` == `composite=;alarm=` built from the REAL ARNs (the CC-API → SDK hop); and Lambda E's env resolves `UPSTREAM_TOPIC_ARN` / `UPSTREAM_COMPOSITE_ARN` / `UPSTREAM_PARAM_NAME` / `UPSTREAM_JOINED` to the real upstream attributes — a mismatch fails with a message naming the broken link. It then destroys with `--force` and asserts every named resource is gone (each carries an OWN `cdkd:integ-fixture` tag; assertions resolve by name / state — never `aws:cdk:path`) plus the S3 state file removed. The EC2 Instance live-lookup path is integ-covered by the sibling `deletion-ordering-complex` fixture (an ELBv2 IP-target group registered with a Lambda-less EC2 instance's `PrivateIp`). Unit tests in `tests/unit/deployment/intrinsic-functions.test.ts` cover the CompositeAlarm ARN construction and the EC2 `PrivateIp` live-lookup (mocked `DescribeInstances`). New scenario tag `deep-getatt-chain-resolution` in `KNOWN_SCENARIOS` (`scripts/build-scenario-coverage-matrix.ts`); coverage matrices regenerated (the fixture also newly registers `AWS::CloudWatch::CompositeAlarm` in the integ-coverage matrix). (NOTE: needs `/run-integ deep-getatt-chains` against real AWS before merge.) - ✅ **New `sdk-ccapi-crossref` integ surfaces bugs at the SDK-Provider <-> Cloud Control API cross-reference boundary (test-only — no `src/` change)** — `tests/integration/sdk-ccapi-crossref/`. cdkd's [#614](https://github.com/go-to-k/cdkd/issues/614) routing flips an SDK-registered resource to the generic Cloud Control API path the moment its template sets a top-level property the SDK Provider would silently drop, which **bypasses the SDK Provider's `create()` / `delete()` entirely** — typed attribute writes never happen and the physical id becomes whatever CC API returns. `Fn::GetAtt` / `Ref` references crossing that SDK <-> CC seam (and CC API's physical-id shapes) are a documented fragile area (memory `feedback_silent_drop_forces_cc_api_routing` + `feedback_cc_api_routing_bypasses_sdk_delete_logic`), but no integ exercised it directly. The new `CdkdSdkCcApiCrossrefExample` stack forces a heterogeneous routing mix in ONE stack (no VPC / NAT): `KinesisStream` (`AWS::Kinesis::Stream`, silent-drop `DesiredShardLevelMetrics`) and `CcLambda` (`AWS::Lambda::Function`, silent-drop `RuntimeManagementConfig`) auto-route via CC API, while `ExecRole` (`AWS::IAM::Role`) and `StreamArnParam` (`AWS::SSM::Parameter`) stay on the SDK path. The routing was confirmed against this cdkd version with the `findActionableSilentDrops` registry helper on the synthesized template before finalizing. It crosses the boundary with `Fn::GetAtt` in **both directions**: (A) SDK -> CC — `StreamArnParam.Value = Fn::GetAtt(KinesisStream, 'Arn')` (an SDK-routed consumer reading a CC-routed producer's attribute, whose CC physical id is the stream NAME not the Arn); (B) CC -> SDK — `CcLambda.Role = Fn::GetAtt(ExecRole, 'Arn')` (a CC-routed consumer reading an SDK-routed producer's attribute). `verify.sh` (BSD/macOS-portable, real-rc capture + explicit `[verify] PASS`) deploys, asserts from state that the two silent-drop resources are `provisionedBy: 'cc-api'` and the other two are `'sdk'` (proves the mixed routing), asserts **cross-ref A** (the SSM parameter's value on AWS equals the real Kinesis stream ARN), asserts **cross-ref B** (the Lambda's configured role on AWS equals the real IAM role ARN), asserts the silent-drop `RuntimeManagementConfig.UpdateRuntimeOn` reached AWS (`FunctionUpdate` — the CC route forwarded the full property map), then `cdkd destroy --force` (which exercises the CC delete path that bypasses the SDK provider `delete()` for the stream + Lambda) and asserts every named resource (stream / function / role / parameter) and the state file are gone. New scenario tag `sdk-ccapi-crossref-boundary` in the canonical taxonomy (`scripts/build-scenario-coverage-matrix.ts`); coverage matrices regenerated. (NOTE: not yet run against real AWS — needs `/run-integ sdk-ccapi-crossref` before merge.) +- ✅ **New `custom-resource-getatt-data` failure-seeking integ for the Custom Resource response-`Data` attribute path consumed via `Fn::GetAtt` into a dependent resource (refs [#756](https://github.com/go-to-k/cdkd/issues/756) / [#804](https://github.com/go-to-k/cdkd/issues/804), test-only — no `src/` change)** — `tests/integration/custom-resource-getatt-data/**`. The existing `custom-resource-provider` fixture exercises the CDK Provider framework's async `isCompleteHandler` polling path but only asserts the CR's own output; it does NOT prove that a CR's response `Data` flows THROUGH cdkd's intrinsic resolver INTO another resource's property — the fragile path (#756 / #804: CR `Data` attributes only exist after the CR's backing Lambda runs and returns SUCCESS, so a resolver / DAG-ordering bug there is silent unless a dependent's value is read back). The new `CdkdCrGetAttDataExample` stack closes that gap with the cheapest shape: an inline NodeJS Lambda-backed `AWS::CloudFormation::CustomResource` whose handler returns `Data: { ComputedValue: 'computed-integ', Another: 'another-', NumericValue: '42' }` directly in its response payload (the simple synchronous path, no Provider framework / VPC), consumed by THREE `AWS::SSM::Parameter` dependents whose `Value` is `Fn::GetAtt(MyCustomResource, '')` — one per Data key (multiple keys catch a resolver that only wires the first attribute; the stringified-number key catches non-text-Data mishandling), with an explicit `addDependency(cr)` on one parameter to exercise the DAG ordering (CR must complete + have `attributes` populated before the dependent provisions). `verify.sh` (BSD/macOS-portable, real-rc capture + explicit `=== PASS ===`) deploys, then reads each SSM parameter back from AWS (`aws ssm get-parameter`) and asserts its `Value` equals the value the CR handler returned — the load-bearing check that the CR `Data.` attribute resolved into the dependent property (a blank/wrong value would otherwise pass unnoticed). It also cross-checks the resolved `ComputedValue` in `state.outputs`, then destroys and asserts the state file, all three SSM parameters, and the backing Lambda (resolved by physical id from state) are gone. New scenario tag `custom-resource-getatt-data` in `KNOWN_SCENARIOS` (`scripts/build-scenario-coverage-matrix.ts`); coverage matrices regenerated. (NOTE: not yet run against real AWS — needs `/run-integ custom-resource-getatt-data` before merge.) - ✅ **Hardened post-destroy assertions across three new behavior-class integ fixtures so a silently-skipped delete cannot pass as clean (issue [#831](https://github.com/go-to-k/cdkd/issues/831), test-only — no `src/` change)** — `tests/integration/{update-replace,destroy-interrupt,deployment-events}/verify.sh`. Code review of the new `update-replace` integ (PR #830) surfaced a hardening gap shared by several of the new fixtures: their post-destroy assertions checked only that `state.json` was gone (and S3 buckets), but did NOT explicitly assert that the named NON-bucket resources (Lambda / IAM Role / SecurityGroup / SNS Topic) were actually deleted from AWS. Per the `feedback_protection_integ_must_instantiate_resource` rule ("state-empty misses an orphan carrying no stack name"), a `state destroy` that silently skips a resource (e.g. an SG delete blocked by a lingering ENI, or an IAM role that is not VPC-bound and orphans independently) would leave a real orphan while the test still passed. The fixtures now assert each named resource is NOT-FOUND in AWS after destroy: **`update-replace`** — the Lambda (`WorkerFn`, `aws lambda get-function-configuration`), IAM Role (`WorkerRole`, `aws iam get-role`), and SecurityGroup (`WorkerSg`, `aws ec2 describe-security-groups`) physical ids captured from state in Phase 1 must each error post-destroy, on top of the two existing bucket checks; plus a belt-and-suspenders `aws s3 rb --force` of the predictable `cdkd-update-replace-{account}-{region}-v1`/`-v2` bucket names in the cleanup trap so a re-run is not blocked after a mid-replacement crash that left a v1/v2 bucket behind. **`destroy-interrupt`** — already resolved the backing Lambda + VPC (covering subnets/SG/ENI implicitly) + each SSM parameter id from state; added EXPLICIT not-found assertions for the CR handler's IAM Role (`aws iam get-role` — NOT VPC-bound, so the VPC-gone assert did NOT cover it) and the Lambda SecurityGroup (`aws ec2 describe-security-groups` — belt-and-suspenders on top of the VPC-gone implication). **`deployment-events`** — already asserted the SSM parameter gone; added an SNS Topic not-found assertion (`aws sns get-topic-attributes` against the deterministic `${STACK}-topic` ARN resolved via `sts get-caller-identity`) plus a matching topic delete in the failure-cleanup path. All three scripts stay BSD/macOS-portable (no `grep -P` / `date -d`), use `jq has()` for boolean probes, capture real exit codes, and print `[verify] PASS` only on full success — following the own-`cdkd:integ-fixture`-tag pattern already used by `rollback-failure-injection` (these three resolve physical ids from cdkd state instead, which is equally reliable and needs no `lib/` change). Resource TYPES are unchanged, so the integ-coverage / scenario-coverage matrices regenerated with no diff. (NOTE: `update-replace` and `deployment-events` need a real-AWS re-run to validate the new assertions; `destroy-interrupt` adds only two cheap not-found checks to an already-passing fixture.) - ✅ **New `s3-asset-deploy` integ exercises the S3 file/ZIP asset-publishing path during a real `cdkd deploy`** — `tests/integration/s3-asset-deploy/**`. Most Lambda fixtures use inline code (`Code.fromInline`) or never assert the asset upload itself, leaving cdkd's `FileAssetPublisher` (S3 zip + upload, content-addressed skip-if-exists) without a dedicated end-to-end regression. The new `CdkdS3AssetDeployExample` stack closes that gap: a Lambda whose code comes from a **local multi-file directory** (`lambda/` — handler + `helpers/` + `vendored/` Python sub-packages, so the asset is a genuine multi-file ZIP, not a trivial single file) forces cdkd to zip the directory + upload it to the CDK bootstrap asset bucket and wire the function's `Code.S3Bucket`/`Code.S3Key` to the uploaded object; AND a generic `s3_assets.Asset` (`asset-data/`) is uploaded to the same bucket with its resolved `s3BucketName`/`s3ObjectKey` threaded into the Lambda as `CONFIG_BUCKET`/`CONFIG_KEY` env vars (synth confirms both are emitted as `Fn::Sub`-backed bucket + literal key refs, exercising cdkd's intrinsic resolver). `verify.sh` (BSD/macOS-portable, real-rc + explicit `[verify] PASS`) deploys, asserts the function's `CodeSize > 500` bytes (proves it ran from the uploaded ZIP, not inline), invokes it and asserts the handler marker (`cdkd-s3-asset-deploy-marker-v1`) plus a non-zero `configBytes` from the generic-asset S3 read-back (proving that upload reached AWS and the bucket/key env wiring resolved), then destroys and asserts the Lambda + state file are gone with 0 errors. The bootstrap-bucket asset OBJECTS persist by design (cdkd does not own / delete the CDK bootstrap bucket) — the script deliberately does NOT assert their absence and notes this. New scenario tag `s3-asset-deploy` in the canonical taxonomy (`scripts/build-scenario-coverage-matrix.ts`); coverage matrices regenerated. (NOTE: not yet run against real AWS — needs `/run-integ s3-asset-deploy` before merge.) - ✅ **New `drift-revert-arrays` integ broadens drift coverage to TAG-heavy / ARRAY-heavy resource types (refs issue [#802](https://github.com/go-to-k/cdkd/issues/802))** — `tests/integration/drift-revert-arrays/` (new fixture; no `src/` change). Issue #802 added `src/analyzer/drift-normalize.ts` (`canonicalizeTagListsDeep` + `canonicalizeIdArraysDeep`) so a benign AWS-side reorder of a tag list (`{Key,Value}[]`) or a resource-id / ARN array no longer surfaces as phantom drift, but the existing `drift-revert` / `drift-revert-vpc` fixtures carry none of those unordered-set array shapes — the canonicalization path had unit coverage only. This fixture (`CdkdDriftArraysExample`) deploys an S3 Bucket, SNS Topic, SQS Queue (each with six user tags), an IAM ManagedPolicy with a multi-statement document carrying multiple `Action[]` (plain scalar arrays — intentionally NOT canonicalized) + multiple `Resource[]` ARN arrays (canonicalized) + six tags, and a VPC (`natGateways: 0`, no NAT cost) + SecurityGroup with four CIDR ingress rules + six tags. `verify.sh` asserts (a) **no false positive on a clean deploy** (`cdkd drift` exit 0 even though AWS reorders the tag lists / ARN arrays on readback), (b) **no false positive on an induced reorder** (`inject-drift.ts reorder` re-PUTs the same six S3 tags reversed; `cdkd drift` still exit 0 — proves `canonicalizeTagListsDeep`), (c) **true drift still detected** (`inject-drift.ts drift` changes a tag VALUE + adds a managed-policy Action + authorizes a new SG ingress rule out of band; `cdkd drift` exit 1), then `cdkd drift --revert -y` reverts and a follow-up `cdkd drift` is clean, and destroy leaves 0 orphans. The script is BSD/macOS-portable (no `grep -P` / `date -d`), captures each `cdkd drift` real exit code, hard-fails with a canonicalizer-naming message, and prints `[verify] PASS` only on full success. The `subnet-…` / `sg-…` resource-id branch of `canonicalizeIdArraysDeep` stays unit-covered (`tests/unit/analyzer/drift-normalize.test.ts`); the integ exercises the ARN branch of the same function end-to-end. New scenario tag `drift-revert-array-canonicalization`. diff --git a/docs/integ-coverage.md b/docs/integ-coverage.md index c8e257a14..180cd41b7 100644 --- a/docs/integ-coverage.md +++ b/docs/integ-coverage.md @@ -113,7 +113,7 @@ Registered without an integ fixture, with an explicit `// allow-no-integ: [`dynamodb-ondemand`](../tests/integration/dynamodb-ondemand/) (l2)
[`log-pipeline`](../tests/integration/log-pipeline/) (l2)
[`sdk-ccapi-crossref`](../tests/integration/sdk-ccapi-crossref/) (l1,literal) | | `AWS::KinesisFirehose::DeliveryStream` | [`log-pipeline`](../tests/integration/log-pipeline/) (l1,literal) | | `AWS::Lambda::EventSourceMapping` | [`dynamodb-streams`](../tests/integration/dynamodb-streams/) (literal) | -| `AWS::Lambda::Function` | [`api-cognito`](../tests/integration/api-cognito/) (l2)
[`apigateway`](../tests/integration/apigateway/) (l2)
[`bench-cdk-sample`](../tests/integration/bench-cdk-sample/) (l2)
[`cache-streaming`](../tests/integration/cache-streaming/) (l2)
[`cc-api-fallback`](../tests/integration/cc-api-fallback/) (l1)
[`cc-api-fallback-transitions`](../tests/integration/cc-api-fallback-transitions/) (l1,literal)
[`cloudfront-function-url`](../tests/integration/cloudfront-function-url/) (l2)
[`composite-stack`](../tests/integration/composite-stack/) (l2,literal)
[`custom-resource-provider`](../tests/integration/custom-resource-provider/) (l2)
[`data-pipeline`](../tests/integration/data-pipeline/) (l2)
[`deep-getatt-chains`](../tests/integration/deep-getatt-chains/) (l1)
[`destroy-interrupt`](../tests/integration/destroy-interrupt/) (l2)
[`docker-image-asset`](../tests/integration/docker-image-asset/) (l2,literal)
[`dynamodb-streams`](../tests/integration/dynamodb-streams/) (l2)
[`ecr`](../tests/integration/ecr/) (l2)
[`efs-lambda`](../tests/integration/efs-lambda/) (l2)
[`event-driven`](../tests/integration/event-driven/) (l2)
[`eventbridge`](../tests/integration/eventbridge/) (l2)
[`export`](../tests/integration/export/) (l2,literal)
[`full-stack-demo`](../tests/integration/full-stack-demo/) (l2)
[`iam-propagation-stress`](../tests/integration/iam-propagation-stress/) (l2)
[`lambda`](../tests/integration/lambda/) (l2)
[`lambda-versioning`](../tests/integration/lambda-versioning/) (l2,literal)
[`local-invoke`](../tests/integration/local-invoke/) (l2)
[`local-invoke-buildkit`](../tests/integration/local-invoke-buildkit/) (l2)
[`local-invoke-container`](../tests/integration/local-invoke-container/) (l2)
[`local-invoke-dotnet`](../tests/integration/local-invoke-dotnet/) (l1,l2)
[`local-invoke-from-cfn-stack`](../tests/integration/local-invoke-from-cfn-stack/) (l2)
[`local-invoke-from-cfn-stack-multi-stack`](../tests/integration/local-invoke-from-cfn-stack-multi-stack/) (l2)
[`local-invoke-from-state`](../tests/integration/local-invoke-from-state/) (l2)
[`local-invoke-java`](../tests/integration/local-invoke-java/) (l1,l2)
[`local-invoke-layers`](../tests/integration/local-invoke-layers/) (l2)
[`local-invoke-provided`](../tests/integration/local-invoke-provided/) (l1,l2)
[`local-invoke-python`](../tests/integration/local-invoke-python/) (l2)
[`local-invoke-ruby`](../tests/integration/local-invoke-ruby/) (l1,l2)
[`local-start-api`](../tests/integration/local-start-api/) (l2)
[`local-start-api-container`](../tests/integration/local-start-api-container/) (l2)
[`local-start-api-rest-v1-non-proxy`](../tests/integration/local-start-api-rest-v1-non-proxy/) (l2)
[`local-start-api-websocket`](../tests/integration/local-start-api-websocket/) (l2,literal)
[`log-pipeline`](../tests/integration/log-pipeline/) (l2)
[`macro-expansion`](../tests/integration/macro-expansion/) (literal)
[`microservices`](../tests/integration/microservices/) (l2)
[`migrate-from-cfn`](../tests/integration/migrate-from-cfn/) (l2)
[`monitoring`](../tests/integration/monitoring/) (l2)
[`multi-asset`](../tests/integration/multi-asset/) (l2)
[`multi-resource`](../tests/integration/multi-resource/) (l2)
[`multi-stack-deps`](../tests/integration/multi-stack-deps/) (l2)
[`orphan-resource`](../tests/integration/orphan-resource/) (l2,literal)
[`propagation-races-2`](../tests/integration/propagation-races-2/) (l2)
[`recreate-mixed-direction`](../tests/integration/recreate-mixed-direction/) (l1)
[`recreate-via-cc-api`](../tests/integration/recreate-via-cc-api/) (l1)
[`recreate-via-sdk-provider`](../tests/integration/recreate-via-sdk-provider/) (l1)
[`rollback-failure-injection`](../tests/integration/rollback-failure-injection/) (l2)
[`s3-asset-deploy`](../tests/integration/s3-asset-deploy/) (l2)
[`scheduled-task`](../tests/integration/scheduled-task/) (l2)
[`sdk-ccapi-crossref`](../tests/integration/sdk-ccapi-crossref/) (l1,literal)
[`serverless-api`](../tests/integration/serverless-api/) (l2)
[`sns-sqs-event`](../tests/integration/sns-sqs-event/) (l2)
[`stepfunctions`](../tests/integration/stepfunctions/) (l2)
[`update-replace`](../tests/integration/update-replace/) (l2,literal)
[`vpc-lambda`](../tests/integration/vpc-lambda/) (l2)
[`vpc-lambda-cr-race`](../tests/integration/vpc-lambda-cr-race/) (l2) | +| `AWS::Lambda::Function` | [`api-cognito`](../tests/integration/api-cognito/) (l2)
[`apigateway`](../tests/integration/apigateway/) (l2)
[`bench-cdk-sample`](../tests/integration/bench-cdk-sample/) (l2)
[`cache-streaming`](../tests/integration/cache-streaming/) (l2)
[`cc-api-fallback`](../tests/integration/cc-api-fallback/) (l1)
[`cc-api-fallback-transitions`](../tests/integration/cc-api-fallback-transitions/) (l1,literal)
[`cloudfront-function-url`](../tests/integration/cloudfront-function-url/) (l2)
[`composite-stack`](../tests/integration/composite-stack/) (l2,literal)
[`custom-resource-getatt-data`](../tests/integration/custom-resource-getatt-data/) (l2)
[`custom-resource-provider`](../tests/integration/custom-resource-provider/) (l2)
[`data-pipeline`](../tests/integration/data-pipeline/) (l2)
[`deep-getatt-chains`](../tests/integration/deep-getatt-chains/) (l1)
[`destroy-interrupt`](../tests/integration/destroy-interrupt/) (l2)
[`docker-image-asset`](../tests/integration/docker-image-asset/) (l2,literal)
[`dynamodb-streams`](../tests/integration/dynamodb-streams/) (l2)
[`ecr`](../tests/integration/ecr/) (l2)
[`efs-lambda`](../tests/integration/efs-lambda/) (l2)
[`event-driven`](../tests/integration/event-driven/) (l2)
[`eventbridge`](../tests/integration/eventbridge/) (l2)
[`export`](../tests/integration/export/) (l2,literal)
[`full-stack-demo`](../tests/integration/full-stack-demo/) (l2)
[`iam-propagation-stress`](../tests/integration/iam-propagation-stress/) (l2)
[`lambda`](../tests/integration/lambda/) (l2)
[`lambda-versioning`](../tests/integration/lambda-versioning/) (l2,literal)
[`local-invoke`](../tests/integration/local-invoke/) (l2)
[`local-invoke-buildkit`](../tests/integration/local-invoke-buildkit/) (l2)
[`local-invoke-container`](../tests/integration/local-invoke-container/) (l2)
[`local-invoke-dotnet`](../tests/integration/local-invoke-dotnet/) (l1,l2)
[`local-invoke-from-cfn-stack`](../tests/integration/local-invoke-from-cfn-stack/) (l2)
[`local-invoke-from-cfn-stack-multi-stack`](../tests/integration/local-invoke-from-cfn-stack-multi-stack/) (l2)
[`local-invoke-from-state`](../tests/integration/local-invoke-from-state/) (l2)
[`local-invoke-java`](../tests/integration/local-invoke-java/) (l1,l2)
[`local-invoke-layers`](../tests/integration/local-invoke-layers/) (l2)
[`local-invoke-provided`](../tests/integration/local-invoke-provided/) (l1,l2)
[`local-invoke-python`](../tests/integration/local-invoke-python/) (l2)
[`local-invoke-ruby`](../tests/integration/local-invoke-ruby/) (l1,l2)
[`local-start-api`](../tests/integration/local-start-api/) (l2)
[`local-start-api-container`](../tests/integration/local-start-api-container/) (l2)
[`local-start-api-rest-v1-non-proxy`](../tests/integration/local-start-api-rest-v1-non-proxy/) (l2)
[`local-start-api-websocket`](../tests/integration/local-start-api-websocket/) (l2,literal)
[`log-pipeline`](../tests/integration/log-pipeline/) (l2)
[`macro-expansion`](../tests/integration/macro-expansion/) (literal)
[`microservices`](../tests/integration/microservices/) (l2)
[`migrate-from-cfn`](../tests/integration/migrate-from-cfn/) (l2)
[`monitoring`](../tests/integration/monitoring/) (l2)
[`multi-asset`](../tests/integration/multi-asset/) (l2)
[`multi-resource`](../tests/integration/multi-resource/) (l2)
[`multi-stack-deps`](../tests/integration/multi-stack-deps/) (l2)
[`orphan-resource`](../tests/integration/orphan-resource/) (l2,literal)
[`propagation-races-2`](../tests/integration/propagation-races-2/) (l2)
[`recreate-mixed-direction`](../tests/integration/recreate-mixed-direction/) (l1)
[`recreate-via-cc-api`](../tests/integration/recreate-via-cc-api/) (l1)
[`recreate-via-sdk-provider`](../tests/integration/recreate-via-sdk-provider/) (l1)
[`rollback-failure-injection`](../tests/integration/rollback-failure-injection/) (l2)
[`s3-asset-deploy`](../tests/integration/s3-asset-deploy/) (l2)
[`scheduled-task`](../tests/integration/scheduled-task/) (l2)
[`sdk-ccapi-crossref`](../tests/integration/sdk-ccapi-crossref/) (l1,literal)
[`serverless-api`](../tests/integration/serverless-api/) (l2)
[`sns-sqs-event`](../tests/integration/sns-sqs-event/) (l2)
[`stepfunctions`](../tests/integration/stepfunctions/) (l2)
[`update-replace`](../tests/integration/update-replace/) (l2,literal)
[`vpc-lambda`](../tests/integration/vpc-lambda/) (l2)
[`vpc-lambda-cr-race`](../tests/integration/vpc-lambda-cr-race/) (l2) | | `AWS::Lambda::LayerVersion` | [`lambda`](../tests/integration/lambda/) (l2)
[`local-invoke-layers`](../tests/integration/local-invoke-layers/) (l2) | | `AWS::Lambda::Permission` | [`cloudfront-function-url`](../tests/integration/cloudfront-function-url/) (l1,literal)
[`export`](../tests/integration/export/) (literal)
[`migrate-from-cfn`](../tests/integration/migrate-from-cfn/) (literal)
[`propagation-races-2`](../tests/integration/propagation-races-2/) (l1,literal)
[`serverless-api`](../tests/integration/serverless-api/) (l1) | | `AWS::Lambda::Url` | [`composite-stack`](../tests/integration/composite-stack/) (literal) | @@ -139,14 +139,14 @@ Registered without an integ fixture, with an explicit `// allow-no-integ: [`migrate-from-cfn`](../tests/integration/migrate-from-cfn/) (literal)
[`sns-sqs-event`](../tests/integration/sns-sqs-event/) (l2) | | `AWS::SQS::Queue` | [`basic`](../tests/integration/basic/) (l1)
[`bench-cdk-sample`](../tests/integration/bench-cdk-sample/) (l2)
[`bench-sdk`](../tests/integration/bench-sdk/) (l2)
[`composite-stack`](../tests/integration/composite-stack/) (l2,literal)
[`conditions-update-2`](../tests/integration/conditions-update-2/) (l1)
[`data-pipeline`](../tests/integration/data-pipeline/) (l2)
[`drift-revert-arrays`](../tests/integration/drift-revert-arrays/) (l2)
[`event-driven`](../tests/integration/event-driven/) (l2)
[`eventbridge`](../tests/integration/eventbridge/) (l2)
[`full-stack-demo`](../tests/integration/full-stack-demo/) (l2)
[`iam-propagation-stress`](../tests/integration/iam-propagation-stress/) (l2)
[`intrinsics-torture`](../tests/integration/intrinsics-torture/) (l2)
[`microservices`](../tests/integration/microservices/) (l2)
[`migrate-from-cfn`](../tests/integration/migrate-from-cfn/) (l2)
[`multi-resource`](../tests/integration/multi-resource/) (l2)
[`rollback-failure-injection`](../tests/integration/rollback-failure-injection/) (l1)
[`sns-sqs-event`](../tests/integration/sns-sqs-event/) (l2) | | `AWS::SQS::QueuePolicy` | [`iam-propagation-stress`](../tests/integration/iam-propagation-stress/) (literal)
[`migrate-from-cfn`](../tests/integration/migrate-from-cfn/) (literal) | -| `AWS::SSM::Parameter` | [`bench-sdk`](../tests/integration/bench-sdk/) (l2)
[`composite-stack`](../tests/integration/composite-stack/) (l2,literal)
[`conditions-and-if`](../tests/integration/conditions-and-if/) (l1)
[`conditions-update-2`](../tests/integration/conditions-update-2/) (l1)
[`context-test`](../tests/integration/context-test/) (l2)
[`cross-region-state-bucket`](../tests/integration/cross-region-state-bucket/) (l1)
[`cross-stack-references`](../tests/integration/cross-stack-references/) (l1)
[`deep-getatt-chains`](../tests/integration/deep-getatt-chains/) (l1)
[`deletion-policy-retain`](../tests/integration/deletion-policy-retain/) (l2,literal)
[`deployment-events`](../tests/integration/deployment-events/) (l2)
[`destroy-interrupt`](../tests/integration/destroy-interrupt/) (l2,literal)
[`export-nested-stack`](../tests/integration/export-nested-stack/) (l2,literal)
[`import-nested-stack`](../tests/integration/import-nested-stack/) (l2,literal)
[`import-value-strong-ref`](../tests/integration/import-value-strong-ref/) (l2)
[`infra-security`](../tests/integration/infra-security/) (l2)
[`intrinsics-torture`](../tests/integration/intrinsics-torture/) (l1,literal)
[`intrinsics-torture-2`](../tests/integration/intrinsics-torture-2/) (l1,literal)
[`legacy-bucket-name-fallback`](../tests/integration/legacy-bucket-name-fallback/) (l2)
[`legacy-state-migration`](../tests/integration/legacy-state-migration/) (l2)
[`local-invoke-from-cfn-stack-multi-stack`](../tests/integration/local-invoke-from-cfn-stack-multi-stack/) (l2)
[`microservices`](../tests/integration/microservices/) (l2)
[`multi-region-same-stack`](../tests/integration/multi-region-same-stack/) (l2)
[`nested-stack`](../tests/integration/nested-stack/) (l2)
[`nested-stack-3level`](../tests/integration/nested-stack-3level/) (l2)
[`nested-stack-deep`](../tests/integration/nested-stack-deep/) (l2)
[`rds-full-stack`](../tests/integration/rds-full-stack/) (l2)
[`rollback-failure-injection`](../tests/integration/rollback-failure-injection/) (l2)
[`schema-v5-to-v6-migration`](../tests/integration/schema-v5-to-v6-migration/) (l2)
[`schema-v6-to-v7-migration`](../tests/integration/schema-v6-to-v7-migration/) (l2)
[`schema-v7-to-v8-migration`](../tests/integration/schema-v7-to-v8-migration/) (l1,l2)
[`sdk-ccapi-crossref`](../tests/integration/sdk-ccapi-crossref/) (l1,literal)
[`state-info-command`](../tests/integration/state-info-command/) (l2)
[`vpc-lookup`](../tests/integration/vpc-lookup/) (l2) | +| `AWS::SSM::Parameter` | [`bench-sdk`](../tests/integration/bench-sdk/) (l2)
[`composite-stack`](../tests/integration/composite-stack/) (l2,literal)
[`conditions-and-if`](../tests/integration/conditions-and-if/) (l1)
[`conditions-update-2`](../tests/integration/conditions-update-2/) (l1)
[`context-test`](../tests/integration/context-test/) (l2)
[`cross-region-state-bucket`](../tests/integration/cross-region-state-bucket/) (l1)
[`cross-stack-references`](../tests/integration/cross-stack-references/) (l1)
[`custom-resource-getatt-data`](../tests/integration/custom-resource-getatt-data/) (l2)
[`deep-getatt-chains`](../tests/integration/deep-getatt-chains/) (l1)
[`deletion-policy-retain`](../tests/integration/deletion-policy-retain/) (l2,literal)
[`deployment-events`](../tests/integration/deployment-events/) (l2)
[`destroy-interrupt`](../tests/integration/destroy-interrupt/) (l2,literal)
[`export-nested-stack`](../tests/integration/export-nested-stack/) (l2,literal)
[`import-nested-stack`](../tests/integration/import-nested-stack/) (l2,literal)
[`import-value-strong-ref`](../tests/integration/import-value-strong-ref/) (l2)
[`infra-security`](../tests/integration/infra-security/) (l2)
[`intrinsics-torture`](../tests/integration/intrinsics-torture/) (l1,literal)
[`intrinsics-torture-2`](../tests/integration/intrinsics-torture-2/) (l1,literal)
[`legacy-bucket-name-fallback`](../tests/integration/legacy-bucket-name-fallback/) (l2)
[`legacy-state-migration`](../tests/integration/legacy-state-migration/) (l2)
[`local-invoke-from-cfn-stack-multi-stack`](../tests/integration/local-invoke-from-cfn-stack-multi-stack/) (l2)
[`microservices`](../tests/integration/microservices/) (l2)
[`multi-region-same-stack`](../tests/integration/multi-region-same-stack/) (l2)
[`nested-stack`](../tests/integration/nested-stack/) (l2)
[`nested-stack-3level`](../tests/integration/nested-stack-3level/) (l2)
[`nested-stack-deep`](../tests/integration/nested-stack-deep/) (l2)
[`rds-full-stack`](../tests/integration/rds-full-stack/) (l2)
[`rollback-failure-injection`](../tests/integration/rollback-failure-injection/) (l2)
[`schema-v5-to-v6-migration`](../tests/integration/schema-v5-to-v6-migration/) (l2)
[`schema-v6-to-v7-migration`](../tests/integration/schema-v6-to-v7-migration/) (l2)
[`schema-v7-to-v8-migration`](../tests/integration/schema-v7-to-v8-migration/) (l1,l2)
[`sdk-ccapi-crossref`](../tests/integration/sdk-ccapi-crossref/) (l1,literal)
[`state-info-command`](../tests/integration/state-info-command/) (l2)
[`vpc-lookup`](../tests/integration/vpc-lookup/) (l2) | | `AWS::SecretsManager::Secret` | [`composite-stack`](../tests/integration/composite-stack/) (l2,literal)
[`event-driven`](../tests/integration/event-driven/) (l2)
[`full-stack-demo`](../tests/integration/full-stack-demo/) (l2)
[`local-run-task-from-state`](../tests/integration/local-run-task-from-state/) (l2,literal) | | `AWS::ServiceDiscovery::PrivateDnsNamespace` | [`drift-revert-vpc`](../tests/integration/drift-revert-vpc/) (l2,literal)
[`local-ecs-service-connect`](../tests/integration/local-ecs-service-connect/) (literal) | | `AWS::ServiceDiscovery::Service` | [`local-ecs-service-connect`](../tests/integration/local-ecs-service-connect/) (literal) | | `AWS::StepFunctions::StateMachine` | [`iam-propagation-stress`](../tests/integration/iam-propagation-stress/) (l2)
[`stepfunctions`](../tests/integration/stepfunctions/) (l2) | | `AWS::WAFv2::WebACL` | [`wafv2`](../tests/integration/wafv2/) (l1,literal) | -## Resource types referenced in integs without an SDK Provider (26) +## Resource types referenced in integs without an SDK Provider (27) These resource types appear in integ fixtures but no SDK Provider is registered for them — they fall through to the Cloud Control API fallback. Listed here for visibility; not actionable on its own. @@ -154,6 +154,7 @@ These resource types appear in integ fixtures but no SDK Provider is registered - `AWS::ApplicationAutoScaling::ScalableTarget` - `AWS::Athena::NamedQuery` - `AWS::Athena::WorkGroup` +- `AWS::CloudFormation::CustomResource` - `AWS::CloudFront::OriginAccessControl` - `AWS::CloudWatch::CompositeAlarm` - `AWS::CodePipeline::Pipeline` diff --git a/docs/scenario-coverage.md b/docs/scenario-coverage.md index 1c0817569..a9872654f 100644 --- a/docs/scenario-coverage.md +++ b/docs/scenario-coverage.md @@ -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 @@ -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) | |---|---|---| @@ -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/)
[`bench-cdk-sample`](../tests/integration/bench-cdk-sample/)
[`bench-sdk`](../tests/integration/bench-sdk/)
[`full-stack-demo`](../tests/integration/full-stack-demo/)
[`lambda`](../tests/integration/lambda/)
[`microservices`](../tests/integration/microservices/)
[`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/)
[`custom-resource-provider`](../tests/integration/custom-resource-provider/)
[`destroy-interrupt`](../tests/integration/destroy-interrupt/)
[`vpc-lambda-cr-race`](../tests/integration/vpc-lambda-cr-race/) | +| `custom-resource-getatt-data` | Custom Resource response `Data` consumed via `Fn::GetAtt(CR, 'Data.')` / `Fn::GetAtt(CR, '')` 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/) | diff --git a/scripts/build-scenario-coverage-matrix.ts b/scripts/build-scenario-coverage-matrix.ts index 7a099b8f8..e6ea34965 100644 --- a/scripts/build-scenario-coverage-matrix.ts +++ b/scripts/build-scenario-coverage-matrix.ts @@ -129,6 +129,8 @@ const KNOWN_SCENARIOS: Record = { '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.')` / `Fn::GetAtt(CR, '')` 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).', diff --git a/tests/integration/custom-resource-getatt-data/.scenarios.json b/tests/integration/custom-resource-getatt-data/.scenarios.json new file mode 100644 index 000000000..125c12c19 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/.scenarios.json @@ -0,0 +1,5 @@ +{ + "scenarios": [ + "custom-resource-getatt-data" + ] +} diff --git a/tests/integration/custom-resource-getatt-data/README.md b/tests/integration/custom-resource-getatt-data/README.md new file mode 100644 index 000000000..c46ddca61 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/README.md @@ -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, '')` (a.k.a. `Data.`) — 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, '')` — 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-` / `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 +``` diff --git a/tests/integration/custom-resource-getatt-data/bin/app.ts b/tests/integration/custom-resource-getatt-data/bin/app.ts new file mode 100644 index 000000000..09bd6af82 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/bin/app.ts @@ -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, + }, +}); diff --git a/tests/integration/custom-resource-getatt-data/cdk.json b/tests/integration/custom-resource-getatt-data/cdk.json new file mode 100644 index 000000000..140bebb51 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/cdk.json @@ -0,0 +1,8 @@ +{ + "app": "node bin/app.ts", + "context": { + "cdkd": { + "stateBucket": "your-cdkd-state-bucket" + } + } +} diff --git a/tests/integration/custom-resource-getatt-data/lib/cr-getatt-data-stack.ts b/tests/integration/custom-resource-getatt-data/lib/cr-getatt-data-stack.ts new file mode 100644 index 000000000..c851fb7f0 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/lib/cr-getatt-data-stack.ts @@ -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, '')` (a.k.a. `Data.`) 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(, '')` + * resolve to `attributes['']` 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: "", + * Another: "another-", + * NumericValue: "", + * } + * | + * +--> 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 ` 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', + }); + } +} diff --git a/tests/integration/custom-resource-getatt-data/package.json b/tests/integration/custom-resource-getatt-data/package.json new file mode 100644 index 000000000..aca898c23 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/package.json @@ -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" +} diff --git a/tests/integration/custom-resource-getatt-data/tsconfig.json b/tests/integration/custom-resource-getatt-data/tsconfig.json new file mode 100644 index 000000000..f88af0113 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/tsconfig.json @@ -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" + ] +} diff --git a/tests/integration/custom-resource-getatt-data/verify.sh b/tests/integration/custom-resource-getatt-data/verify.sh new file mode 100755 index 000000000..bd8bed293 --- /dev/null +++ b/tests/integration/custom-resource-getatt-data/verify.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# verify.sh — Custom Resource `Data` GetAtt -> dependent property integ. +# +# Failure-seeking: a Custom Resource returns `Data: { ComputedValue, Another, +# NumericValue }`; three SSM parameters consume those via +# `Fn::GetAtt(CR, '')`. After deploy, each SSM parameter is read back +# from AWS and its Value asserted to equal the value the CR handler returned. +# This proves the CR `Data` attribute resolved THROUGH cdkd's intrinsic +# resolver INTO the dependent resource's property (fragile per #756 / #804 — +# CR attributes only exist after the CR Lambda runs). If the GetAtt-of-CR-Data +# resolves wrong/empty or the deploy fails, this FAILs with specifics. +# +# Required env vars: +# STATE_BUCKET — cdkd state bucket (e.g. cdkd-state-{accountId}) +# AWS_REGION — defaults to us-east-1 +# +# BSD-portable (macOS): no `grep -P`, no `date -d`, no GNU-only flags. + +set -euo pipefail + +cd "$(dirname "$0")" + +STACK="CdkdCrGetAttDataExample" +REGION="${AWS_REGION:-us-east-1}" +STATE_KEY="cdkd/${STACK}/${REGION}/state.json" + +# Expected values the CR handler returns (see lib/cr-getatt-data-stack.ts). +# Seed is the literal "integ"; Region is the deploy region. +EXPECTED_COMPUTED="computed-integ" +EXPECTED_ANOTHER="another-${REGION}" +EXPECTED_NUMERIC="42" + +# SSM parameter names (must match parameterName in the stack, with id=STACK). +PARAM_PREFIX="/cdkd-integ/cr-getatt-data/${STACK}" +PARAM_COMPUTED="${PARAM_PREFIX}/computed" +PARAM_ANOTHER="${PARAM_PREFIX}/another" +PARAM_NUMERIC="${PARAM_PREFIX}/numeric" + +LOCAL_DIST="$(cd ../../../dist && pwd)/cli.js" + +LAMBDA_ARN="" + +cleanup() { + echo "==> Cleanup: dropping any leftover state + AWS resources" + set +eu + if [ -x "${LOCAL_DIST}" ] && [ -n "${STATE_BUCKET:-}" ]; then + node "${LOCAL_DIST}" state destroy "${STACK}" \ + --state-bucket "${STATE_BUCKET}" \ + --region "${REGION}" \ + --yes + fi + if [ -n "${STATE_BUCKET:-}" ]; then + aws s3 rm "s3://${STATE_BUCKET}/${STATE_KEY}" >/dev/null 2>&1 || true + aws s3 rm "s3://${STATE_BUCKET}/cdkd/${STACK}/${REGION}/lock.json" >/dev/null 2>&1 || true + fi + # Best-effort delete of the SSM parameters in case a partial destroy left them. + for p in "${PARAM_COMPUTED}" "${PARAM_ANOTHER}" "${PARAM_NUMERIC}"; do + aws ssm delete-parameter --region "${REGION}" --name "${p}" >/dev/null 2>&1 || true + done + set -eu +} + +trap cleanup EXIT + +if [ -z "${STATE_BUCKET:-}" ]; then + echo "FAIL: STATE_BUCKET env var is required" >&2 + exit 1 +fi + +if [ ! -f "${LOCAL_DIST}" ]; then + echo "FAIL: local binary not built at ${LOCAL_DIST} — run 'vp run build' from repo root first" >&2 + exit 1 +fi + +echo "==> Installing fixture deps" +if [ ! -d node_modules ]; then + npm install +fi + +echo "==> Pre-run cleanup" +cleanup + +# --- Phase 1: deploy -------------------------------------------------- +echo "==> Phase 1: deploy with the local binary" +node "${LOCAL_DIST}" deploy "${STACK}" \ + --state-bucket "${STATE_BUCKET}" \ + --region "${REGION}" \ + --yes + +STATE=$(aws s3 cp "s3://${STATE_BUCKET}/${STATE_KEY}" - 2>/dev/null) +if [ -z "${STATE}" ]; then + echo "FAIL: no state file at s3://${STATE_BUCKET}/${STATE_KEY} after deploy" >&2 + exit 1 +fi + +# Resolve the backing Lambda ARN (CDK auto-named) from state so the +# post-destroy orphan check can target it precisely. +LAMBDA_ARN=$(echo "${STATE}" | jq -r '[.resources | to_entries[] | select(.value.resourceType == "AWS::Lambda::Function") | .value.physicalId] | first // ""') +echo " resolved backing Lambda: ${LAMBDA_ARN:-}" + +# Sanity: the CR's resolved ComputedValue should be in state.outputs too +# (belt-and-suspenders cross-check alongside the on-AWS SSM read below). +OUT_COMPUTED=$(echo "${STATE}" | jq -r '.outputs.ComputedValueResolved // ""') +if [ "${OUT_COMPUTED}" != "${EXPECTED_COMPUTED}" ]; then + echo "FAIL: state output ComputedValueResolved is '${OUT_COMPUTED}', expected '${EXPECTED_COMPUTED}'" >&2 + echo " (the CR Data attribute did NOT resolve into the output — GetAtt-of-CR-Data broken)" >&2 + echo "${STATE}" | jq .outputs >&2 + exit 1 +fi +echo " OK: state output ComputedValueResolved == '${EXPECTED_COMPUTED}'" + +# --- Assertion: each SSM parameter Value on AWS == the CR's returned Data --- +# This is the load-bearing check: it proves the CR `Data.` attribute +# flowed THROUGH the intrinsic resolver INTO the dependent SSM parameter's +# Value property. A blank/wrong value would otherwise pass silently. +assert_param() { + local name="$1" expected="$2" label="$3" + set +e + local out rc + out=$(aws ssm get-parameter --region "${REGION}" --name "${name}" \ + --query 'Parameter.Value' --output text 2>/tmp/cr-getatt-ssm-err) + rc=$? + set -e + if [ "${rc}" -ne 0 ]; then + echo "FAIL: get-parameter exited ${rc} for ${name} (${label})" >&2 + cat /tmp/cr-getatt-ssm-err >&2 || true + exit 1 + fi + if [ "${out}" != "${expected}" ]; then + echo "FAIL: SSM ${label} (${name}) Value is '${out}', expected '${expected}'" >&2 + echo " => Fn::GetAtt(CR, '') of the CR response Data did NOT resolve correctly into the dependent SSM parameter." >&2 + exit 1 + fi + echo " OK: ${label} (${name}) == '${expected}' on AWS" +} + +echo "==> Asserting CR Data resolved into each dependent SSM parameter" +assert_param "${PARAM_COMPUTED}" "${EXPECTED_COMPUTED}" "ComputedValue" +assert_param "${PARAM_ANOTHER}" "${EXPECTED_ANOTHER}" "Another" +assert_param "${PARAM_NUMERIC}" "${EXPECTED_NUMERIC}" "NumericValue" + +# --- Phase 2: destroy ------------------------------------------------- +echo "==> Phase 2: destroy" +node "${LOCAL_DIST}" destroy "${STACK}" \ + --state-bucket "${STATE_BUCKET}" \ + --region "${REGION}" \ + --force + +if aws s3 ls "s3://${STATE_BUCKET}/${STATE_KEY}" >/dev/null 2>&1; then + echo "FAIL: state file s3://${STATE_BUCKET}/${STATE_KEY} still exists after destroy" >&2 + exit 1 +fi +echo " OK: state file is gone" + +# The CR + backing Lambda + the three SSM parameters must all be gone. +for p in "${PARAM_COMPUTED}" "${PARAM_ANOTHER}" "${PARAM_NUMERIC}"; do + if aws ssm get-parameter --region "${REGION}" --name "${p}" >/dev/null 2>&1; then + echo "FAIL: SSM parameter ${p} still exists after destroy (orphan)" >&2 + exit 1 + fi +done +echo " OK: all three SSM parameters are gone" + +if [ -n "${LAMBDA_ARN}" ]; then + if aws lambda get-function --region "${REGION}" --function-name "${LAMBDA_ARN}" >/dev/null 2>&1; then + echo "FAIL: backing Lambda ${LAMBDA_ARN} still exists after destroy (orphan)" >&2 + exit 1 + fi + echo " OK: backing Lambda is gone" +fi + +echo "" +echo "=== PASS: Custom Resource Data GetAtt -> dependent property integ ==="