---
id: w1/m14/t002
title: "Operator reconciler: `KeyValue` → Valkey workload + Service + connection Secret"
worker: worker1
status: done
estimate: 45m
depends_on: [w1/m14/t001]
---

## Objective

Add an operator reconciler that projects a `KeyValue` CR into a single-instance Valkey workload (StatefulSet, or Deployment + PVC) plus a ClusterIP Service (internal service DNS) plus a generated password Secret — mechanism only, no DB, no business logic. Mirror the structure of the `Database`→CNPG controller but target a plain Valkey instance sized to the CR's tier.

## Context

- `lego/operator/internal/controller/database_controller.go` — the analog to copy: watches the CR, resolves the tier from `lego/types/tiers`, projects owned runtime objects, sets status (phase, host). New file: `lego/operator/internal/controller/keyvalue_controller.go`.
- Manager wiring: register the new reconciler alongside the existing controllers (the same place `DatabaseReconciler.SetupWithManager` is called — `cmd/manager/` / the controller setup).
- Owned objects per `KeyValue`: a single-instance Valkey **StatefulSet (or Deployment + PVC)**, a **ClusterIP Service** giving the internal DNS name `<name>.<tenant-ns>.svc:6379`, and a **Secret** holding the generated password. Set owner references so delete cascades.
- Tier → resources: read the Valkey tier catalog added in t001 and set `resources.requests==limits` + storage size accordingly (mirror `tier_resources_test.go` expectations for Postgres).
- RBAC markers live on the controller (`./...`); after adding them run `make manifests` from `lego/operator/` to regenerate role rules.
- Keep it runtime-mechanism only per `lego/operator/CLAUDE.md`: the operator imports `types/`, never `backend/`.

## Steps

1. Add `lego/operator/internal/controller/keyvalue_controller.go`: a `KeyValueReconciler` that reconciles the CR into a Valkey StatefulSet (or Deployment + PVC), a ClusterIP Service, and a password Secret, all owner-referenced to the CR.
2. Resolve the tier from `lego/types/tiers` and apply compute (`requests==limits`) + storage to the workload.
3. Generate a password into the Secret on first reconcile (idempotent — reuse if present); set `status.phase` and `status.host` (the internal service DNS).
4. Wire `SetupWithManager` into the manager setup and add RBAC markers; run `make manifests generate` from `lego/operator/`.

## Files

- `lego/operator/internal/controller/keyvalue_controller.go` (new)
- `cmd/manager/` (or wherever reconcilers are registered) — wire `SetupWithManager`
- `lego/operator/config/rbac/` (generated from RBAC markers)

## Acceptance criteria

- [ ] Applying a `KeyValue` CR creates a single-instance Valkey workload, a ClusterIP Service exposing the internal DNS host, and a password Secret — all owner-referenced to the CR.
- [ ] The workload's compute + storage match the CR's tier from the Valkey catalog.
- [ ] `status.phase` and `status.host` (internal service DNS) are populated on reconcile.
- [ ] `cd lego/operator && go build ./...` is clean and `make manifests generate` leaves RBAC/CRD output consistent.

## Out of scope

- The optional public Traefik TCP/SNI route and connection-info Secret shape (t003).
- envtest coverage (t005).
- Any REST/GraphQL/MCP/dashboard surface (deferred to w2/w5).
