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:

  1. Given an incus server and a client certificate, When the operator runs tf-incus-image, Then the server holds an image with alias nixos-fediversity.
  2. Given the upload is done, When the operator reads the outputs, Then they see fingerprint and alias.
  3. Given the group is dev, When the catalog lists the effects, Then it does not offer tf-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:

  1. Given the image is made from node a, When node b starts from it, Then b does not report hostname a.

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:

  1. Given TF_INCUS_IMAGE_ALIAS=x, When the effect applies, Then the image has alias x.
  2. Given TF_INCUS_IMAGE_NODE is unset, When the effect evaluates, Then it builds from the first node of the group.
  3. Given TF_INCUS_IMAGE_NODE names no node of the group, When the effect evaluates, Then evaluation fails on the node enum.

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:

  1. Given a local incus server, When the developer runs dev-tf-incus-image, Then the server holds image nixos-fediversity with openssh, hello and user operator.

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_token is set and the client certificate already exists: the provider ignores the token.
  • The client certificate does not exist: enroll-client-cert.sh runs in preRun and spends the token once, before tofu starts.
  • TF_VAR_incus_config_dir holds no client.crt or client.key and no token is set: the apply fails at run time.
  • The deployment method is not tf-incus-hosts: the setup has no image option.
  • An exported TF_VAR_host or TF_VAR_port overrides 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_image from the two tarballs, with alias alias.
  • FR-005: The effect MUST output fingerprint and alias.
  • FR-006: The source node MUST be node when it is set, and the first node of the group when it is not.
  • FR-007: node MUST be one of the group's node names, or null.
  • FR-008: The default alias MUST be nixos-fediversity, from core/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_dir at run time.
  • FR-011: The effect MUST run the certificate enrollment in preRun, before tofu.
  • FR-012: host and port MUST 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_ALIAS and TF_INCUS_IMAGE_NODE from 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.crt and client.key in 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-hosts passes.

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-deployment sets deployerPubKey, but core/setups/dev-tf-incus-image.nix never reads it (#2131). A comment in catalog.nix says the key goes into root's authorized keys.
  • variables.tf declares category, and main.tf does not use it (#2131).