feat: support newer provider plugin-protocol features via targetVersions (RFC-04) - #296
Conversation
Draft PR #296 review notes / verification resultsI built and published a small reusable demo harness against this PR head so the new provider-protocol features can be exercised with real generated AWS bindings: https://github.com/sakul-learning/cdktn-provider-features-demo PR head used for the run: Verification performedUsing the PR-head packages directly, not npmjs:
The AWS provider schema from Terraform
Generated bindings confirmed:
OpenTofu UX / integration notes from the demoThese are non-blocking, but important feedback from trying to use the generated bindings in a real app:
Non-blocking findings / suggested follow-ups
Overall: the core provider-function and ephemeral-resource generated bindings worked in real synth/plan flows for Terraform and OpenTofu once the UX issues above were accounted for. The largest current generated-coverage gap is that Terraform exposes AWS list resources, actions, and resource identities, but I did not see first-class generated bindings for those schema sections yet. |
… path CdktfConfig.targetVersions (used by runGetInDir) returned the raw cdktf.json value, bypassing the parseConfig validation the main get handler goes through — and a malformed range then made semver.intersects throw inside the fetch-time emission check, crashing cdktn get with a raw stack trace. Two layers: the getter now validates via commons validateTargetVersions (warn + ignore invalid targets; generation proceeds, only the warning/ stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid target range as not-wanted instead of throwing — a best-effort diagnostics path must never break get. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Terraform ephemeral blocks support only precondition/postcondition in lifecycle — createBeforeDestroy, preventDestroy, ignoreChanges and replaceTriggeredBy are state-oriented concepts that do not apply to stateless ephemeral resources. New TerraformEphemeralResourceLifecycle replaces the full TerraformResourceLifecycle on the ephemeral API; narrowing later would be jsii-breaking, so it lands before first release. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…generated JSDoc Calling a provider-defined function inside the configuration block of the same provider asks Terraform/OpenTofu to evaluate the function while that provider is still being configured — a self-referential cycle. Generated method JSDoc and TerraformProviderFunction.invoke now carry the caveat. Also documents why write-only usage registration deliberately skips ephemeral resources: write-only is a state concept, ephemeral resources have no state, and no provider schema in the RFC-04 sweep combines the two. From draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… path CdktfConfig.targetVersions (used by runGetInDir) returned the raw cdktf.json value, bypassing the parseConfig validation the main get handler goes through — and a malformed range then made semver.intersects throw inside the fetch-time emission check, crashing cdktn get with a raw stack trace. Two layers: the getter now validates via commons validateTargetVersions (warn + ignore invalid targets; generation proceeds, only the warning/ stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid target range as not-wanted instead of throwing — a best-effort diagnostics path must never break get. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Terraform ephemeral blocks support only precondition/postcondition in lifecycle — createBeforeDestroy, preventDestroy, ignoreChanges and replaceTriggeredBy are state-oriented concepts that do not apply to stateless ephemeral resources. New TerraformEphemeralResourceLifecycle replaces the full TerraformResourceLifecycle on the ephemeral API; narrowing later would be jsii-breaking, so it lands before first release. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…generated JSDoc Calling a provider-defined function inside the configuration block of the same provider asks Terraform/OpenTofu to evaluate the function while that provider is still being configured — a self-referential cycle. Generated method JSDoc and TerraformProviderFunction.invoke now carry the caveat. Also documents why write-only usage registration deliberately skips ephemeral resources: write-only is a state concept, ephemeral resources have no state, and no provider schema in the RFC-04 sweep combines the two. From draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ac13f35 to
71148c5
Compare
… path CdktfConfig.targetVersions (used by runGetInDir) returned the raw cdktf.json value, bypassing the parseConfig validation the main get handler goes through — and a malformed range then made semver.intersects throw inside the fetch-time emission check, crashing cdktn get with a raw stack trace. Two layers: the getter now validates via commons validateTargetVersions (warn + ignore invalid targets; generation proceeds, only the warning/ stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid target range as not-wanted instead of throwing — a best-effort diagnostics path must never break get. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Terraform ephemeral blocks support only precondition/postcondition in lifecycle — createBeforeDestroy, preventDestroy, ignoreChanges and replaceTriggeredBy are state-oriented concepts that do not apply to stateless ephemeral resources. New TerraformEphemeralResourceLifecycle replaces the full TerraformResourceLifecycle on the ephemeral API; narrowing later would be jsii-breaking, so it lands before first release. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…generated JSDoc Calling a provider-defined function inside the configuration block of the same provider asks Terraform/OpenTofu to evaluate the function while that provider is still being configured — a self-referential cycle. Generated method JSDoc and TerraformProviderFunction.invoke now carry the caveat. Also documents why write-only usage registration deliberately skips ephemeral resources: write-only is a state concept, ephemeral resources have no state, and no provider schema in the RFC-04 sweep combines the two. From draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71148c5 to
244dd68
Compare
Once any provider function was invoked in a long-lived process, later unrelated apps in the same process (sequential apps in one jest file being the common case) saw the process-global usedProviderFunctions set and could fail validation despite never using provider functions — the new validator registers unconditionally on every stack. The Fn registry shares the design and the same cross-App leak, but its validator is feature-flag-gated. The registries stay process-global (tokens are scope-free; per-stack attribution is impossible by design) — but App construction now resets both, scoping usage to the current synthesis session. Within one App the global registry remains by design: every stack resolves the same targetVersions from App context, so a sibling-stack validation reports the same error the using stack would. Regression tests per review: app1 uses a provider function under satisfying targets, then app2 with default targets and no usage synths clean in the same process; analogous flag-gated Fn-registry case. From PR #296 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s too The fetch-time emission-gap warning lived inside readProviderSchema, so cachedAccess bypassed it entirely on a hit: a schema cached by an old CLI was reused silently even when the project's targetVersions admit the newer sections that fetch could never have contained. The warning moves to readSchema and now runs after every provider schema resolves — cached or fresh — using the cli_name/cli_version stamps the schema itself carries (falling back to probing the current binary when stamps are absent). Regression test drives readSchema twice against a temp cache dir: one producer call, two warnings. From PR #296 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
providerFeatureConstraints (usage boundaries, packages/cdktn) and SCHEMA_EMISSION_BOUNDARIES (fetching-CLI emission boundaries, @cdktn/provider-schema) now name features-matrix.json as their source, cross-reference each other, and spell out the deliberate OpenTofu provider-functions exception (language support 1.7.0 vs schema emission 1.8.0). The matrix README gains an in-repo consumers section. An automated drift check between the maps and the matrix is tracked in #309. From PR #296 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
While extending 1. Provider-function bindings that return a documented "dynamic" type are force-coerced to string, silently dropping struct-typed resultsSome public static conditionIf(condition: any, valueIfTrue: any, valueIfFalse: any, providerLocalName?: string): any {
return cdktn.Token.asString(cdktn.TerraformProviderFunction.invoke(providerLocalName ?? "cfncompat", "condition_if", [condition, valueIfTrue, valueIfFalse])) as any;
}That's fine for the common string-return case, but breaks when the result is assigned to a struct-typed resource attribute: Repro: new KinesisStream(this, "stream", {
streamEncryption: CfncompatProviderFunctions.conditionIf(
someCondition, {}, { encryptionType: "KMS", keyId: "alias/aws/kinesis" },
),
});
Workaround: call Suggested fix: only wrap a provider function's result in 2.
|
… path CdktfConfig.targetVersions (used by runGetInDir) returned the raw cdktf.json value, bypassing the parseConfig validation the main get handler goes through — and a malformed range then made semver.intersects throw inside the fetch-time emission check, crashing cdktn get with a raw stack trace. Two layers: the getter now validates via commons validateTargetVersions (warn + ignore invalid targets; generation proceeds, only the warning/ stamp lose them), and checkSchemaEmissionGapFamilies treats an invalid target range as not-wanted instead of throwing — a best-effort diagnostics path must never break get. Found in draft-PR verification (#296 review notes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k
left a comment
There was a problem hiding this comment.
Cleanup - and rebase required
# Conflicts: # packages/@cdktn/provider-schema/package.json # pnpm-lock.yaml
Per review on #296: official JSDoc grammar puts only the type expression inside @param/@returns braces. The generated set/list semantics ("Terraform set; ordering is not guaranteed and duplicate values are removed.") and cdktn.Token.nullValue() guidance move out of docstringType/returnDocstringType into prose emitted after the parameter name (joined with the schema description) and after the @returns braces, via optional docstringNote/returnDocstringNote model fields. Nested plainArrayElementType labels are dropped entirely.
…tokens
The resolvable-resolved-object branch of resolveExpressionPart (new on
this branch) serialized map keys into the Terraform expression verbatim,
so a key containing a quote, backslash, control character, ${...} or
%{...} produced an invalid expression or a template sequence. Keys are
now serialized as Terraform quoted-string literals (JSON.stringify plus
Terraform's own $${ / %%{ escapes), with a regression test covering
hostile keys via Token.asAny({...}).
The pre-existing direct-object branches keep their behavior - tracked in
#350. Also rewords review-round narration in provider-functions.test.ts
comments to present-tense statements evaluated against the merge base.
…e() write-only coverage Addresses review 4731098548 on #296: - Reword remaining intra-PR narration ("Round N", "anymore", "used to") across validations.test.ts, write-only.test.ts, terraform-ephemeral-resource.test.ts, resource-emitter.ts and the provider-generator tests into present-tense statements that hold against the squash-merge base; regression framings now say explicitly when they refer to released (merge-base) behavior. - Drop the not.toMatch(registerProviderFeatureUsage) assertion - it tested absence of an internal call from a superseded design. - Pin that a token RESOLVING to null (cdktn.Token.nullValue()) is treated as write-only omission, complementing the literal-null tests. - Document in _runPreparingResolve why cross-stack references stay sound without this stack's own backend injection (registerIncomingCrossStackReference materializes the origin stack's backend itself).
Fifth review round: conflict resolution + cleanup review + blocking escaping fixPushed Conflict resolution — @so0k review 4731098548 — all 16 comments addressed (threaded replies on each):
@sakul-learning comment 5016340571:
Verification: provider-generator 105/105; cdktn 541/542 (the one failure is the pre-existing env-only |
…resolve eslint @typescript-eslint/no-this-alias flagged the 'const resource = this' capture. The resolve() member becomes an arrow function (lexical 'this' is the resource); toString() stays a shorthand method since its 'this' must be the wrapper token object.
|
Follow-up on two non-blocking review points at Terraform object-key escaping — non-blockingIn One serializer-completeness edge case remains: JavaScript's The follow-up is tracked in #350. This PR does not need to broaden its scope into a complete Terraform string serializer. Generated collection documentation — use the compact recursive formIn
This yields compact descriptions such as At |
…erraform collections Follows jsteinich's recommendation (relayed in #296 (comment)): collection docstring types describe the Terraform structure recursively - list -> Array<T>, set -> Set<T> - e.g. list(set(string)) documents as Array<Set<string>>. Lists lose their prose note (the notation is the default semantics); sets keep the ordering/duplicates note since the jsii type remains an array and a reader must not literally pass a JS Set. Fixture + assertions cover list(set(string)) and set(list(string)).
|
Re #296 (comment) — both points handled:
|
|
Final verification at The public demo in sakul-learning/cdktn-provider-features-demo#5 was regenerated against this PR head; its resulting AWS, AWSCC, and cfncompat bindings are committed at Verification summary:
The rare U+0008/U+000C JSON-escape case is non-blocking and tracked with the broader object-key escaping follow-up in #350. Verdict: APPROVE. |
#349 ("Upgrade dependencies for all") was branched from an older main (pre-#345 Node 22, pre-#277 cdktf removal) and now conflicts. Rather than merge its stale tree, this pulls in its genuine, non-cli upgrades so #349 can be closed as superseded by this PR: - @cdktn/commons: follow-redirects 1.15.11 -> 1.16.0, semver 7.7.4 -> 7.8.5 - @cdktn/hcl2cdk: @babel/{generator,template,types} -> 7.29.7, tsx 4.6.1 -> 4.23.1 - @cdktn/provider-schema: semver 7.7.4 -> 7.8.5 - typescript devDep floor ^5.0.0 -> ^5.0.4 across commons/hcl2cdk/hcl2json/ provider-generator/provider-schema provider-schema's semver was bumped to 7.8.5 for lockfile consistency (it postdates #349's base via #296, so #349 never saw it). The cli/cli-core bumps #349 also carried are already in this PR. Verified: build + unit tests green for commons, hcl2cdk (279 tests / 202 snapshots), hcl2json, provider-generator, provider-schema, and cdktn-cli.
🤖 Release PR — merge to cut a new release. Kept open and rebased as commits land on `main`. --- <details><summary>0.24.0</summary> ## [0.24.0](v0.23.4...v0.24.0) (2026-08-06) ### ⚠ BREAKING CHANGES * **lib:** validate Terraform function versions by default ([#362](#362)) * **deps:** Require Node 22 minimum ([#345](#345)) * **lib:** preserve symlinks in TerraformAsset walkers ([#321](#321)) * **cli:** replace node-fetch with undici ([#306](#306)) ### Features * **lib:** canonical asset hashes behind the canonicalAssetHashes feature flag ([#323](#323)) ([76dd4ff](76dd4ff)) * **lib:** validate Terraform function versions by default ([#362](#362)) ([4ac0736](4ac0736)) * support newer provider plugin-protocol features via targetVersions (RFC-04) ([#296](#296)) ([90322f9](90322f9)) ### Bug Fixes * **cli:** don't downgrade prebuilt providers on a transient registry failure ([#298](#298)) ([a960c5c](a960c5c)) * **cli:** include dev dependencies in npm version lookup ([#280](#280)) ([955204a](955204a)) * **docs:** fix stale constructs pin breaking with()/IMixin docs ([#305](#305)) ([605cf63](605cf63)) * **gha:** Allow pnpm to update the lockfile after package updates ([#318](#318)) ([a899b7c](a899b7c)) * **gha:** Fix pnpm upgrade workflow ([#335](#335)) ([e1a69fc](e1a69fc)) * **gha:** flip merged release PR label to autorelease: tagged ([#302](#302)) ([8d64f6c](8d64f6c)) * **gha:** mint the Go-publish token from the CDKTN Maintainers app ([#369](#369)) ([71921ce](71921ce)) * **gha:** mint the Go-publish token from the open-constructs-cdktn App ([#368](#368)) ([179f10c](179f10c)) * **gha:** pass the Go-publish App token as x-access-token userinfo ([#370](#370)) ([4e3ff19](4e3ff19)) * **lib:** Disallow constructs 10.8 until support can be added ([#363](#363)) ([8bdae0d](8bdae0d)) * **lib:** preserve symlinks in TerraformAsset walkers ([#321](#321)) ([6360e20](6360e20)) * typo in `moveFromId` JSDoc ([#355](#355)) ([e1cf8ce](e1cf8ce)) ### Miscellaneous Chores * **cli:** replace Ink + React with smaller-tree CLI libraries ([#264](#264)) ([a6aff7e](a6aff7e)) * **cli:** replace node-fetch with undici ([#306](#306)) ([1317141](1317141)) * **deps:** bump glob to 13.0.6 ([#307](#307)) ([47ee2bb](47ee2bb)) * **deps:** bump the github-actions-backward-compatible group with 2 updates ([#295](#295)) ([eab2a01](eab2a01)) * **deps:** replace lerna with nx ([#315](#315)) ([94999fc](94999fc)) * **deps:** Require Node 22 minimum ([#345](#345)) ([2bf315d](2bf315d)) * **deps:** update ci-info to 4.4.0 across all packages ([#329](#329)) ([557a163](557a163)) * **deps:** update fs-extra to 11.3.6 across all packages ([#328](#328)) ([95753a9](95753a9)) * **deps:** update minimatch to 10.2.5 ([#330](#330)) ([d1c1c53](d1c1c53)) * **deps:** Update sscaff to v2.0.388 ([#331](#331)) ([38ea0ba](38ea0ba)) * **deps:** Update zod to v4.4.3 ([#332](#332)) ([c10ee35](c10ee35)) * **deps:** Upgrade dependencies ([#347](#347)) ([45d3a66](45d3a66)) * remove cdktf from tests ([#277](#277)) ([dc9a8e9](dc9a8e9)) * ship Terraform 1.15.8 in the jsii-terraform image ([#367](#367)) ([deaa9b0](deaa9b0)) * Upgrade dependencies for lib ([#348](#348)) ([d616f17](d616f17)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
## Summary - Adds [`cdktn-io/terraform-provider-cfncompat`](https://github.com/cdktn-io/terraform-provider-cfncompat/releases/tag/v0.1.0) (`v0.1.0`) to `provider.json` as `cfncompat` - Adds `cfncompat` to the primary `repos` stack in `sharded-stacks.json` ## Why draft `cfncompat` exposes its functionality entirely as [17 provider-defined functions](https://github.com/cdktn-io/terraform-provider-cfncompat/releases/tag/v0.1.0) (`provider::cfncompat::*`), implementing CloudFormation intrinsic functions for use in Terraform. CDKTN doesn't yet generate bindings for provider-defined functions — that support is being added in [open-constructs/cdk-terrain#296](open-constructs/cdk-terrain#296) (RFC-04), which is still a draft upstream. Keeping this PR in draft until open-constructs/cdk-terraim#296 lands and ships in a CDKTN release, since generating bindings for this provider today would produce an empty/unusable package (no resources/data sources, only functions). ## Test plan - [x] Validated `provider.json` and `sharded-stacks.json` remain valid JSON - [x] Verified the `cfncompat` key satisfies the provider name/key matching validation in `main.ts` (`validateProviderNames`) - [ ] Re-run `yarn build && yarn synth` once dependencies support provider-function generation - [ ] Mark ready for review once open-constructs/cdk-terrain#296 lands --- _Generated by [Claude Code](https://claude.ai/code/session_01B46R1TJvB3ib7rjK3sxRZw)_ Co-authored-by: Claude <noreply@anthropic.com>
…ons (RFC-04) (open-constructs#296) Implements RFC-04 — **provider feature availability**: support the newer provider plugin-protocol capability families with `targetVersions`-aware codegen and synth-time validation. Proposal, dataset, sweep tooling and interactive report live in **open-constructs/cdktn-planning** → `RFCS/04-provider-feature-availability/` (only the merged matrix is vendored here, matching the function-availability split). Builds directly on the open-constructs#269 foundation (`targetVersions` + `ValidateFeatureTargetSupport`) and the open-constructs#268 usage-registry pattern. ## Guiding decision **Generate the full surface the schema offers; narrow per project at synth time via `targetVersions`.** Generation-time filtering would fork the generated API by project configuration — impossible for prebuilt providers and hostile to caching. Constraints baked into the validations are the per-product `>=` ranges from the sweep dataset. ## Review guide (one commit per RFC rollout item) 1. **`chore: vendor provider-feature availability matrix`** — dataset digest + README only, no behavior change. 2. **`feat(lib): TerraformEphemeralResource`** — new public base class synthesizing to the top-level `ephemeral` key (JSON + HCL renderer), refs as `ephemeral.<type>.<id>.<attr>`, no provisioners/connection/import/move. Constructor registers the target-version validation unconditionally (new API surface — only fires on use, no feature flag per RFC). Internal `providerFeatureConstraints` map sourced from the vendored matrix. 3. **`feat(provider-generator): acquire newer provider-protocol schema sections`** (Phase 0, a bugfix on its own) — commons types for `functions` / `ephemeral_resource_schemas` / `resource_identity_schemas` / `write_only`; sanitizer walks ephemeral schemas; fetched schemas stamped with the fetching CLI; **cache key now includes CLI product+minor** (a schema fetched once with an old CLI no longer poisons every later generation); fetch-time warning when the fetching binary structurally cannot emit sections the targets admit. 4. **`feat(provider-generator): generate ephemeral resource bindings`** — third schema family mirroring the `data_` pipeline (`EphemeralRandomPassword` in `ephemeral-random-password/`), config extends `TerraformEphemeralMetaArguments`, no `generateConfigForImport`. Zero churn in existing snapshots (ephemeral models append after resources/data sources; class-name dedup is order-dependent). 5. **`feat(provider-generator): generate provider-defined function bindings`** — `TimeProviderFunctions.rfc3339Parse(ts)` → `${provider::time::rfc3339_parse(...)}`; namespace defaults to the registry short name with a `providerLocalName` override (local names change the namespace, aliases don't). Runtime flows through one public jsii chokepoint (`TerraformProviderFunction.invoke`) feeding a usage registry; `TerraformStack` validates usage against `terraform >=1.8.0` / `opentofu >=1.7.0` — note the deliberate asymmetry: OpenTofu *language* support (1.7.0) predates its schema *emission* (1.8.0), so generation and validation use different boundaries. 6. **`feat(provider-generator): deprecate write-only attribute getters, validate usage`** — providers never persist write-only values (every read is `null` by protocol contract), so the state-backed getter is a trap: emitted `@deprecated` now, removal rides the next prebuilt major (JSII-breaking otherwise). Setting one (setter or constructor config) registers usage via a new protected `TerraformResource.registerProviderFeatureUsage` hook — generated code reaches the validation machinery only by extending base classes. 7. **`feat(cli): thread targetVersions into cdktn get`** — `GetOptions.targetVersions` → `ConstructsMaker` → `readSchema`, driving the Phase 0 fetch-time warning; `constraints.json` gains diagnostic stamps (`targetVersions`, fetching `cli`) without affecting the `filterAlreadyGenerated` staleness logic (targets don't change codegen output, so they must not force regeneration). ## Test coverage - Generator: snapshot tests against **real `terraform providers schema -json` fragments** from the sweep (random ephemeral, time functions incl. object returns, vault `*_wo`), plus a synthetic fixture for variadic/reserved-name mapping branches. Zero churn in pre-existing snapshots. - Core: validation matrices mirroring `validations.test.ts` (admit/exclude per product, hint text, the OpenTofu 1.7.0 asymmetry, registry resets). - Schema: emission-gap logic matrix, cache-key suffix behavior, sanitizer walk over ephemeral schemas; CLI stamps in network snapshots are stubbed (environment-dependent). ## Known/deferred - `matchers.test.ts › toPlanSuccessfully` fails in this environment before and after these changes (downloads the real docker provider; verified pre-existing via stash on the unmodified tree). - Deferred per RFC Phase 4: resource identity consumption (schema plumbing already carried), list resources / actions / state stores (Terraform-only today; re-evaluate on opentofu/opentofu#3787). - **Follow-ups tracked as issues**: open-constructs#308 (nested write-only registration), open-constructs#309 (matrix drift check / in-repo tooling), open-constructs#310 (`hcl2cdk` ephemeral conversion), open-constructs#311 (edge-provider schema cross-language coverage), open-constructs#312 (ephemeral integration test gated on TF >= 1.10), open-constructs#313 (pre-existing HCL renderer defects), open-constructs#304 (first-class `provider_meta`; open-constructs#314 closed as its duplicate), open-constructs#316 (remove the deprecated write-only getters at the next prebuilt-provider major — the Phase 3 end state). New this round: open-constructs#336 (structural typing for object-shaped function params/returns), open-constructs#337 (runtime CI thin slice on terraform 1.15.x + opentofu 1.12.x — CI's Terraform ceiling is 1.6.5, so the new schema sections are covered by fixture unit tests, the edge-provider compile slice below, and the external demo harness until open-constructs#337 lands). ## Review findings addressed (from the demo-harness verification comment) - **Fixed — `targetVersions` validation bypass**: `CdktfConfig.targetVersions` (the `runGetInDir` path) now validates via commons `validateTargetVersions` (warn + ignore), and the emission-gap check treats invalid ranges as not-wanted instead of throwing — a malformed range previously crashed `cdktn get` via `semver.intersects`. - **Fixed — ephemeral lifecycle narrowed**: new `TerraformEphemeralResourceLifecycle` (`precondition`/`postcondition` only) replaces the full managed-resource lifecycle on the ephemeral API, before it ships as jsii surface. - **Fixed (docs) — provider-fn self-reference cycle**: generated provider-function JSDoc and `TerraformProviderFunction.invoke` now warn against calling a provider's functions inside that same provider's configuration block. - **Documented — ephemeral × write-only**: registration deliberately skips ephemeral resources — write-only is a state concept, ephemeral resources have no state, and no schema in the sweep (incl. vault's 16 ephemeral resources) combines the two. - **Follow-up — nested write-only registration** (open-constructs#308): `write_only` inside nested blocks gets the deprecated getter but skips usage registration; deep config scanning deserves its own PR (a miss degrades to the plan-time error, not silent breakage). - **Pre-existing / by design**: the AWS barrel-import OOM predates this PR (lazy-index is the existing mitigation); `Fn.ephemeralasnull` + `sensitive` on outputs is Terraform semantics (docs candidate); list resources / actions / resource identity codegen is the RFC Phase 4 deferral noted above. ## Second review round addressed - **Fixed — cross-App usage-registry leak**: `App` construction now resets both usage registries (a new App = a new synthesis session). Within an App the process-global registry is by design — every stack resolves the same `targetVersions` from App context — but usage no longer leaks into later, unrelated Apps in the same process (the reviewer's jest scenario). Regression tests cover the exact sequence; the flag-gated `Fn` registry had the same latent leak and is reset too. - **Fixed — emission-gap warning on cache hits**: the warning moved from `readProviderSchema` into `readSchema`, running for cached *and* fresh schemas using the `cli_name`/`cli_version` stamps the schema carries. Regression test: two `readSchema` calls against a cache dir → one producer call, two warnings. - **Docs — matrix as source of truth**: both hand-maintained maps now name `features-matrix.json` as their source and cross-reference each other; automated drift check tracked in open-constructs#309. ## Third review round addressed (cfncompat/awscc demo findings) - **Fixed — dynamic-typed provider-function returns coerced to string**: generated wrappers for `dynamic`/`object`/`map` return types no longer wrap in `Token.asString(...)`; they return the raw `invoke()` `IResolvable`, which `Tokenization.isResolvable()` recognizes — struct-typed attribute assignments (`OutputReference.internalValue`) no longer vanish silently from synth output. - **Fixed — `invoke()` dropped literal `null` positional arguments**: arguments are now validated per position (variadic flattening stays in the generated wrappers where the signature is known), and a latent third bug this exposed was fixed too: `FunctionCall` rendered args via `Array.prototype.join`, which collapses `null` entries to empty strings — resolved `null`/`undefined` now render as the Terraform `null` keyword. Regression tests pin the exact `provider::cfncompat::condition_if(true, null, {"a" = 1})` rendering. ## Fourth review round addressed (jsteinich's CHANGES_REQUESTED) Seven commits on top of the reviewed head, one per finding group (inline replies on each thread carry the details): 1. **Schema fidelity** — `FunctionParameter` gains `is_nullable`, `FunctionSignature` gains `deprecation_message`, and both drop `description_kind` (verified against `internal/command/jsonfunction/{function,parameter}.go` and the OpenTofu equivalent: that field does not exist on function JSON; `allow_null_value`/`allow_unknown_values` are protocol-only names — the old comment was wrong). OpenTofu emits no `deprecation_message` at all (divergence, documented). 2. **Recursive, honest type mapping** — collections recurse (`list(number)` → `number[]` via `Token.asNumberList`, `set(bool)` param → `Array<boolean | IResolvable>`, nested compose); object/map/dynamic returns are declared `IResolvable` instead of `any` (property access on a token is now a compile error); `is_nullable` consumed (trailing → jsii-optional, mid-position → `any`, variadic → `any[]`, docstrings say `T | null`); `deprecation_message` → `@deprecated`; name collisions hard-fail generation with an actionable error. Structural helpers for object shapes: open-constructs#336. 3. **App-owned usage registries** — recording moved to token-resolve time keyed by the resolving stack's `node.root` (WeakMaps); the App-constructor resets and the "pathological" disclaimer are gone; the interleaved-App false negative is structurally impossible and pinned by regression tests for both registries. 4. **Functions move onto the provider class** — `time.functions.rfc3339Parse(ts)` via a memoized getter fed by `this.terraformResourceType` (the exact `required_providers` key, so the namespace is correct by construction); the static class and `providerLocalName` parameter are gone, mooting the `providerAlias` naming question. Per-method self-reference JSDoc dropped (kept once on `invoke`; docs PR: open-constructs/cdk-terrain-docs#29). 5. **Cleanups** — `registerProviderFeatureUsage` moved to `TerraformElement` (ephemeral resources reuse it); enum casts replaced by a membership guard + direct `ProviderFeature` indexing; hints get their own map instead of runtime label-mangling (messages byte-identical); write-only registration treats explicit `null` as omission (`!= null`, both constructor and setter paths, four pinned tests). 6. **Demo-validation fixes** — exercising the generated bindings in a real app ([demo PR](sakul-learning/cdktn-provider-features-demo#5)) caught three bugs snapshots couldn't: the sibling import needed `/index` under `nodenext`; the wrapper constructor's TS parameter property is rejected by Node's native type-stripping (jsii accepts it — Node is the stricter consumer); variadic rest params need `Array<T>` for union element types. 7. **Edge-provider compile coverage** (closes open-constructs#311) — the edge schema now carries nine functions (one per generator branch, incl. all three nullability shapes and deprecation), an ephemeral resource, and a `write_only` attribute; its bindings ride the existing postbuild jsii + pacmak flow for all five languages and the `test/<lang>/edge` native-compile suites, independent of CI's Terraform version. **External validation** (full matrix in the [demo PR](sakul-learning/cdktn-provider-features-demo#5)): terraform 1.15.7 and opentofu 1.12.3 generate byte-identical bindings modulo registry-host doc links; schema cache keys per CLI product+minor; omitted/explicit/mid-position nullable args all render the bare `null` keyword in position; generated bindings pass tsc + jsii + pacmak; the targetVersions validation error was demonstrated live through the resolve-time registry. ## Resolves Terraform CDK issues - hashicorp/terraform-cdk#3886 - hashicorp/terraform-cdk#3686 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
🤖 Release PR — merge to cut a new release. Kept open and rebased as commits land on `main`. --- <details><summary>0.24.0</summary> ## [0.24.0](open-constructs/cdk-terrain@v0.23.4...v0.24.0) (2026-08-06) ### ⚠ BREAKING CHANGES * **lib:** validate Terraform function versions by default ([open-constructs#362](open-constructs#362)) * **deps:** Require Node 22 minimum ([open-constructs#345](open-constructs#345)) * **lib:** preserve symlinks in TerraformAsset walkers ([open-constructs#321](open-constructs#321)) * **cli:** replace node-fetch with undici ([open-constructs#306](open-constructs#306)) ### Features * **lib:** canonical asset hashes behind the canonicalAssetHashes feature flag ([open-constructs#323](open-constructs#323)) ([76dd4ff](open-constructs@76dd4ff)) * **lib:** validate Terraform function versions by default ([open-constructs#362](open-constructs#362)) ([4ac0736](open-constructs@4ac0736)) * support newer provider plugin-protocol features via targetVersions (RFC-04) ([open-constructs#296](open-constructs#296)) ([90322f9](open-constructs@90322f9)) ### Bug Fixes * **cli:** don't downgrade prebuilt providers on a transient registry failure ([open-constructs#298](open-constructs#298)) ([a960c5c](open-constructs@a960c5c)) * **cli:** include dev dependencies in npm version lookup ([open-constructs#280](open-constructs#280)) ([955204a](open-constructs@955204a)) * **docs:** fix stale constructs pin breaking with()/IMixin docs ([open-constructs#305](open-constructs#305)) ([605cf63](open-constructs@605cf63)) * **gha:** Allow pnpm to update the lockfile after package updates ([open-constructs#318](open-constructs#318)) ([a899b7c](open-constructs@a899b7c)) * **gha:** Fix pnpm upgrade workflow ([open-constructs#335](open-constructs#335)) ([e1a69fc](open-constructs@e1a69fc)) * **gha:** flip merged release PR label to autorelease: tagged ([open-constructs#302](open-constructs#302)) ([8d64f6c](open-constructs@8d64f6c)) * **gha:** mint the Go-publish token from the CDKTN Maintainers app ([open-constructs#369](open-constructs#369)) ([71921ce](open-constructs@71921ce)) * **gha:** mint the Go-publish token from the open-constructs-cdktn App ([open-constructs#368](open-constructs#368)) ([179f10c](open-constructs@179f10c)) * **gha:** pass the Go-publish App token as x-access-token userinfo ([open-constructs#370](open-constructs#370)) ([4e3ff19](open-constructs@4e3ff19)) * **lib:** Disallow constructs 10.8 until support can be added ([open-constructs#363](open-constructs#363)) ([8bdae0d](open-constructs@8bdae0d)) * **lib:** preserve symlinks in TerraformAsset walkers ([open-constructs#321](open-constructs#321)) ([6360e20](open-constructs@6360e20)) * typo in `moveFromId` JSDoc ([open-constructs#355](open-constructs#355)) ([e1cf8ce](open-constructs@e1cf8ce)) ### Miscellaneous Chores * **cli:** replace Ink + React with smaller-tree CLI libraries ([open-constructs#264](open-constructs#264)) ([a6aff7e](open-constructs@a6aff7e)) * **cli:** replace node-fetch with undici ([open-constructs#306](open-constructs#306)) ([1317141](open-constructs@1317141)) * **deps:** bump glob to 13.0.6 ([open-constructs#307](open-constructs#307)) ([47ee2bb](open-constructs@47ee2bb)) * **deps:** bump the github-actions-backward-compatible group with 2 updates ([open-constructs#295](open-constructs#295)) ([eab2a01](open-constructs@eab2a01)) * **deps:** replace lerna with nx ([open-constructs#315](open-constructs#315)) ([94999fc](open-constructs@94999fc)) * **deps:** Require Node 22 minimum ([open-constructs#345](open-constructs#345)) ([2bf315d](open-constructs@2bf315d)) * **deps:** update ci-info to 4.4.0 across all packages ([open-constructs#329](open-constructs#329)) ([557a163](open-constructs@557a163)) * **deps:** update fs-extra to 11.3.6 across all packages ([open-constructs#328](open-constructs#328)) ([95753a9](open-constructs@95753a9)) * **deps:** update minimatch to 10.2.5 ([open-constructs#330](open-constructs#330)) ([d1c1c53](open-constructs@d1c1c53)) * **deps:** Update sscaff to v2.0.388 ([open-constructs#331](open-constructs#331)) ([38ea0ba](open-constructs@38ea0ba)) * **deps:** Update zod to v4.4.3 ([open-constructs#332](open-constructs#332)) ([c10ee35](open-constructs@c10ee35)) * **deps:** Upgrade dependencies ([open-constructs#347](open-constructs#347)) ([45d3a66](open-constructs@45d3a66)) * remove cdktf from tests ([open-constructs#277](open-constructs#277)) ([dc9a8e9](open-constructs@dc9a8e9)) * ship Terraform 1.15.8 in the jsii-terraform image ([open-constructs#367](open-constructs#367)) ([deaa9b0](open-constructs@deaa9b0)) * Upgrade dependencies for lib ([open-constructs#348](open-constructs#348)) ([d616f17](open-constructs@d616f17)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
Implements RFC-04 — provider feature availability: support the newer provider plugin-protocol capability families with
targetVersions-aware codegen and synth-time validation. Proposal, dataset, sweep tooling and interactive report live in open-constructs/cdktn-planning →RFCS/04-provider-feature-availability/(only the merged matrix is vendored here, matching the function-availability split).Builds directly on the #269 foundation (
targetVersions+ValidateFeatureTargetSupport) and the #268 usage-registry pattern.Guiding decision
Generate the full surface the schema offers; narrow per project at synth time via
targetVersions. Generation-time filtering would fork the generated API by project configuration — impossible for prebuilt providers and hostile to caching. Constraints baked into the validations are the per-product>=ranges from the sweep dataset.Review guide (one commit per RFC rollout item)
chore: vendor provider-feature availability matrix— dataset digest + README only, no behavior change.feat(lib): TerraformEphemeralResource— new public base class synthesizing to the top-levelephemeralkey (JSON + HCL renderer), refs asephemeral.<type>.<id>.<attr>, no provisioners/connection/import/move. Constructor registers the target-version validation unconditionally (new API surface — only fires on use, no feature flag per RFC). InternalproviderFeatureConstraintsmap sourced from the vendored matrix.feat(provider-generator): acquire newer provider-protocol schema sections(Phase 0, a bugfix on its own) — commons types forfunctions/ephemeral_resource_schemas/resource_identity_schemas/write_only; sanitizer walks ephemeral schemas; fetched schemas stamped with the fetching CLI; cache key now includes CLI product+minor (a schema fetched once with an old CLI no longer poisons every later generation); fetch-time warning when the fetching binary structurally cannot emit sections the targets admit.feat(provider-generator): generate ephemeral resource bindings— third schema family mirroring thedata_pipeline (EphemeralRandomPasswordinephemeral-random-password/), config extendsTerraformEphemeralMetaArguments, nogenerateConfigForImport. Zero churn in existing snapshots (ephemeral models append after resources/data sources; class-name dedup is order-dependent).feat(provider-generator): generate provider-defined function bindings—TimeProviderFunctions.rfc3339Parse(ts)→${provider::time::rfc3339_parse(...)}; namespace defaults to the registry short name with aproviderLocalNameoverride (local names change the namespace, aliases don't). Runtime flows through one public jsii chokepoint (TerraformProviderFunction.invoke) feeding a usage registry;TerraformStackvalidates usage againstterraform >=1.8.0/opentofu >=1.7.0— note the deliberate asymmetry: OpenTofu language support (1.7.0) predates its schema emission (1.8.0), so generation and validation use different boundaries.feat(provider-generator): deprecate write-only attribute getters, validate usage— providers never persist write-only values (every read isnullby protocol contract), so the state-backed getter is a trap: emitted@deprecatednow, removal rides the next prebuilt major (JSII-breaking otherwise). Setting one (setter or constructor config) registers usage via a new protectedTerraformResource.registerProviderFeatureUsagehook — generated code reaches the validation machinery only by extending base classes.feat(cli): thread targetVersions into cdktn get—GetOptions.targetVersions→ConstructsMaker→readSchema, driving the Phase 0 fetch-time warning;constraints.jsongains diagnostic stamps (targetVersions, fetchingcli) without affecting thefilterAlreadyGeneratedstaleness logic (targets don't change codegen output, so they must not force regeneration).Test coverage
terraform providers schema -jsonfragments from the sweep (random ephemeral, time functions incl. object returns, vault*_wo), plus a synthetic fixture for variadic/reserved-name mapping branches. Zero churn in pre-existing snapshots.validations.test.ts(admit/exclude per product, hint text, the OpenTofu 1.7.0 asymmetry, registry resets).Known/deferred
matchers.test.ts › toPlanSuccessfullyfails in this environment before and after these changes (downloads the real docker provider; verified pre-existing via stash on the unmodified tree).hcl2cdkephemeral conversion), Edge-provider schema: cross-language compile coverage for ephemeral resources, provider functions, write-only attributes #311 (edge-provider schema cross-language coverage), Integration test: ephemeral resources end-to-end, gated on Terraform >= 1.10 #312 (ephemeral integration test gated on TF >= 1.10), fix(lib): HCL synth corrupts or drops core blocks (terraform{} extras, top-level overrides, lifecycle conditions) #313 (pre-existing HCL renderer defects), lib: add addProviderMeta() helper for terraform { provider_meta } blocks (value stays any) #304 (first-classprovider_meta; feat(lib): first-class provider_meta support (aws user_agent module attribution) #314 closed as its duplicate), Remove write-only attribute getters at the next prebuilt-provider major (deprecated in #296) #316 (remove the deprecated write-only getters at the next prebuilt-provider major — the Phase 3 end state). New this round: provider-generator: structural typing for object-shaped provider-function parameters and returns #336 (structural typing for object-shaped function params/returns), CI: thin-slice runtime coverage for newer-protocol features on current stable Terraform AND OpenTofu #337 (runtime CI thin slice on terraform 1.15.x + opentofu 1.12.x — CI's Terraform ceiling is 1.6.5, so the new schema sections are covered by fixture unit tests, the edge-provider compile slice below, and the external demo harness until CI: thin-slice runtime coverage for newer-protocol features on current stable Terraform AND OpenTofu #337 lands).Review findings addressed (from the demo-harness verification comment)
targetVersionsvalidation bypass:CdktfConfig.targetVersions(therunGetInDirpath) now validates via commonsvalidateTargetVersions(warn + ignore), and the emission-gap check treats invalid ranges as not-wanted instead of throwing — a malformed range previously crashedcdktn getviasemver.intersects.TerraformEphemeralResourceLifecycle(precondition/postconditiononly) replaces the full managed-resource lifecycle on the ephemeral API, before it ships as jsii surface.TerraformProviderFunction.invokenow warn against calling a provider's functions inside that same provider's configuration block.write_onlyinside nested blocks gets the deprecated getter but skips usage registration; deep config scanning deserves its own PR (a miss degrades to the plan-time error, not silent breakage).Fn.ephemeralasnull+sensitiveon outputs is Terraform semantics (docs candidate); list resources / actions / resource identity codegen is the RFC Phase 4 deferral noted above.Second review round addressed
Appconstruction now resets both usage registries (a new App = a new synthesis session). Within an App the process-global registry is by design — every stack resolves the sametargetVersionsfrom App context — but usage no longer leaks into later, unrelated Apps in the same process (the reviewer's jest scenario). Regression tests cover the exact sequence; the flag-gatedFnregistry had the same latent leak and is reset too.readProviderSchemaintoreadSchema, running for cached and fresh schemas using thecli_name/cli_versionstamps the schema carries. Regression test: tworeadSchemacalls against a cache dir → one producer call, two warnings.features-matrix.jsonas their source and cross-reference each other; automated drift check tracked in In-repo consistency check for the provider-feature availability matrix and its hand-maintained maps #309.Third review round addressed (cfncompat/awscc demo findings)
dynamic/object/mapreturn types no longer wrap inToken.asString(...); they return the rawinvoke()IResolvable, whichTokenization.isResolvable()recognizes — struct-typed attribute assignments (OutputReference.internalValue) no longer vanish silently from synth output.invoke()dropped literalnullpositional arguments: arguments are now validated per position (variadic flattening stays in the generated wrappers where the signature is known), and a latent third bug this exposed was fixed too:FunctionCallrendered args viaArray.prototype.join, which collapsesnullentries to empty strings — resolvednull/undefinednow render as the Terraformnullkeyword. Regression tests pin the exactprovider::cfncompat::condition_if(true, null, {"a" = 1})rendering.Fourth review round addressed (jsteinich's CHANGES_REQUESTED)
Seven commits on top of the reviewed head, one per finding group (inline replies on each thread carry the details):
FunctionParametergainsis_nullable,FunctionSignaturegainsdeprecation_message, and both dropdescription_kind(verified againstinternal/command/jsonfunction/{function,parameter}.goand the OpenTofu equivalent: that field does not exist on function JSON;allow_null_value/allow_unknown_valuesare protocol-only names — the old comment was wrong). OpenTofu emits nodeprecation_messageat all (divergence, documented).list(number)→number[]viaToken.asNumberList,set(bool)param →Array<boolean | IResolvable>, nested compose); object/map/dynamic returns are declaredIResolvableinstead ofany(property access on a token is now a compile error);is_nullableconsumed (trailing → jsii-optional, mid-position →any, variadic →any[], docstrings sayT | null);deprecation_message→@deprecated; name collisions hard-fail generation with an actionable error. Structural helpers for object shapes: provider-generator: structural typing for object-shaped provider-function parameters and returns #336.node.root(WeakMaps); the App-constructor resets and the "pathological" disclaimer are gone; the interleaved-App false negative is structurally impossible and pinned by regression tests for both registries.time.functions.rfc3339Parse(ts)via a memoized getter fed bythis.terraformResourceType(the exactrequired_providerskey, so the namespace is correct by construction); the static class andproviderLocalNameparameter are gone, mooting theproviderAliasnaming question. Per-method self-reference JSDoc dropped (kept once oninvoke; docs PR: docs(concepts): provider-defined functions + caveats cdk-terrain-docs#29).registerProviderFeatureUsagemoved toTerraformElement(ephemeral resources reuse it); enum casts replaced by a membership guard + directProviderFeatureindexing; hints get their own map instead of runtime label-mangling (messages byte-identical); write-only registration treats explicitnullas omission (!= null, both constructor and setter paths, four pinned tests)./indexundernodenext; the wrapper constructor's TS parameter property is rejected by Node's native type-stripping (jsii accepts it — Node is the stricter consumer); variadic rest params needArray<T>for union element types.write_onlyattribute; its bindings ride the existing postbuild jsii + pacmak flow for all five languages and thetest/<lang>/edgenative-compile suites, independent of CI's Terraform version.External validation (full matrix in the demo PR): terraform 1.15.7 and opentofu 1.12.3 generate byte-identical bindings modulo registry-host doc links; schema cache keys per CLI product+minor; omitted/explicit/mid-position nullable args all render the bare
nullkeyword in position; generated bindings pass tsc + jsii + pacmak; the targetVersions validation error was demonstrated live through the resolve-time registry.Resolves Terraform CDK issues
🤖 Generated with Claude Code