derive nix modules from TF provider schemas - #6
Open
KiaraGrouwstra wants to merge 18 commits into
Open
Conversation
Generic Terraform-schema conversion helpers, so each pairing's resource surface can be derived from its provider's own schema rather than hand-written. Pinned source-only: nix-tf-schema's flake builds its `lib` from its own nixpkgs pin for x86_64 alone, and this flake evaluates for aarch64 too. We instantiate `conversion.nix` against our own `pkgs`, keeping one nixpkgs in play. This does make CI depend on git.fediversity.eu being reachable.
Hand-writing each pairing's resource surface makes provider drift invisible: an attribute added, removed or retyped upstream only surfaces when `tofu apply` fails, if at all. `mkResourceTypes` derives the surface from a vendored provider schema instead, so drift is an eval-time error at `nix flake check`. A pairing supplies a per-collection overlay carrying only what a schema cannot state -- the NixOS-facing descriptions, the reference graph, and a short list of documented corrections. There is deliberately no way to declare an option with no schema counterpart (`refs` is the sole exception), which is what makes the drift check total. `checks` is `deepSeq`'d into `resourceTypes`, so a pairing cannot use the generated surface without also firing every assertion. Nothing consumes this yet. Assisted-by: Claude:claude-opus-5
`nix build .#<svc>-provider-schema` extracts the pinned provider's schema in a sandbox (`tofu providers schema -json`, offline via `opentofu.withPlugins`) and normalizes it. `nix run .#update-provider-schemas` installs both over the vendored copies, so a nixpkgs bump becomes: refresh, then let `nix flake check` name every resource and attribute that moved. The schemas are vendored rather than read at eval time: the flake evaluates for aarch64-linux as well, and IFD would mean running a foreign-arch provider binary during evaluation. Each pairing's `lib.nix` now exports `provider` and `providerSource`, the single source of truth the schema tooling reads. Assisted-by: Claude:claude-opus-5
`services/forgejo/provider-schema.json` is the normalized output of `tofu providers schema -json` for the pinned svalabs/forgejo 1.5.0, and `schema.nix` parses it. Committing it keeps evaluation IFD-free, which matters because the flake evaluates for aarch64-linux too. The new `forgejo-schema-current` check rebuilds the schema and diffs it against the vendored copy, so a provider whose schema changes without a version bump still fails CI. Nothing derives options from it yet. Assisted-by: Claude:claude-opus-5
`external_tracker`, `internal_tracker` and `external_wiki` are the only nested blocks the forgejo provider has, and no test or example exercised them -- so a refactor of the resource surface would move them blind. The fixture declares the built-in tracker on one repository and the external tracker plus external wiki on another (a repository routes issues to one tracker or the other, never both), and asserts each block arrived intact through the live API. Assisted-by: Claude:claude-opus-5
The VM test's `services.forgejo.runtime` block moves to `fixtures.nix`, and the new `forgejo-rendered-fixtures` package renders it through the real option system and renderer. That makes the fixture serve twice: the VM test proves it converges against a live Forgejo, and the rendered `.tf.json` snapshot lets a refactor of the resource surface be checked with a diff. The test derivation is byte-identical either side of this commit. Assisted-by: Claude:claude-opus-5
The line escaped the previous commit because treefmt's local cache had already recorded the file as formatted; the `formatting` check builds in a fresh tree and reformats it. Assisted-by: Claude:claude-opus-5
…hema
The 15 resource specs were hand-transcribed from the provider's
documentation: every attribute name, type, optionality and description
written out by hand, with nothing checking them against the provider. A
provider bump could add, remove or retype an attribute and the only symptom
would be an apply-time error on a live host.
They are now derived from `services/forgejo/provider-schema.json` by
`modules/lib/tf-schema.nix`. What each collection still states by hand is
what a schema cannot carry: the Terraform label prefix, which attribute the
attrset key fills, the Forgejo token scope, the reference graph between
collections, and the NixOS-facing collection description. Everything else --
attribute names, Nix types, required-vs-optional, secrets, block wrapping,
descriptions -- comes from the schema, and any drift between the two is now
an eval-time error.
`nixTfSchema` reaches the module via `_module.args`, injected by the flake:
a NixOS module cannot resolve a flake input by path.
One deliberate correction: `organization_id` is dropped from
`forgejo_team`, `forgejo_organization_action_secret` and
`forgejo_organization_action_variable`. The provider requires exactly one of
`organization` / `organization_id`, and the existing `organization`
reference already accepts both a managed sibling's key and a literal name,
so exposing the numeric twin could only be used to violate that constraint.
Verified:
- `forgejo-rendered-fixtures` builds to the same store path as before the
refactor -- the generated `.tf.json` is byte-for-byte unchanged.
- `checks.x86_64-linux.forgejo` builds to the same store path as before, so
the VM test's whole system closure is unchanged.
- Options doc: 169 options before and after, none added or removed, none
changed between required and optional. 110 descriptions now come from the
provider instead of hand prose. Five types widen: three `int` -> `number`
(faithful to the schema's `number`), and `repository_webhooks.config` /
`teams.units_map` go from a required `attrsOf str` to a nullable one --
a collection default of `{ }` cannot express "unset", so both stay
enforced via `requiredAttrs` instead.
Assisted-by: Claude:claude-opus-5
Three gaps in what `nix flake check` proved after the surface became schema-derived: - The generator's drift assertions only fired as a side effect of forcing `resourceTypes`, so drift surfaced as whichever VM test or example happened to evaluate first -- a confusing place to read the error. - Nothing recorded what the pairing covers. "Is this resource modelled, and how much of it?" could only be answered by reading `lib.nix`. - Nothing rendered the user-facing option surface, so an API change could only be reviewed as a diff of generator input, not of generator output. `<svc>-schema-coverage` builds the coverage table from the generator's new `coverage` output, forcing every assertion on its own along the way, and is the source for the pairing README's resource table (and, once a pairing has one, its `unsupported` list). `<svc>-options-doc` renders the option surface with `nixosOptionsDoc`; `warningsAreErrors` stays on, so an option without a description fails the check. Both are generic over pairings: keycloak already gets an options doc, and picks up a coverage report as soon as it exports `coverage`. Assisted-by: Claude:claude-opus-5
The README read as though the option surface were hand-maintained, and gave no answer to "how do I move to a newer provider?" -- which is now a mechanical procedure with a check that reports exactly what changed. Points the resource table at `forgejo-options-doc` and `forgejo-schema-coverage` as the authoritative lists, explains why `organization_id` has no option, and adds a "Provider updates" section covering the bump procedure, what drift the check reports, and `forceOptional` as the release valve for a newly-required attribute. Assisted-by: Claude:claude-opus-5
Commits the normalized schema of the pinned keycloak/keycloak provider, so the pairing can derive its resource surface from the provider's own declaration instead of a hand transcription. Vendored rather than extracted at eval time: the flake evaluates for aarch64-linux as well, and IFD would mean running a foreign-arch provider binary during evaluation. Provider schemas are platform-independent, so one committed file per provider version is correct. Activates `keycloak-schema-current`, which rebuilds the schema from the pinned provider and fails if the committed copy differs -- catching a provider that changes its schema without changing its version. Assisted-by: Claude:claude-opus-5
`keycloak_openid_client_js_policy`, `keycloak_openid_script_protocol_mapper` and `keycloak_saml_script_protocol_mapper` were removed from the provider along with Keycloak's `upload-scripts` feature, but the pairing kept offering options for them. Declaring any of the three produced a `.tf.json` naming a resource type the provider no longer implements, so the reconciler failed at apply time on a live host -- the exact class of drift the vendored schema now makes an eval-time error. Nothing but the removal is intended here: it keeps the schema-derived refactor a pure refactor rather than mixing a behaviour fix into it. Assisted-by: Claude:claude-opus-5
The six `services.keycloak.runtime` blocks the VM tests declare move to `fixtures.nix`, and `keycloak-rendered-fixtures` renders them through the real option system and renderer into a `.tf.json` snapshot. That snapshot is the acceptance evidence for the coming schema-derived resource surface: build it before and after, and an empty diff proves the wire format is untouched. Pure refactor -- all six test derivations resolve to the same store path. Assisted-by: Claude:claude-opus-5
…chema
`services/keycloak/lib.nix` now builds its 97 resource collections with
`modules/lib/tf-schema.nix` from `services/keycloak/provider-schema.json`
instead of declaring every attribute by hand. What stays hand-written is
what the provider schema cannot express: the NixOS-facing resource
descriptions, the reference graph, and the per-resource corrections
(`omit`, `extraSecrets`, `requiredAttrs`).
The generator refuses to ignore a schema resource silently, so eval is
only green once every resource is either modelled or listed in
`unsupported` with a reason. That set -- the four resources new in
provider 5.8.0 -- therefore lands here rather than in the follow-up, and
its four entries are the complete statement of what this pairing does not
cover.
`omitEverywhere = [ "id" ]` is new in the generator: every sdk/v2
resource declares a synthetic `id`, which is the resource's own identity,
computed on apply, and nothing a configuration declares. It is a
provider-wide dialect artifact, not a per-resource correction, and a
global check reports it as stale once no modelled resource declares it.
Two references the hand-written surface lacked: `roles.client` (a role
scoped to a client rather than to the realm) and `groups.organization`.
Verification:
- Rendered fixtures (`nix build .#keycloak-rendered-fixtures`) are
byte-identical to the pre-refactor snapshot except `smtp_server.auth`,
which now renders as `[ { ... } ]`. That is the deliberate correctness
fix: the schema marks it `nesting_mode: "list", max_items: 1`, so
Terraform reads it as a one-element list, and the hand-written
`blockAttrs` list omitted it. Nothing else changed and nothing was
removed.
- Options doc: 1217 -> 1238 options; 22 added, 1 removed, 917
descriptions changed, 124 types changed, 81 became required. The single
removal, `kubernetes_identity_providers.<name>.hide_on_login_page`, is
drift the schema catches -- the provider marks it computed-only ("This
is always set to true for Kubernetes identity provider."). Type changes
are `int` -> `number` widenings plus one fix
(`ldap_user_federations.<name>.cache.eviction_day` was typed `str`).
- `nix flake check` green, including all five keycloak VM tests.
Assisted-by: Claude:claude-opus-5
Model the four resources provider 5.8.0 added, which the previous commit had to list as `unsupported` for eval to be green: - `oidc_openshift_v4_identity_providers` and `spiffe_identity_providers`, both per-realm IdPs keyed by alias, joining the six existing IdP collections as targets of the mapper `identity_provider` reference. - `openid_client_regex_policies`, an authorization policy matching a token claim against a regular expression; same realm and `resource_server` references as the other client policies. - `workflows`, a realm event trigger plus an ordered `step` list. `unsupported` is now empty and the coverage report reads 101 of 101. The option surface grows by 68 options; rendered fixtures are unchanged, since no fixture uses these resources. They are derived from the schema and checked at eval, but not exercised against a live Keycloak -- the existing VM tests cover the resources they always covered. Assisted-by: Claude:claude-opus-5
The README read as though the option surface were hand-maintained, and gave no answer to "how do I move to a newer provider?" -- which is now a mechanical procedure with a check that reports exactly what changed. Points the resource tables at `keycloak-options-doc` and `keycloak-schema-coverage` as the authoritative lists, adds the four collections new in provider 5.8.0 to those tables, and adds a "Provider updates" section covering the bump procedure, what drift the check reports, and `forceOptional` as the release valve for a newly-required attribute. Assisted-by: Claude:claude-opus-5
…ration
The contract still told a new pairing to enumerate every provider resource
by hand, which is no longer how either implemented pairing works and would
produce a surface with no drift detection.
Rewrites "Modeling the resource surface" around `mkResourceTypes`, with a
table splitting what the schema derives from what the overlay must state,
the correction fields and the rule that every overlay key names a real
schema path. Adds "Vendored provider schema" (what is committed, how it is
normalized, why never IFD), "Two schema dialects" (sdk/v2 `block_types`
needing `[ { ... } ]` wrapping vs plugin-framework `nested_type`, and which
pairing is which), and "Drift is a hard error" (the assertion set,
`unsupported` with mandatory reasons, and the three checks). Records the
derivation as a settled decision, and documents the four force-optional
cases with the reason each exists -- the collection one especially, since
an empty `listOf` would silently strip server-side state.
Also updates the repository layout, the `lib.nix`/`module.nix` signatures,
`fixtures.nix` and the rendered-fixtures snapshot, the README outline, and
the Development section with the schema-update and before/after diff
commands.
Assisted-by: Claude:claude-opus-5
The header and the pairings table still called Keycloak "designed, not yet built" at provider 5.7.0. It has been implemented for a while: all 101 resources of provider 5.8.0 are modelled and six VM tests prove them against a live instance. Also names it as the pairing to read for the sdk/v2 schema dialect and for a large resource surface, which forgejo (plugin-framework, 15 resources) cannot demonstrate. Assisted-by: Claude:claude-opus-5
Author
|
one design decision here was inlining the generated schemas. one alternate route would be to load those dynamically by IFD. that would give less git bloat, tho it would make for more run-time calculations, on top of not working with cross-compilation. |
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.
Use nix-tf-schema to derive nix modules from TF provider schemas, reducing the maintainable surface in favor of generic code plus generated json. Adds CI checks against drift from the underlying providers.
Closes #5.