Skip to content

Add the NixOps4 integrations, native and upstream-compatible - #142

Merged
clhodapp merged 9 commits into
mainfrom
feat/nixops4
Oct 29, 2026
Merged

clhodapp merged 9 commits into
mainfrom
feat/nixops4

Conversation

@clhodapp

@clhodapp clhodapp commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #141. Implements the NixOps4 part of section 21 of the design document and supersedes #139, which built NixOps4 machines as flake-time children.

The root. A deployment's root is a deferred composite declared at the flake top under the name of the output it fills: caisson.caisson-nixops4-composite.configurations.nixops4, or caisson.nixops4.configurations.nixops4 for upstream code. Its entry is published as the nixops4 output, { _type; rootFunction; getProviders; }. rootFunction calls the entry's finalize with the outputValues and resourceProviderSystem NixOps4 passes and returns the export. getProviders calls finalize with the system alone, so every output read throws, and builds the root's providers. Published names continue through members: nixops4, nixops4.web.

Native integrations.

  • caisson-nixops4-composite and caisson-nixops4-resource are deferred configurations (caisson-core's mkDeferredConfiguration). A composite finalizes each member it declares by calling finalize on the member's entry, with { outputValues = args.outputValues.<member>; resourceProviderSystem; }, and returns the finalized members as its children.
  • Composite and resource are two classes. A composite has NixOps4's providers option and no type. A resource has NixOps4's resource, type and providers options and no members.
  • A composite's members module argument holds, for each member, outputs and absoluteMemberPath, built from the args and the declared names. No member's config is reachable through it, or through the composite's caisson.<integration>.configurations, which hold entries.
  • argsNeeded lists the deploying system and, per member, one arg per name in a resource's outputsSkeleton. A resource lists the outputs a tool may cache in cacheableOutputs.
  • The root's discoverArgs { system } is a program that runs nixops4 state dump for each resource named by a resource's state option, and prints the recorded outputs as outputValues, with the deploying system.
  • The NixOS adapter (module nixos of the resource class, selected by its default module) deploys the NixOS configuration declared beneath a resource as nixops4-nixos does: nix copy and bin/apply switch over SSH, with the same SSH options, and bash from the package set caisson selects for the deploying system.
  • The composite takes defaultPkgs, the package set the root's providers, discovery program and adapters run on.

Upstream-compatible integration.

  • nixops4 takes root modules as written for NixOps4's mkRoot. The root and every member are evaluated by caisson with NixOps4's class nixops4Component.
  • A composite decides each member's type with upstream's test (options.resourceType.isDefined), through a probe evaluation over NixOps4's base modules. A member that sets type and also declares members is refused.
  • NixOps4's members option is declared again as deferred modules whose value is the finalized members' config, and members is the module argument of the same value, so config.members.x.outputs.y, members.x.outputs.y and state = members.x work as upstream. Every composite sets unsafeCrossChildReferences for nixops4Component, so the childless view reads the same config.
  • A member that imports nixops4-nixos gets nixos.module evaluated as a caisson NixOS configuration beneath it. nixos.configuration is read-only, so the class module forces nixpkgs to a value whose lib.nixosSystem returns that configuration. The shell comes from caisson's package set.
  • cacheableOutputs is an allow-list by member path.

Integrations builder. mkModuleConfiguration passes argsNeeded and discoverArgs from an integration's evaluate to the manifest, and takes children from it when the integration returns them. A deferred entry that carries exportsTo is published like a manifest.

Also.

Named errors. A composite whose member names or kinds read members or outputs, and a resource whose outputsSkeleton reads its own outputs, fail with an error naming the composite or resource. Without these errors, both cases end in an infinite recursion. The names are taken from an evaluation in which those reads throw, which costs one extra module evaluation per composite and resource.

Tests.

  • The pinned-world probes cover the root's args and their sources, composites nested two levels deep (member paths, argsNeeded, argsSource, members of a composite member, a NixOS machine inside), a resource whose type depends on another member's output, the native members argument holding only outputs and member paths, the root's providers, a root inside a NixOS machine, entries no parent finalizes, upstream reads through members at two levels, a NixOS member built through nixops4-nixos that matches the system eval-config.nix builds from the same modules, and a second deferred integration beside nixops4 in one flake.
  • The new check deferred-errors runs nix eval on eleven cases and matches the exact error text: a missing output at the root and inside nested composites (caisson-deferred: needs outputValues.a.b.source.stdout), members or kinds read from outputs, an outputsSkeleton read from its own outputs, a name declared as composite and resource, two roots, a root under another name, and output values that are not a set.
  • The new check nixops4-discovery builds the root's discovery program for one and for two state resources, runs it against nixops4 state dump output recorded from a real deployment, and compares the JSON with the args argsNeeded names.
  • A real nixops4 apply of a native and an upstream-compatible deployment with the local provider, run outside the test suite, applied in output order, passed the values through nested composites, was a no-op the second time, and reported a cycle through outputs as Cycle detected.
  • At this head, pinned-world, deferred-errors, nixops4-discovery, nix-unit, eval-weight, argument-errors and treefmt pass locally. Every x86_64-linux check, the VM tests included, passed at a5204f8; the commits since change only the NixOps4 integrations and tests.

🤖 Generated with Claude Code

clhodapp and others added 9 commits October 28, 2026 20:20
A caisson-nixops4 configuration, named nixops4 for the output it fills, publishes the root NixOps4 reads: rootFunction and getProviders over the root composite, a deferred caisson-nixops4-composite configuration declared under the name root. NixOps4 evaluates the root when it deploys, so the composite is finalized with the args NixOps4 passes: the output values and the deploying system.

A composite holds its members as deferred caisson-nixops4-composite and caisson-nixops4-resource configurations beneath it and finalizes each with its part of those args. Composite and resource are two classes. A composite has NixOps4's providers option and no type; a resource has NixOps4's resource, type and providers options and no members. A component reads the outputs of its composite's members, and nothing else of them, through the members module argument.

The NixOS adapter, the module nixos of the resource class, deploys the NixOS configuration declared beneath a resource as nixops4-nixos does, with the same SSH options, and runs the script with bash from the package set caisson selects for the deploying system. The default module of the resource class selects it and gives the NixOS configurations beneath the nixops4-apply NixOS module, which imports the apply module of nixops4-nixos.

argsNeeded names the deploying system and the outputs of the members, and a resource lists the outputs a tool may cache in cacheableOutputs.

caisson-core is pinned to the head of nix-caisson/caisson-core#57, where a function argsNeeded receives the name and parent of the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nixops4 configuration, named nixops4, takes the root modules of a NixOps4 deployment as NixOps4 users write them for mkRoot, and caisson evaluates every component, with NixOps4's class name nixops4Component and NixOps4's resource, type and providers options. A composite collects members.<name> and decides each member's integration by upstream's test, with a probe evaluation over NixOps4's base modules: a member whose resourceType is defined is a caisson-nixops4-resource configuration, any other a caisson-nixops4-composite configuration. A member that sets type and declares members is refused.

The members module argument of a composite holds its members' configs, as upstream's does, so every composite sets unsafeCrossChildReferences for the class of its members. A member that imports nixops4-nixos gets its nixos.module evaluated as a caisson NixOS configuration beneath it: the module nixos of the nixops4Component class forces nixpkgs to a value whose lib.nixosSystem returns that configuration, since nixos.configuration is read-only, and takes the shell from caisson's package set. cacheableOutputs is the allow-list of outputs a tool may cache, by member path.

The eval-weight baseline is re-measured: the core module of every class declares the configurations of every integration, so the four integrations of the NixOps4 work add to caisson's overhead in every evaluation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A root composite, native or upstream-compatible, takes stateResources, the member paths of the resources that hold the state of the deployment, as nixops4 state dump takes them. It then gives discoverArgs: for each system a package config builds a set for, a program that runs nixops4 state dump for each of them in the working directory and prints the recorded outputs as outputValues, with the deploying system. A finalized composite publishes deferredChildArgs, the args it gives a member, so a tool can finalize a member's entry itself.

caisson-core is pinned to the head of nix-caisson/caisson-core#57, where discoverArgs may be derivations by system or a function of the entry's name and parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
caisson-core now calls discoverArgs with the system of the tool that runs it, so a root composite builds one discovery program, for that system, rather than one for every system its package configs build a set for.

caisson-core is pinned to the head of nix-caisson/caisson-core#57, where argsNeeded and discoverArgs are always functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…teConfiguration and mkResourceConfiguration

They return what a mkConfiguration returns, built on mkModuleConfiguration; the With of compositeWith and resourceWith said nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… finalize members with their part of its args

The root of a deployment is a deferred caisson-nixops4-composite configuration declared under the name `nixops4`, for the output it fills, and the upstream-compatible nixops4 configuration is such a root of class nixops4Component. The caisson-nixops4 declaration integration is removed. Its helpers are published by the resource integration.

A composite evaluates with its args, `outputValues` and `resourceProviderSystem`, finalizes each member once with that member's part of them, and returns the members' manifests as its children. Its manifest names the args it takes in `argsNeeded`: the deploying system and, by member, the outputs each resource names, cacheable where the resource lists them. The root gives `discoverArgs`, which finds the resources that hold state from their `state` option. A native composite's `members` holds each member's outputs and `absoluteMemberPath`, from the args and the declared names. An upstream composite declares `members` again as deferred modules, and reading it gives the members' configs, so `members.x.outputs.y`, `config.members.x.outputs.y` and `state = members.x` work.

The integrations builder passes `argsNeeded`, `discoverArgs` and `children` through from an integration's evaluate, publishes the entry of a deferred configuration from the `exportsTo` its record holds, and passes no registries up from an entry.

caisson-core is pinned to 2293708 on nix-caisson/caisson-core#57, where `finalize { args, argsSource }` fills the args from the manifest's own `argsNeeded`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d dumps from the root, and name the errors of members and outputs named from outputs

The discovery program looped with `for state in <paths>`, which
shellcheck refuses (SC2043) when a root has one state resource, so the
program did not build. It loops over a bash array now. It also nested
the outputs of a state resource inside a composite twice: `nixops4
state dump` keys the resources from the root, and the program prefixed
the path of the composite again. The new `nixops4-discovery` check
builds the program of two roots against a stand-in for NixOps4 and
compares what it prints from recorded dumps with the paths the root
names in `argsNeeded`.

A composite that decided its members from `members` or their outputs,
and a resource that named its outputs from its own outputs, ended in an
infinite recursion. A composite now takes the names of its members from
an evaluation in which `members` fails with an error naming the
composite, and a resource takes the names of its outputs from one in
which its outputs fail with an error naming the resource.

The pinned-world suite gains probes for nested composites, a machine in
a nested composite, these errors, one member under a name, one root per
flake, a root inside a machine, entries a parent does not finalize,
`getProviders`, and nested upstream composites. The `deferred-errors`
check reads the text of the errors, `caisson-deferred: needs <path>`
included, with the real evaluator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot a set

caisson-core b9de3d0 names the path of args given as a non-attrset
where argsNeeded declares a set. The deferred-errors check reads that
message for output values given as a string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@clhodapp
clhodapp changed the base branch from feat/deferred-and-cross-child to main October 29, 2026 03:27
@clhodapp
clhodapp merged commit ec291bc into main Oct 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant