feat: support newer provider plugin-protocol features via targetVersions (RFC-04)#296
feat: support newer provider plugin-protocol features via targetVersions (RFC-04)#296so0k wants to merge 30 commits into
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>
| // 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. |
There was a problem hiding this comment.
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.
|
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>
1818644 to
30b1724
Compare
| // `Tokenization.isResolvable()` downstream - see the mapReturnType | ||
| // docstring above). `IResolvable` is assignable to `any`, so no cast is | ||
| // needed either. | ||
| return { |
There was a problem hiding this comment.
Similar to the parameters case, this isn't that useful to consumers.
There was a problem hiding this comment.
Returns are honest as of 80945aa: list/set(string|number) unwrap via Token.asList/asNumberList; everything that can't go through a lossless Token helper (object/map/dynamic/other collections) is declared cdktn.IResolvable instead of any — so property access on a token (result.hours) is now a compile error instead of a silent undefined. This also surfaced in the demo migration, where the stricter types caught two real call sites.
There was a problem hiding this comment.
We do have some token supported map types which could be supported similar to the list types
There was a problem hiding this comment.
Could be useful to add a return type to the documentation similar to what is being done for the parameter types.
There was a problem hiding this comment.
You're right, we under-used our own toolbox — 08433c6 wires map(string|number|bool) returns through Token.asStringMap/asNumberMap/asBooleanMap (and map(*) through asAnyMap), so typed map results now feed typed map parameters directly. Composition pinned by test.
There was a problem hiding this comment.
Added in 08433c6 — every generated method now carries @returns {<type>}.
The FunctionParameter/FunctionSignature types in commons diverged from what `providers schema -json` actually emits (verified against hashicorp/terraform internal/command/jsonfunction and the OpenTofu equivalent): - parameters carry `is_nullable` (from the protocol's AllowNullValue) - now modeled; the comment claiming `allow_null_value` / `allow_unknown_values` could appear in the JSON was wrong (those are plugin-protocol names; `allow_unknown_values` has no JSON representation at all). - function signatures carry `deprecation_message` (Terraform only; OpenTofu doesn't emit it - divergence noted in a comment). - neither functions nor their parameters have `description_kind` in the JSON (that field exists only on attribute/block schemas) - removed. Also spell out why list_resource_schemas / action_schemas / state_store_schemas stay untyped (real CLI sections, preserved verbatim through parse/sanitize/cache, consumption deferred per RFC-04 Phase 4). Consumption of the new fields lands in the follow-up commit.
…pping Addresses the type-fidelity review findings on the provider-function generator: - Parameter mapping is now recursive: bool becomes `boolean | IResolvable` (input-position union, matching the resource attribute convention), list/set recurse on their element type (list(number) -> number[], set(bool) -> Array<boolean | IResolvable>, list(list(string)) -> string[][]), and map of a primitive gets a real indexed-signature type. Unrepresentable elements widen to any[] but the docstring keeps the honest element shape. - Return mapping: list/set(string) -> Token.asList, list/set(number) -> Token.asNumberList; every other non-primitive shape (object, map, dynamic, other collections) is declared `IResolvable` instead of `any` - the raw invoke() result was already returned, but `any` invited property access on a token (result.hours) that silently yields undefined; IResolvable surfaces that at compile time. - `is_nullable` is consumed: trailing-compatible nullable fixed params become jsii-optional (omission renders the Terraform null keyword), mid-position nullable params widen to any, nullable variadics widen to any[]; docstrings state `T | null` in all three cases. - `deprecation_message` is surfaced as an `@deprecated` JSDoc tag. - Generation now hard-fails with an actionable error when two function names or two parameter names sanitize to the same identifier, instead of silently shadowing/dropping one. Fixture coverage: number/bool/nested collections, object returns, typed and untyped maps, deprecation, all three nullability shapes, and unit tests pinning both collision errors. Snapshot churn is confined to provider-function outputs.
The Fn.* and provider-function usage registries were process-global Sets reset by App's constructor. That approximation had a real false negative (flagged in review): construct App A, use a gated function, construct App B before synthesizing A - B's constructor reset wiped A's recorded usage, so unsupported target versions passed validation silently. Usage is now recorded at token-RESOLVE time in FunctionCall.resolve(): IResolveContext.scope is always the resolving TerraformStack, so records are keyed by its node.root - the owning App - in WeakMaps. Validations read only their own root's records. Ownership is structural: interleaved Apps cannot cross-contaminate, the App-constructor resets (and the "known limitation" disclaimer) are gone, and App.synth()'s ordering guarantee (prepareStack's preparing resolve for all stacks runs before any stack's validations) means recorded usage is always visible to the validations that read it. Semantics sharpened as a side effect: a function token that never lands in any rendered output is no longer validated - only usage that actually reaches synthesized configuration matters. Regression tests pin the interleaved-App scenario for both registries.
…lass Provider-defined function bindings become instance methods reachable from the generated provider construct: const time = new TimeProvider(this, "time"); time.functions.rfc3339Parse(input); replacing the static class + trailing `providerLocalName?` parameter. The `functions` getter (emitted only for providers that declare functions) constructs the wrapper with `this.terraformResourceType` - the exact value terraform-provider.ts keys required_providers off, so the namespace is correct by construction, including cdktf.json `name` constraint overrides. The wrapper's constructor still takes a local name, remaining usable directly for exotic setups. This resolves the review discussion around the parameter's naming (`providerLocalName` vs `providerAlias`) by eliminating it: users no longer supply a namespace at all, and provider config aliases - which never selected function namespaces - can't be confused into that role. It also nudges users toward having the provider declared before calling its functions, which required_providers rendering needs anyway. The per-method self-reference-cycle JSDoc is dropped (review consensus: too narrow an edge case to repeat on every method); the warning remains once on TerraformProviderFunction.invoke and moves to the docs. Generation hard-fails with an actionable error if a provider's own config schema would emit a `functions` attribute getter that the new getter would collide with (no provider in the sweep does).
…xing, deliberate write-only null rule Four review cleanups on the provider-feature validation machinery: - registerProviderFeatureUsage (and its per-instance dedup set) moves from TerraformResource up to TerraformElement, so any element subclass is covered without moving it again; TerraformEphemeralResource now uses the hook instead of hand-building the same validation. - The Record<string, ...> casts around the feature maps are gone: a hasOwnProperty membership guard (for jsii callers passing arbitrary strings at runtime) followed by direct ProviderFeature-keyed indexing keeps the compile-time relationship the enum exists for. - The validation hint is no longer derived by capitalizing the label at runtime; a providerFeatureHints map sits next to providerFeatureConstraints/providerFeatureLabels as its own deliberate source of truth. Rendered messages are byte-identical. - Write-only usage registration treats explicit null as omission (Terraform semantics: null == omitted): the generated constructor guard becomes `!= null` and the setter path gets the same guard. Tests pin all four cases (constructor/setter x null/value).
…der nodenext and Node type-stripping Three defects surfaced by exercising the generated bindings in a real app (cdktn-provider-features-demo) instead of only snapshotting them: - The provider class's sibling import of the functions wrapper lacked /index, so nodenext/ESM resolution looked for ../provider-functions.js and failed; now emits '../provider-functions/index<ext>'. - The wrapper constructor used a TS parameter property, which Node's native type-stripping rejects with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX (it requires a synthesized assignment, not type erasure) - breaking any project that executes .ts directly. Now a declared field plus a body assignment. (jsii accepted the parameter property; Node's stripper is the stricter consumer.) - Variadic rest parameters rendered as `T[]`, which for a union element type binds as `boolean | (IResolvable[])`; now `Array<T>`. Validated end-to-end against the demo harness: tsc --noEmit clean, all 9 examples synthesize, provider::<name>::<fn>() expressions in output, and the generated package still passes tsc + jsii + pacmak.
…s, ephemeral resources, write-only Closes #311. Extends the edge-provider schema - whose bindings are jsii-compiled and pacmak-codegenned for all five languages at postbuild and native-compiled by the test/<lang>/edge integration suites - with the three new-protocol surfaces from this PR: - nine provider-defined functions, one per generator branch: plain string, nullable trailing (jsii-optional), nullable mid-position (widens to any), nullable variadic, bool variadic (union-element rest param), deprecation_message, object return, list(number) return, map(string) parameter; - an ephemeral resource (required/optional/computed attributes plus a nested block) extending TerraformEphemeralResource; - a write_only attribute on an existing managed resource, so the deprecated getter and both != null registration guards land in compiled bindings. This is the compile-slice of the CI-coverage gap: every workflow's Terraform tops out at 1.6.5 (pre-1.8), so no CI job can fetch a schema containing these sections - the edge fixture is binary-independent and rides existing jobs. The runtime slice (real cdktn get on terraform 1.15.x + opentofu 1.12.x) is tracked in #337. Verified locally: jsii + pacmak --code-only clean for all five targets; typescript/go/csharp edge integration tests pass against the regenerated bindings (java needs mvn, unavailable in this environment; python edge remains describe.skip'd - both pre-existing gaps noted in #311).
|
I found two remaining correctness gaps at 1.
|
…nction collision checks A provider function named "constructor" emitted a second class constructor (invalid TypeScript), and "provider_local_name" camelCased into a method colliding with the wrapper's private providerLocalName field - both produced broken output instead of the diagnostic that function-to-function collisions already get. buildProviderFunctionsModel now rejects any function whose sanitized method name lands on a member the emitter itself puts on the wrapper class (constructor, providerLocalName), with an actionable error naming the provider, the Terraform function name, and the generated identifier, thrown at the model level - before any output is written. Parameter names are deliberately not checked against providerLocalName: they live in method scope and merely shadow the field.
… not mutation history Registration of WRITE_ONLY_ATTRIBUTES usage was event-based and sticky: the first non-null constructor/setter value armed the target-version validation forever, so clearing the value (explicit null or reset) before synth still failed projects targeting < 1.11 - an error claiming something false about the synthesized artifact. A Lazy/IResolvable producer that resolved to nothing also counted as usage, penalizing exactly the conditional/mixin patterns targetVersions encourages. Registration now happens at token-resolve time, mirroring the function-usage registry: generated synthesizeAttributes()/ synthesizeHclAttributes() wrap write-only values in TerraformResource.markWriteOnlyAttribute(), a transparent IResolvable that defers to the real resolver and registers usage only when the RESOLVED result is non-null. undefined/null pass through unwrapped, so omission filtering is untouched. The mutation-time guards in the generated constructor and setters are deleted entirely - validation describes what renders, structurally. Consequences, all pinned by tests: set-then-null, set-then-reset, and constructor-then-reset synth cleanly on old targets with the attribute absent; a value present at synth still fails; re-setting after a clear re-arms; a Lazy producer resolving to undefined does not register while the same producer resolving to a value does; dual resolve passes (preparing + final) dedup to one validation. The write-only fixture gains an optional attribute so the reset path is exercised (the required one cannot generate a reset method).
|
Both findings addressed and pushed ( 1. Wrapper-member collisions → 2. Sticky write-only validation → Your full matrix is pinned in |
|
Just an idea. Maybe use the JSDoc |
…ed to undefined
In HCL synthesis, attributes are {value, isBlock, type, storageClassType}
descriptors. A token value (e.g. a write-only marker wrapping a Lazy that
produces undefined) survives the generated pre-resolution filter; generic
resolution then strips only the undefined `value` key, and
renderAttributes() fell through to renderFuzzyJsonExpression() on the
metadata-only shell - emitting a bogus assignment built from internal
descriptor fields instead of omitting the attribute.
renderAttributes() now recognizes the orphaned-descriptor shape (the
exact metadata key combination the generator emits, with both `value`
and `dynamic` absent - only `value` can ever be token-typed, so the
shell is unambiguous) and omits the attribute entirely. Explicit null
values keep rendering as null assignments.
Not specific to write-only: any attribute whose token resolves to
undefined in HCL mode hit this path (the #313 HCL-defects family) -
the write-only marker contract just made it load-bearing. Regression
tests land with the companion lifecycle commit (shared write-only test
harness).
…ynthesis pass Round-6 review findings: resolve-time discovery only worked through App.synth() (which happens to prepare stacks before validating), and registrations were lifetime-sticky once a value had rendered. Usage now has explicit epoch semantics - it describes the current synthesis pass, discovered by a prepare step every validation-enabled entry point establishes: - Element registrations are mode-tagged: structural (the element's existence is the usage - ephemeral resources; never reset) vs resolve-discovered (write-only; deactivated at the start of each preparing resolve, re-armed only when resolution rediscovers a real value). constructs has no removeValidation, so the installed validation is a thin gate that reports nothing while inactive. - ISynthesisSession gains optional `stacksPrepared`; App.synth() sets it (it already prepares all stacks first), and StackSynthesizer.synthesize() prepares the stack itself - including the per-root function-usage epoch reset - when reached without it. - Testing.synth()/synthHcl() with validations now run the discovery half of prepareStack() (split out as an internal method so no backend is injected into existing snapshots) plus the function-epoch reset before validating. Previously these entry points validated before any resolve ran, silently passing configured write-only values and gated functions on unsupported targets. - The function-usage registries adopt the same epoch: recorded usage clears at the start of each synthesis session, so render-once, remove, re-synth no longer reports stale usage - for Fn.*, provider functions, and write-only alike. Regressions pinned: old-target failures now surface through Testing.synth, Testing.synthHcl, and direct synthesizer use (for both write-only and provider functions); synth-fail -> reset -> re-synth passes; a Lazy producer flipping value -> undefined across passes passes; multi-stack Apps keep sibling usage intact; structural (ephemeral) registrations survive epochs; HCL output omits write-only attributes whose token resolves to undefined (regressions for the companion renderer fix). Doc comments on the marker and registries now state exactly this invariant instead of the stronger claims the review called out. BREAKING CHANGE (testing semantics): Testing.synth/synthHcl with runValidations=true now run the preparing resolve before validations, so validations see late-bound (token/Lazy) usage - previously they ran against pre-resolve state and could miss it. Consumers who added custom prepare wrappers around Testing APIs can remove them.
Round 6 (private delta review): validation lifecycle now has explicit epoch semantics — 📝 release-notes material belowTwo commits (
📝 For the release notes (behavior change in
|
|
Re-evaluated at For the HCL follow-up, two behaviors should be retained explicitly:
These are concrete HCL follow-up acceptance cases, but I am not requesting changes to #296 for them. At this head, the configured validation test, |
| } | ||
| if (element === "bool") { | ||
| return { | ||
| tsType: "Array<boolean | cdktn.IResolvable>", |
There was a problem hiding this comment.
Other places in the codebase use Array<boolean | cdktn.IResolvable> | cdktn.IResolvable which allows for the entire list to be a token.
There was a problem hiding this comment.
Adopted in 08433c6 — list/set(bool) is now Array<boolean | cdktn.IResolvable> | cdktn.IResolvable, and the same whole-collection union applies to nested collections and the any[] fallback (each branch now cites its attribute-type-model counterpart). string[]/number[] stay plain, matching resource inputs (encoded list tokens already satisfy them). This is also what makes function-result → function-parameter composition typecheck — pinned by composition tests (set(bool) result into set(bool) param).
| const element = type[1]; | ||
|
|
||
| if (element === "string") { | ||
| return { tsType: "string[]", docstringType: "Array<string>" }; |
There was a problem hiding this comment.
Perhaps distinguish between list and set in the doc string
There was a problem hiding this comment.
Done in 08433c6 — set-typed params/returns are labeled [set] in the docstring so the unordered/deduped semantics are visible.
| return { | ||
| tsType: child.tsType.includes("|") | ||
| ? `Array<${child.tsType}>` | ||
| : `${child.tsType}[]`, |
There was a problem hiding this comment.
This check isn't really accurate for what compatible types are actually generated as in other parts of the codebase for nested collection types. Perhaps there's a simple way to tie into the normal generation.
There was a problem hiding this comment.
Agreed the heuristic was wrong — replaced in 08433c6 with explicit per-branch mappings, each commented with the attribute-type-model.ts branch it mirrors (nested collections follow the non-simple-element rule: string[][] | cdktn.IResolvable). We mirrored rather than physically reused: the resource machinery is entangled with struct/storage-class generation that function expressions don't have, but the type matrix is now convention-identical and tested as such.
| * leaves `undefined` in the invoke args array, which `FunctionCall` | ||
| * already renders as the Terraform `null` keyword. | ||
| * - A nullable parameter followed by a required one keeps its position | ||
| * (jsii can't express "optional but not trailing"), but widens its type |
There was a problem hiding this comment.
Is this actually true? Optional vs nullable can be different concepts. I'm used to this being true for actually optional parameters, but not true for nullable parameters, but different languages could have different rules.
Just want to double check as this is complexity that would be nice if we didn't need it.
There was a problem hiding this comment.
Fair question — verified against jsii's model rather than assumed: Java and C# cannot omit parameters (jsii TypeScript-restrictions doc), so a jsii-optional parameter there is satisfied by passing null (cf. the @nullable discussion aws/aws-cdk#6026) — i.e. optional≈nullable is exactly jsii's cross-language behavior. TS/Python omit; Java/C# pass null; both render the Terraform null keyword positionally (pinned by test in 08433c6). The trailing param additionally accepts Token.nullValue() now, so the two concepts also converge in the typed API.
|
|
||
| return { | ||
| ...model, | ||
| tsType: "any", |
There was a problem hiding this comment.
This is pretty painful from a useability perspective.
There was a problem hiding this comment.
Fixed in 08433c6 — mid-position nullables are no longer any: they're T | cdktn.IResolvable, with Token.nullValue() as the explicit-null spelling (it resolves to the bare null keyword — proving that surfaced two pre-existing expression-rendering bugs, fixed in a2ea87a: a resolvable resolving to null rendered as "" and a resolvable resolving to a raw array rendered via Array.toString).
| for (const fn of functions) { | ||
| const existingTerraformName = seenBy.get(fn.methodName); | ||
| if (existingTerraformName !== undefined) { | ||
| throw new Error( |
There was a problem hiding this comment.
Errors for these two cases is probably fine to start with, but has the potential to become a challenge to support long term. Perhaps a follow-up issue to dedupe them.
There was a problem hiding this comment.
Agreed — tracked as a follow-up in #340 (deterministic dedupe strategy to replace the hard-fail long-term, plus evaluating jsii's per-language reserved-word lists). Meanwhile the prebuilt sweep below shows no real provider can currently trip the errors.
|
|
||
| // Parameter names that collide with a reserved word/identifier in one of the | ||
| // jsii target languages; mirrors tools/generate-function-bindings mapParameter. | ||
| const RESERVED_PARAMETER_NAMES: { [name: string]: string } = { |
There was a problem hiding this comment.
I was thinking of the jsii reserved list, but I'm not sure we actually use that anywhere now that I look for it.
| `Provider-defined functions of the ${model.providerName} provider.`, | ||
| ); | ||
| comment.end(); | ||
| this.code.openBlock(`export class ${model.className}`); |
There was a problem hiding this comment.
I was thinking the functions themselves would be available rather than a separate class access.
The updated approach does eliminate the need for the extra parameter, but it still obscures them a bit. Perhaps that is ok for the reduced name collision risk.
The name collision generation failure is still not ideal, but might be ok for now (should at least check pre-built providers)
| * the lifetime of the element, gated on/off via the registration's | ||
| * `active` flag instead of being re-added or removed. | ||
| */ | ||
| private readonly _registeredProviderFeatures = new Map< |
There was a problem hiding this comment.
We should consider just defining the type and only instantiating it if a registration is necessary. I would expect most elements to not actually use this which means we could save a decent amount of memory in a large stack.
There was a problem hiding this comment.
I was thinking the functions themselves would be available rather than a separate class access.
The updated approach does eliminate the need for the extra parameter, but it still obscures them a bit. Perhaps that is ok for the reduced name collision risk.The name collision generation failure is still not ideal, but might be ok for now (should at least check pre-built providers)
Agree we should do a data-sweep across pre-built provider schema if any function names post normalization cause collisions and will produce errors when this is merged. There's also grafana and mongodb providers requested to be added to pre-built providers so we could at least include those in our local datasweep assuming we will eventually (preferably sooner than later) add them to the pre-built list as well.
Ref:
There was a problem hiding this comment.
Done in a2ea87a — the map is allocated on first registration; elements that never register (the vast majority) no longer pay for it.
There was a problem hiding this comment.
| * the validations that read it afterwards. | ||
| */ | ||
| const usedFunctions = new Set<string>(); | ||
| const usedFunctionsByRoot = new WeakMap<IConstruct, Set<string>>(); |
There was a problem hiding this comment.
This has gone through a couple iterations, but ultimately, it still seems like this isn't place in the correct scope for its actual usage.
There was a problem hiding this comment.
Third time was the charm — you were right that the scope was still wrong, and it was more than hygiene: targetVersions can be overridden per stack, so root-keyed usage let a stack targeting old versions fail on a sibling's rendered function. As of a2ea87a the state lives on the TerraformStack itself (the construct whose constructor registers the validations and whose context resolves targetVersions): recorded via context.scope at resolve time, cleared by the stack's own preparing resolve, read by the stack's own validations. The module-global registry file is deleted outright, along with every session-level reset — per-stack epochs need no coordination. Sibling-stacks-with-different-targetVersions tests pin the fix, including the shared-token-attributed-to-both case.
… nullable ergonomics Third type-fidelity pass, aligning provider-function signatures with the established resource-input conventions branch for branch (each mapping now cites its attribute-type-model counterpart; the string- based parens heuristic is gone): - list/set(bool), nested collections, and any[]-fallback collections gain `| IResolvable` so a whole-collection token is accepted - which is what makes function-result -> function-parameter composition typecheck (a set(bool) result is an IResolvable). string[]/number[] stay plain, as resource inputs do (encoded list tokens already satisfy them). - map(string|number|bool) returns use the existing Token.asStringMap/ asNumberMap/asBooleanMap helpers (map(*) -> asAnyMap) instead of falling back to raw IResolvable - typed map results now feed typed map parameters directly. - Nullable parameters drop both `any` widenings: mid-position and variadic nullables become `T | IResolvable` (pass Token.nullValue() for an explicit null), trailing nullables stay jsii-optional and gain the same union. jsii's cross-language model makes optional~nullable sound: TS/Python omit, Java/C# pass null, both render the Terraform null keyword. - Docs: [list]/[set] distinction in docstrings, @returns lines, and the variadic comment says "array parameter" (it never was a rest parameter). Fixture adds list(bool)/list(object)/all four map-return shapes and two composition pairs; runtime tests pin Token.nullValue() rendering and whole-collection-token inputs.
…collections correctly Function-usage state moves from module-global WeakMaps keyed by the App root onto the owning TerraformStack itself - the scope where the validations are registered, and whose context chain resolves targetVersions. Root-keyed recording had a real false positive: targetVersions can be overridden per stack, so a stack targeting old versions failed on a sibling's rendered usage. Now each stack records (via context.scope at resolve time), clears its own sets in _runPreparingResolve (the per-stack epoch), and validates only what it rendered - errors carry accurate stack attribution. usage-registry.ts is deleted along with every session-level reset (App.synth, Testing, direct-synthesizer): per-stack clearing needs no coordination. Tests: sibling stacks with independent targetVersions (compatible sibling using a gated function no longer fails the incompatible one; the inverse fails only the using stack; a shared token rendered in both stacks is attributed to both), for provider functions and Fn.*. Also fixes two pre-existing expression-rendering bugs surfaced while proving Token.nullValue() behavior instead of assuming it: a resolvable resolving to null rendered as "" (dropping the argument) instead of the null keyword, and a resolvable resolving to a raw array/object rendered via default stringification (true,false - which Terraform would parse as two arguments) instead of [true, false]. Both paths now collapse/format resolved values correctly. Per review suggestion, TerraformElement's provider-feature registration map is now allocated lazily on first registration - most elements never register, so large stacks stop paying for empty Maps.
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