Feature Specification: Effect netbox-seed

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

netbox-seed creates in NetBox the objects that the Incus deploy (tf-incus.md) reads but does not create: the attribution tags, the cluster type, the cluster and an optional tenant. The shared behavior of a standalone effect is in spec.md. The verbs and the state rules are in 004.

User Scenarios & Testing (mandatory)

User Story 1 - Seed a fresh NetBox for the Incus deploy (Priority: P1)

An operator has a NetBox with no objects of the project. They run netbox-seed once with the NetBox address and a token file. The effect creates the tags, the cluster type and the cluster. The Incus deploy then finds these objects by name.

Why this priority: Without the seed, the deploy has no cluster to put its machines in.

Independent Test: An integration test, integrations-effect-run, runs netbox-seed from the catalog against a fake NetBox and a fake state backend. It asserts the objects of a first run, that a second run writes nothing and a plan reports no changes, and the outputs. A VM test of the Incus deploy creates the same kinds of objects through the NetBox building blocks. test-netbox-consumers applies the seed graph to a real NetBox, asserts that NetBox holds the cluster with its type and tag and the tenant with its tag, and asserts that a plan after the apply reports no changes.

Acceptance Scenarios:

  1. Given an empty NetBox, When the operator runs netbox-seed, Then NetBox holds the default tag, cluster type and cluster.
  2. Given a finished seed, When the operator runs it again with the same inputs, Then OpenTofu reports no changes.
  3. Given a finished seed, When the operator reads the outputs, Then they get the identifiers of the cluster, the tenant and the cluster type, and the tag names.

User Story 2 - Seed a second deployment in the same cluster (Priority: P2)

An operator runs two deployments against one NetBox cluster. They give a tenant to each seed. Each seed creates one tenant, and each deploy keys its machines on its own tenant.

Why this priority: NetBox keys the uniqueness of a machine on its cluster, its tenant and its name. Two deployments without tenants collide, and NetBox refuses the second.

Independent Test: integrations-effect-run runs a seed with a tenant on the state of a first seed, and asserts that the one write is the tenant, that the tenant has the tags, and that the output holds its identifier. apps-tf-incus deploys with a tenant that no seed created against a real NetBox, and asserts scenario 2.

Acceptance Scenarios:

  1. Given a tenant, When the operator runs netbox-seed, Then NetBox holds that tenant with the attribution tags.
  2. Given a tenant that reaches the deploy but not the seed, When the deploy runs, Then the deploy fails because the tenant does not exist.

User Story 3 - Seed against objects that exist (Priority: P3)

The NetBox of an operator already holds the cluster type or the tags. The operator tells the seed not to create them. The effect then reads the cluster type by name and does not manage the tags.

Why this priority: A shared NetBox can hold objects that other teams own. The seed must not take ownership of them.

Independent Test: integrations-effect-run runs one seed with an empty tag list and with the cluster type to read, against a NetBox that holds that type. It asserts the first two scenarios for that one run. A second seed runs with createTags = false, against a NetBox that holds the tag, and asserts the third scenario.

Acceptance Scenarios:

  1. Given an existing cluster type and the seed told not to create it, When the seed applies, Then the cluster refers to the existing type, and the state holds no cluster type.
  2. Given an empty tag list, When the seed applies, Then it creates no tags, and the cluster carries no tags.
  3. Given an existing tag and the seed told not to create it, When the seed applies, Then it creates no tag, the cluster carries the tag, and the state holds no tag.

User Story 4 - Plan without a change (Priority: P3)

An operator runs the plan verb of netbox-seed against a production NetBox. The plan shows the changes and creates nothing.

Why this priority: A plan that applies changes production without review.

Independent Test: An integration test that a plan stays a plan, and that a graph without an HTTP backend keeps local state when the caller exports a backend address. integrations-effect-run runs the plan verb of netbox-seed against an empty fake NetBox and asserts that it writes nothing.

Acceptance Scenarios:

  1. Given an empty NetBox, When the operator runs the plan, Then NetBox still holds no cluster.

Edge Cases

  • No NetBox address: the graph evaluates, and OpenTofu fails at run time.
  • An empty cluster name: the effect creates no cluster and does not read the cluster type.
  • A tag list that is not a JSON array: the evaluation fails.
  • No seed state address: the state is a local file, keyed on the cluster name. A second workstation does not see it.
  • Two seeds with different cluster names: each gets its own state key, so the second seed does not rename the first cluster.
  • A token that the caller exports wins over the token file.

Requirements (mandatory)

Functional Requirements

  • FR-001: The effect MUST create a tag for each name in the tag list, and none when the seed must not create tags.
  • FR-002: When the cluster name is not empty, the effect MUST create the cluster type, or read it by name when the seed must not create it.
  • FR-003: The effect MUST create the cluster, tagged with the tag list, only when the cluster name is not empty.
  • FR-004: The effect MUST create the tenant, tagged with the tag list, only when the tenant name is not empty.
  • FR-005: The cluster and the tenant MUST depend on the tags, because NetBox resolves tags by name.
  • FR-006: The effect MUST read the API token from the token file at run time, and a token that the caller exports MUST win. The derivation MUST hold only the path of the token file, never the token.
  • FR-007: The state key MUST end in the cluster name, so that seeds for different clusters keep separate states.
  • FR-008: Without a seed state address, the state MUST be a local file, and a backend address that the caller exports MUST NOT change it. The seed state address MUST set the HTTP backend address, and the effect MUST NOT read the general backend address for it.
  • FR-009: The effect MUST read only its declared keys. An absent key MUST keep the default of the effect. Each key MUST have the same name as the key that the Incus deploy reads, so that the seed and the deploy agree.
  • FR-010: The effect MUST expose the OpenTofu verbs as netbox-seed and one variant per verb.
  • FR-011: The effect MUST use the NetBox provider at its pinned version. The graph MUST give no version, and the plugin mirror of the effect MUST hold only the pinned version.
  • FR-012: The effect MUST declare no effect metadata and MUST be a building block, so the operator form does not offer it.

Key Entities

  • Tag: An attribution marker. Its description names the group.
  • Cluster type: The type of the cluster. The seed creates it or reads it.
  • Cluster: The cluster that the Incus deploy puts its machines in.
  • Tenant: Optional. It separates the machines of two deployments in one cluster.
  • Seed state: An OpenTofu state, keyed on the cluster name. It is a local file, or it lives at the seed state address.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: After one apply on an empty NetBox, the Incus deploy finds its cluster and tags by name.
  • SC-002: A second apply with the same inputs reports zero changes.
  • SC-003: A plan creates zero NetBox objects.
  • SC-004: The effect reads no undeclared environment key.
  • SC-005: The graph has no version literal, and the lock file of the effect selects the pinned provider version.
  • SC-006: The effect is a building block and declares no effect metadata.

Assumptions

  • The operator has a NetBox API token with write access to tags, cluster types, clusters and tenants.
  • The machine that runs the effect can reach NetBox.
  • The operator runs the seed before the first Incus deploy.
  • The tags that the deploy names exist before the deploy. The seed creates them, or a different party does.

Known Deviations

None.