Feature Specification: Resource family

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/lib/data-model.nix, core/resources/default.nix, core/lib/coordinate-resource.nix, core/lib/coordinate-provider.nix, core/setups/hosts-common.nix, core/setups/wiring/resource-routing.nix, core/setups/wiring/supporting-requests.nix, core/checks/nix-unit-tests.nix, core/checks/nix-unit-suites.nix), pull requests #1184, #1234, #1235 and #1241.

User Scenarios & Testing (mandatory)

This spec covers what all resources share. The general model of a resource (a request type plus a policy, the fail-at-eval rule for a missing provider, and the inert unselected provider) is in 001 Data Model. The wiring that calls the policies is in 005 Setups and Wiring. Each resource has its own spec, listed in the Index.

User Story 1 - Add a resource to the registry (Priority: P1)

A developer needs a new kind of thing that components can ask for. The developer adds a directory core/resources/<name>/ with a default.nix that sets resources.<name>. The developer adds the directory to the imports of core/resources/default.nix. A component then requests it under an instance key, and an environment holds a policy for it under a label.

Why this priority: components reach every external need through a resource. Without a registry entry, no component can request it.

Independent Test: no single check covers the registry. Each resource has its own suite (see the item specs in the Index). The registry is evaluated by every check that evaluates the model. nix-unit-lib-data-model covers the input types of apply and process, and nix-unit-resources-smtp covers a policy that sets apply-input-type.

Acceptance Scenarios:

  1. Given a resource in the imports of core/resources/default.nix, When a component sets resources."<instance-key>".<name> = { ... }, Then the request type of that resource checks the value.
  2. Given a policy without its own process, When a setup calls process on the requests of some components, Then the policy receives one list of the matching requests and returns apply of that list.
  3. Given a resource whose policy overrides process, When a setup calls it, Then the policy sees the requests keyed by component (for example secrets).
  4. Given a policy, When a caller gives apply or process an argument whose outer shape is not the declared input type, Then the evaluation fails with an error that names the option and the type.

User Story 2 - Select a provider per deployment (Priority: P1)

A setup needs each node to use the provider its deployment stands up. The environment policy of a contract-backed resource carries a routing module. core/setups/hosts-common.nix sets it with mkDefault from the providerModules table of core/setups/wiring/resource-routing.nix. A test can override the row. A node that consumes a resource that its deployment does not provide fails the NixOS evaluation with an assertion that names the node and the resource.

Why this priority: the routing row decides which provider fulfills a request. A wrong row gives a node a provider that does not exist.

Independent Test: nix-unit-setups-resource-routing (core/setups/wiring/resource-routing-test.nix).

Acceptance Scenarios:

  1. Given a deployment whose components provides list has a provider, When a node requests that resource, Then providerModules gives the row for that provider.
  2. Given a node with a non-empty consumerSide for a contract resource, When the deployment provides no such resource, Then unprovided returns the resource and the node evaluation fails with an assertion.
  3. Given a resource that a deployment does not provide, When no node consumes it, Then no assertion fires.

User Story 3 - Collect requests that no single component owns (Priority: P2)

A shared provider node in the hosting-provider group must know what other groups ask of it. The model declares supportingRequests for requests not tied to a single component. core/setups/wiring/supporting-requests.nix evaluates a requesting group once and collects the processed requests of every node per field. The garage, postgres, openbao and authelia nodes of the hosting provider read these collections.

Why this priority: shared providers serve other groups. Without the collection, they do not provision what those groups request.

Independent Test: nix-unit-setups-supporting-requests (core/setups/wiring/supporting-requests-test.nix).

Acceptance Scenarios:

  1. Given an operator group that requests a resource, When the hosting-provider provider node evaluates, Then the collection of that field holds the request.
  2. Given a collection of one group, When a provider node reads it, Then only that group's model is forced.

Edge Cases

  • A resource that wraps no contract (firewall, network, nixos-module, otel) returns a NixOS module or a list of modules from apply. It does not read its routing option.
  • garage has no row in providerModules. Its routing is outside that table.
  • provided answers true for a contract resource that no node of the group must provide: garage and ssl always, oidc, postgresql and redis through the cross-group route, smtp through a third-party relay, netbox and dns through an external service, and spiffe through the hosting provider's spire when the group has no spire on its roster.
  • The unprovided assertion does not cover a resource whose policy declares no consumerSide (firewall, generateFiles, network, nixos-module, otel, secrets).
  • A policy that needs per-component keys overrides process. The default process flattens the keys to "<app>.<instance-key>" and drops them. Three such policies give apply the keys and set apply-input-type: generateFiles (by instance name), secrets (by app, then by secret name) and smtp (by app). postgresql overrides process but gives apply a list.
  • process gets only the requests of the component on its node (FR-005), so a policy does not combine the requests of different components. supportingRequests collects them across components and groups, and the contract provider combines them.
  • A resource with a coordinate-resource policy (dns, netbox) emits nothing unless both the request and the policy set enable.

Requirements (mandatory)

Functional Requirements

  • FR-001: core/resources/default.nix MUST import every resource directory: oidc, dns, firewall, garage, generateFiles, ldap, netbox, nixos-module, network, otel, postgresql, redis, secrets, smtp, spiffe, ssl.
  • FR-002: Each resource MUST set description, request and policy under resources.<name>.
  • FR-003: Each policy MUST set resource-type and apply. The setup MUST reach a policy only through process.
  • FR-004: The default process MUST select the requests that carry the resource name, flatten them over components and instance keys, and call apply with the list.
  • FR-004a: process MUST take attrsOf component-resources: the requests by component, then by instance key. apply MUST take the type in the apply-input-type of the policy, which is a list of requests by default. A call of either MUST check the outer shape of its argument: each listOf and attrsOf layer of the type, and that each value below the last layer is an attribute set. The check MUST NOT merge the argument through the type, so the requests are not evaluated again.
  • FR-005: A setup MUST call process with only the requests of the node's own component (getSomeAttrs [ component ] required-resources in core/setups/hosts-common.nix).
  • FR-006: A contract-backed policy MUST take its provider selection from a routing module that the setup sets with mkDefault.
  • FR-007: core/lib/coordinate-resource.nix MUST resolve a contract in the consumer's own evaluation: it imports the contract, the provider modules and routing, and sets contracts.<contract>.want.<consumerName>.default.request.
  • FR-008: core/lib/coordinate-provider.nix MUST declare fediversity.<contract>-<flavor> options and register contracts.<contract>.providers.<provider>.module, which stays inert until a routing row selects it.
  • FR-009: A node that consumes a contract resource without a provider in its deployment MUST fail evaluation with an assertion per resource. A contract resource is a resource whose policy type declares consumerSide. resourceRouting.contractResources reads this set off the policy declarations. A contract resource that resourceRouting.provided has no answer for MUST fail evaluation.
  • FR-010: supportingRequests MUST be a deferred module of class fediversity-supporting-requests that every environment can read.
  • FR-011: A collection in supporting-requests.nix MUST force only the group it reads.
  • FR-012: Each pure decision of a resource MUST have a nix-unit suite in core/checks/nix-unit-tests.nix, with a description.

Key Entities

  • Resource: an entry resources.<name> with a request type and a policy.
  • Request: the value a component sets at resources."<instance-key>".<name>.
  • Policy: the value an environment sets at environments.<env>.resources."<label>".<name>. It holds resource-type, apply-input-type, apply and process.
  • Label: the key under which an environment holds a policy, for example "database" for postgresql or "age" for secrets.
  • routing: a deferred NixOS module in a contract-backed policy that selects and configures a provider.
  • consumerSide: the list of NixOS modules that a contract-backed apply returns for the consumer node.
  • providerModules: the routing table in core/setups/wiring/resource-routing.nix, one row per resource.
  • supportingRequests: requests not tied to one component, collected across groups for the shared provider nodes.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-resources passes. It groups the suites whose test files live under core/resources/ and runs on ram-4g from .forgejo/workflows/nix-unit-resources.yaml.
  • SC-002: nix-unit-setups-resource-routing and nix-unit-setups-supporting-requests pass.
  • SC-003: Every resource in FR-001 has an item spec in the Index.

Assumptions

  • The setup gives each node the policies of one environment.
  • A component that needs a resource sets a request. It does not read another node's configuration.
  • The contract types come from the vendored contracts library under nix/contracts/.

Known Deviations

  • core/resources/default.nix has the comment FIXME consider which more could be handled by (cross-node) contracts. Several resources still render NixOS modules directly.

Index