Feature Specification: Stateless deployment programs

Created: 2026-09-22

Status: Implemented

Input: Reconstructed from the render and the effects of core at 0bf4ecd36, 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, and gives the coordinates of the deployment in the environment. The program deploys, and its result tells if the deployment is healthy. A person can run the same program, and its sibling verbs, by hand from a shell.

Why this priority: this is the main product of core. api wraps these programs, and continuous deployment runs them.

Independent Test: a VM test per deployment method that deploys a trivial host and checks the result.

Acceptance Scenarios:

  1. Given the coordinates of a method in the environment, When the caller applies the deployment, Then the program deploys the machines and reports success.
  2. Given a deployment where a unit fails, When the program finishes, Then it reports failure and names the failed unit.
  3. 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 names of the programs that exist and the sets of environment keys that they read, per method and for the standalone effects and the nodes.

Independent Test: a unit test of the published key set. Each key has a description, and each key that a program reads is in the set.

Acceptance Scenarios:

  1. 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.
  2. 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. The OpenTofu methods keep their state in the 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: an integration test where a steady-state apply runs against an empty backend. It fails and makes no resources.

Acceptance Scenarios:

  1. Given a cold start, When the caller applies, Then the program bootstraps the state and moves it to the backend.
  2. Given a steady deployment whose state is lost, When the caller applies, Then the program refuses to continue.
  3. Given a plan, When it runs, Then it changes nothing.
  4. Given a destroy, When it completes, Then the state is detached from the backend.
  5. Given a stale lock of this program, When the caller runs a verb, Then the program takes the lock back.

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 and the provider disagree about a contested layer (resources, effects). A version-layer setting decides which side gives each layer.

Requirements (mandatory)

Functional Requirements

  • FR-001: The names of the programs MUST follow one grammar: group, then method and machine, or effect, then an optional verb. Each group MUST also have a NixOS configuration per machine and aggregate targets over all its machines (all and combined).
  • FR-002: The methods MUST include ssh, tf, tf-incus, tf-incus-operator, k8s and k8s-operator. The standalone effects MUST include the DNS zone (OctoDNS), the registrars and the NetBox seed.
  • FR-003: The OpenTofu programs MUST offer the life-cycle verbs (apply, plan, destroy), the read verbs (refresh, validate, output, show, state listing and state detail), import and the release of a lock. Every verb MUST go through the state bootstrap.
  • FR-004: Each program MUST also be an app and a development shell that has its verb siblings.
  • FR-005: A program MUST read all its coordinates from declared environment variables, and core MUST publish the key sets.
  • FR-006: core MUST 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 report failure when a unit fails.
  • FR-009: The render MUST 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, k8s, k8s-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-backend integration checks pass.
  • SC-002: Every program of a group renders from the core flake alone.
  • SC-003: A front end forwards only the variables in the published key sets.

Assumptions

  • api queues the programs, reports progress and authorizes requests (api/docs/design/). core does 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.
  • A pin of the source, baked into the program, fixes the source that the program deploys. It is an input of the program, not state.

Known Deviations

  • The programs read the deploy coordinates, the hosting configuration and the source pins from the environment of the process at evaluation time. Thus each render or deploy of a program, in the CD workflows, in the deploy runs of api and by hand, must allow impure evaluation. Without it, Nix reads each variable as empty. A flake takes no arguments, so a pure form needs configurable flakes in Nix upstream. The schema build of api is the one caller where the hosting configuration and the pin of the provider are the main reason for impure evaluation.