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:
- Given a resource in the
importsofcore/resources/default.nix, When a component setsresources."<instance-key>".<name> = { ... }, Then the request type of that resource checks the value. - Given a policy without its own
process, When a setup callsprocesson the requests of some components, Then the policy receives one list of the matching requests and returnsapplyof that list. - Given a resource whose policy overrides
process, When a setup calls it, Then the policy sees the requests keyed by component (for examplesecrets). - Given a policy, When a caller gives
applyorprocessan 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:
- Given a deployment whose components
provideslist has a provider, When a node requests that resource, ThenproviderModulesgives the row for that provider. - Given a node with a non-empty
consumerSidefor a contract resource, When the deployment provides no such resource, Thenunprovidedreturns the resource and the node evaluation fails with an assertion. - 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:
- Given an operator group that requests a resource, When the hosting-provider provider node evaluates, Then the collection of that field holds the request.
- 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 fromapply. It does not read itsroutingoption. garagehas no row inproviderModules. Its routing is outside that table.providedanswerstruefor a contract resource that no node of the group must provide:garageandsslalways,oidc,postgresqlandredisthrough the cross-group route,smtpthrough a third-party relay,netboxanddnsthrough an external service, andspiffethrough the hosting provider's spire when the group has no spire on its roster.- The
unprovidedassertion does not cover a resource whose policy declares noconsumerSide(firewall,generateFiles,network,nixos-module,otel,secrets). - A policy that needs per-component keys overrides
process. The defaultprocessflattens the keys to"<app>.<instance-key>"and drops them. Three such policies giveapplythe keys and setapply-input-type:generateFiles(by instance name),secrets(by app, then by secret name) andsmtp(by app).postgresqloverridesprocessbut givesapplya list. processgets only the requests of the component on its node (FR-005), so a policy does not combine the requests of different components.supportingRequestscollects them across components and groups, and the contract provider combines them.- A resource with a
coordinate-resourcepolicy (dns,netbox) emits nothing unless both the request and the policy setenable.
Requirements (mandatory)
Functional Requirements
- FR-001:
core/resources/default.nixMUST 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,requestandpolicyunderresources.<name>. - FR-003: Each policy MUST set
resource-typeandapply. The setup MUST reach a policy only throughprocess. - FR-004: The default
processMUST select the requests that carry the resource name, flatten them over components and instance keys, and callapplywith the list. - FR-004a:
processMUST takeattrsOf component-resources: the requests by component, then by instance key.applyMUST take the type in theapply-input-typeof the policy, which is a list of requests by default. A call of either MUST check the outer shape of its argument: eachlistOfandattrsOflayer 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
processwith only the requests of the node's own component (getSomeAttrs [ component ] required-resourcesincore/setups/hosts-common.nix). - FR-006: A contract-backed policy MUST take its provider selection from a
routingmodule that the setup sets withmkDefault. - FR-007:
core/lib/coordinate-resource.nixMUST resolve a contract in the consumer's own evaluation: it imports the contract, the provider modules androuting, and setscontracts.<contract>.want.<consumerName>.default.request. - FR-008:
core/lib/coordinate-provider.nixMUST declarefediversity.<contract>-<flavor>options and registercontracts.<contract>.providers.<provider>.module, which stays inert until aroutingrow 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.contractResourcesreads this set off the policy declarations. A contract resource thatresourceRouting.providedhas no answer for MUST fail evaluation. - FR-010:
supportingRequestsMUST be a deferred module of classfediversity-supporting-requeststhat every environment can read. - FR-011: A collection in
supporting-requests.nixMUST force only the group it reads. - FR-012: Each pure decision of a resource MUST have a
nix-unitsuite incore/checks/nix-unit-tests.nix, with adescription.
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 holdsresource-type,apply-input-type,applyandprocess. - Label: the key under which an environment holds a policy, for example
"database"forpostgresqlor"age"forsecrets. 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-backedapplyreturns for the consumer node.providerModules: the routing table incore/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-resourcespasses. It groups the suites whose test files live undercore/resources/and runs onram-4gfrom.forgejo/workflows/nix-unit-resources.yaml. - SC-002:
nix-unit-setups-resource-routingandnix-unit-setups-supporting-requestspass. - 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.nixhas the commentFIXME consider which more could be handled by (cross-node) contracts. Several resources still render NixOS modules directly.