Feature Specification: Stateless Deployment Programs
Created: 2026-09-22
Status: Implemented
Input: Reconstructed from the code at 0bf4ecd36 (core/render.nix, core/render/, core/effects/, core/effects/env-config.nix, core/effects/tf/registrars.nix, core/docs/md/), pull requests #886, #1184, #1510 and #1790, api/specification.md, and the design notes of the maintainers.
User Scenarios & Testing (mandatory)
User Story 1 - Deploy a group with one program (Priority: P1)
A hosting provider or a job runner deploys a group of machines. It runs one
program from core, for example nix run ./core#<group>-<method>-<machine>-<verb>,
with the coordinates in environment variables. The program deploys, and its
exit code tells if the deployment is healthy.
Why this priority: this is the main product of core. api wraps these
programs, and continuous deployment runs them.
Independent Test: run the deployment-<method>-hosts check. It deploys a
trivial host with each method in a NixOS test and checks the result.
Acceptance Scenarios:
- Given the coordinates of a method in the environment, When the caller runs the
applyverb, Then the program deploys the machines and exits0. - Given a deployment where a unit fails, When the program finishes, Then it exits non-zero and names the failed unit.
- Given a unit in a restart loop, or a unit that cannot start, When the program finishes, Then the verdict reports it.
User Story 2 - Know which programs exist and which variables they read (Priority: P1)
A front-end must reject an unknown deployment before it starts a job, and
must forward the correct environment variables. core publishes the list of
attribute names that exist (deployment-attributes, deploymentAttributeNames)
and the sets of environment keys (deployEnvKeys, deployEnvKeysByMethod,
standaloneEnvKeys, nodeEnvKeys).
Independent Test: evaluate lib.deployEnvKeys. Each key has a description,
and each key that a deployment program reads is in the set.
Acceptance Scenarios:
- Given a name that is not in the published list, When a front-end checks it, Then the front-end rejects it before it starts a job.
- Given a new module that reads a variable, When it declares the variable, Then the variable is in the published key set with its description.
User Story 3 - Keep the deployment state outside the program (Priority: P1)
The program keeps no state of its own. OpenTofu methods store their state in the HTTP state backend that the caller gives. The first run bootstraps the state locally and then moves it to the backend. A later run that finds no state where state must be does not continue.
Independent Test: run the integrations-steady-refuses-lost-state check.
A steady-state apply with an empty backend fails and makes no resources.
Acceptance Scenarios:
- Given a cold start, When the caller runs
apply, Then the program bootstraps the state and moves it to the backend. - Given a steady deployment whose state is lost, When the caller runs
apply, Then the program refuses to continue. - Given a
plan, When it runs, Then it changes nothing. - Given a
destroy, When it completes, Then the state is detached from the backend. - Given a stale lock of this program, When the caller runs a verb, Then the program takes the lock back.
User Story 4 - Use the same programs from a shell (Priority: P3)
Each program is also an app and a development shell with its verb siblings on
the PATH, so a person can run the same steps by hand.
Edge Cases
- An unknown registrar: the evaluation fails and names the valid registrars.
- A malformed DS or DNSKEY value of the registrar: the evaluation fails.
- A node has no address: the zones do not include it.
- The operator pin (
OPERATOR_MODULE_SOURCE) and the provider disagree about a contested layer (resources, effects).VERSION_LAYERSdecides which side each layer comes from.
Requirements (mandatory)
Functional Requirements
- FR-001: The names of the programs MUST follow one grammar:
<group>-<method>-<machine>[-<verb>],<group>-<effect>[-<verb>], and<group>-<machine>for the NixOS configurations. Each group MUST have the aggregate targetsallandcombined. - FR-002: The methods MUST include
ssh,tf,tf-incusandtf-incus-operator. The standalone effects MUST include the DNS zone (OctoDNS), the registrars and the NetBox seed. - FR-003: The OpenTofu programs MUST offer the verbs
apply,destroy,plan,refresh,validate,output,show,state-list,state-show,importandforce-unlock, and every verb MUST go through the bootstrap wrapper. - FR-004: Each package MUST also be an app and a development shell.
- FR-005: A program MUST read all its coordinates from declared environment variables, and
coreMUST publish the key sets. - FR-006:
coreMUST publish the list of program names that exist. - FR-007: A program MUST keep no state between runs. It MUST refuse a steady-state run on lost state.
- FR-008: A deployment MUST report failed units, restart loops and start faults, and MUST exit non-zero when a unit fails.
- FR-009:
renderMUST NOT require the components of a front-end to render the programs of a group.
Key Entities
- Group: a set of published and internal machines (for example
operator,hosting-provider,dev,hypervisor). - Method: how machines are deployed (
ssh,tf,tf-incus,tf-incus-operator). - Effect: a standalone step that is not a machine (DNS zone, registrar, NetBox seed, image).
- Verb: one operation of a program.
- Environment key: a declared variable with a description and a default.
- State backend: the HTTP backend that holds the state of one deployment.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: The
deployment-*checks (methods, verdicts, CI environment, evaluation cache) and the state-backendintegrations-*checks pass. - SC-002: Every program of a group renders from
./core#alone. - SC-003: A front-end forwards only the variables in the published key sets.
Assumptions
-
apiqueues the programs, reports progress and authorizes requests (api/specification.md).coredoes none of these. -
A program keeps no state of its own between runs. Some state is inherent to a deployment, and the caller or the provider owns it, not the program:
- the state backend, which holds the resources that the program made;
- the directory mount per deployment in OpenBao;
- the PostgreSQL database per deployment.
-
The baked
SELF_MODULE_SOURCEpin fixes the source that a program deploys. It is an input of the program, not state.
Known Deviations
The code has one known deviation:
- The deploy coordinates come from the environment through
builtins.getEnv,envOrandoptionalEnv. These are thecore.lib.deployEnvKeysset and the structural pins, for exampleHOSTING_CONFIG,PIN_EFFECT,SELF_MODULE_SOURCE,OPERATOR_MODULE_SOURCE,VERSION_LAYERSandDNS_DOMAIN. Thus each call that renders or deploys a program needs--impure:core/render/hosts.nix,hostingCfgincore/render/boundary.nix,core/effects/common/deploy.sh, the CD workflows and the deploy runs of the api. Without--impure, Nix reads each of these variables as"". A FIXME inhosts.nixasks for--argin place of--impure. A flake does not take arguments, so this change needs configurable flakes in Nix upstream. The schema build inapi/src/fediversity_api/schema_build.pyis the one caller whereHOSTING_CONFIGandPIN_EFFECTare the main reason for--impure.