Skip to content

Commit e55ff37

Browse files
authored
feat(provisioning): CC-API enrichment-coverage matrix + CI critic (#869)
1 parent 66cd250 commit e55ff37

9 files changed

Lines changed: 4055 additions & 0 deletions

File tree

.claude/rules/code-layout.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ paths:
6969
- **src/provisioning/ec2-termination-protection.ts** - Shared `--remove-protection` helper for `AWS::EC2::Instance`: `disableInstanceApiTermination()` (flip `DisableApiTermination` off, idempotent, errors swallowed at debug), `isTerminationProtectionPropagationError()` (matches the "may not be terminated. Modify its disableApiTermination" 400 from both `TerminateInstances` and the Cloud Control `DeleteResource` wrapper), and `TERMINATION_PROTECTION_MAX_ATTEMPTS`. Used by `EC2Provider.deleteInstance` (SDK path) and `CloudControlProvider.delete` (CC-API path — an instance routes through Cloud Control whenever its template trips the #614 silent-drop routing) so `--remove-protection` works regardless of which delete path the instance takes; the modify WRITE lags the delete READ, so both callers flip-off + retry the delete to close the propagation window. ALSO used by `ASGProvider.delete` (issue #796): an `AWS::AutoScaling::AutoScalingGroup` whose launch template sets `DisableApiTermination: true` launches instances that survive the group's `DeleteAutoScalingGroup(ForceDelete: true)` (ASG-level DeletionProtection + ForceDelete governs only the group + scale-in protection, not EC2-level termination protection), so under `--remove-protection` the provider enumerates the group's current instances and flips each one's `DisableApiTermination` off before the force delete — the ASG's own async terminate loop then absorbs the modify-WRITE propagation lag, so no per-instance delete retry is needed there. An ASG can ALSO route via Cloud Control when its template sets a silent-drop property such as `AvailabilityZoneIds` (#614 routing) — Cloud Control's `DeleteResource` cannot `ForceDelete` a protected ASG or clear its protection, so `CloudControlProvider.delete` detects `removeProtection === true && resourceType === 'AWS::AutoScaling::AutoScalingGroup'` and delegates to `new ASGProvider().delete(...)` (the single source of truth for protected-ASG deletion), keeping the SDK and CC routing paths behaviourally identical (issue #798; CDK's L2 emits `availabilityZones` names not `AvailabilityZoneIds`, so this CC path only fires for hand-written L1 / imported templates).
7070
- **src/provisioning/unsupported-types.ts** + **unsupported-types.generated.ts** - Pre-flight unsupported-type rejection. The `.generated.ts` ships the provider-coverage Tier 3 set (`ProvisioningType: NON_PROVISIONABLE`) into the runtime, codegen'd from `docs/_generated/provider-coverage.json` by `scripts/gen-unsupported-types.ts` (`vp run gen:unsupported-types`; CI fails on drift). The hand-written `.ts` adds `isNonProvisionable()` + `unsupportedTypeIssueUrl()`; both are consulted by `CloudControlProvider.isSupportedResourceType` (rejects Tier 3) and `ProviderRegistry.validateResourceTypes` (per-type error + issue link). The `--allow-unsupported-types` escape hatch routes named types through Cloud Control via `ProviderRegistry.allowUnsupportedTypes()`.
7171
- **src/provisioning/property-coverage.ts** + **property-coverage.generated.ts** - Pre-flight property-level rejection (parallel to unsupported-types but at top-level CFn property granularity). The `.generated.ts` ships per-Tier-1-type `{ handled, silentDrop }` records, codegen'd from `tests/fixtures/cfn-schemas/*.json` + each SDK provider's `handledProperties` / `unhandledByDesign` declarations by `scripts/gen-property-coverage.ts` (`vp run gen:property-coverage`; CI fails on drift; the codegen parses provider sources via the TypeScript Compiler API so no `dist/` bootstrap is needed). The hand-written `.ts` adds `getPropertyCoverage()` + `findSilentDropProperties()` + `unsupportedPropertyIssueUrl()`; all are consulted by `ProviderRegistry.validateResourceProperties` (per-resource per-property error + 1-click GitHub issue link + dedup'd re-run command). The `--allow-unsupported-properties` escape hatch (deploy only) routes named `<Type>:<Prop>` entries past the reject via `ProviderRegistry.allowUnsupportedProperties()`. Tier 2 (Cloud Control) types are intentionally NOT in the generated map — CC forwards the full property map to AWS, so no write-side silent drop is possible.
72+
- **scripts/gen-enrichment-coverage.ts** + **docs/_generated/enrichment-coverage.{json,md}** - CC-API enrichment-coverage completeness matrix + CI critic (`vp run gen:enrichment-coverage` / `vp run audit:enrichment-coverage:check`; CI fails on drift AND on a pure-CC latent gap). Makes the enrichment-gap bug class (#844 / #864 / #865 / #866) non-regressing. The generator parses the `enrichResourceAttributes` switch in `src/provisioning/cloud-control-provider.ts` via the TypeScript Compiler API (per-`case` `enriched['Attr']` keys, flat-keys like `Endpoint.Address` matched to the nested readOnly prop `Endpoint`), cross-references each type's `readOnlyProperties` from the cached CFn schema fixtures (`tests/fixtures/cfn-schemas/*.json`), and classifies each into `enriched` / `no-computed-attr` / `sdk-fallback-gap` (gap on an SDK-backed type — only exposed on the #614 CC-fallback path, informational) / `unenriched-computed` (gap on a pure-CC type with no SDK provider — the real bug class). The `--check` critic hard-fails ONLY on `unenriched-computed`. A readOnly prop that is the type's `primaryIdentifier` is auto-classified not-a-gap (the resolver's physicalId fallback resolves it); `scripts/refresh-cfn-schemas.mjs` captures `primaryIdentifier` into the fixtures for this. The seed `ENRICHMENT_ALLOW_LIST` carves out `AWS::MSK::Cluster` (Arn == primaryIdentifier) + `AWS::Elasticsearch::Domain` (Tier-3 non-provisionable). Classifier is unit-tested (`tests/unit/scripts/gen-enrichment-coverage.test.ts`). NO AWS integ (pure static analysis / codegen).
7273
- **src/provisioning/write-only-properties.ts** - Write-only property resolution for Cloud Control UPDATE patches (issue #809). `getTopLevelWriteOnlyProperties(resourceType)` resolves the type's registry-schema `writeOnlyProperties` via `cloudformation:DescribeType`, reduced to top-level containing property names (nested `/properties/Foo/Bar` strips to `Foo`), cached per type in a module-level promise map for the deploy lifetime (only SUCCESSFUL lookups are cached; a DescribeType failure warns and falls back to an empty set for that update WITHOUT caching, so a transient throttle does not poison write-only re-inclusion for the rest of the deploy — a later update of the same type retries; a Schema-less response is a successful "no write-only props" lookup, warning-free). Consumed by `CloudControlProvider.update`, which strips these properties from the PREVIOUS side before patch generation so the patch always carries `add` ops for write-only properties in the desired state — Cloud Control applies patches read-modify-write and read handlers cannot return write-only properties, so a write-only property absent from the patch document would be dropped on every UPDATE (e.g. `AWS::ECS::Service.VolumeConfigurations` hard-fails; other types lose config silently). Mirrors terraform-provider-awscc's prior-state clearing. `clearWriteOnlyPropertiesCache()` is test-only.
7374
- **src/types/** - Type definitions (config, state, resources, assembly, etc.)
7475
- **src/utils/** - Logger, ANSI color helpers (`colors.ts` — `green` / `yellow` / `red` / `cyan` / `gray` / `bold` / `dim` inline wrappers; kept in a separate module from `logger.ts` so test files that `vi.mock('../../../src/utils/logger.js', ...)` don't accidentally strip color helpers and crash any code path that imports them), per-resource status-line formatter (`resource-line.ts` — `formatResourceLine(op, logicalId, resourceType, verbOverride?)` builds the shared `<glyph> <id> (<type>) <verb>` line printed by `cdkd deploy` / `cdkd destroy` for created / updated / deleted; every successful op renders a check ✓ — never a cross ✗ — and is distinguished by COLOR not glyph (green created / yellow updated / green-check-plus-red-verb deleted), so no success line is ever mistaken for the red ✗ "Failed to delete" failure path; `verbOverride` swaps the verb word, e.g. `'updated (metadata)'`), live progress renderer (multi-line in-flight task display), error handler (incl. `normalizeAwsError` for AWS SDK v3 synthetic UnknownError → actionable HTTP-status-keyed messages), AWS client factory, AWS region resolver (`aws-region-resolver.ts` — caches bucket-region lookups via `GetBucketLocation` so the state-bucket S3 client can be rebuilt for the bucket's actual region), state-bucket client rebuilder (`bucket-region-client.ts` — `rebuildClientForBucketRegion(client, bucket, opts)`, the single shared helper extracted in issue #827 from the three near-identical `ensureClientForBucket()` copies in `S3StateBackend` / `LockManager` / `ExportIndexStore`; does the cached-region probe + same-region short-circuit (returns `null` = keep the original client) + credential-reusing rebuild that does NOT destroy a shared client by default, with per-store knobs `destroyOldClient` / `reuseClientCredentials` / `profile` / `credentials` / `tolerateNonStandardClient`; kept in its OWN module — not folded into `aws-region-resolver.ts` — so the per-store tests' `vi.mock('aws-region-resolver.js')` of `resolveBucketRegion` is still intercepted cross-module, and each store retains its own `clientResolved` / `resolveInFlight` memoization), stack output buffer (`stack-context.ts` — `AsyncLocalStorage`-backed per-stack log buffer used by `cdkd deploy` when more than one stack is running concurrently; the logger pushes into the active buffer instead of writing to stdout, and the deploy CLI flushes each buffer atomically when its stack finishes so per-stack output blocks don't interleave), single-flight cleanup memoizer (`single-flight.ts` — wraps an async cleanup function so concurrent / repeated callers await the SAME underlying invocation; used by `cdkd local invoke` / `local start-api` to close the SIGINT-during-outer-finally race against shared mutable state like `containerId` / `servers[]` / tmpdir sets), docker subprocess helper (`docker-cmd.ts` — `getDockerCmd()` resolves the CLI binary via `CDK_DOCKER` env var for podman / finch / nerdctl parity; `runDockerStreaming` / `spawnStreaming` route every docker subprocess call through streaming spawn so BuildKit's progress output doesn't hit Node's `execFile` `maxBuffer` ceiling, mirror chunks to stdout/stderr when the logger is at debug level (`--verbose`), and reject with a `SpawnError` carrying the captured streams)

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ jobs:
9898
exit 1
9999
fi
100100
101+
# Fail CI when docs/_generated/enrichment-coverage.{json,md} is stale
102+
# relative to the CFn schema fixtures + the enrichResourceAttributes
103+
# switch in src/provisioning/cloud-control-provider.ts. The matrix records
104+
# which CC-routable types' computed readOnly Fn::GetAtt attributes are
105+
# populated by enrichment (the bug class fixed in #844/#864/#865/#866).
106+
# Same diff-on-stale shape as the integ-coverage / property-coverage
107+
# guards above. The second step is the CRITIC: it hard-fails on a pure-CC
108+
# (no-SDK-provider) latent gap that is neither enriched nor allow-listed.
109+
- name: enrichment-coverage matrix is up-to-date
110+
run: |
111+
vp run gen:enrichment-coverage
112+
if ! git diff --quiet -- docs/_generated/enrichment-coverage.json docs/_generated/enrichment-coverage.md; then
113+
echo "::error::enrichment-coverage matrix is stale — run \`vp run gen:enrichment-coverage\` and commit the regenerated docs/_generated/enrichment-coverage.{json,md}"
114+
git --no-pager diff -- docs/_generated/enrichment-coverage.json docs/_generated/enrichment-coverage.md
115+
exit 1
116+
fi
117+
- run: vp run audit:enrichment-coverage:check
118+
101119
runtime-compat:
102120
runs-on: ubuntu-latest
103121
needs: check-build-test

0 commit comments

Comments
 (0)