Skip to content

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

Open
so0k wants to merge 17 commits into
mainfrom
feat/provider-feature-availability
Open

feat: support newer provider plugin-protocol features via targetVersions (RFC-04)#296
so0k wants to merge 17 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

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 (Nested write-only attributes don't register targetVersions feature usage #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 In-repo consistency check for the provider-feature availability matrix and its hand-maintained maps #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.

Resolves Terraform CDK issues

🤖 Generated with Claude Code

@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 added a commit that referenced this pull request Jul 2, 2026
… 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 added a commit that referenced this pull request Jul 2, 2026
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>
so0k added a commit that referenced this pull request Jul 2, 2026
…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

so0k added a commit that referenced this pull request Jul 5, 2026
… 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 added a commit that referenced this pull request Jul 5, 2026
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>
so0k added a commit that referenced this pull request Jul 5, 2026
…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>
@so0k so0k force-pushed the feat/provider-feature-availability branch from ac13f35 to 71148c5 Compare July 5, 2026 12:55
so0k added a commit that referenced this pull request Jul 7, 2026
… 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 added a commit that referenced this pull request Jul 7, 2026
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>
so0k added a commit that referenced this pull request Jul 7, 2026
…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>
@so0k so0k force-pushed the feat/provider-feature-availability branch from 71148c5 to 244dd68 Compare July 7, 2026 02:56
@so0k so0k marked this pull request as ready for review July 7, 2026 12:30
@so0k so0k requested a review from a team as a code owner July 7, 2026 12:30
so0k added a commit that referenced this pull request Jul 7, 2026
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>
so0k added a commit that referenced this pull request Jul 7, 2026
…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>
so0k added a commit that referenced this pull request Jul 7, 2026
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>
Comment on lines +126 to +129
// Only managed resources extend TerraformResource (and therefore have
// registerProviderFeatureUsage available); data sources, providers, and
// ephemeral resources do not, so their write-only attributes (if any)
// only get the deprecated getter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

deliberately not exposed there because the combination doesn't occur - verified empirically across every provider in the 1.15.6 sweep dump — zero ephemeral attributes carry write_only.

@vincenthsh

Copy link
Copy Markdown
Contributor

While extending cdktn-provider-features-demo to port real aws-cdk-lib L2 construct logic onto generated awscc bindings + the cfncompat provider's intrinsic-function polyfills, I hit two cdktn-side bugs (not cfncompat bugs) worth flagging here. Full context/examples: examples/l2-kinesis-stream, README "UX notes" section.

1. Provider-function bindings that return a documented "dynamic" type are force-coerced to string, silently dropping struct-typed results

Some provider::*::* functions (e.g. cfncompat's condition_if, matching Fn::If) are documented as returning a dynamic type — either branch of Fn::If can be a string or an object. But the generated TS wrapper unconditionally routes the result through Token.asString(...):

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: Tokenization.isResolvable() doesn't recognize a Token.asString-wrapped value, so the struct's generated setter (e.g. KinesisStreamStreamEncryptionOutputReference.set internalValue) treats it as a plain object with no known keys and the whole attribute silently disappears from synth output — no warning, no error.

Repro:

new KinesisStream(this, "stream", {
  streamEncryption: CfncompatProviderFunctions.conditionIf(
    someCondition, {}, { encryptionType: "KMS", keyId: "alias/aws/kinesis" },
  ),
});

terraform plan shows no stream_encryption key at all in the planned resource.

Workaround: call cdktn.TerraformProviderFunction.invoke("cfncompat", "condition_if", [...]) directly, bypassing the generated wrapper — its return type (IResolvable, no Token.asString) is handled correctly by struct setters.

Suggested fix: only wrap a provider function's result in Token.asString(...) when its schema-declared return type is actually string-shaped; leave functions documented/typed as dynamic (condition_if, find_in_map, select, ...) as a raw IResolvable.

2. TerraformProviderFunction.invoke(...) silently drops literal null positional arguments from any call

TerraformProviderFunction.invoke's runtime validates its whole args array with a single variadic(anyValue) validator — the same one used for genuinely variadic argument lists (e.g. condition_and/condition_or's conditions). variadic's underlying listOf() unconditionally filters out null/undefined entries, which is correct for a variadic list, but wrong for a fixed-arity call: passing null for one positional argument (e.g. condition_if(condition, value_if_true, value_if_false)'s value_if_true) drops it from the argument list entirely instead of preserving its slot.

Repro:

TerraformProviderFunction.invoke("cfncompat", "condition_if", [someCondition, null, { a: 1 }])

terraform plan fails:

Error: Not enough function arguments
while calling provider::cfncompat::condition_if(condition, value_if_true, value_if_false)
Function "provider::cfncompat::condition_if" expects 3 argument(s). Missing value for "value_if_false".

(Terraform actually complains about the third argument going missing, since null — meant for the second slot — got filtered and everything shifted.)

Workaround: avoid passing null; substitute an empty struct ({}) or other schema-appropriate sentinel where possible. Not a great substitute for CloudFormation's Aws.NO_VALUE (property omission) semantics, but the closest expressible result today.

Suggested fix: TerraformProviderFunction.invoke should validate fixed-arity provider functions per-position (preserving null/undefined in their slot) rather than treating the whole args array as one variadic list; the "flatten variadic trailing args" behavior should only apply when a function is genuinely variadic.

so0k added a commit that referenced this pull request Jul 7, 2026
… 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 so0k force-pushed the feat/provider-feature-availability branch from 2365bea to 1818644 Compare July 7, 2026 17:01
so0k added a commit that referenced this pull request Jul 7, 2026
… them

TerraformProviderFunction.invoke validated its whole args array with a
single variadic(anyValue) — and variadic()/listOf() filters out
null/undefined entries, which is right for a genuinely variadic list
but wrong for a fixed-arity positional call: invoke("cfncompat",
"condition_if", [cond, null, value]) lost the null and Terraform
failed with 'Not enough function arguments' (the shift makes it blame
the wrong slot). Arguments are now validated per position, so every
slot — including null/undefined — is preserved.

Preserving the slot exposed a second latent defect: FunctionCall
renders its arguments via Array.prototype.join, which collapses
null/undefined entries to empty strings, so the slot rendered as
'(cond, , value)' instead of the Terraform null keyword.
resolveExpressionPart now renders resolved null/undefined as 'null'
(a legal expression in every Terraform context this layer serves).

Generated wrappers for genuinely variadic functions already flatten
the trailing array before calling invoke, so each element stays its
own positional slot — behavior unchanged.

Reported in PR #296 verification against the cfncompat provider's
condition_if (CloudFormation Fn::If polyfill).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jul 7, 2026
…unction results

Wrappers for functions whose schema return type is dynamic/object/map
coerced the result through Token.asString(...) as any (mirroring the
Fn asAny convention). That encoded-string token is not recognized by
Tokenization.isResolvable(), so when the result was assigned to a
struct-typed resource attribute the generated OutputReference setter
treated it as a plain object with no known keys and the attribute
silently vanished from synth output — no warning, no error.

The generated body now returns the raw TerraformProviderFunction.invoke
result (an IResolvable, which struct setters recognize and resolve
correctly); the declared return type stays any. String, number, bool
and list mappings are unchanged. Synthetic fixture gains a
condition_if-style dynamic-return function so the dynamic branch has
its own snapshot coverage.

Reported in PR #296 verification: cfncompat condition_if results
assigned to awscc struct attributes disappeared from terraform plan.

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

Copy link
Copy Markdown
Contributor

I also did a local provider-schema cache/binding-generation simulation across Terraform/OpenTofu CLI versions to sanity-check the cache-busting behavior around the new schema sections.

What I verified:

  • Cache entries are keyed by provider plus fetching CLI product/minor version, so a schema fetched with an older binary does not poison later generation with a newer binary.
  • Re-running with the same CLI version hits the existing cache entry.
  • Moving across version boundaries creates a new cache entry:
    • Terraform 1.7.x → 1.8.x busts and starts seeing provider functions.
    • OpenTofu 1.6.x → 1.7.x busts but still does not see provider functions in tofu providers schema -json.
    • OpenTofu 1.7.x → 1.8.x busts and starts seeing provider functions.
  • The simulation also had to account for the OpenTofu registry key shape (registry.opentofu.org/...) rather than assuming Terraform's registry.terraform.io/... prefix.

One documentation point I think is worth making very explicit because it is easy to misread the matrix:

OpenTofu provider-function language support starts at 1.7.0, so synth-time targetVersions validation should allow provider functions for opentofu >=1.7.0. But provider-function schema emission for binding generation starts at OpenTofu 1.8.0, so users need to run cdktn get with tofu >=1.8.0 if they expect provider-function bindings to be generated.

In other words, targeting OpenTofu 1.7 can be valid for using provider functions, but generating those bindings requires an OpenTofu 1.8+ generator binary. The current split between usage constraints and SCHEMA_EMISSION_BOUNDARIES looks intentional; I’d just document the skew prominently near the matrix/README and/or the fetch-time warning text so future maintainers don’t “fix” the apparent mismatch in the wrong direction.

Comment thread packages/cdktn/src/terraform-resource.ts Outdated

@eduardomourar eduardomourar left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overall looks good to me. Just a few non-blocking comments.

so0k and others added 17 commits July 13, 2026 08:24
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>
… 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>
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>
… them

TerraformProviderFunction.invoke validated its whole args array with a
single variadic(anyValue) — and variadic()/listOf() filters out
null/undefined entries, which is right for a genuinely variadic list
but wrong for a fixed-arity positional call: invoke("cfncompat",
"condition_if", [cond, null, value]) lost the null and Terraform
failed with 'Not enough function arguments' (the shift makes it blame
the wrong slot). Arguments are now validated per position, so every
slot — including null/undefined — is preserved.

Preserving the slot exposed a second latent defect: FunctionCall
renders its arguments via Array.prototype.join, which collapses
null/undefined entries to empty strings, so the slot rendered as
'(cond, , value)' instead of the Terraform null keyword.
resolveExpressionPart now renders resolved null/undefined as 'null'
(a legal expression in every Terraform context this layer serves).

Generated wrappers for genuinely variadic functions already flatten
the trailing array before calling invoke, so each element stays its
own positional slot — behavior unchanged.

Reported in PR #296 verification against the cfncompat provider's
condition_if (CloudFormation Fn::If polyfill).

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

Wrappers for functions whose schema return type is dynamic/object/map
coerced the result through Token.asString(...) as any (mirroring the
Fn asAny convention). That encoded-string token is not recognized by
Tokenization.isResolvable(), so when the result was assigned to a
struct-typed resource attribute the generated OutputReference setter
treated it as a plain object with no known keys and the attribute
silently vanished from synth output — no warning, no error.

The generated body now returns the raw TerraformProviderFunction.invoke
result (an IResolvable, which struct setters recognize and resolve
correctly); the declared return type stays any. String, number, bool
and list mappings are unchanged. Synthetic fixture gains a
condition_if-style dynamic-return function so the dynamic branch has
its own snapshot coverage.

Reported in PR #296 verification: cfncompat condition_if results
assigned to awscc struct attributes disappeared from terraform plan.

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

The feature argument was an open-ended string validated only at runtime.
Export a string-valued ProviderFeature enum (values unchanged, so wire
format, registry keys, and error messages stay identical) and use it in
the registerProviderFeatureUsage signature so misuse is caught at compile
time in TypeScript and surfaced as a proper enum in all jsii languages.
The runtime unknownProviderFeature guard stays as the backstop for
plain-JS and non-TypeScript jsii callers.

Generated bindings now emit
cdktn.ProviderFeature.WRITE_ONLY_ATTRIBUTES instead of the bare literal
(compile-coupled to the signature change). TerraformEphemeralResource
reuses providerFeatureLabels via the enum instead of duplicating the
label literal; the constraint/label maps themselves stay internal.

Addresses review feedback on #296.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cover the boundary conditions raised in review of #296: floors and pins
exactly at a feature's minimum, pins and floors just below it, an empty
targets object fed directly to the comparator, and prerelease-floor
subset semantics (documented as inherent npm-semver behavior). On the
emission-gap side, cover a fetching CLI exactly at a family boundary and
one with no version metadata. In the get-time plumbing tests, cover a
partial (single-product) targetVersions stamped verbatim without default
injection, a schema without CLI identity omitting the cli stamp, and
changed targetVersions between runs not forcing regeneration.

Expected values verified against the repo's own semver
(subset/lt/intersects) rather than assumed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@so0k so0k force-pushed the feat/provider-feature-availability branch from 1818644 to 30b1724 Compare July 13, 2026 00:27

@sakul-learning sakul-learning left a comment

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.

Approved.

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.

4 participants