Feature Specification: Effect tf-incus-image
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f
(core/effects/tf/incus/image/effect.nix,
core/effects/tf/incus/image/main.tf,
core/effects/tf/incus/image/variables.tf,
core/setups/tf-incus-image.nix, core/setups/hosts-common.nix,
core/render/catalog.nix, core/setups/dev-tf-incus-image.nix,
core/setups/nixos/dev-container.nix).
The shared behavior of a standalone effect is in spec.md. The
graph grammar, the verbs and the state rules are in
../004-deployment-programs/spec.md.
The incus deploy that starts containers from the image is in
../009-deployment-methods/tf-incus.md.
This file does not repeat them.
User Scenarios & Testing (mandatory)
User Story 1 - Upload the base image of a group (Priority: P1)
An operator runs tf-incus-image for a group. The effect builds a NixOS
container image from one node's configuration. It uploads the image to the
incus server under an alias. The incus deploy then starts each container
from that image.
Why this priority: The incus deploy needs an image on the server before it can create a container.
Independent Test: Check deployment-tf-incus-hosts uploads an image
with the catalog package dev-tf-incus-image, which is this effect for
the dev group, and starts containers from it.
Acceptance Scenarios:
- Given an incus server and a client certificate, When the
operator runs
tf-incus-image, Then the server holds an image with aliasnixos-fediversity. - Given the upload is done, When the operator reads the outputs,
Then they see
fingerprintandalias. - Given the group is
dev, When the catalog lists the effects, Then it does not offertf-incus-image.
User Story 2 - Containers get their own hostname (Priority: P1)
The image comes from the configuration of one node. Each container that starts from it gets its own hostname from the deploy.
Why this priority: If the image fixes /etc/hostname, every
container keeps the imaged node's name for its life. journald then
records a wrong _HOSTNAME.
Independent Test: nix-unit-effects-tf-incus-image-hostname
evaluates the image configuration as the effect does. It checks that
the image has no /etc/hostname and keeps the node name in
networking.hostName, and that the node configuration without the
image changes has the file. No check starts a container from the image.
Acceptance Scenarios:
- Given the image is made from node
a, When nodebstarts from it, Thenbdoes not report hostnamea.
User Story 3 - Choose the alias and the source node (Priority: P2)
An operator sets TF_INCUS_IMAGE_ALIAS or TF_INCUS_IMAGE_NODE. The
effect then uploads under that alias, or builds from that node.
Why this priority: Two groups on one incus server need two aliases. A node with a large closure can be a poor image source.
Independent Test: No check sets these keys.
Acceptance Scenarios:
- Given
TF_INCUS_IMAGE_ALIAS=x, When the effect applies, Then the image has aliasx. - Given
TF_INCUS_IMAGE_NODEis unset, When the effect evaluates, Then it builds from the first node of the group. - Given
TF_INCUS_IMAGE_NODEnames no node of the group, When the effect evaluates, Then evaluation fails on thenodeenum.
User Story 4 - Upload the local-dev image (Priority: P3)
A developer runs dev-tf-incus-image. The effect uploads a small test
image to localhost. dev-tf-incus-bootstrap calls this attribute. The
catalog builds it from core/setups/dev-tf-incus-image.nix, and the image
configuration imports core/setups/nixos/dev-container.nix.
Why this priority: Local development needs an image without a full group.
Independent Test: Check deployment-tf-incus-hosts runs the catalog
package dev-tf-incus-image against its incus server, with TF_VAR_host
set to that server. Before the run, the server has no alias
nixos-fediversity. After the run, the alias targets the fingerprint that
the effect gives. A second run reads the local state and does not change
the images on the server.
Acceptance Scenarios:
- Given a local incus server, When the developer runs
dev-tf-incus-image, Then the server holds imagenixos-fediversitywithopenssh,helloand useroperator.
Edge Cases
- The alias already exists on the server: the provider has no upsert, so the apply fails. The caller must delete the old image first.
TF_VAR_tokenis set and the client certificate already exists: the provider ignores the token.- The client certificate does not exist:
enroll-client-cert.shruns inpreRunand spends the token once, beforetofustarts. TF_VAR_incus_config_dirholds noclient.crtorclient.keyand no token is set: the apply fails at run time.- The deployment method is not
tf-incus-hosts: the setup has noimageoption. - An exported
TF_VAR_hostorTF_VAR_portoverrides the baked value.
Requirements (mandatory)
Functional Requirements
- FR-001: The effect MUST build the root file system from the source node's
deployments.tf-incus-hosts.nixos-configuration. - FR-002: The image configuration MUST set
environment.etc.hostname.enable = false. - FR-003: The effect MUST build the image metadata from a separate evaluation of
lxc-image-metadata.nix. - FR-004: The effect MUST create one
incus_imagefrom the two tarballs, with aliasalias. - FR-005: The effect MUST output
fingerprintandalias. - FR-006: The source node MUST be
nodewhen it is set, and the first node of the group when it is not. - FR-007:
nodeMUST be one of the group's node names, or null. - FR-008: The default alias MUST be
nixos-fediversity, fromcore/effects/common/image-alias.nix. - FR-009: The effect MUST connect to
https://<host>:<port>, with port 8443 by default. - FR-010: The effect MUST read the client certificate from
TF_VAR_incus_config_dirat run time. - FR-011: The effect MUST run the certificate enrollment in
preRun, beforetofu. - FR-012:
hostandportMUST be runtime-overridable. - FR-013: The catalog MUST NOT offer the effect for group
dev. - FR-014: The catalog MUST bake
httpBackend = null, so the state is a keyed local file. - FR-015: The effect MUST read only
TF_INCUS_IMAGE_ALIASandTF_INCUS_IMAGE_NODEfrom the environment. - FR-016: The effect MUST expose the verbs of
tfVerbs.
Key Entities
- Image: an incus image made of a root file system tarball and a metadata tarball. The alias names it.
- Source node: the node whose configuration becomes the image.
- Client certificate:
client.crtandclient.keyin the incus config directory. - Trust token: a one-time incus token. It enrolls the client certificate.
- Image state: an OpenTofu state in a keyed local file.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: After one apply, the incus server lists exactly one image with the alias.
- SC-002: Every container that starts from the image reports its own hostname.
- SC-003:
deployment-tf-incus-hostspasses.
Assumptions
- The machine that runs the effect can build the source node's closure.
- The incus server accepts a client certificate or a trust token from the caller.
- The caller removes an old image with the same alias before an apply.
Known Deviations
dev-tf-incus-image-deploymentsetsdeployerPubKey, butcore/setups/dev-tf-incus-image.nixnever reads it (#2131). A comment incatalog.nixsays the key goes intoroot's authorized keys.variables.tfdeclarescategory, andmain.tfdoes not use it (#2131).