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:
- Given a group with DNS nodes, When the operator runs
<group>-octodns-zone, Then the program runs thesyncverb of theoctodnsoption of thetf-incus-hostssetup for that group. - Given the
octodns-zoneeffect, When the catalog lists its key set, Then the set is thetf-incuskey set plus the keys of theoctodnsreader. - Given a registrar effect, When the catalog lists its key set, Then the set is the
tf-incuskey 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:
- Given a group with no DNS nodes and DNS routing that is not external, When the catalog enumerates its effects, Then
octodns-zoneis not in the list. - Given no
REGISTRARin the environment, When the catalog enumerates the effects of any group, Then no registrar effect is in the list. - Given the
devgroup, When the catalog enumerates its effects, Then the generatedtf-incus-imageis not in the list, and the hand-bounddev-tf-incus-imageanswers 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:
- Given the
packagesoutput, When the operator lists it, Then it hasnetbox-seedand onenetbox-seed-<verb>for each verb intf-verbs.nix. - 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:
- Given
octodns-zoneandtf-incus-image, which declareeffect-metaand nonodes, Whencore/effects/default.nixbuildseffectMeta, Then both havekind = "effect". - Given the registrar and
tf-netbox-*effects, which are inbuildingBlocksand declare noeffect-meta, WheneffectMetais built, Then they are not in it. - Given an effect that declares no
effect-metaand is not inbuildingBlocks, Whennix-unit-effects-registryruns, Then it fails. An effect with both marks also fails.
Edge Cases
availableruns 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.
availablethen 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_ADDRESSand keeps a keyed local state. - A registrar step is offered only when the registrar has that step in
core/effects/tf/registrars.nixand every coordinate in itsneedsis set. - The root select by name has
dev-tf-incus-imageand its verbs. It does not havenetbox-seedordev-tf-incus-bootstrap, which are inpackagesonly.
Requirements (mandatory)
Functional Requirements
- FR-001:
effectsForincore/render/catalog.nixMUST define each generated effect withavailable,setup,setupArgs,envKeys,project,verbs,runDescriptionandverbDescription. - 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
effectsForMUST go througheffectsForGroup, so that the enumerated names and the buildable attributes are the same set. - FR-004:
availableMUST NOT evaluate a setup. - FR-005: The catalog MUST name a generated effect
<group>-<effect>, with<group>-<effect>-<verb>for each verb in itsverbs. - FR-006:
standaloneEffectsMUST describe each hand-bound standalone deployment withdeployment,run,effect,group,source,verbs,envKeysanddescription.standaloneDeploymentsMUST build them.core/render/outputs.nixMUST 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
availablefor that group. - FR-008:
core/effects/common/standalone-env.nixMUST declare the coordinates of theimage,registrar,netboxSeed,incusBootstrapandoctodnsreaders with a description and a default text.incusBootstrapandoctodnshave noenvConfig: their programs read the keys at run time. - FR-009: The standalone key sets MUST NOT join
deployEnvKeys.env-keys.nixMUST expose them asstandaloneEnvKeys. - FR-010: Each effect reader MUST read only its declared keys, as the
test-*-env-reads-only-declaredtests check. The program of a run-time reader MUST read exactly its declared keys, astest-incus-bootstrap-env-matches-the-runnerandtest-octodns-env-matches-the-verbscheck on the program sources. - FR-011:
attributeFamiliesMUST 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-metaoption.core/effects/default.nixMUST put it ineffectSettingswhen it has nonodesroster. Each other effect MUST be inbuildingBlocks. An effect MUST NOT have both marks. - FR-013: The
sourceof each hand-bound standalone deployment MUST name a file of the repository that is not undercore/checks/, asnix-unit-render-catalog-sourcechecks. The source ofdev-tf-incus-imageiscore/setups/dev-tf-incus-image.nix.
Key Entities
- Generated effect: One entry in
effectsFor, instantiated per group whereavailableis true. - Hand-bound standalone deployment: One entry in
standaloneEffects, built bystandaloneDeployments, with a bare name. - Standalone reader: One
withEnvKeysreader instandalone-env.nix:image,registrar,netboxSeed,incusBootstraporoctodns. - Effect settings: The
effect-metaentries withkind = "effect"ineffectSettings. - Building block: An effect in
buildingBlocks. It declares noeffect-meta, and the form does not offer it.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix flake showand 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-configshows. - SC-003: Every name in
attributeFamilies.groups.<group>.effectsbuilds.
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-bootstrapornetbox-seedagainst a target (principle VII, #2129). The generated effects andnetbox-seedread 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-dnssecandregistrar-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-imageanddev-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.