Feature Specification: Effect dev-tf-incus-bootstrap

Created: 2026-10-07 Status: Implemented Input: Reconstructed from the code at 093e68b6f (core/effects/tf/incus/host/bootstrap.sh, core/effects/tf/incus/common.nix, core/render/catalog.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 reads the env file is in ../009-deployment-methods/tf-incus.md. The image upload is in image.md. This file does not repeat them.

User Scenarios & Testing (mandatory)

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

A developer has a local incus server and a running TF backend. They run dev-tf-incus-bootstrap with OUT_TF_ENV and INCUS_CONFIG_DIR. The runner uploads the base image and mints a trust token. It writes the values that the incus deploy needs to OUT_TF_ENV.

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: No check covers this runner.

Acceptance Scenarios:

  1. Given a local incus server and a TF backend on 127.0.0.1:8888, When the developer runs the runner, Then OUT_TF_ENV holds TF_VAR_token, TF_VAR_image_fingerprint and TF_VAR_incus_config_dir.
  2. Given the runner is done, When the developer lists the incus images, Then the server holds image nixos-fediversity with the written fingerprint.
  3. Given the runner is done, When the developer lists the trust entries, Then the server holds tokens deployer-image and deployer-hosts.

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

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

Why this priority: The incus provider has no upsert for an image. A second upload under the same alias fails without the delete.

Independent Test: No check covers a second run.

Acceptance Scenarios:

  1. Given image nixos-fediversity exists, When the runner runs, Then the old image is gone and a new one has the alias.
  2. Given no image has the alias, When the runner deletes it, Then the runner ignores the "not found" error and continues.

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

A developer runs the runner with no TF backend. The runner stops before it mints a token. It prints a hint to run nix run .#terraform-backend-dev.

Why this priority: A token is single-use. A run that fails after the mint wastes it.

Independent Test: No check covers this case.

Acceptance Scenarios:

  1. Given nothing listens on 127.0.0.1:8888, When the runner runs, Then it fails within 5 seconds and mints no token.

Edge Cases

  • OUT_TF_ENV or INCUS_CONFIG_DIR is unset: the runner fails at once.
  • IMAGE_FLAKE is unset: the runner builds the image from the flake in the current directory.
  • The image upload returns an empty fingerprint: the runner fails and writes no env file.
  • The parent directory of OUT_TF_ENV does not exist: the runner creates it.

Requirements (mandatory)

Functional Requirements

  • FR-001: The runner MUST fail when OUT_TF_ENV or INCUS_CONFIG_DIR is unset.
  • FR-002: The runner MUST check that the TF backend at devTfBackend of core/effects/tf/incus/common.nix (127.0.0.1:8888) accepts a connection within 5 seconds, before it mints a token.
  • FR-003: The runner MUST mint trust token deployer-image with incus config trust add.
  • FR-004: The runner MUST delete the image alias of dev-tf-incus-image-deployment in core/render/catalog.nix (nixos-fediversity) before the upload, and ignore a "not found" error.
  • FR-005: The runner MUST upload the image with nix run ${IMAGE_FLAKE}#dev-tf-incus-image, and pass TF_VAR_incus_config_dir and TF_VAR_token.
  • FR-006: The runner MUST read the fingerprint from the fingerprint output and fail when it is empty.
  • FR-007: The runner MUST mint trust token deployer-hosts for the incus deploy.
  • FR-008: The runner MUST write TF_VAR_token, TF_VAR_image_fingerprint and TF_VAR_incus_config_dir to OUT_TF_ENV.
  • FR-009: The catalog MUST expose the runner as dev-tf-incus-bootstrap, with no verbs and no group.
  • FR-010: The catalog entry MUST declare OUT_TF_ENV, INCUS_CONFIG_DIR and IMAGE_FLAKE as its env keys, from the incusBootstrap reader of core/effects/common/standalone-env.nix.
  • FR-011: The runner MUST give bootstrap.sh the image alias and the TF backend host and port as arguments, from the definitions in FR-002 and FR-004. bootstrap.sh MUST hold no copy of them. core.lib.devTfBackend MUST give the same backend to the front-end that serves it.

Key Entities

  • Env file: the file at OUT_TF_ENV. The incus deploy reads its TF_VAR_* values.
  • Trust token: a one-time incus token. deployer-image serves the image upload, and deployer-hosts serves the deploy.
  • Base image: the image under the alias of dev-tf-incus-image-deployment.
  • Dev TF backend: the OpenTofu HTTP state backend at devTfBackend. terraform-backend-dev listens on it, and api-http-stack points its deploys at it.
  • Incus config 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 env file as its incus input.
  • SC-002: A second run ends with exactly one image under the alias.
  • SC-003: A run without a backend mints zero tokens.
  • SC-004: nix-unit-env-config passes, so the runner reads no env key that its catalog entry does not declare.
  • SC-005: nix-unit-effects-tf-incus-bootstrap passes, so the runner arguments equal the definitions that own them.

Assumptions

  • The developer runs the runner on the host of the incus server, as a user that can run incus config trust add.
  • The TF backend from terraform-backend-dev runs on devTfBackend, unless the developer sets TERRAFORM_BACKEND_ADDR.
  • The front-end that calls the runner sets the file owner, the paths and the service restart.

Known Deviations

  • The runner checks the TF backend, but the image upload keeps its state in a local file.
  • No check runs the runner.