Future Considerations

Decoupled versions

Issue: #304.

When the versions of applications are decoupled from the deployment configuration, the deployment procedures will need version information, for example a version on a submit.

The deploying-vs-deployed version boundary

Issue: #972.

A deployment carries an optional module source, so that the configuration of an operator is validated, migrated and deployed against a pinned tree, not against the current revision of the hosting provider (see Schema and migration sources and Decoupled versions (#304)). The pin started as the naive route: it ran the complete deployment from the pin, while the environment configuration of the provider (#848) and its deployment method follow the current revision of the provider. The boundary below states where the cut is, so that a version skew cannot cause silent mismatches.

It is implemented. The provider tree is the eval entry point and pulls the pin in. For the contested layers (resources and effects), api selects the newer of the two before the eval. The configuration across the two trees is addressed through one stable attribute. A check at submit refuses a method skew early, and forward compatibility keeps an older configuration valid against newer resource modules.

What is pinned vs provider-current

The deploy runs the deployment program of the selected attribute in the pinned tree, with an environment. So the tree is pinned, but the selector into it and the provider data given to it are current to the provider:

Deploy input Side
The entry point, the setups, the deploy scripts, the tools provider-current
The resources and effects layers (contested) the newer of the pin and the provider
The data model, the schema and renames, the exposed deployment attributes pinned
The deployment method selector, the system and nixpkgs provider-current
The deploy environment that core declares, and the TF variables provider-current
The hosting configuration provider-current
The module type and the rename log of the built-in default schema provider-current

api forwards the deploy environment as an opaque map with the keys that core declares, also from its own process environment, which is how a coordinate from a broker reaches a deploy. A new effect adds its keys with no change to api.

The hosting configuration curates the operator form: its environments mirror the environments of the pinned data model, and its effect defaults fix leaves of the form (a schema prune plus a baked value). It is one file that reaches the three consumers that must agree: the prune of the built-in schema, the eval for each module source, and the deploy eval.

The module type and the rename log apply only to the built-in default schema. A type from outside declares renames that no log of ours carries, so such a deployment gives its own log or none.

Invariant

A deploy is valid only when the pinned tree of the operator exposes the deployment attribute and program that the provider selects, and accepts the environments that the provider gives. Otherwise the deploy must be refused before it runs, not fail during the deploy.

Mismatch surface

A skew between the pinned tree and the current inputs of the provider can break in four ways:

  1. Missing deployment attribute. The attribute of the provider may not exist in an older pin, so the deploy fails late with a raw Nix trace. api can prevent this by listing the attributes of the pin at submit: the source is allowlisted there, so the eval is the same trust class as the deploy after it.
  2. Method not in the pin. The preferred method of the provider (incus, tf, ssh) may never have existed in a historical pin.
  3. Environments schema skew. Hosting configuration shaped for the current resource options is parsed by the pinned data model. Without resource versions (#634), an added or renamed option makes the pinned eval refuse or silently drop hosting configuration.
  4. Self-pin ambiguity. An absent source freezes to the revision of api (the provider). This is correct for the schema baseline, but on a migration it silently pins the version of the new provider, not the old version of the operator.

Ratified rules

These are implemented.

  • Detect at submit. The check of the deployment attribute runs at submit, beside the schema validation, on the same bounded build path and with no new fetch. The operator sees a clear error in the form (an invalid configuration that names the pin revision, the requested attribute and the attributes that the pin exposes), not a raw Nix trace at the start of the deploy. This detects cases 1 and 2, but does not resolve them; see Decoupling the deployment method from the pin.

    The check evaluates the allowlisted pin. This is safe at submit, because submit comes after the allowlist, and the deploy after it runs the Nix of the same pin (see Why the */get_for variants are authenticated and allowlisted). Thus the eval is the same trust class as the deploy that it gates. A committed manifest of the attributes is only a possible cost optimization, not a security requirement.

  • A migration needs an explicit module source. On a move between hosts, an absent source is an error, not a silent freeze to the self-pin: the operator must name the version they move from. The self-pin is right for a first submit against the baseline of the provider, and wrong when an operator moves off the revision of another provider (case 4). A submit marks itself as a migration, and api refuses an absent source before it resolves the self-pin, so a submit cannot launder a migration into a self-pin.

  • Environments skew waits for #634 and for default bindings. The submit check can report case 3 but cannot reconcile it. The version marker of #634 will let api detect the skew before the deploy. The skew is not symmetric, and its common direction can be recovered: when the pin of the operator is newer and brings an application that needs a resource that the provider never configured, a default resource binding that ships with the application fills the gap. So no version owns the resource layer: the requirement travels with the application, the binding is current to the provider, and a default binding bridges the two. Only a required resource that the provider cannot bind or default stays unrecoverable.

    Forward compatibility is implemented and a regression test enforces it: a new resource option has a default, and a change of type or name is a rename module that derives the new value from the old one. Thus an older configuration evaluates against newer resource modules. The worked example is a rename in the garage resource, with a test that the old shape evaluates and that the value arrives at the new path. The #634 marker and the full default-binding reconciliation are future work.

Decoupling the deployment method from the pin

The check at submit is a guard rail for a coupling that should possibly not exist. The pin of the operator freezes the deployment method only because the method is in the same tree as the configuration. But the method is a concern of the hosting provider: a provider knows how to put a NixOS system on its own infrastructure. To freeze it to the revision of the operator is the design smell behind cases 1 and 2.

The seam half-exists: the effect layer already evaluates a configuration module to a system with no TF, so the configuration is data that does not depend on the method, only stored below the namespace of a method. Two facts make a naive "pin a closure" contract harder:

  • The build artifact is made by the method and the configuration together. For example, the incus method needs an image that exists only because the configuration imports the container profile of that method. Thus the stable contract is the configuration modules plus the resource bindings, evaluated against the profile of the provider method at deploy time. This makes a method current to the provider correct.
  • Modules are functions, which do not serialize (NixOS/nix#4000). The effect layer passes a configuration by reference, through a generated configuration that imports the caller module again; its only limit was that the reference is to one tree.

Three contracts can cross the boundary between the trees. The trusted-source allowlist (#1097) decides the trust analysis: the deploy already runs the Nix of an allowlisted pin, so an eval of an allowlisted operator source adds no code-execution surface.

  • Data-only override. The operator gives option values that the provider merges as a module. This is the smallest trust surface, but data cannot add an import, so it cannot add an application that the base of the provider does not have. It covers settings skew, not new-application skew, which is the likely skew of a migration.
  • Import by path through a stable entry attribute (recommended; implemented). The provider tree fetches the allowlisted operator source and gets its configurations through one well-known attribute, not through file paths: a file path is internal layout that a refactor breaks, and layout drift is most likely across a version skew. This gives full module expressiveness with no fork, under the trust of the deploy. Data-only stays an optimization on top for settings skew.
  • Serialization or a Nix fork (residual). The operator tree sends an evaluated result, so the provider never evaluates foreign modules. Upstream Nix does not favor function serialization (NixOS/nix#15671), so this needs a fork, and buys only the case where neither side will evaluate across the trust boundary.

The check at submit stays the guard for the method skew that import by path cannot resolve (a pin that never had the method of the provider).

The panel meets the same cut in #723 and its exploratory work #1071, from the other direction: how an operator sets the method configuration. The first cut of #1071 injects per-deployment TF variables and connection coordinates, which is the data-only override, and it reports the same ceiling: it cannot configure a full NixOS configuration. The way past is the same stable entry attribute, with the trust on the allowlist. The exposure of effect options over the API is the same trust class as the deploy that it gates. So settings go data-only, and module-shaped method configuration goes through allowlist-gated import by path.

Parallel deployment handling (#242)

If several builds of one deployment could run in parallel (not the default replace semantics), this would affect:

  • The detail would list more than one active build.
  • Build management (cancel, inspect) would work for each build, not for each deployment.

The current design supports this through the enqueue mode and the tracking of each build. The replace mode does not start a second build while a build of the deployment runs. It does queue after a queued build, because the concurrency key of neither mode reaches Windmill (see Replace semantics under Windmill).

Endpoints for switching between hosting providers

Issue: #100.

Operators should be able to take their deployed applications and move them easily to another hosting provider.

A migration crosses the version boundary of The deploying-vs-deployed version boundary (#972): the operator can move off a provider that runs a different Fediversity revision. So the migration must carry an explicit module source that names the version the operator moves from; an absent source (which freezes to the revision of the new provider) is an error here. The attribute and program that the target provider selects, and the environments that it gives, are then checked against that pin at submit, so that a method or resource shape that the pin does not expose is refused in the form and does not fail during the deploy.

Imperative REST endpoints

Front-ends can expect button-style interactions ("restart service", "scale up") that map better to imperative operations than to full configuration deployments. Deployment actions are the route for toggles that are part of the desired state, such as suspend, and for operations of an application, which do not change the desired state.

Unstaged configurations

Draft (unstaged) configurations, edited before a deploy, are a concern of the web application, not of the API. Web apps can keep drafts locally or in their own database. This is the "intent fact" of the front-end; see Two-store model and deletion for how it relates to the runtime state of api, and how the two converge on deletion.

Tracking rolling revisions of a fork

The on-demand schema and rename table of a fork is implemented (see Schema and migration sources). What stays open is to let an operator follow newer revisions of their fork over time. Now a fetch pins the revision that the module source names, so api does not see a fork update until the operator points at the new revision. Rolling updates would need a "latest" pointer that the fork publishes (a ref or tag that the fetch follows), or a periodic new resolve by api, plus a cache invalidation keyed on the resolved revision and not on the source as written. For each resolved revision, the deploy still crosses the deploying-vs-deployed version boundary (#972): api must check the current attribute, program and environments of the provider against the resolved pin before the deploy.

OpenTelemetry integration (#184)

Build progress can integrate with OpenTelemetry for structured observability. The pub/sub event schema allows this: the data of a progress event can carry span and trace context.