Skip to content

feat: support newer provider plugin-protocol features via targetVersions (RFC-04)#296

Draft
so0k wants to merge 10 commits into
mainfrom
feat/provider-feature-availability
Draft

feat: support newer provider plugin-protocol features via targetVersions (RFC-04)#296
so0k wants to merge 10 commits into
mainfrom
feat/provider-feature-availability

Conversation

@so0k

@so0k so0k commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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-planningRFCS/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)

  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 bindingsTimeProviderFunctions.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 getGetOptions.targetVersionsConstructsMakerreadSchema, 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 Support 'query' CLI and provider 'list' resources as in Terraform 1.14 opentofu/opentofu#3787).
  • Follow-ups tracked, not built: hcl2cdk conversion of ephemeral {} blocks; ephemeral entry in the edge-provider schema (cross-language compile coverage); a typescript/synth-app-style integration test gated on CI Terraform >= 1.10 (repo pins 1.7.5 via mise).

🤖 Generated with Claude Code

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: 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.

so0k and others added 7 commits July 2, 2026 11:09
Merged sweep observations + source-verified CLI serializer history for the
provider plugin-protocol capability families (provider functions, ephemeral
resources, write-only attributes, resource identity, list resources, actions,
state stores) across Terraform 1.5.7-1.15.x and OpenTofu 1.6.0-1.12.x.

Source dataset for the providerFeatureConstraints map in packages/cdktn.
Sweep tooling, fixtures, report and proposal live in
open-constructs/cdktn-planning RFCS/04-provider-feature-availability
(same split as tools/generate-function-bindings/function-availability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New public base class for ephemeral resources (Terraform >=1.10 /
OpenTofu >=1.11), synthesizing into the top-level `ephemeral` key of
cdk.tf.json and an `ephemeral "type" "name"` block in HCL output.
References resolve as ephemeral.<type>.<id>.<attr>. Ephemeral blocks
support no provisioners/connection and are not importable/movable, so
those APIs are omitted.

The constructor registers ValidateFeatureTargetSupport (from #269)
unconditionally: synth fails when the project's declared targetVersions
admit releases without ephemeral resources, naming the offending range.

providerFeatureConstraints is the hand-maintained per-product constraint
map for the provider-protocol feature families, sourced from
tools/provider-feature-availability/features-matrix.json; internal by
design (generated bindings reach it by extending base classes).

Part of RFC-04 (provider feature availability), rollout item 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions

Phase 0 of RFC-04 — schema acquisition correctness:

- Type the newer providers-schema sections in @cdktn/commons:
  ephemeral_resource_schemas, functions (FunctionSignature),
  resource_identity_schemas, and the write_only attribute flag.
  List/action/state-store sections stay untyped (nothing consumes them;
  they pass through the in-place sanitizer untouched).
- Extend sanitizeProviderSchema's attribute-doubling walk to
  ephemeral_resource_schemas.
- Stamp fetched schemas with the fetching CLI ({cli_name, cli_version},
  best-effort) — emission of the new sections is a property of the
  fetching CLI, not just the provider.
- Make the experimental schema cache honest: the cache key now includes
  the fetching CLI product+minor, so upgrading the CLI re-fetches richer
  schemas instead of serving section-less ones forever.
- Warn at fetch time when the CLI is the bottleneck: if targetVersions
  admit versions whose sections the fetching binary structurally cannot
  emit, log which features will be missing and the minimum CLI versions
  that would emit them. (Nothing passes targetVersions yet; the CLI
  thread-through is a follow-up rollout item.)

Part of RFC-04 (provider feature availability), rollout item 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third schema family in buildResourceModels, mirroring the data-source
pipeline with an ephemeral_ type prefix: random_password generates
EphemeralRandomPassword in ephemeral-random-password/, extending
cdktn.TerraformEphemeralResource (which enforces targetVersions at
synth). Generated Config interfaces extend
TerraformEphemeralMetaArguments (no provisioners/connection) and no
generateConfigForImport is emitted — ephemeral resources are not
importable. Ephemeral models are appended after resources and data
sources so class-name dedup stays order-stable (zero churn in existing
snapshots).

Snapshot-tested against a real terraform 1.15.6 providers-schema
fragment of hashicorp/random (ephemeral + managed coexistence).

Part of RFC-04 (provider feature availability), rollout item 4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Providers shipping schema `functions` now get a per-provider
provider-functions namespace: TimeProviderFunctions.rfc3339Parse(ts)
renders ${provider::time::rfc3339_parse(...)}. Methods default the
provider:: namespace to the registry short name and take an optional
providerLocalName override (local names change the namespace, aliases
do not). Type mapping follows the Fn bindings rules, extended with an
object/map/dynamic -> any branch (the real time provider functions all
return objects).

Runtime flows through one public jsii chokepoint,
TerraformProviderFunction.invoke, which records usage in a dedicated
registry; TerraformStack now registers
ValidateProviderFunctionTargetSupport unconditionally, so synth fails
when provider functions are used and the declared targetVersions admit
releases without them (terraform >=1.8.0 / opentofu >=1.7.0 — OpenTofu
language support predates its schema emission, hence the asymmetry with
the fetch-time boundary).

Snapshot-tested against the real terraform 1.15.6 hashicorp/time
schema fragment plus a synthetic fixture covering variadic parameters,
primitive/list returns and reserved parameter names.

Part of RFC-04 (provider feature availability), rollout item 5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…idate usage

write_only attributes (e.g. the vault *_wo pairs) now flow into
AttributeModel. The setter stays; the getter is emitted @deprecated
with the protocol contract spelled out — providers never persist
write-only values, so every read of that token is null and the
state-backed getter is a trap. Removing it outright is JSII-breaking
for regenerated/prebuilt providers, so: deprecate now, remove at the
next prebuilt major.

Setting a write-only attribute (via setter or constructor config, which
bypasses the setter) calls the new protected
TerraformResource.registerProviderFeatureUsage hook, which registers
ValidateFeatureTargetSupport once per construct: synth fails when
targetVersions admit terraform <1.11 / opentofu <1.11 with the standard
upgrade-or-narrow-targets message. Generated bindings reach the
validation machinery only by extending base classes, per the RFC
guiding decision. Struct-level (nested) write-only attributes get the
deprecated getter but no usage registration (no construct node) —
accepted v1 limitation.

Snapshot-tested against the real terraform 1.15.6 hashicorp/vault
vault_alicloud_secret_backend fragment (secret_key_wo +
secret_key_wo_version).

Part of RFC-04 (provider feature availability), rollout item 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GetOptions gains targetVersions, threaded from cdktf.json at all three
creation sites (get, provider upgrade, runGetInDir) into
ConstructsMaker and down to readSchema — driving the fetch-time
warning when the fetching CLI structurally cannot emit schema sections
the declared targets admit.

constraints.json is additionally stamped with the targets and the
fetching CLI identity for diagnostics and cache debugging. Both fields
are diagnostics-only: targetVersions does not affect the generated
surface (full surface always generated, narrowing happens at synth), so
filterAlreadyGenerated's staleness check deliberately ignores them and
legacy constraints.json files stay valid.

Part of RFC-04 (provider feature availability), rollout item 7 — the
last item of the RFC's PR split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sakul-learning

Copy link
Copy Markdown
Contributor

Draft PR #296 review notes / verification results

I 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: 78299b4b9ece16c45ab26a0cd28dec08e8ba57e9.

Verification performed

Using the PR-head packages directly, not npmjs:

  • pnpm run assert:prhead-deps confirms cdktn resolves from the PR-head worktree and the CLI comes from the PR-head cdktn-cli bundle.
  • Terraform 1.15.7:
    • captured AWS provider schema
    • generated AWS bindings
    • ran schema inspection and generated-feature assertions
    • ran pnpm exec tsc --noEmit
    • ran pnpm run synth:all
    • ran pnpm run plan:all
  • OpenTofu 1.12.3:
    • reused the Terraform-generated bindings
    • ran generated-feature assertions
    • ran pnpm exec tsc --noEmit
    • ran pnpm run synth:all
    • ran pnpm run plan:all

The AWS provider schema from Terraform 1.15.7 exposed:

  • provider functions: 4
  • ephemeral resources: 10
  • list resources: 153
  • provider actions: 11
  • resource identities: 437

Generated bindings confirmed:

  • provider functions: yes
  • ephemeral resources: yes
  • list resources: no first-class generated bindings observed
  • provider actions: no first-class generated bindings observed
  • resource identities: no first-class generated bindings observed

OpenTofu 1.12.3 successfully planned the examples with the Terraform-generated bindings. A separate OpenTofu schema probe exposed provider functions, ephemeral resources, and resource identities, but reported list_resource_schemas: 0 and action_schemas: 0, so Terraform 1.15.7 is the better generator binary for this comprehensive AWS capability test.

UX / integration notes from the demo

These are non-blocking, but important feedback from trying to use the generated bindings in a real app:

  • I switched the examples from the AWS barrel import (./.gen/providers/aws) to direct generated-module imports (provider, provider-functions, and the specific ephemeral resource module). The barrel import path pulled in thousands of generated AWS classes and hit the Node/TypeScript OOM path during synth.
  • After that import change, I had to replace the remaining namespace-style references (provider.*, providerFunctions.*, and ephemeral...*) with direct class references like AwsProvider, AwsProviderFunctions, and EphemeralAwsSecretsmanagerRandomPassword.
  • The ephemeral output needs both Fn.ephemeralasnull(...) and sensitive: true so Terraform and OpenTofu accept the plan. Without ephemeralasnull, the ephemeral value is used in a non-ephemeral context; without sensitive, the output still fails because it is derived from secret ephemeral data.
  • I removed a trial use of AwsProviderFunctions.userAgent(...) inside AwsProvider configuration after OpenTofu reported a provider self-reference/cycle. The mistaken assumption was that provider-defined functions behave like pure static helpers that are safe to use while configuring that same provider. The generated call is still a provider-namespaced function (provider::aws::user_agent), so putting it inside the aws provider block asks Terraform/OpenTofu to evaluate an AWS provider function while the AWS provider is still being configured. The demo now exercises provider functions in outputs instead, after provider configuration exists.

Non-blocking findings / suggested follow-ups

  • targetVersions validation bypass in cdktn get path
    targetVersions is validated in parseConfig (packages/@cdktn/commons/src/config.ts), but cdktn get appears to use CdktfConfig.read() plus the raw targetVersions getter (packages/@cdktn/cli-core/src/lib/cdktf-config.ts, then passed through in packages/@cdktn/cli-core/src/lib/get.ts). That path appears to bypass the parseConfig validator, so malformed targetVersions can reach generation.

  • Ephemeral resources expose too much lifecycle surface
    TerraformEphemeralMetaArguments currently exposes lifecycle?: TerraformResourceLifecycle. Terraform ephemeral blocks only support lifecycle precondition / postcondition; they do not support the full managed-resource lifecycle surface such as ignore_changes, replace_triggered_by, create_before_destroy, etc. I’d introduce a narrowed lifecycle type for ephemeral resources, e.g. only precondition and postcondition, and use that in TerraformEphemeralMetaArguments.

  • Nested write-only attributes do not register feature usage
    The constructor-time write-only registration only iterates top-level assignable attributes in resource.configStruct.assignableAttributes. Provider schemas can put write_only inside nested blocks, not only on top-level resource config attributes, so nested write-only usage can skip registerProviderFeatureUsage("writeOnlyAttributes").

  • Generated ephemeral resources skip write-only feature usage registration
    The generator currently limits write-only registration to classes whose parent is TerraformResource; generated ephemeral resources do not get that registration path. Maybe current provider schemas do not combine ephemeral resources with write-only attributes, but since this PR is adding generalized newer-protocol support, I’d either cover the ephemeral path or explicitly document/filter why that combination cannot happen.

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.

so0k and others added 3 commits July 2, 2026 15:20
… 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>
Comment on lines +13 to +15
const SELF_REFERENCE_CYCLE_JSDOC =
"Note: provider-defined functions are evaluated by the provider itself — do not call this inside the configuration of the same provider (Terraform reports a self-referential cycle).";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if useful to have this in every JSDocs - it's a very limited edge case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants