Feature Specification: Effect family effects

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/render/catalog.nix, core/render/outputs.nix, core/effects/default.nix, core/effects/common/standalone-env.nix, core/effects/common/env-keys.nix, core/effects/common/effect-meta.nix), pull requests #1666, #1790 and #1869.

A standalone effect is a program that is not a per-node deploy. It does one job beside the deploys: it builds an image, publishes a zone, acts on a registrar or seeds NetBox. This spec covers what all standalone effects share. The program grammar and the verbs are in 004. The methods are in 009. Each effect has its own item spec, listed in the Index.

User Scenarios & Testing (mandatory)

User Story 1 - Run a group effect by its generated name (Priority: P1)

An operator runs <group>-<effect> or <group>-<effect>-<verb>. The catalog builds the effect from the same setup evaluation as a method, and takes one top-level option out of it. The effect acts with the same coordinates as the deploy of that group.

Why this priority: An effect that reads other coordinates than the deploy publishes records or a delegation that do not match the nodes.

Independent Test: nix-unit-env-config (test-image-env-reads-only-declared, test-registrar-env-reads-only-declared) and nix-unit-render-env. No check runs a generated effect against a live target.

Acceptance Scenarios:

  1. Given a group with DNS nodes, When the operator runs <group>-octodns-zone, Then the program runs the sync verb of the octodns option of the tf-incus-hosts setup for that group.
  2. Given the octodns-zone effect, When the catalog lists its key set, Then the set is the tf-incus key set plus the keys of the octodns reader.
  3. Given a registrar effect, When the catalog lists its key set, Then the set is the tf-incus key set plus the registrar keys.

User Story 2 - Enumerate only the effects a group can run (Priority: P1)

The api, nix flake show and the docs site enumerate the effect names of each group. An effect that a group cannot run is not in the list and has no attribute.

Why this priority: An attribute that throws on evaluation takes down the enumeration for every group.

Independent Test: nix-unit-render-env covers the catalog. No check asserts that effectsForGroup drops an effect whose available is false.

Acceptance Scenarios:

  1. Given a group with no DNS nodes and DNS routing that is not external, When the catalog enumerates its effects, Then octodns-zone is not in the list.
  2. Given no REGISTRAR in the environment, When the catalog enumerates the effects of any group, Then no registrar effect is in the list.
  3. Given the dev group, When the catalog enumerates its effects, Then the generated tf-incus-image is not in the list, and the hand-bound dev-tf-incus-image answers to that name.

User Story 3 - Run a hand-bound standalone deployment (Priority: P2)

Some effects belong to no group. The catalog binds them by hand and exposes them under bare names: dev-tf-incus-image, dev-tf-incus-bootstrap and netbox-seed.

Why this priority: These effects come before a provider has nodes. The base image and the NetBox seed are inputs of the first deploy.

Independent Test: nix-unit-env-config (test-netbox-seed-env-reads-only-declared), and deployment-tf-incus-hosts, which runs the catalog package dev-tf-incus-image against its incus host and checks the image alias there before and after the run. No check applies dev-tf-incus-bootstrap or netbox-seed.

Acceptance Scenarios:

  1. Given the packages output, When the operator lists it, Then it has netbox-seed and one netbox-seed-<verb> for each verb in tf-verbs.nix.
  2. Given dev-tf-incus-bootstrap, When the operator lists its verbs, Then there are none, because it is a shell runner and not an OpenTofu graph.

User Story 4 - Pick an effect in the operator form (Priority: P3)

The form offers a branch for each operator-facing effect. An effect that declares effect-meta and has no nodes roster gets kind = "effect" and joins effectSettings.

Why this priority: The form must not hand-list effects. A new effect with effect-meta gets its branch without a front-end change.

Independent Test: nix-unit-lib covers the data model. nix-unit-effects-registry checks that each effect declares effect-meta or is in buildingBlocks. No check asserts the content of effectSettings.

Acceptance Scenarios:

  1. Given octodns-zone and tf-incus-image, which declare effect-meta and no nodes, When core/effects/default.nix builds effectMeta, Then both have kind = "effect".
  2. Given the registrar and tf-netbox-* effects, which are in buildingBlocks and declare no effect-meta, When effectMeta is built, Then they are not in it.
  3. Given an effect that declares no effect-meta and is not in buildingBlocks, When nix-unit-effects-registry runs, Then it fails. An effect with both marks also fails.

Edge Cases

  • available runs before a setup is evaluated. It reads only group data and environment coordinates, so enumeration forces no setup.
  • The glue effects are keyed by the union of DNS labels across all groups. available then narrows each one to the groups that run that label.
  • The image effect has no HTTP state backend on any provider. It ignores TF_HTTP_ADDRESS and keeps a keyed local state.
  • A registrar step is offered only when the registrar has that step in core/effects/tf/registrars.nix and every coordinate in its needs is set.
  • The root select by name has dev-tf-incus-image and its verbs. It does not have netbox-seed or dev-tf-incus-bootstrap, which are in packages only.

Requirements (mandatory)

Functional Requirements

  • FR-001: effectsFor in core/render/catalog.nix MUST define each generated effect with available, setup, setupArgs, envKeys, project, verbs, runDescription and verbDescription.
  • FR-002: A generated effect MUST be a projection (project) of one setup evaluation. It MUST NOT have its own setup module.
  • FR-003: Every consumer of effectsFor MUST go through effectsForGroup, so that the enumerated names and the buildable attributes are the same set.
  • FR-004: available MUST NOT evaluate a setup.
  • FR-005: The catalog MUST name a generated effect <group>-<effect>, with <group>-<effect>-<verb> for each verb in its verbs.
  • FR-006: standaloneEffects MUST describe each hand-bound standalone deployment with deployment, run, effect, group, source, verbs, envKeys and description. standaloneDeployments MUST build them. core/render/outputs.nix MUST expose them under bare names.
  • FR-007: A hand-bound standalone deployment that is the instance of a generated effect MUST name that effect and group. The generated effect MUST then be not available for that group.
  • FR-008: core/effects/common/standalone-env.nix MUST declare the coordinates of the image, registrar, netboxSeed, incusBootstrap and octodns readers with a description and a default text. incusBootstrap and octodns have no envConfig: their programs read the keys at run time.
  • FR-009: The standalone key sets MUST NOT join deployEnvKeys. env-keys.nix MUST expose them as standaloneEnvKeys.
  • FR-010: Each effect reader MUST read only its declared keys, as the test-*-env-reads-only-declared tests check. The program of a run-time reader MUST read exactly its declared keys, as test-incus-bootstrap-env-matches-the-runner and test-octodns-env-matches-the-verbs check on the program sources.
  • FR-011: attributeFamilies MUST list the generated effects of each group and the hand-bound standalone deployments, with their setups, verbs, key sets and descriptions, for the docs site.
  • FR-012: An effect that the form offers MUST declare the internal effect-meta option. core/effects/default.nix MUST put it in effectSettings when it has no nodes roster. Each other effect MUST be in buildingBlocks. An effect MUST NOT have both marks.
  • FR-013: The source of each hand-bound standalone deployment MUST name a file of the repository that is not under core/checks/, as nix-unit-render-catalog-source checks. The source of dev-tf-incus-image is core/setups/dev-tf-incus-image.nix.

Key Entities

  • Generated effect: One entry in effectsFor, instantiated per group where available is true.
  • Hand-bound standalone deployment: One entry in standaloneEffects, built by standaloneDeployments, with a bare name.
  • Standalone reader: One withEnvKeys reader in standalone-env.nix: image, registrar, netboxSeed, incusBootstrap or octodns.
  • Effect settings: The effect-meta entries with kind = "effect" in effectSettings.
  • Building block: An effect in buildingBlocks. It declares no effect-meta, and the form does not offer it.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix flake show and the api enumeration evaluate no setup to list the effects.
  • SC-002: Every environment variable that an effect reader reads is in its declared key set, as nix-unit-env-config shows.
  • SC-003: Every name in attributeFamilies.groups.<group>.effects builds.

Assumptions

  • The operator runs an effect after the deploy that it depends on, or before the deploy that depends on it. The catalog does not order effects.
  • The registrar credentials and the NetBox token come from the caller environment.

Known Deviations

  • No check runs a generated effect, dev-tf-incus-bootstrap or netbox-seed against a target (principle VII, #2129). The generated effects and netbox-seed read their coordinates and secrets from the environment at evaluation, and a check cannot set that environment.

Index

  • octodns.md: octodns-zone, which publishes the DNS records of a group to its authoritative service.
  • registrars.md: registrar-domain, registrar-nameservers, registrar-dnssec and registrar-glue-<label>, which register the apex and delegate it to the nameservers of the group.
  • netbox-seed.md: netbox-seed, which seeds NetBox with the cluster, cluster type and tag that the incus deploy reads.
  • image.md: tf-incus-image and dev-tf-incus-image, which build the base Incus image and upload it.
  • incus-bootstrap.md: dev-tf-incus-bootstrap, which provisions the incus credentials and image of a local-dev deployer.