Packages / Redis operatorView repository ↗
ARCHITECTURE & OPERATIONS

Redis, under
reconciliation.

A field manual for the Green Redis operator. Follow desired state from a local configuration to a Kubernetes controller and the Redis Droplet it keeps running.

GREEN ONLYKUBERNETES v1alpha1DIGITALOCEAN + R2SOURCE-GUIDED
2 boundariesPackage + controller
1 DropletPer RedisDeployment
7 commandsFrom render to recovery

Green, Red and Blue

Choose the Clojure, TypeScript or Python Package Skill. Each provides build, create, check, rehearse, drill, restart and delete with the same desired state and ownership guards. The native controllers use their matching Kubernetes SDK and Redis workflow.

npx skills add getcolors/redis-operator --skill package-redis-operator-red
npx skills add getcolors/redis-operator --skill package-redis-operator-blue

Copy the installed red or blue launcher to your deployment root. Build the controller with scripts/image.sh REGISTRY SHA red or blue, then pin its digest in colors.yml. Any launcher can probe any controller colour. Changing controller language runs one convergence to record its configuration.

Red source · Blue source

One repository. Two execution boundaries.

The package installs the control plane. The controller keeps one external Redis Droplet converged per RedisDeployment.

SYSTEM TOPOLOGY01 / CONTROL & DATA
Redis operator system architectureThe local Green launcher applies a RedisDeployment to Kubernetes. The controller uses the Redis package, DigitalOcean API, and SSH to manage a separate Droplet. A persistent volume holds SSH keys and convergence records. R2 holds compute state and backups. KUBERNETES · colors-redis RDB applyAPI + SSH PACKAGE / LOCAL./greencolors.yml → manifests DESIRED STATERedisDeploymentspec.config + status CONTROLLERcolors.redisobserve → converge DIGITALOCEANRedis 7.2 Dropletloopback · authenticated redis-credentials5 env vars · Secret Persistent volumework + SSH keys Cloudflare R2compute state / backupsstate¹
Redis runs on the Droplet. The Kubernetes Pod runs the controller and its workflow toolchain. ¹ The controller accesses remote state using the credentials injected into its process; the PVC stores local workflow files.
ON YOUR MACHINE

The Package Skill

The copied green launcher resolves a pinned library. validate checks desired state, tools renders manifests and wraps transports, workflow selects the lifecycle graph, and operator implements operational proofs.

INSIDE KUBERNETES

The controller image

colors.main starts green.kubernetes with one worker. colors.redis supplies validate, identity, observe, converge and delete callbacks. colors.probe runs explicit checks inside the Pod.

main.clj · 8–13
(let [transport (client/kubectl-client (if (= context "--in-cluster")
                                         {:in-cluster? true} {:context context}))
        runtime (k8s/start! (k8s/controller {:packages [(redis/package)] :client transport
                                           :namespace (or namespace "colors-dev")
                                           :workers 1 :poll-ms 2000}))
        stopped (promise)]
src/colors/main.clj ↗

Start with a render.

Build and create --dry-run work without credentials or cluster access. A real create installs infrastructure and requires explicit authorization.

Terminal · deployment project
# Install the package into a deployment project
npx skills add getcolors/redis-operator --skill package-redis-operator-green
cp .agents/skills/package-redis-operator-green/green green

# After filling in a non-secret colors.yml
./green build
./green create --dry-run

That installs the package-redis-operator-green Package Skill and copies its launcher to the deployment root; the launcher answers seven verbs — build, create, check, rehearse, drill, restart and delete — detailed in the command reference.

Provide an existing Kubernetes cluster, a usable KUBECONFIG, its explicit kube-context, a controller image digest, externally managed R2 state and backup buckets, and public worker/developer IPv4 /32 SSH sources. The launcher needs Babashka; live operations also use kubectl. The controller image contains the provisioning toolchain.

Terminal · live deployment
# Once credentials are present and creation is authorized
./green create
./green check

# Diagnostic reads: substitute your actual context and resource
kubectl --context <kube-context> get rd -n colors-redis
kubectl --context <kube-context> logs -n colors-redis   deployment/colors-redis-operator
01Preflight + render
02Namespace + Secret
03Pull Secret + install
04Apply resource
05Wait for Ready
Create graph: install waits for CRD Established and controller rollout before applying the custom resource. Ready may take up to 45 minutes.

build writes two files under .colors/<profile>/operator/: manifests.json (Namespace, CRD, ServiceAccount, Role, RoleBinding, PVC, Deployment) and redis-deployment.json. Credentials are applied separately on stdin. Dry-run skips the live steps but still renders these files.

src/io/github/getcolors/redis_operator/workflow.clj ↗

Observe first. Converge when needed.

Existence, desired-state match and live service health are independent observations. Unknown state is never permission to recreate.

01Read owned R2 state
02Query recorded ID
03Check provider + PING
04Compare success marker
05Return observation
The Green Kubernetes runtime consumes the adapter’s exists?, matches? and ready? values and drives the lifecycle.
EXPLORE AN OBSERVATIONILLUSTRATIVE · NO LIVE CALLS
redis.clj · provider-matches?
(defn provider-matches? [opts node droplet]
  (and (= (:name node) (:name droplet))
       (= (:digitalocean-region opts) (get-in droplet [:region :slug]))
       (= (:digitalocean-size opts) (:size_slug droplet))
       (= (:digitalocean-image opts) (get-in droplet [:image :slug]))
       (some #(and (= "public" (:type %)) (= (:ip node) (:ip_address %)))
             (get-in droplet [:networks :v4]))))
src/colors/redis.clj ↗

A successful convergence writes operator-success.edn with a SHA-256 configuration hash and the provider ID. The hash excludes event and resource metadata. An unchanged hash alone is insufficient: provider identity, region, size, image, recorded public IP and authenticated Redis PING must also match.

redis.clj · config-hash
(defn config-hash [config]
  (let [desired (dissoc config :green/event :green.kubernetes/resource)]
    (apply str (map #(format "%02x" (bit-and 255 %))
                    (.digest (MessageDigest/getInstance "SHA-256")
                             (.getBytes (pr-str (into (sorted-map) desired)) "UTF-8"))))))
src/colors/redis.clj ↗

converge calls the Redis package workflow with :green/event :create, then re-reads owned state before persisting the success marker. Healthy periodic checks skip that workflow. Partial initial compute state requests convergence so the compute library can resume its ownership protocol.

Desired state is a contract.

A flat, non-secret colors.yml becomes spec.config plus the operator’s deployment settings. Validation reports all errors together with exit code 2.

colors.yml · selected fields
# Selected fields; this is not a complete deployable configuration.
profile: redis-operator-example
kube-context: do-ams3-doks-fixture
namespace: colors-redis
reconcile-interval: 60s
deletion-policy: Retain
compute-prevent-destroy: true
provider-compute: digitalocean
provider-backend: r2

The API is colors.getcolors.ai/v1alpha1, kind RedisDeployment, short name rd, with namespace scope and a status subresource. Only state: running is supported. The CRD freezes profile presence and value, r2-bucket and r2-endpoint. Together, the backend endpoint, bucket and resolved profile identify ownership.

tools.clj · resource
(defn resource
  "The RedisDeployment. `spec.suspend` is deliberately absent: the CRD
  defaults it on creation and an apply must never reset a suspension."
  [opts]
  {:apiVersion "colors.getcolors.ai/v1alpha1" :kind "RedisDeployment"
   :metadata {:name (:resource-name opts) :namespace (:namespace opts)}
   :spec {:state "running"
          :deletionPolicy (:deletion-policy opts)
          :reconcileInterval (:reconcile-interval opts)
          :config (validate/config opts)}})
src/io/github/getcolors/redis_operator/tools.clj ↗
Full configuration reference 26 keys
KeyDefaultContract
profilerequiredNames the work directory, the custom resource (unless resource-name is set) and the Redis package profile, which keys its R2 state and names its Droplet. [a-zA-Z0-9][a-zA-Z0-9._-]{0,127}; immutable once created.
kube-contextrequiredThe kubectl context every call passes as --context.
namespacecolors-redisNamespace for the controller, the Secret and the resource. DNS label.
resource-nameprofileName of the RedisDeployment. DNS label.
imagerequiredController image, <repository>@sha256:<64 hex>. Built by scripts/image.sh.
image-pull-secretabsentName of an existing pull Secret in the namespace (the one DOKS registry integration injects). Absent renders no imagePullSecrets. create waits up to 120 s for it.
reconcile-interval60sspec.reconcileInterval; positive duration in ms, s, m or h.
deletion-policyRetainspec.deletionPolicy: Retain or Destroy. delete patches it to Destroy regardless, because reaching delete means the guard was lifted.
compute-prevent-destroytrueCommitted guard; delete exits 2 unless it is false.
doks-cluster-idabsentOptional DOKS cluster UUID. When set, drill fetches the node pools' Droplet IDs and refuses any of them; when absent, workers are excluded by their k8s: tags and by name only.
provider-computedigitaloceanFixed by the CRD.
provider-backendr2Fixed by the CRD.
redis-imagerequiredRedis image pinned tag@sha256:....
redis-portrequired1–65535.
redis-backup-r2-bucketrequiredBucket for RDB backup sets.
redis-backup-r2-endpointrequiredhttps:// endpoint of that bucket.
redis-backup-r2-regionrequiredUsually auto.
redis-backup-oncalendarrequiredsystemd OnCalendar expression for the backup timer.
redis-backup-retention-daysrequiredPositive integer.
redis-backup-max-age-hoursrequiredPositive integer; the monitor's freshness bound.
digitalocean-regionrequiredDroplet region slug.
digitalocean-sizerequiredDroplet size slug.
digitalocean-imagerequiredDroplet image slug.
digitalocean-ssh-sourcesrequiredNon-empty list of public IPv4 /32 networks admitted to SSH: every DOKS worker (the controller's egress) and every developer. Private, loopback, link-local, shared, documentation and reserved ranges are rejected.
r2-bucketrequiredBucket for the Redis package's compute state. Immutable.
r2-endpointrequiredhttps:// endpoint of that bucket. Immutable.
skills/package-redis-operator-green/references/configuration.md ↗

Credentials enter at runtime.

Only five COLORS_PAR_* variables enter the controller. The controller rejects other configuration overrides at startup.

01Process environment
02kubectl apply stdin
03redis-credentials
04Pod envFrom
05Workflow options
Credential path: Secret application suppresses output because kubectl errors may echo the input document.
Environment variable names · no values
COLORS_PAR_DO_TOKEN
COLORS_PAR_R2_ACCESS_KEY_ID
COLORS_PAR_R2_SECRET_ACCESS_KEY
COLORS_PAR_REDIS_BACKUP_R2_ACCESS_KEY_ID
COLORS_PAR_REDIS_BACKUP_R2_SECRET_ACCESS_KEY
redis.clj · check-environment!
(defn check-environment! [env]
  (when (some #(and (str/starts-with? % "COLORS_PAR_")
                    (not (contains? credential-vars %))) (keys env))
    (throw (ex-info "Unexpected COLORS_PAR environment override; desired configuration must come from the resource" {})))
  (when (some #(str/blank? (get env %)) (keys credential-vars))
    (throw (ex-info "Required operator credentials are missing" {}))))
src/colors/redis.clj ↗

The package reads the process environment; it does not parse .envrc.private. KUBECONFIG selects the kubeconfig and every launcher-side kubectl call supplies --context. The probe’s internal kubectl call runs inside the Pod using the in-cluster environment.

The adapter forces DigitalOcean compute, R2 backend, unmanaged Redis storage buckets and compute-prevent-destroy: true during convergence. Work defaults to /data/work (COLORS_WORKDIR can override that root). Deletion temporarily lifts the guard only after the resource explicitly selects Destroy.

Seven verbs. Explicit evidence.

Operational success is proved against the resource generation, cloud identity and live Redis service.

CommandEffectWhat it proves / requires
buildRender onlyNo credentials or cluster contact.
createInstall + convergeFive credentials; rejects a suspended/deleting resource. Ready timeout: 45 min.
checkRead + probeReady at current generation, authenticated PING, retained failure count.
rehearseBackup restore rehearsalSuspend with acknowledgement; run Redis package rehearsal; cautiously resume.
drillDelete owned DropletExact acknowledgement + DO token + identity proof. Recovery timeout: 40 min.
restartRestart controllerExactly one replica; old Pod must stop; reconciliation after the new Pod starts must preserve infrastructure.
deleteDestroy + uninstallLift destruction guard; finish finalizer before deleting namespace and possibly CRD.

Ready is polled, not sampled once

check and operational preconditions poll every 5 seconds for up to 180 seconds. Transient Reconciling and unobserved generations are allowed. Failed, Invalid, Blocked, suspension and deletion end normal readiness waits. Create tolerates Failed while the controller retries; the drill recovery loop also continues through failed reconciliation passes.

tools.clj · ready?
(defn ready?
  "Ready at the current generation: the controller has observed this spec and
  its Ready condition is True."
  [cr]
  (let [generation (get-in cr [:metadata :generation])
        status (:status cr)]
    (and (some? generation)
         (= generation (:observedGeneration status))
         (= "Ready" (:phase status))
         (boolean (some #(and (= "Ready" (:type %)) (= "True" (:status %)))
                        (:conditions status))))))
src/io/github/getcolors/redis_operator/tools.clj ↗

The probe crosses the boundary

Before any probe exec, the launcher waits for exactly one non-terminating Running controller Pod whose logs since its start time include RedisDeployment controller running. This avoids racing the controller’s dependency downloads. It then execs bb -m colors.probe inside the running controller. The probe reads the CR and owned compute state, then SSHes to the recorded node. Redis authentication is read inside the Redis container and placed in REDISCLI_AUTH, keeping passwords out of command arguments. Marker keys and values are constrained to a safe token alphabet.

probe.clj · safe-token
(defn safe-token [value]
  (when-not (and (string? value) (re-matches #"[A-Za-z0-9:_-]{1,160}" value))
    (throw (ex-info "Invalid probe token" {}))) value)
src/colors/probe.clj ↗
OperationEvidence under .colors/<profile>/evidence/
rehearsebackup-rehearsal.json · UID, suspension/resume generations, result, resume blocking reason
drillself-healing.json · before/after IDs, excluded workers, marker survival, fresh write result
restartcontroller-restart.json · before/after identity, new Pod start time, reconciliation timestamps

Three different recovery proofs.

Backup rehearsal, machine replacement and controller restart answer different operational questions.

A. Rehearse a backup

01Ready
02Suspend + CAS
03Await acknowledgement
04Run rehearsal
05Re-read + resume
CAS: the JSON patch tests metadata.resourceVersion. Suspension must be acknowledged at the new generation before the workflow runs.
tools.clj · patch-resource!
(defn patch-resource!
  "A JSON patch guarded by a resourceVersion test, so a concurrent edit fails
  the operation instead of being overwritten."
  [opts current patch]
  (kubectl opts ["patch" resource-type (:resource-name opts) "-n" (:namespace opts)
                 "--type=json" "-o" "json" "-p"
                 (json/generate-string
                  (into [{:op "test" :path "/metadata/resourceVersion"
                          :value (get-in current [:metadata :resourceVersion])}]
                        patch))]
           {:json? true}))
src/io/github/getcolors/redis_operator/tools.clj ↗

Rehearsal resumes only when UID and suspension generation still match and remote completion is certain. A completed but failed rehearsal can safely resume while reporting failure. An uncertain exec, changed resource or unreadable re-check prevents automatic resume. The original error is preserved.

B. Delete a Droplet and prove self-healing

Destructive operation · explicit authorization required
COLORS_PAR_DRILL_DELETE_OWNED_DROPLET=true ./green drill
01Prove ownership
02SET / GET marker
03DELETE exact ID
04Wait for new ID
05Verify fresh write
A single DELETE follows all ownership checks. Recovery keeps the same resource UID and generation and confirms the old Droplet now returns 404.

The live ID must match recorded state. Droplet name, probe name and profile must be identical. The recorded IP must appear among public IPv4 addresses. Any k8s: tag rejects the target; with doks-cluster-id, node-pool Droplet IDs are explicitly excluded too. Probe/JSON decode failures during recovery are retried. Marker survival is recorded, but is not required for the service-recovery proof.

C. Restart the controller, preserve the Droplet

Operational mutation · controller restart
./green restart
01Read baseline
02Scale to 0
03Wait for old Pod
04Scale to 1
05Fresh reconcile
The new Ready status must have lastReconcileTime strictly after the new Pod’s start time. The pre-restart timestamp is retained as evidence only.

The command requires a nonblank new Pod start time and rejects the old Pod UID if still present. Success also requires unchanged resource UID, generation, provider ID and convergence-record modification time, plus live health. If the old Pod cannot be confirmed stopped, the command intentionally leaves replicas at zero. It never force-deletes that Pod.

src/io/github/getcolors/redis_operator/operator.clj ↗

Finalization comes before teardown.

Retain is the resource default. The package delete command explicitly selects Destroy after its separate guard is lifted.

Destructive operation · explicit authorization required
COLORS_PAR_COMPUTE_PREVENT_DESTROY=false ./green delete
01Guard + active resource
02Set Destroy
03Delete resource
04Wait for finalizer
05Delete namespace
06Check / remove CRD
Finalizer wait: up to 30 minutes. Namespace deletion: up to 15 minutes. Keep the CRD if any RedisDeployment remains across the cluster.

The controller finalizer is colors.getcolors.ai/infrastructure. While it runs, the controller and its credentials must remain available. A confirmed missing Droplet skips unreachable application cleanup, but still performs local SSH configuration cleanup and removes owned shared compute resources.

The namespace deletion removes the controller installation and namespaced objects. Use a dedicated namespace for this installation. The committed compute-prevent-destroy stays true; even delete --dry-run refuses with exit 2 until its guard is lifted.

src/io/github/getcolors/redis_operator/workflow.clj ↗

Follow the decision. Retain the detail.

Controller stdout explains each observe, converge and delete outcome. Detailed workflow failures stay on the persistent volume.

Diagnostics · substitute angle-bracket placeholders
./green check

kubectl --context <kube-context> logs -n colors-redis   deployment/colors-redis-operator

kubectl --context <kube-context> exec -n colors-redis   deployment/colors-redis-operator --   ls -1 /data/work/<profile>/failures

kubectl --context <kube-context> exec -n colors-redis   deployment/colors-redis-operator --   cat /data/work/<profile>/failures/<file>

Failure reports retain the failed step, exit code, workflow error, optional Ansible recap and trace. Exact values of every nonempty COLORS_PAR_* environment variable are replaced with ***, longest first. Reports are atomically written as <UTC timestamp>-<step>.log, with mode 0600 in a 0700 directory; only the newest 20 remain.

redis.clj · mask
(defn mask
  "Replace the exact value of every COLORS_PAR_* variable in `env` with ***,
  longest values first so a value that contains another is masked whole."
  [text env]
  (reduce (fn [text value] (str/replace text value "***"))
          (str text)
          (->> env
               (filter (fn [[k v]] (and (str/starts-with? (str k) "COLORS_PAR_") (not (str/blank? v)))))
               (map second)
               distinct
               (sort-by count >))))
src/colors/redis.clj ↗
SymptomInterpretation / next check
Repeated ReconcilingUse the polling check. A single status read often catches an observation in progress.
Failed with retained fileRead the named failure report for OpenTofu or Ansible diagnostics. Raw output is not put in status or events.
Repeated provider-driftCheck region, size, image slug and recorded public IP against spec.config. Retired image slugs and external resizing can cause repeated convergence.
Suspended after rehearseRead backup-rehearsal.json and verify no remote workflow remains before resuming.
Restart leaves replicas at zeroThe old controller did not conclusively stop. Establish its process state before starting another.
Ready but fresh check failsThe health probe requires an authenticated live PING; stale status alone is insufficient.

Read, test, pin, release.

The package library and the controller image share source history, but deployments consume two independent pins.

Development commands · local controller requires live credentials
bb test
bb golden
./scripts/launcher.sh
bb -e "(require 'colors.main 'colors.probe) (println :controller-loads)"

# Run the controller locally with explicit context and namespace
bb controller <context> <namespace>

The tests cover observation, convergence markers, retained diagnostics, probe suspension, validation, rendering, transports and lifecycle invariants. Golden fixtures protect the CRD and rendered installation, including RBAC, security context, pull Secret and the 10,800-second shutdown grace period. The launcher test copies the payload into a temporary standalone deployment and verifies credential-free renders and refusal guards.

Source changeDeployment update
src/colors/Build and push a new controller image with scripts/image.sh after authorization; update the image digest.
src/io/github/getcolors/redis_operator/Commit and push when authorized; run bb pin from a clean pushed commit, then refresh the consumer’s skill and launcher copy.
CRD resourceUpdate both the package pin and controller image.
Consumer refresh · launcher is a copy
# After an authorized, clean, pushed package revision and bb pin
npx skills update -p -y
cp .agents/skills/package-redis-operator-green/green green

For local work across repositories, use REDIS_OPERATOR_LIB_ROOT, GREEN_LIB_ROOT, REDIS_LIB_ROOT and COLORS_COMPUTE_LIB_ROOT; do not hand-edit pins. The package repository’s root launcher is a symlink, while a deployment’s installed launcher is a copy.

scripts/image.sh is the image build/push entry point. It uses DOCKER_CONFIG, targets linux/amd64, tags by source SHA, labels the revision and prints the immutable digest. The Dockerfile installs Babashka, kubectl, OpenTofu, Ansible, SSH, Redis CLI, AWS CLI, Git and Java. First startup and cache misses resolve pinned dependencies and need outbound access to GitHub and Maven repositories. The PVC also mounts /root/.gitlibs, /root/.m2, /root/.deps.clj and /app/.cpcache, preserving dependency caches across Pod restarts.

Dockerfile ↗ deps.edn ↗ .github/workflows/cicd.yml ↗

The source behind the manual.

Implementation excerpts are copied from this checkout. Diagrams and operational descriptions explain those exact contracts.

FileResponsibility
src/colors/main.clj ↗Controller startup, worker count and shutdown
src/colors/redis.clj ↗Observation, convergence, identity, logs and delete
src/colors/probe.clj ↗Health, markers and suspended backup rehearsal
src/io/github/getcolors/redis_operator/validate.clj ↗Configuration, credentials and destructive guards
src/io/github/getcolors/redis_operator/tools.clj ↗Manifest rendering, transports and ownership checks
src/io/github/getcolors/redis_operator/workflow.clj ↗Preflight and lifecycle graph
src/io/github/getcolors/redis_operator/operator.clj ↗Readiness, rehearse, drill and restart
src/resources/io/github/getcolors/redis_operator/crd.yml ↗Resource schema and immutable identity
skills/package-redis-operator-green/green ↗Standalone launcher and dependency bootstrap
tasks/pin.clj ↗Clean, pushed revision pinning
scripts/golden.sh ↗Golden fixture comparison and secret guard
scripts/launcher.sh ↗Standalone launcher contract
scripts/image.sh ↗Image build and digest publication

Source revision e59a5abab935035bf466a5c7159c195337e5c2ee. The original Green source links below remain pinned to this revision. The runtime choices section links to the current Red and Blue implementations. Pinned dependency internals are outside this repository’s source-reading scope. Source links point to this revision; no live infrastructure status is implied.