Repository navigation
fix(apply): every engine validates the contract before planning - #714
Merged
Merged
Conversation
…tract before planning `fluid apply` ran its pre-apply contract gate (pre-0.7 rejection plus JSON-schema validation, the check `fluid plan` and `fluid validate` run) only on the native engine's non-build path. Two early returns sat above both call sites: the OpenTofu dispatch, which every aws / gcp / snowflake / confluent and IaC plugin cloud takes, and the `needs_build` return for `--mode amend-and-build` / `replace-and-build`. A contract with an out-of-enum `orchestration.engine`, unknown keys or an end-of-life fluidVersion reached `tofu` or the build runner and could exit 0. The gate now runs once per apply, after the plan-binding and data-loss gates and before engine dispatch, the same placement the data-loss gate already uses for the same reason. A plan.json is checked on its digest-attested embedded contract; a YAML source is loaded once and the native path applies exactly the contract that was validated. The events are unchanged (`apply_contract_invalid`, `contract_version_unsupported`), there is no new flag, and a plan with no embedded contract is still the engine's error (`opentofu_engine_no_contract`). Three existing test fixtures only passed because the gap existed; two are now schema-valid, and the Command Center report tests, whose fixture carries a top-level `version` the run report reads but the schema does not allow, stand the gate down explicitly. Bug fix inside existing code: reuses `_gate_contract_for_apply`, adds no validation logic.
…an upgrade note docs/apply.md gains a "Contract validation" section: where the gate runs, its two events and exit code, the real output for an aws contract with an out-of-enum orchestration.engine, and what is checked for a plan.json. CHANGELOG [Unreleased] Upgrade notes: cloud applies and build-augmented modes of a contract that was never schema-valid now stop before planning; run `fluid validate --env`, fix, apply again. No flag skips the check.
…evel parser Parametrises the refusal cases (schema-invalid, pre-0.7) across modes and both engines with shared helpers, and parses through fluid_build.cli.build_parser instead of a hand-built subparser copy.
…efusal One writer produces either input form, so each refusal (schema-invalid, pre-0.7) is exercised as a YAML file and as a digest-bound plan.json on both engines and every mode, and the shared refusal helper asserts the guarded engine or build runner never ran.
… applies the copy that was checked
Two gaps in the schema gate this branch hoisted above the engine dispatch:
- A plan.json whose `contract` was missing, empty or not an object returned
unvalidated. resolve_apply_engine could not classify it and fell back to
the native engine, which ran the plan's actions through
build_provider("aws").apply and exited 0. planDigest is an unkeyed
sha256, so it proves the file is unchanged, not who wrote it. Such a plan
now stops with apply_plan_missing_contract (exit 1) on every engine, with
or without --no-verify-plan-binding, mirroring the build runner's
plan_missing_contract and the OpenTofu engine's opentofu_engine_no_contract.
- The gate validated one copy and the consumers re-read the file:
resolve_apply_engine, the OpenTofu hooks, apply_via_opentofu and
run_builds_from_args each loaded args.contract again, so a file swapped
after the gate reached tofu or the build runner unvalidated. run() now
loads the source once (_load_apply_source) and passes that dict to the
engine choice, the gate, the hooks, apply_via_opentofu(contract=...) and
run_builds_from_args(contract=...). The engine still resolves
{{ env.* }} placeholders, on a copy.
A contract that fails to load also logs its traceback under --debug again,
as the native try block did before the load moved out of it.
The native provider-detection unit tests now pin the native engine: they
reached it only because the engine was chosen from a re-read of the real
local fixture while the native path applied the mocked aws/gcp contract.
…ed copy is the applied copy docs/apply.md said the OpenTofu engine refuses a plan with no embedded contract (opentofu_engine_no_contract). It never reached that engine: it fell back to the native one and applied unchecked. Document apply_plan_missing_contract, and that the engine choice, hooks, tofu, the build runner and the native provider all take the one copy that was checked. The Upgrade-notes bullet says the same.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
fluid applynever schema-validated the contract on the OpenTofu engine, which is every cloud provider's default (aws, gcp, snowflake, confluent, plugin clouds).cli/apply.py::runreturned early forapply_engine == "opentofu"while both_gate_contract_for_applycall sites sat below it.fluid planand localapplydid validate, so only the cloud apply path was open. An out-of-enumorchestration.engine(or a pre-0.7 contract) reached planning.amend-and-build,replace-and-build,dry-run).CLIErrorevents as the native path:apply_contract_invalid,contract_version_unsupported. There is no new flag and no escape hatch.plan.jsonwith no embedded contract is refused instead of falling back to the native engine and exiting 0.CHANGELOG.md: cloud and build-augmented applies of a schema-invalid contract now stop before planning.Type of Change
apply_contract_invalid.Documentation
Checklist
test_apply_schema_gate_every_engine.py, plusapplyhook tests driven through the real top-level parser, for both YAML andplan.jsoninputs)ruff check,black(24.10.0), import-linter and the mypy strict hotspots are cleanTesting
Real
fluidCLI infc-test:3.12(no network, fakeHOME, no AWS credentials,tofuabsent),origin/mainvs this branch:orchestration.engine: not-a-real-engineopentofu_engine_no_tofuapply_contract_invalid, exit 1, never reaches the enginecontract_version_unsupported, exit 1plan.jsonembedding the invalid contract,--mode amendapply_contract_invalid, exit 1plan.jsonwith no embedded contract--mode amend-and-buildout/rows.parquet, exit 0apply_contract_invalid, exit 1