Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
timeout-minutes: 15
services:
redis:
image: redis:7
image: valkey/valkey:9
ports:
- 6379:6379
options: >-
Expand Down
5 changes: 3 additions & 2 deletions deploy/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions deploy/compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions deploy/compose/tests/compose.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---------
Expand Down
4 changes: 2 additions & 2 deletions deploy/k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ You need:
- optionally, a certificate and key valid for the supervisor Route's host,
`moca-moca.<apps domain>`.

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
Expand Down Expand 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
Expand Down
10 changes: 5 additions & 5 deletions deploy/k8s/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 6 additions & 4 deletions deploy/k8s/base/redis.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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/<pid>/cmdline and in `ps` on the node.
command:
Expand Down
2 changes: 1 addition & 1 deletion deploy/knative/README-kind.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand Down
2 changes: 1 addition & 1 deletion deploy/knative/README-ocp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 1 addition & 1 deletion deploy/knative/redis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: redis
image: redis:7-alpine
image: valkey/valkey:9-alpine
ports:
- containerPort: 6379
resources:
Expand Down
18 changes: 10 additions & 8 deletions deploy/vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -730,26 +730,28 @@ 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:
>
> ```bash
> # Inspect candidates first: the newest Mountpoint/dump.rdb mtime is usually the one you want.
> podman volume ls
> podman run --rm -v <old-volume>:/data docker.io/redis:7-alpine ls -l /data
> podman run --rm -v <old-volume>:/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 <old-volume>:/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
> ```
>
> 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
Expand Down
23 changes: 12 additions & 11 deletions deploy/vm/setup-vm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=()
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading