Feature Specification: Effect dev-tf-incus-bootstrap

Created: 2026-10-07 Status: Implemented Input: Reconstructed from the code of core/effects/tf/incus/ and core/render/ at 14504f80d.

dev-tf-incus-bootstrap prepares a local deployer for the Incus deploy (tf-incus.md). It uploads the base image (image.md), mints the Incus credentials and writes them to an environment file that the deploy reads. The shared behavior of a standalone effect is in spec.md.

User Scenarios & Testing (mandatory)

User Story 1 - Prepare a local deployer (Priority: P1)

A developer has a local Incus server and a running local state backend. They run the runner with the path of the environment file and of the Incus configuration directory. The runner uploads the base image, mints the trust tokens and writes the values that the Incus deploy needs to the environment file.

Why this priority: The local Incus deploy needs an image fingerprint and a credential. Without the runner, the developer collects them by hand.

Independent Test: integrations-incus-bootstrap runs the script of the runner with a fake incus and a fake nix, and checks the calls, their order and the environment file. No test runs it against a real Incus server. A unit test checks that the arguments of the runner equal the definitions that own them, and a unit test checks that the runner reads only its declared keys.

Acceptance Scenarios:

  1. Given a local Incus server and a local state backend, When the developer runs the runner, Then the environment file holds the deploy token, the image fingerprint and the Incus configuration directory.
  2. Given a finished run, When the developer lists the Incus images, Then the server holds the base image under its alias with the written fingerprint.
  3. Given a finished run, When the developer lists the trust entries, Then the server holds one token for the image upload and one for the deploy.

User Story 2 - Run the bootstrap again (Priority: P2)

A developer runs the runner a second time. The runner deletes the old image, uploads a new one and writes a new environment file.

Why this priority: The Incus provider cannot update an image in place. A second upload under the same alias fails without the delete.

Independent Test: integrations-incus-bootstrap checks that the runner deletes both aliases before the upload, that it ignores an image that is not found, and that it stops on each other delete error. No test runs a second run against a real Incus server.

Acceptance Scenarios:

  1. Given an image under the alias, When the runner runs, Then the old image is gone and a new one has the alias.
  2. Given no image under the alias, When the runner deletes it, Then the runner ignores that the image is not found, and continues.

User Story 3 - Fail early without a state backend (Priority: P3)

A developer runs the runner with no local state backend. The runner stops before it mints a token, and tells the developer how to start the backend.

Why this priority: A token can be used once. A run that fails after the mint wastes it.

Independent Test: integrations-incus-bootstrap runs the script with no listener on the backend port, and checks that it fails and that it calls no incus command.

Acceptance Scenarios:

  1. Given no local state backend, When the runner runs, Then it fails within a few seconds and mints no token.

Edge Cases

  • The path of the environment file or of the Incus configuration directory is not given: the runner fails at once.
  • No image flake is given: the runner builds the image from the flake in the current directory.
  • The image upload gives an empty fingerprint: the runner fails and writes no environment file.
  • The directory of the environment file does not exist: the runner creates it.

Requirements (mandatory)

Functional Requirements

  • FR-001: The runner MUST fail when the path of the environment file or of the Incus configuration directory is not given.
  • FR-002: Before it mints a token, the runner MUST check that the local state backend accepts a connection within a short time. The backend serves the deploy that reads the environment file. The image upload does not use it.
  • FR-003: The runner MUST mint one trust token for the image upload and one for the deploy.
  • FR-004: Before the upload, the runner MUST delete the image under the alias of the local-dev image deployment and the image under <alias>-stub, and MUST ignore that an image is not found. Each other delete error MUST stop the run before the upload.
  • FR-005: The runner MUST build dev-tf-incus-image from the image flake with the nix of iets-classic, and MUST upload the image with its main program, with the Incus configuration directory and the upload token. The runner MUST NOT use nix run, because iets gives nix run to the real Nix.
  • FR-006: The runner MUST read the fingerprint from the output of the upload, and MUST fail when it is empty.
  • FR-007: The runner MUST write the deploy token, the image fingerprint and the Incus configuration directory to the environment file.
  • FR-008: The catalog MUST expose the runner as dev-tf-incus-bootstrap, with no verbs and no group, and with its declared keys from the Incus bootstrap reader.
  • FR-009: The runner MUST get the image alias and the address of the state backend as arguments from the definitions that own them, and MUST hold no copy of them. The front-end that serves the local state backend MUST use the same backend address.

Key Entities

  • Environment file: The file that the runner writes and the Incus deploy reads.
  • Trust token: A one-time Incus token. One serves the image upload, one serves the deploy.
  • Base image: The image under the alias of the local-dev image deployment.
  • Local state backend: The OpenTofu HTTP state backend of local development. The local api deploys also use it.
  • Incus configuration directory: The directory that holds the client certificate.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: After one run, the Incus deploy starts with only the environment file as its Incus input.
  • SC-002: A second run ends with exactly one image under the alias.
  • SC-003: A run without a state backend mints zero tokens.
  • SC-004: The runner reads no environment key that its catalog entry does not declare.
  • SC-005: The arguments of the runner equal the definitions that own them.

Assumptions

  • The developer runs the runner on the host of the Incus server, as a user that can add trust entries.
  • The local state backend runs at its shared address, unless the developer gives a different one.
  • The front-end that calls the runner sets the file owner, the paths and the service restart.
  • The image upload keeps its state in a local file, as FR-010 of image.md states. FR-004 makes each run idempotent whatever that state holds, so a lost state or a state on a different machine is harmless.

Known Deviations

  • No test runs the runner against a real Incus server or builds the real image. integrations-incus-bootstrap runs its script with a fake incus and a fake nix, so acceptance scenarios 2 and 3 of User Story 1, scenario 1 of User Story 2 and SC-001 and SC-002 have no test.