diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fc4e758..149fd8fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 15 services: redis: - image: redis:7 + image: valkey/valkey:9 ports: - 6379:6379 options: >- diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 2831a41a..b45cf272 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -71,7 +71,7 @@ cd ~/.serverless-harness && docker compose down # stop it (Redis state go | `supervisor` | `packages/supervisor` and its `SH_WORKERS` forked turn workers | `moca-brain` | `127.0.0.1:8080` only | | `sandbox-relay` | `packages/sandbox-relay`: sandboxes attach here and it mirrors them into Redis | `moca-brain` + `moca-sandbox` | no | | `sandbox` | one `remote-worker` (`SANDBOX_ID=sh-sandbox-0`) dialing the relay | `moca-sandbox` | no | -| `redis` | `docker.io/redis:7-alpine`, the image `setup-vm.sh` pins | `moca-brain` | no | +| `redis` | `docker.io/valkey/valkey:9-alpine`, the image `setup-vm.sh` pins | `moca-brain` | no | | `control-plane` | `packages/control-plane` (MU1), only under the `control-plane` profile | `moca-brain` | `127.0.0.1:8090` only | **The supervisor and its whole worker pool are one container.** The supervisor starts its @@ -257,7 +257,8 @@ The first three run in CI through `make test-deploy`. lease store on the `moca-redis` named volume with AOF on, the same as the VM path (#410), so `docker compose down && up` keeps them — as do the control plane's credentials, on their own volume. `docker compose down -v` removes both and starts clean. Nothing here backs either volume - up, migrates one across a Redis image change, or bounds its growth. + up, migrates one across a Redis image change, or bounds its growth. (The Valkey swap keeps + existing volumes readable — Valkey 9 loads Redis 7.x AOF/RDB — but nothing here verifies that.) - **No offline login.** Logging in goes through GitHub's device flow, so it needs internet access and an OAuth app. A loopback-only development identity provider is a possible follow-up; it is security-sensitive enough to need its own ADR. diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index 64d67cc1..97eb5cf2 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -15,16 +15,17 @@ name: serverless-harness services: - # docker.io/redis:7-alpine, as deploy/vm/setup-vm.sh's start_redis pins. Unpublished: this Redis - # has no auth and holds sh:sandbox:records, so whoever can write to it chooses the executor for - # every turn. Only moca-brain reaches it -- no sandbox shares a network with it (MI1 §5 R8). + # docker.io/valkey/valkey:9-alpine, as deploy/vm/setup-vm.sh's start_redis pins. Unpublished: + # this Valkey has no auth and holds sh:sandbox:records, so whoever can write to it chooses the + # executor for every turn. Only moca-brain reaches it -- no sandbox shares a network with it + # (MI1 §5 R8). The image ships a `redis-server` symlink, so the command stays unchanged. # # Named volume + AOF, same as the VM path (#410) and the Kubernetes one: `docker compose down` # removes containers but keeps named volumes, so sessions, the ownership index and the lease store # come back with the stack instead of being silently forgotten. `docker compose down -v` is still # the way to start clean. redis: - image: docker.io/redis:7-alpine + image: docker.io/valkey/valkey:9-alpine command: [redis-server, --appendonly, 'yes'] volumes: - moca-redis:/data diff --git a/deploy/compose/tests/compose.test.sh b/deploy/compose/tests/compose.test.sh index 8df474bf..68add13c 100755 --- a/deploy/compose/tests/compose.test.sh +++ b/deploy/compose/tests/compose.test.sh @@ -229,8 +229,8 @@ published_cp="$(jq -r '.services | to_entries[] | select(.value.ports) | .key' " jq -e '.services["control-plane"].ports | length == 1 and .[0].host_ip == "127.0.0.1" and .[0].target == 8080 and .[0].published == "8090"' "$OUT_CP" >/dev/null || fail "the control plane must publish exactly 127.0.0.1:8090->8080: $(jq -c '.services["control-plane"].ports' "$OUT_CP")" -[[ "$(jq -r '.services.redis.image' "$OUT")" == docker.io/redis:7-alpine ]] || - fail "redis image must match deploy/vm/setup-vm.sh's start_redis (docker.io/redis:7-alpine)" +[[ "$(jq -r '.services.redis.image' "$OUT")" == docker.io/valkey/valkey:9-alpine ]] || + fail "redis image must match deploy/vm/setup-vm.sh's start_redis (docker.io/valkey/valkey:9-alpine)" pass "only the supervisor's 8080 (and the control plane's 8090) are published, on loopback; redis is the VM path's image" # --- 7. the sandbox shares no network with Redis, the supervisor or anything else (MI1 R8) --------- diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md index 43017854..1d7b083a 100644 --- a/deploy/k8s/README.md +++ b/deploy/k8s/README.md @@ -414,7 +414,7 @@ You need: - optionally, a certificate and key valid for the supervisor Route's host, `moca-moca.`. -The cluster pulls ghostunnel and `redis:7-alpine` from Docker Hub, so it needs access to +The cluster pulls ghostunnel and `valkey/valkey:9-alpine` from Docker Hub, so it needs access to `docker.io`. ### 9.1 Bring it up @@ -689,7 +689,7 @@ pin it). On OpenShift, drop `--context kind-moca`. - **A proxy in your shell.** `kind create` copies `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` into the node's containerd. A proxy on `127.0.0.1` then points at the node itself, and in-cluster pulls - (`redis:7-alpine`) fail with `proxyconnect tcp: dial tcp 127.0.0.1:...`. Create the cluster + (`valkey/valkey:9-alpine`) fail with `proxyconnect tcp: dial tcp 127.0.0.1:...`. Create the cluster without them, then run `setup.sh` as usual (it reuses an existing `moca` cluster): ```bash diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml index b813bcfe..9534e0cf 100644 --- a/deploy/k8s/base/kustomization.yaml +++ b/deploy/k8s/base/kustomization.yaml @@ -18,10 +18,10 @@ images: - name: moca-remote-worker newName: ghcr.io/rossoctl/moca-remote-worker newTag: latest - # Redis holds sh:sandbox:records and every session, so it runs a digest, not a moving tag. The + # Valkey holds sh:sandbox:records and every session, so it runs a digest, not a moving tag. The # tag stays deploy/vm/setup-vm.sh's (the version's source of truth); the digest is that tag's # multi-arch index, resolved when the pin was last moved. To move it: resolve the tag's current - # digest (`docker buildx imagetools inspect docker.io/redis:7-alpine`) and replace it here. - - name: docker.io/redis - newTag: 7-alpine - digest: sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499 + # digest (`docker buildx imagetools inspect docker.io/valkey/valkey:9-alpine`) and replace it here. + - name: docker.io/valkey/valkey + newTag: 9-alpine + digest: sha256:bf09d58e96d6b4524a15396b65b242bbe18beebbcc15a04d8ee75ddac62fae0b diff --git a/deploy/k8s/base/redis.yaml b/deploy/k8s/base/redis.yaml index fa9ee5ba..86f15ccb 100644 --- a/deploy/k8s/base/redis.yaml +++ b/deploy/k8s/base/redis.yaml @@ -1,8 +1,10 @@ -# Redis holds sh:sandbox:records -- whoever can write it chooses the executor for every turn -- so +# Valkey holds sh:sandbox:records -- whoever can write it chooses the executor for every turn -- so # it has a password (spec §3.4) and only these callers (NetworkPolicy below): the supervisor, the # relay and the control plane, plus moca-provisioner when workload sandboxes are on # (workload-sandboxes/kustomization.yaml adds it; it holds the same read-write password). AOF on a -# PVC, so sessions and presence survive a pod restart (the VM's #410 pain). +# PVC, so sessions and presence survive a pod restart (the VM's #410 pain). The image ships +# redis-server / redis-cli symlinks (uid 999 / gid 1000, same as redis), so the StatefulSet and +# the probes stay unchanged across the image swap. apiVersion: v1 kind: ServiceAccount metadata: @@ -43,14 +45,14 @@ spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true - runAsUser: 999 # the image's redis user (uid 999, gid 1000) + runAsUser: 999 # the image's valkey user (uid 999, gid 1000) runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: redis - image: docker.io/redis:7-alpine # the tag deploy/vm/setup-vm.sh pins; base/kustomization.yaml adds the digest + image: docker.io/valkey/valkey:9-alpine # the tag deploy/vm/setup-vm.sh pins; base/kustomization.yaml adds the digest # The password is in redis.conf (requirepass), mounted from the Secret, never in argv: # --requirepass would put it in /proc//cmdline and in `ps` on the node. command: diff --git a/deploy/knative/README-kind.md b/deploy/knative/README-kind.md index 1ab4b366..1834def2 100644 --- a/deploy/knative/README-kind.md +++ b/deploy/knative/README-kind.md @@ -198,7 +198,7 @@ env and reference it from `SH_MODEL_HEADERS` via `${VAR}` (the default Bearer is | Knative Serving + Kourier | Direct YAML apply from upstream releases | | KEDA | Direct YAML apply (async leaf ScaledJob support) | | Knative config | Autoscaler tuning (20s stable-window), PVC feature flags, security-context flag | -| Redis | Lightweight in-repo Deployment (`redis:7-alpine`) | +| Redis | Lightweight in-repo Deployment (`valkey/valkey:9-alpine`) | | Sandbox | Pre-baked image (`sandbox.yaml`, `USER 65532`) | | `leaf-work` PVC | `ReadWriteOnce`, default StorageClass | | Harness | Knative Service (`service.yaml`) | diff --git a/deploy/knative/README-ocp.md b/deploy/knative/README-ocp.md index dbce47ae..66e8aae2 100644 --- a/deploy/knative/README-ocp.md +++ b/deploy/knative/README-ocp.md @@ -71,7 +71,7 @@ creates a real Route per Knative Service (`oc get ksvc serverless-harness -o jso | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Knative Serving (+ Kourier) | **Red Hat OpenShift Serverless Operator** (OLM Subscription in `openshift-serverless`) + a `KnativeServing` CR in `knative-serving`. Kourier is bundled. | | Knative config | Autoscaler tuning + the `podspec-persistent-volume-claim`/`-write`/`-securitycontext` feature flags are set in the **`KnativeServing` CR spec** (the operator reverts direct `config-*` ConfigMap patches). | -| Redis | Lightweight in-repo Deployment (`redis:7-alpine`), runs under `restricted-v2`. | +| Redis | Lightweight in-repo Deployment (`valkey/valkey:9-alpine`), runs under `restricted-v2`. | | Sandbox | Pre-baked image ([`sandbox.Dockerfile`](sandbox.Dockerfile), `USER 65532`), pulled from GHCR (`ghcr.io/rossoctl/moca-sandbox:latest`, republished by `build.yaml` on every push to `main`; override with `--sandbox-image`). | | Sandbox `/workspace` PVC | `ReadWriteOnce` (Sandbox CR `volumeClaimTemplates`), cluster-default StorageClass. | | Harness | Knative Service applied via the [`overlays/ocp`](overlays/ocp) kustomize overlay; SA granted the `nonroot-v2` SCC. | diff --git a/deploy/knative/redis.yaml b/deploy/knative/redis.yaml index 00b4f618..f6a6f6ba 100644 --- a/deploy/knative/redis.yaml +++ b/deploy/knative/redis.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: redis - image: redis:7-alpine + image: valkey/valkey:9-alpine ports: - containerPort: 6379 resources: diff --git a/deploy/vm/README.md b/deploy/vm/README.md index 14c90d09..e87eb95f 100644 --- a/deploy/vm/README.md +++ b/deploy/vm/README.md @@ -700,10 +700,10 @@ data that was in it). ### Hosts installed before this change -Earlier versions started Redis with no `-v`. `docker.io/redis:7-alpine` declares `VOLUME /data`, so -each run's `podman run --replace` moved the data to a **fresh anonymous volume** and orphaned the -previous one: those sessions were unreachable but never deleted, and a long-lived rig can hold a -stack of them (`podman volume ls`). +Earlier versions started Redis with no `-v`. `docker.io/redis:7-alpine` (what the VM stack ran +before the Valkey swap) declares `VOLUME /data`, so each run's `podman run --replace` moved the +data to a **fresh anonymous volume** and orphaned the previous one: those sessions were unreachable +but never deleted, and a long-lived rig can hold a stack of them (`podman volume ls`). On such a host the next run **stops** and makes you choose. It will not decide the fate of those sessions for you, and it will not carry on and quietly strand them: nothing changes, and `sh-redis` @@ -730,8 +730,8 @@ by-hand recipe below is the route; `=1` will no longer do it for you, and that i `moca-redis` holds real sessions by then, and adopting over them would trade one loss for another. > **Do not just copy a `dump.rdb` into the volume and start Redis.** With `--appendonly yes`, -> Redis 7 loads the AOF and **ignores** an existing `dump.rdb` — verified against -> `redis:7-alpine` under podman 6.1.1 — so that yields a silently **empty** Redis. The conversion +> Valkey loads the AOF and **ignores** an existing `dump.rdb` — verified against `redis:7-alpine` +> (podman 6.1.1) and `valkey/valkey:9-alpine` — so that yields a silently **empty** Redis. The conversion > has to run with appendonly **off** first, so the RDB is what gets loaded, and only then > `CONFIG SET appendonly yes`. `SH_REDIS_ADOPT_VOLUME=1` does this in the right order. The same > trap applies to recovering an orphaned volume by hand: @@ -739,10 +739,10 @@ by-hand recipe below is the route; `=1` will no longer do it for you, and that i > ```bash > # Inspect candidates first: the newest Mountpoint/dump.rdb mtime is usually the one you want. > podman volume ls -> podman run --rm -v :/data docker.io/redis:7-alpine ls -l /data +> podman run --rm -v :/data docker.io/valkey/valkey:9-alpine ls -l /data > # Load the RDB with appendonly OFF, then convert. > podman run -d --rm --name redis-recover -v :/data \ -> docker.io/redis:7-alpine redis-server --appendonly no +> docker.io/valkey/valkey:9-alpine redis-server --appendonly no > podman exec redis-recover redis-cli DBSIZE # non-zero, or you have the wrong volume > podman exec redis-recover redis-cli CONFIG SET appendonly yes > podman exec redis-recover redis-cli SHUTDOWN NOSAVE # closes the connection; expected @@ -750,6 +750,8 @@ by-hand recipe below is the route; `=1` will no longer do it for you, and that i > > Then point `MOCA_REDIS_VOLUME` at that volume and re-run `setup-vm.sh`. +The Valkey swap is one-way: once Valkey rewrites the AOF or RDB, the data needs Valkey to load. + ## Where the env file lives `/etc/serverless-harness/supervisor.env` (mode 0640, root-owned — `install_env` runs as diff --git a/deploy/vm/setup-vm.sh b/deploy/vm/setup-vm.sh index a2e7c766..d62a9173 100755 --- a/deploy/vm/setup-vm.sh +++ b/deploy/vm/setup-vm.sh @@ -241,7 +241,7 @@ REDIS_ABSENT="no container" # `podman container inspect` on a missing container exits non-zero and prints to stderr: under # `set -euo pipefail` that would abort the whole script on a first install, so the call is guarded # and its stderr dropped. Image is compared by the configured NAME, not the resolved digest: this -# script pins a floating `:7-alpine` tag, so comparing digests would replace Redis -- and on a +# script pins a floating `:9-alpine` tag, so comparing digests would replace Redis -- and on a # pre-#410 container, LOSE ITS DATA -- every time upstream rebuilt the tag. redis_container_diff() { local image port volume appendonly out=() @@ -252,7 +252,7 @@ redis_container_diff() { volume="$(podman container inspect sh-redis \ --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}' 2>/dev/null)" appendonly="$(podman container inspect sh-redis --format '{{.Config.CreateCommand}}' 2>/dev/null)" - [[ "$image" == *redis:7-alpine* ]] || out+=("image is '$image', expected docker.io/redis:7-alpine") + [[ "$image" == *valkey/valkey:9-alpine* ]] || out+=("image is '$image', expected docker.io/valkey/valkey:9-alpine") [[ "$port" == *127.0.0.1:6379* ]] || out+=("published port is '$port', expected 127.0.0.1:6379") if [[ "$volume" != "$MOCA_REDIS_VOLUME" ]]; then # The anonymous-volume case (this image's own VOLUME /data) is the pre-#410 container every @@ -302,11 +302,12 @@ redis_container_diff() { # SH_REDIS_ADOPT_VOLUME=0 explicit opt-out: start fresh, leaving the old volume orphaned. # # THE ORDER BELOW IS LOAD-BEARING, and it is not the order the issue proposed. Verified against -# redis:7-alpine (podman 6.1.1): with `--appendonly yes`, Redis loads the AOF and IGNORES an -# existing dump.rdb -- so "copy dump.rdb in, then start" yields an EMPTY Redis, silently. The -# conversion must therefore happen with appendonly OFF (so the RDB is loaded), then CONFIG SET -# appendonly yes writes the AOF from what was loaded. After that, start_redis's own -# `--appendonly yes` reads it correctly. +# redis:7-alpine (podman 6.1.1), unchanged under valkey/valkey:9-alpine (Valkey ships compat +# redis-server / redis-cli symlinks): with `--appendonly yes`, the server loads the AOF and +# IGNORES an existing dump.rdb -- so "copy dump.rdb in, then start" yields an EMPTY store, +# silently. The conversion must therefore happen with appendonly OFF (so the RDB is loaded), +# then CONFIG SET appendonly yes writes the AOF from what was loaded. After that, start_redis's +# own `--appendonly yes` reads it correctly. adopt_redis_volume() { local diff="$1" # Only the state this function knows how to migrate: a container on an anonymous /data volume. @@ -375,7 +376,7 @@ adopt_redis_volume() { redis_adopt_target_nonempty() { podman volume exists "$MOCA_REDIS_VOLUME" 2>/dev/null || return 1 [[ -n "$(podman run --rm -v "$MOCA_REDIS_VOLUME:/data" --entrypoint sh \ - docker.io/redis:7-alpine -c 'ls -A /data' 2>/dev/null)" ]] + docker.io/valkey/valkey:9-alpine -c 'ls -A /data' 2>/dev/null)" ]] } # The migration itself. Every step checks its own exit status (see the caller: errexit does not @@ -394,12 +395,12 @@ redis_adopt_migrate() { # its own /data, so the copy needs no volume id -- which is what makes this safe on a host with # several orphans: it is always THIS container's data, never a guess at which orphan is current. podman run --rm --volumes-from sh-redis -v "$MOCA_REDIS_VOLUME:/adopt" \ - --entrypoint sh docker.io/redis:7-alpine -c 'cp -p /data/dump.rdb /adopt/dump.rdb' || return 1 + --entrypoint sh docker.io/valkey/valkey:9-alpine -c 'cp -p /data/dump.rdb /adopt/dump.rdb' || return 1 # Conversion, appendonly OFF so the RDB is what gets loaded (see the comment above). Started # detached and shut down explicitly rather than left to --rm: SHUTDOWN NOSAVE after the rewrite # is what guarantees the AOF is complete on disk before start_redis opens the same volume. podman run -d --rm --name sh-redis-adopt -v "$MOCA_REDIS_VOLUME:/data" \ - docker.io/redis:7-alpine redis-server --appendonly no >/dev/null || return 1 + docker.io/valkey/valkey:9-alpine redis-server --appendonly no >/dev/null || return 1 for ((i = 0; i < SH_REDIS_ADOPT_PING_TRIES; i++)); do podman exec sh-redis-adopt redis-cli PING >/dev/null 2>&1 && break sleep 1 @@ -499,7 +500,7 @@ start_redis() { [[ "$diff" == "$REDIS_ABSENT" ]] || log "replacing sh-redis: $diff" podman run -d --name sh-redis --replace --restart=always \ -p 127.0.0.1:6379:6379 -v "$MOCA_REDIS_VOLUME:/data" \ - docker.io/redis:7-alpine redis-server --appendonly yes + docker.io/valkey/valkey:9-alpine redis-server --appendonly yes } # Podman's --restart=always covers a container that exits, but explicitly NOT a host reboot: diff --git a/deploy/vm/tests/setup-vm.test.sh b/deploy/vm/tests/setup-vm.test.sh index 1ac35c7d..b3aab4e3 100755 --- a/deploy/vm/tests/setup-vm.test.sh +++ b/deploy/vm/tests/setup-vm.test.sh @@ -88,13 +88,13 @@ if [[ "$1" == "container" && "$2" == "inspect" ]]; then [[ -n "${MOCK_REDIS_EXISTS-}" ]] || exit 1 case "$*" in # The defaults are what a REAL podman prints for the container start_redis creates, verbatim from - # a live probe (podman 6.1.1): .ImageName normalizes `docker.io/redis:7-alpine` to - # `docker.io/library/redis:7-alpine`, and the Ports template leaves a trailing space. Both are + # a live probe (podman 6.1.1): .ImageName returns the registry-qualified name + # `docker.io/valkey/valkey:9-alpine`, and the Ports template leaves a trailing space. Both are # why redis_container_diff compares with globs rather than equality, so both belong here. - *.ImageName*) printf '%s\n' "${MOCK_REDIS_IMAGE-docker.io/library/redis:7-alpine}" ;; + *.ImageName*) printf '%s\n' "${MOCK_REDIS_IMAGE-docker.io/valkey/valkey:9-alpine}" ;; *NetworkSettings.Ports*) printf '%s\n' "${MOCK_REDIS_PORT-127.0.0.1:6379->6379/tcp }" ;; *.Mounts*) printf '%s\n' "${MOCK_REDIS_VOLUME-moca-redis}" ;; - *.Config.CreateCommand*) printf '%s\n' "${MOCK_REDIS_CMD-[podman run -d --name sh-redis --replace --restart=always -p 127.0.0.1:6379:6379 -v moca-redis:/data docker.io/redis:7-alpine redis-server --appendonly yes]}" ;; + *.Config.CreateCommand*) printf '%s\n' "${MOCK_REDIS_CMD-[podman run -d --name sh-redis --replace --restart=always -p 127.0.0.1:6379:6379 -v moca-redis:/data docker.io/valkey/valkey:9-alpine redis-server --appendonly yes]}" ;; esac fi # `volume inspect --format {{.Anonymous}}`, the ONLY reliable way to tell the pre-#410 anonymous @@ -1298,15 +1298,16 @@ while IFS='|' read -r mock expected; do fail "the replacement must report '$expected' as the difference: $out" done <<'CASES' MOCK_REDIS_IMAGE=docker.io/redis:6-alpine|image is 'docker.io/redis:6-alpine' +MOCK_REDIS_IMAGE=docker.io/library/redis:7-alpine|image is 'docker.io/library/redis:7-alpine' MOCK_REDIS_PORT=0.0.0.0:6379->6379/tcp|published port is MOCK_REDIS_VOLUME=redis-data|/data volume is 'redis-data' -MOCK_REDIS_CMD=[podman run -d --name sh-redis --restart=always -p 127.0.0.1:6379:6379 -v moca-redis:/data docker.io/redis:7-alpine]|without --appendonly yes -MOCK_REDIS_CMD=[podman run -d --name sh-redis --restart=always -p 127.0.0.1:6379:6379 -v moca-redis:/data docker.io/redis:7-alpine redis-server --appendonly no]|without --appendonly yes +MOCK_REDIS_CMD=[podman run -d --name sh-redis --restart=always -p 127.0.0.1:6379:6379 -v moca-redis:/data docker.io/valkey/valkey:9-alpine]|without --appendonly yes +MOCK_REDIS_CMD=[podman run -d --name sh-redis --restart=always -p 127.0.0.1:6379:6379 -v moca-redis:/data docker.io/valkey/valkey:9-alpine redis-server --appendonly no]|without --appendonly yes CASES pass "a difference in image, port, volume or appendonly each replaces sh-redis, saying which" # --- the pre-#410 anonymous volume: the installer STOPS and makes the operator choose (#410) ---- -# redis:7-alpine declares VOLUME /data, so the old no-`-v` form did not destroy data outright -- +# redis:7-alpine (the pre-swap image) declared VOLUME /data, so the old no-`-v` form did not destroy data outright -- # each --replace moved it to a fresh anonymous volume and orphaned the last (the P6.3 rig # accumulated 11). The sessions are therefore still recoverable, which is the whole reason this # state gets a decision rather than a default. @@ -1396,7 +1397,7 @@ fi pass "adoption refuses a non-empty target instead of overwriting the sessions already in it" # With the flag: the ORDER is the claim. The recovery recipe on #410 ("copy dump.rdb in, start -# Redis") yields an EMPTY Redis once AOF is on, because redis:7-alpine loads the AOF and ignores an +# Redis") yields an EMPTY store once AOF is on, because the server loads the AOF and ignores an # existing dump.rdb -- verified live under podman 6.1.1. So SAVE must precede the copy (or the file # is as old as the last save point), the conversion must run with appendonly OFF (or the RDB is # never read), and only then may CONFIG SET appendonly yes write the AOF. diff --git a/docs/operations/redis-backup-recovery.md b/docs/operations/redis-backup-recovery.md index f032b7e7..858839d7 100644 --- a/docs/operations/redis-backup-recovery.md +++ b/docs/operations/redis-backup-recovery.md @@ -5,11 +5,12 @@ ## How Redis is deployed -A single-replica StatefulSet running `redis:7-alpine` (pinned by digest) on a `ReadWriteOnce` volume of **1Gi** +A single-replica StatefulSet running `valkey/valkey:9-alpine` (pinned by digest; the RESP-compatible +BSD-3 fork of Redis — the resources keep `redis` names) on a `ReadWriteOnce` volume of **1Gi** using the cluster's default StorageClass. `setup.sh` warns if there is no default class; without one `redis-0` stays Pending. The generated configuration is exactly `requirepass`, `appendonly yes` and `dir /data`. -- **No `appendfsync`, `save`, `maxmemory` or eviction policy is set**, so Redis 7 defaults apply. With AOF that +- **No `appendfsync`, `save`, `maxmemory` or eviction policy is set**, so Valkey 9 defaults apply. With AOF that usually means about one second of writes can be lost on a hard crash (a Redis default, not a repository guarantee). Nothing evicts. - The container limit is **256Mi of memory**, which the data set can outgrow before it fills the 1Gi volume; an OOM diff --git a/docs/reference/security-model.md b/docs/reference/security-model.md index a3562aca..d4af1c64 100644 --- a/docs/reference/security-model.md +++ b/docs/reference/security-model.md @@ -208,8 +208,8 @@ the root `Dockerfile` only. Scorecard runs weekly. Images are pushed to `ghcr.io attested**. The harness image has no `USER` line (the manifests impose non-root), uses floating base tags and includes `kubectl` for the deprecated Knative path. Redis and the OpenShift TLS sidecar are pinned by digest; the harness and remote-worker images are `:latest` unless you pass `--image` (harness) and `--sandbox-image` (remote worker; the -VM uses `SANDBOX_IMAGE`). Redis is digest-pinned only in the Kubernetes manifests; the VM and Compose use -`redis:7-alpine` by tag. +VM uses `SANDBOX_IMAGE`). Valkey is digest-pinned only in the Kubernetes manifests; the VM and Compose use +`valkey/valkey:9-alpine` by tag. ## Known gaps in `SECURITY.md` diff --git a/harness/src/leaf-result-store.ts b/harness/src/leaf-result-store.ts index fb473678..3c7cf9a7 100644 --- a/harness/src/leaf-result-store.ts +++ b/harness/src/leaf-result-store.ts @@ -128,7 +128,7 @@ export class RedisResultStore implements RedisLike { await this.open(); return this.client.del(key); } - /** Atomic read-and-remove (Redis 6.2+; every deployment pins redis:7). The run-lease reaper + /** Atomic read-and-remove (Redis 6.2+; every deployment pins Valkey 9). The run-lease reaper * claims orphaned markers with it, so concurrent sweepers cannot double-finalize one run. */ async getdel(key: string): Promise { await this.open(); diff --git a/packages/supervisor/test/k8s/base-redis.test.ts b/packages/supervisor/test/k8s/base-redis.test.ts index d3ce4a6f..2ff67789 100644 --- a/packages/supervisor/test/k8s/base-redis.test.ts +++ b/packages/supervisor/test/k8s/base-redis.test.ts @@ -24,13 +24,13 @@ describe.skipIf(NO_KUBECTL)('deploy/k8s base: namespaces, defaults, Redis', () = } }); - it('pins Redis to a digest, at the same tag deploy/vm/setup-vm.sh runs', () => { + it('pins Valkey to a digest, at the same tag deploy/vm/setup-vm.sh runs', () => { const image: string = container(find(objs(), 'StatefulSet', 'redis', 'moca'), 'redis').image; - const m = /^docker\.io\/redis:([^@]+)@sha256:[0-9a-f]{64}$/.exec(image); + const m = /^docker\.io\/valkey\/valkey:([^@]+)@sha256:[0-9a-f]{64}$/.exec(image); expect(m, `redis image ${image} is not tag@digest-pinned`).not.toBeNull(); // The VM path is the version's source of truth; a tag bump there must move this pin too. const vm = readFileSync(resolve(REPO_ROOT, 'deploy/vm/setup-vm.sh'), 'utf8'); - expect(vm).toContain(`docker.io/redis:${m![1]}`); + expect(vm).toContain(`docker.io/valkey/valkey:${m![1]}`); }); it('runs Redis as a one-replica StatefulSet with a PVC and its config from the Secret', () => {