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:

  1. Given a routed request with namespace = "mastodon" and namespacePrefix = "uni", When the deployment is evaluated, Then the component gets host, the port of uni-mastodon, and passwordFile = "<passwordDir>/uni-mastodon".
  2. Given the password file, When the OpenBao agent renders it, Then its content comes from database/static-creds/<namespace>, or from database/static-creds/<namespace>-<deployment namespace> in a tenant deployment.
  3. 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:

  1. Given the namespace mastodon, When portOf runs, Then the port is 7288. The test also fixes the vectors of peertube, pixelfed and their uni- forms.
  2. 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:

  1. Given a unit in gateUnits, When it starts, Then an ExecStartPre runs valkey-wait-reachable <host> <port> <tls|plain> [<cafile>]. The mode and the CA file come from the tls and caFile fields of the contract result.
  2. Given the instance answers +PONG, NOAUTH or WRONGPASS, over a verified TLS connection in tls mode or over plain TCP in plain mode, When the probe runs, Then the probe exits 0.
  3. Given no answer after ATTEMPTS tries (default 30, DELAY default 1 second), When the probe ends, Then it exits 1 and the unit does not start.

Edge Cases

  • The group has no valkey node: the policy host throws "group has no valkey machine" when a routed request reads it.
  • In tls mode without a CA file argument, the probe passes SSL_CERT_FILE, when it is set, to openssl s_client as -CAfile.
  • The policy tls.enable is false: the valkey node serves plain text, and each consumer gets tls = false and caFile = null, so it connects with redis://.
  • Each probe attempt has a timeout of 5 seconds.

Requirements (mandatory)

Functional Requirements

  • FR-001: The request MUST accept namespace, consumerName and gateUnits. 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.caFile and routing, and MUST expose the read-only crossNode.
  • FR-002a: The deployment MUST set the default of tls.enable to 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-commander and 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 external provider MUST fulfill a request with host, port, username = "app", passwordFile = "<passwordDir>/<namespace>", tls = tls.enable and caFile (tls.caFile when tls is true, else null).
  • FR-006: portOf MUST be 6400 + (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 app user. app has 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 redis contract: the bridge of the cross-node evaluation sets contracts.redis.instances.<consumerName>.default to the resolved request and result, so contracts.redis.results.<consumerName>.default holds the result. gateUnits MUST be at fediversity.redisConsumers.<consumerName>. The static module consumer.nix MUST 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.
  • external provider: the provider in external-provider.nix that fulfills the redis contract with a remote valkey instance.
  • Probe: valkey-wait-reachable, a PING with retries, over TLS or plain TCP as the contract result says.
  • Resolved request: the data that consumer.nix renders on the consumer node: gateUnits and the contract result.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-resources-redis-port, nix-unit-setups-valkey-gate, nix-unit-setups-valkey-tls-agreement and nix-unit-resources-plain-requests pass.
  • SC-002: contracts-redis-valkey and test-valkey-service pass.
  • SC-003: Two tenants of one app read and write separate keyspaces.

Assumptions

  • The routing of the resource is in setups and wiring. providerModules.redis sets contracts.redis.defaultProvider to the external provider when the group provides redis.
  • The valkey node runs one instance per namespace. See the valkey component.
  • The default user of each instance holds a fixed admin password from a generateFiles bootstrap. The engine rotates only the app user.

Known Deviations

None.