Feature Specification: Resource redis
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/redis/default.nix, core/resources/redis/external-provider.nix, core/resources/redis/port.nix, core/resources/redis/port-test.nix, core/resources/redis/wait-reachable.sh, core/setups/wiring/resource-routing.nix, core/setups/hosts-common.nix), pull requests #1184 and #1206, and issue #527.
User Scenarios & Testing (mandatory)
User Story 1 - Connect an application to its own valkey namespace (Priority: P1)
A component needs a Redis-compatible cache. It states a request with a
namespace and a consumerName. The policy prefixes the namespace and
routes the request to the external provider. The provider gives the
component a host, a port and a password file. The port and the password are
private to the namespace, so two tenants do not share data.
Why this priority: the Fediverse applications keep their queues and caches in Redis.
Independent Test: contracts-redis-valkey (the redis contract with the
reference valkey provider and ACL-isolated namespaces) and
test-valkey-service.
Acceptance Scenarios:
- Given a routed request with
namespace = "mastodon"andnamespacePrefix = "uni", When the deployment is evaluated, Then the component getshost, the port ofuni-mastodon, andpasswordFile = "<passwordDir>/uni-mastodon". - Given the password file, When the OpenBao agent renders it, Then its content comes from
database/static-creds/<namespace>, or fromdatabase/static-creds/<namespace>-<deployment namespace>in a tenant deployment. - Given a request that no provider is routed for, When the deployment is evaluated, Then the resource emits nothing for it, and the component keeps its local Redis.
User Story 2 - Derive the port of a namespace without a registry (Priority: P1)
Each namespace runs as its own valkey@<namespace> instance on the group's
valkey node. The consumer and the valkey node must agree on the port without
a shared table. Both compute it from the namespace name.
Why this priority: a different port on the two sides breaks every connection.
Independent Test: nix-unit-resources-redis-port.
Acceptance Scenarios:
- Given the namespace
mastodon, WhenportOfruns, Then the port is7288. The test also fixes the vectors ofpeertube,pixelfedand theiruni-forms. - Given the namespaces of all operator apps, bare or prefixed, When the ports are computed, Then no two are equal and all are in
[6400, 7400).
User Story 3 - Hold a unit until valkey answers (Priority: P2)
A unit must not start before the valkey instance accepts connections. The
request lists such units in gateUnits. A Laravel migration run needs
this, because a run before the cache answers fails.
Why this priority: a migration that runs against an absent cache fails, and the unit must start again.
Independent Test: nix-unit-setups-valkey-gate checks which units have the gate, and that the probe reaches the address the client of the application uses.
Acceptance Scenarios:
- Given a unit in
gateUnits, When it starts, Then anExecStartPrerunsvalkey-wait-reachable <host> <port> <tls|plain> [<cafile>]. The mode and the CA file come from thetlsandcaFilefields of the contract result. - Given the instance answers
+PONG,NOAUTHorWRONGPASS, over a verified TLS connection intlsmode or over plain TCP inplainmode, When the probe runs, Then the probe exits0. - Given no answer after
ATTEMPTStries (default 30,DELAYdefault 1 second), When the probe ends, Then it exits1and the unit does not start.
Edge Cases
- The group has no
valkeynode: the policyhostthrows "group has novalkeymachine" when a routed request reads it. - In
tlsmode without a CA file argument, the probe passesSSL_CERT_FILE, when it is set, toopenssl s_clientas-CAfile. - The policy
tls.enableis false: the valkey node serves plain text, and each consumer getstls = falseandcaFile = null, so it connects withredis://. - Each probe attempt has a timeout of 5 seconds.
Requirements (mandatory)
Functional Requirements
- FR-001: The request MUST accept
namespace,consumerNameandgateUnits. The request MUST contain only data, so that it can be written to JSON. - FR-002: The policy MUST accept
enable,host,namespacePrefix(default empty),passwordDir(default/run/redis-external),tls.enable,tls.caFileandrouting, and MUST expose the read-onlycrossNode. - FR-002a: The deployment MUST set the default of
tls.enableto whether its PKI plan holds the valkey host (core/setups/hosts-common.nix). The TLS gate of the valkey node, the OpenBao valkey engine,redis-commanderand every consumer MUST read this one value. - FR-003: The resource MUST prefix the namespace as
<namespacePrefix>-<namespace>when the prefix is not empty. - FR-004: The resource MUST emit consumer configuration only for a routed request, and MUST drop an unrouted request without an error.
- FR-005: The
externalprovider MUST fulfill a request withhost,port,username = "app",passwordFile = "<passwordDir>/<namespace>",tls = tls.enableandcaFile(tls.caFilewhentlsis true, else null). - FR-006:
portOfMUST be6400 + (int(sha256(namespace)[0:7], 16) mod 1000). - FR-007: The ports of the operator app namespaces MUST be free of collisions, bare and prefixed.
- FR-008: The consumer MUST connect as the
appuser.apphas full access (~* +@all), and the separate process per namespace gives the isolation. - FR-009: The resource MUST NOT provision the valkey daemon or the OpenBao engine. These come from the valkey and OpenBao components.
- FR-010: The resource MUST give each routed request to the consumer node through the
rediscontract: thebridgeof the cross-node evaluation setscontracts.redis.instances.<consumerName>.defaultto the resolved request and result, socontracts.redis.results.<consumerName>.defaultholds the result.gateUnitsMUST be atfediversity.redisConsumers.<consumerName>. The static moduleconsumer.nixMUST render the start gate from these two values only. The values MUST contain no store paths and no functions, so that they can be written to JSON.
Key Entities
- Namespace: the prefixed name that selects
valkey@<namespace>, its port and its OpenBao static role. externalprovider: the provider inexternal-provider.nixthat fulfills therediscontract with a remote valkey instance.- Probe:
valkey-wait-reachable, aPINGwith retries, over TLS or plain TCP as the contract result says. - Resolved request: the data that
consumer.nixrenders on the consumer node:gateUnitsand the contractresult.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-resources-redis-port,nix-unit-setups-valkey-gate,nix-unit-setups-valkey-tls-agreementandnix-unit-resources-plain-requestspass. - SC-002:
contracts-redis-valkeyandtest-valkey-servicepass. - SC-003: Two tenants of one app read and write separate keyspaces.
Assumptions
- The routing of the resource is in setups and wiring.
providerModules.redissetscontracts.redis.defaultProviderto theexternalprovider when the group providesredis. - The valkey node runs one instance per namespace. See the valkey component.
- The
defaultuser of each instance holds a fixed admin password from agenerateFilesbootstrap. The engine rotates only theappuser.
Known Deviations
None.