Skip to content

feat(cli): validate Terraform CLI feature versions - #237

Merged
jsteinich merged 2 commits into
open-constructs:mainfrom
sakul-learning:feat/terraform-cli-feature-version-validation
Jun 10, 2026
Merged

jsteinich merged 2 commits into
open-constructs:mainfrom
sakul-learning:feat/terraform-cli-feature-version-validation

Conversation

@sakul-learning

Copy link
Copy Markdown
Contributor

Summary

Introduce a Terraform-compatible CLI feature/version validator that can be reused by feature PRs whose support depends on both the selected CLI product and its version.

This adds:

  • parseTerraformCliVersion() to detect Terraform vs OpenTofu from the first line of plain version output:
    • Terraform vX.Y.Z -> terraform
    • OpenTofu vX.Y.Z -> opentofu
  • ValidateTerraformFeatureVersion, a single validation class for the matrix:
    • feature -> CLI product (terraform / opentofu) -> semver constraint
  • tests proving that version -json-style output is not used to infer the product, because OpenTofu keeps the Terraform-compatible terraform_version key.

Intended downstream use

This is meant to unblock/reduce duplication for the S3 useLockfile work in:

A follow-up PR can add the backend-specific validation with:

new ValidateTerraformFeatureVersion("S3 native state locking", {
  terraform: ">=1.10.0",
  opentofu: ">=1.10.0",
});

If future features land at different times, the same class can encode different product constraints, for example:

new ValidateTerraformFeatureVersion("some future feature", {
  terraform: ">=1.12.0",
  opentofu: ">=1.10.0",
});

Test plan

  • yarn jest packages/cdktn/test/validations.test.ts --runInBand
  • yarn lerna run --scope cdktn build
  • yarn nx test cdktn --runInBand

@sakul-learning

Copy link
Copy Markdown
Contributor Author

Follow-up reference for future feature-specific validation PRs: I added a semi-curated generator in the canitf-project repo that emits a CDK Terrain-shaped TypeScript source file for HCL/configuration feature constraints.

Generated file:

Human-controlled input:

Quick usage:

canitf generate-cdktn-source \
  --features-input data/hcl-feature-input.json \
  --generated-features data/features.json \
  --out data/cdktn-feature-constraints.ts

The generated output is intended to line up with this PR's ValidateTerraformFeatureVersion shape, e.g. feature key -> { featureName, constraints, category, sources }, where constraints maps terraform / opentofu to semver ranges.

Longer context: why this exists and how to use it

The broad changelog-derived table in canitf-project is useful for discovery, but too noisy to drop directly into CDK Terrain. The more useful workflow for this PR series is semi-automated:

  1. Fetch official Terraform/OpenTofu release data and build the broad candidate table:

    canitf fetch --per-page 100 --max-pages 3 --out data/releases.json
    canitf build-table \
      --input data/releases.json \
      --json-out data/features.json \
      --markdown-out data/feature-table.md \
      --include-canitf
  2. Maintain the narrowed, human-curated scope in:

    data/hcl-feature-input.json
    

    That file controls the baseline, feature keys, CDK Terrain use cases, explicit semver constraints, and source links. Generated changelog/cani.tf rows can fill missing fields, but human input remains authoritative.

  3. Generate a CDK Terrain-style TypeScript file:

    canitf generate-cdktn-source \
      --features-input data/hcl-feature-input.json \
      --generated-features data/features.json \
      --out data/cdktn-feature-constraints.ts

Current focused feature set:

  • S3 native state locking / S3BackendConfig.useLockfile
  • provider-defined functions
  • configured provider-defined functions
  • override_resource, override_data, override_module
  • ephemeral values and resources
  • write-only attributes
  • backend configuration using locals and variables

Important baseline note: I could not confirm “OpenTofu forked from Terraform 1.7.5”; that appears to be the wrong framing. The local CDK Terrain workspace pins Terraform 1.7.5 in mise.toml, so the generator treats 1.7.5 as the CDK Terrain comparison baseline, not the OpenTofu fork point.

Example generated shape:

export const TERRAFORM_HCL_FEATURE_CONSTRAINTS = {
  "s3-native-state-locking": {
    featureName: "S3 native state locking",
    constraints: {
      terraform: ">=1.10.0",
      opentofu: ">=1.10.0",
    },
    category: "backend",
    cdktnUseCase: "S3BackendConfig.useLockfile",
    sources: [
      "https://developer.hashicorp.com/terraform/language/v1.10.x/upgrade-guides#s3-backend",
      "https://opentofu.org/docs/intro/whats-new/#native-s3-state-locking",
    ],
  },
} as const;

This should make the next PRs easier: update the curated JSON input, regenerate the TS source, then wire the relevant feature key into the specific validation path.

@sakul-learning sakul-learning changed the title feat(cdktn): validate Terraform CLI feature versions feat(cli): validate Terraform CLI feature versions Jun 4, 2026
@so0k

so0k commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

tests are failing with

 FAIL  test/validations.test.ts (5.325 s)
  ● ValidateTerraformFeatureVersion › fails when the CLI product cannot be detected from the first line

    expect(received).toThrowErrorMatchingInlineSnapshot(snapshot)

    Snapshot name: `ValidateTerraformFeatureVersion fails when the CLI product cannot be detected from the first line 1`

    - Snapshot  - 1
    + Received  + 1

      "Validation failed with the following errors:
    -   [MyStack/testResource] Could not determine whether terraform is Terraform or OpenTofu from the first line of terraform version output: {"terraform_version":"1.10.10"}
    +   [MyStack/testResource] Could not determine whether terraform1.5.5 is Terraform or OpenTofu from the first line of terraform1.5.5 version output: {"terraform_version":"1.10.10"}

this is for Terraform 1.5.5 and Terraform 1.6.5

@jsteinich
jsteinich enabled auto-merge (squash) June 10, 2026 02:56
@jsteinich
jsteinich merged commit 32d434c into open-constructs:main Jun 10, 2026
256 checks passed
so0k added a commit that referenced this pull request Jun 12, 2026
Adds the reusable foundation for validating features against the project's
declared Terraform/OpenTofu targets instead of probing whichever binary
happens to be installed during synth:

- resolveTargetVersions(scope): reads the targetVersions context (injected
  by the CLI from cdktf.json), defaulting to the dual product baseline
  (terraform >=1.5.7, opentofu >=1.6.0) when undeclared
- checkFeatureSupportedByTargets: semver range *subset* check — a feature
  passes only if it is supported across the entire declared range of every
  targeted product
- ValidateFeatureTargetSupport: IValidation over the two; never executes a
  binary, so it behaves identically in CI/synth-only environments

Synth-time validations no longer probe the installed binary:

- ValidateTerraformFeatureVersion (merged unreleased in #237, zero call
  sites) is removed together with its execSync machinery; the pure
  parseTerraformCliVersion parser and the TerraformFeatureVersionConstraints
  type remain. Installed-binary verification is an explicit opt-in handled
  by the CLI (validateInstalledBinary).
- The long-released ValidateBinaryVersion and the resource-move probe built
  on it stay unchanged as the legacy exception, both marked with
  TODO(target-versions) for a follow-up migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jun 13, 2026
Adds the reusable foundation for validating features against the project's
declared Terraform/OpenTofu targets instead of probing whichever binary
happens to be installed during synth:

- resolveTargetVersions(scope): reads the targetVersions context (injected
  by the CLI from cdktf.json), defaulting to the dual product baseline
  (terraform >=1.5.7, opentofu >=1.6.0) when undeclared
- checkFeatureSupportedByTargets: semver range *subset* check — a feature
  passes only if it is supported across the entire declared range of every
  targeted product
- ValidateFeatureTargetSupport: IValidation over the two; never executes a
  binary, so it behaves identically in CI/synth-only environments

Synth-time validations no longer probe the installed binary:

- ValidateTerraformFeatureVersion (merged unreleased in #237, zero call
  sites) is removed together with its execSync machinery; the pure
  parseTerraformCliVersion parser and the TerraformFeatureVersionConstraints
  type remain. Installed-binary verification is an explicit opt-in handled
  by the CLI (validateInstalledBinary).
- The long-released ValidateBinaryVersion and the resource-move probe built
  on it stay unchanged as the legacy exception, both marked with
  TODO(target-versions) for a follow-up migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
so0k added a commit that referenced this pull request Jun 24, 2026
Adds the reusable foundation for validating features against the project's
declared Terraform/OpenTofu targets instead of probing whichever binary
happens to be installed during synth:

- resolveTargetVersions(scope): reads the targetVersions context (injected
  by the CLI from cdktf.json), defaulting to the dual product baseline
  (terraform >=1.5.7, opentofu >=1.6.0) when undeclared
- checkFeatureSupportedByTargets: semver range *subset* check — a feature
  passes only if it is supported across the entire declared range of every
  targeted product
- ValidateFeatureTargetSupport: IValidation over the two; never executes a
  binary, so it behaves identically in CI/synth-only environments

Synth-time validations no longer probe the installed binary:

- ValidateTerraformFeatureVersion (merged unreleased in #237, zero call
  sites) is removed together with its execSync machinery; the pure
  parseTerraformCliVersion parser and the TerraformFeatureVersionConstraints
  type remain. Installed-binary verification is an explicit opt-in handled
  by the CLI (validateInstalledBinary).
- The long-released ValidateBinaryVersion and the resource-move probe built
  on it stay unchanged as the legacy exception, both marked with
  TODO(target-versions) for a follow-up migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jsteinich pushed a commit that referenced this pull request Jun 25, 2026
…ion) (#269)

## Summary

> Reworks unreleased
#237 to not run
terraform binary during synth

Prerequisite for #268. Instead of probing whichever Terraform/OpenTofu
binary happens to be installed during synth, projects declare the
runtimes they intend to support, and validations answer: *"is this
feature supported by the project's declared target range?"* —
deterministically, with no binary required. Installed-binary
verification becomes an explicit opt-in for commands that execute the
binary anyway.

```jsonc
// cdktf.json
{
  "targetVersions": {
    "terraform": ">=1.5.7",
    "opentofu": ">=1.6.0"
  },
  "validateInstalledBinary": true // opt-in, off by default
}
```

### Commits

1. **feat(cli): add targetVersions to cdktf.json config** — typed field
in `@cdktn/commons`; `parseConfig` validates product names and
npm-semver ranges (Terraform's `~>` syntax is rejected with a hint — npm
semver would silently parse it as `~` with different semantics) and
threads the declaration to apps through the existing
`CDKTF_CONTEXT_JSON` channel. Also adds `parseTerraformCliVersion` to
commons and the `validateInstalledBinary` flag.
2. **feat(lib): validate features against declared runtime targets** —
`resolveTargetVersions(scope)` (context → dual-baseline default
`terraform >=1.5.7` + `opentofu >=1.6.0`),
`checkFeatureSupportedByTargets` (semver range **subset**: a feature
passes only if supported across the *entire* declared range of every
targeted product), and `ValidateFeatureTargetSupport` (an `IValidation`
that never executes a binary). Synth-time binary probing is removed:
`ValidateTerraformFeatureVersion` (merged unreleased in #237, zero call
sites) is deleted together with its execSync machinery — the pure
`parseTerraformCliVersion` parser and the
`TerraformFeatureVersionConstraints` type remain. The long-released
`ValidateBinaryVersion` and the resource-move probe built on it stay
unchanged as the legacy exception, both marked `TODO(target-versions)`.
3. **feat(cli): opt-in installed-binary verification** — when
`validateInstalledBinary: true`, `initalizeTerraform` (the
diff/deploy/destroy chokepoint, where the binary is about to run anyway)
verifies the installed product/version against the declared targets.
Declared targets are deliberately *not* treated as proof about the local
binary.
4. **feat(cli): write default targetVersions in init templates** — new
projects get the dual baseline explicitly in `cdktf.json`.

### Semantics

- **Subset check**: declaring `terraform: ">=1.5.7"` while using a
feature that needs `>=1.8.0` fails — the project claims to support
1.5.7..1.8.0 where the feature is missing. The fix is raising the
declared floor (or narrowing the range), which keeps the declaration
honest.
- **Missing product key in a feature's constraints** = unsupported by
that product; **missing product key in declared targets** = the project
doesn't target that product.
- **Undeclared** = dual product baseline (strictest, most portable
interpretation).
- **Synth never executes a binary.** The only places a binary is run are
commands that execute it anyway (opt-in `validateInstalledBinary`) and
the legacy `ValidateBinaryVersion`/resource-move path kept for
compatibility.

### Stacked work

#268 (function availability validation) is stacked on this branch: its
`ValidateFunctionVersionSupport` checks used `Fn` functions against the
declared targets via `checkFeatureSupportedByTargets`, gated by the
`validateFunctionVersions` feature flag.

## Test plan

- [x] `@cdktn/commons`: 83/83 — parser accepts constraint-shaped
targets, rejects unknown products / malformed ranges / `~>` / empty
declarations / non-strings; context payload merge
- [x] `cdktn`: 425/426 — resolver default + context override +
malformed-context errors; subset pass/fail; independent product
evaluation; product-not-targeted skip; complex ranges;
`ValidateFeatureTargetSupport` end-to-end with an execSync spy proving
no binary call (the 1 failure is the pre-existing
Docker-daemon-dependent `matchers.test.ts`, unrelated)
- [x] `@cdktn/cli-core`: 146 passed / 30 skipped (dist-dependent) —
installed-binary check helper:
satisfies/outside-range/undeclared-product/unparseable/default-baseline
- [ ] Integration: existing init tests exercise the new template field
in CI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
so0k pushed a commit that referenced this pull request Jul 3, 2026
🤖 Release PR — merge to cut a new release. Kept open and rebased
as commits land on `main`.
---


<details><summary>0.23.4</summary>

##
[0.23.4](v0.23.3...v0.23.4)
(2026-07-02)


### Features

* **cli:** validate Terraform CLI feature versions
([#237](#237))
([32d434c](32d434c))
* declared Terraform/OpenTofu target versions (validation foundation)
([#269](#269))
([0aa2343](0aa2343))
* **lib:** S3-backend - Add `useLockfile` field and update
`dynamodbTable` deprecated JSDoc
([#234](#234))
([32a408f](32a408f))
* **lib:** validate Fn usage against Terraform/OpenTofu versions
([#268](#268))
([cdf8115](cdf8115))


### Bug Fixes

* **cli:** don't mistake terraform error output for a missing-variable
prompt
([#267](#267))
([0ea946f](0ea946f))
* **cli:** release terraform state lock when interrupting diff/deploy
([#284](#284))
([de617db](de617db))
* **cli:** resolve prebuilt Java provider versions via repo1.maven.org
([#285](#285))
([d5af07d](d5af07d))
* **gha:** Adding CHANGELOG.md to prettier ignore list
([#258](#258))
([5160469](5160469))
* **gha:** pull jsii-terraform CI image from GHCR instead of Docker Hub
([#261](#261))
([f19d34b](f19d34b))
* **provider-generator:** copy only publishable files into the jsii
compile bundle
([#292](#292))
([562d4c7](562d4c7))
* **provider-generator:** match shorthand provider sources against
OpenTofu schema keys
([#293](#293))
([fda0103](fda0103))
* **release:** add Changelog title for correct release-please insertion
([#255](#255))
([004818a](004818a))
* **release:** drop devEngines.packageManager that broke npm publish
([ef508a6](ef508a6))
* **tools:** repair generate-function-bindings tool
([#273](#273))
([821e1cf](821e1cf))


### Miscellaneous Chores

* Add Gradle to Mise config
([#271](#271))
([1578270](1578270))
* add pnpm to mise
([#266](#266))
([48ab91f](48ab91f))
* add uv to mise
([#260](#260))
([163cf3d](163cf3d))
* **deps:** bump nrwl/nx-set-shas from
38457b511e60ee4e1da09255b658e480748a1af4 to
afb73a62d26e41464e9254689e1fd6122ee683c1
([#236](#236))
([b5ac018](b5ac018))
* **deps:** bump the github-actions-backward-compatible group across 1
directory with 7 updates
([#235](#235))
([de67d9f](de67d9f))
* **deps:** replace uuid dependency with node:crypto.randomUUID
([#291](#291))
([6d3657a](6d3657a))
* ESLint ignore test edge-provider-bindings
([#270](#270))
([ba407bb](ba407bb))
* Fix Verdaccio logging config
([#272](#272))
([aded64b](aded64b))
* **gha:** parallelize provider binding generation for documentation
examples
([#244](#244))
([0a9c955](0a9c955))
* Migrate package manager from yarn to pnpm
([#170](#170))
([d23da4e](d23da4e))
* migrate planning artifacts to cdktn-planning [skip ci]
([968c358](968c358))
* **release:** add release-please automation for versioning
([#243](#243))
([c759eec](c759eec))
* Remove dependency-pinner tool
([#250](#250))
([42c27d5](42c27d5))
* Remove unused dependencies
([#281](#281))
([c7de65e](c7de65e))
* **tests:** run verdaccio in-process
([#248](#248))
([c17677d](c17677d))
* Update JSII version constraint
([#245](#245))
([d89558f](d89558f))
* Update scheduled workflows to only run on open-constructs/cdk-terrain
([#256](#256))
([ba408cf](ba408cf))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

I'm going to lock this pull request because it has been closed for 30 days. This helps our maintainers find and focus on the active issues. If you've found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jul 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants