Disposable Redis qualification lab
This directory owns the lifecycle contract for the isolated three-node k3s lab. It does not contain Redis workloads, credentials, certificates, or qualification evidence.
Fixed topology
| Instance | CPU | Memory | Disk | Role |
|---|---|---|---|---|
ca-redis-lab-server |
2 | 3G | 12G | k3s server |
ca-redis-lab-agent-1 |
2 | 2560M | 12G | k3s agent |
ca-redis-lab-agent-2 |
2 | 2560M | 12G | k3s agent |
The lab uses pod CIDR 10.52.0.0/16, service CIDR 10.53.0.0/16, and context
ca-redis-lab. versions.env pins the k3s version and Multipass image. Traefik and ServiceLB are
disabled.
Safety model
All state, rendered cloud-init, kubeconfigs, tokens, and raw observations are mode-restricted
beneath the ignored src/build/redis-lab directory. Every canonical ancestor from the repository
root through src/build/redis-lab, plus runtime children, is validated before observation or
mutation; a symlink or real-path escape fails closed. The lifecycle never exports KUBECONFIG,
merges a kubeconfig, or writes the user's default kubeconfig.
Host observation and lab access deliberately use different explicit targets:
- host read-only queries copy the default kubeconfig into
src/build/redis-lab/observations/host-kubeconfigand use its unchanged original context; - lab read-only queries use
src/build/redis-lab/kubeconfigand exact contextca-redis-lab.
This split preserves the host context identity while ensuring no kubectl call relies on an implicit target. Host kubectl mutations are not part of the lifecycle. The observation-only host copy is removed after fingerprint and CIDR observation on success and every handled failure path.
Each exact name gets a private mode-0600 rendered cloud-init file beneath
src/build/redis-lab/cloud-init. It writes only the non-secret ownership marker
RUN_ID|VM_NAME to /var/lib/ca-redis-lab/ownership as root:root mode 0600; launch uses only
that rendered file.
Each name is then atomically reserved as PENDING in run.state before its bounded launch. The
state starts with an exact per-run identity, and each PENDING/CREATED/RECONCILE entry carries
that same identity. A successful launch becomes CREATED only after a bounded
multipass exec <name> -- sudo cat /var/lib/ca-redis-lab/ownership returns the exact marker.
Timeout, launch error, missing/foreign marker, signal, promotion failure, or uncertain cleanup
enters RECONCILE.
Cleanup transitions a recorded entry to RECONCILE, bounded-polls the exact instance and marker,
and issues multipass delete --purge <exact-name> only after the marker matches. It atomically
removes only an entry whose delete succeeded. A late-created matching instance is deleted; an
absent instance, unreadable marker, mismatched/foreign marker, or failed delete is retained as a
tombstone and fails closed without an unproven delete. Existing instance-bearing state blocks a
new preflight, up, or run; a rejected new run does not clean the prior run, and down is the
retry/reconciliation entry point. Existing
allowlisted names without owned state cause up to stop before reservation/launch and are never
adopted or deleted. Wildcards, --all, global purge, and discovered-instance deletion are
forbidden.
The lifecycle lock is nonblocking and exclusive. External children close its descriptor by
default, including detached infrastructure descendants and run -- commands; only lock
acquisition retains descriptor 9.
Multipass list/launch/info/exec/transfer/delete, installation/join, and kubectl calls have fixed
time bounds. up succeeds only after the exact server and two agents all report Ready=True
within the bounded poll budget; incomplete or not-ready inventory enters marker-proven run-owned
cleanup.
The k3s runtime is amd64-only and fail-closed in this slice. versions.env pins the immutable
release URL and exact SHA-256 for v1.33.3+k3s1. The lifecycle performs a bounded host download,
verifies the digest, transfers the binary to each exact VM, verifies the transferred digest and
reported binary version inside each VM, and only then installs/starts it. It does not execute a
network installer or a curl | sh pipeline.
The generated lab kubeconfig is accepted only in the pinned single-cluster/single-context/
single-user block grammar. A tracked AWK state machine has one explicit transition for every
allowlisted line and publishes no output until the complete document reaches its exact final
state. It rejects missing, duplicate, reordered, unknown, whitespace-altered, quoted, tagged, or
explicit keys; anchors, aliases, merge keys, tabs, CRLF, document markers, trailing content, and
all flow collections except exact preferences: {}. Only the exact cluster/context/user identity,
current-context, and loopback API server are rewritten. CA data, client certificate/key data,
and an optional canonical namespace are byte-preserved.
Rendering uses a same-directory kubeconfig.next, applies mode 0600, and replaces the
destination only after render and permission success. The renderer must be a readable regular
non-symlink file at its canonical tracked path, and both destination paths are protected by the
runtime symlink contract. Renderer, permission, or move failure removes both candidate and
destination, performs no lab kubectl, and enters exact marker-proven current-run cleanup.
Assigned Service ClusterIPs cannot prove the host service CIDR. When a host kubeconfig exists,
callers must supply one or more canonical, comma- or space-separated IPv4 CIDRs through
REDIS_LAB_HOST_SERVICE_CIDRS. Missing, malformed, or overlapping input fails before launch:
REDIS_LAB_HOST_SERVICE_CIDRS=10.43.0.0/16 \
infra/redis-lab/bin/redis-lab preflight
Commands
The real lifecycle is for a trusted local or dedicated runner only:
REDIS_LAB_HOST_SERVICE_CIDRS=10.43.0.0/16 infra/redis-lab/bin/redis-lab preflight
REDIS_LAB_HOST_SERVICE_CIDRS=10.43.0.0/16 infra/redis-lab/bin/redis-lab up
infra/redis-lab/bin/redis-lab down
REDIS_LAB_HOST_SERVICE_CIDRS=10.43.0.0/16 infra/redis-lab/bin/redis-lab run -- command
REDIS_LAB_HOST_SERVICE_CIDRS=10.43.0.0/16 \
infra/redis-lab/bin/redis-lab run --retain-on-failure -- command
run establishes its cleanup obligation before entering the inner up, keeps it through the
post-up/pre-command handoff and user command, then tears down after command success or failure and
compares the canonical pre/post host fingerprints after cleanup. A successful direct up retains
the lab by design. Local --retain-on-failure intentionally leaves the recorded lab for diagnosis
and skips an isolation-success claim; CI=true rejects that option before launch.
The blocking contract is VM-free:
cd src
./gradlew :adapter:outbound:cache-redis:redisLabContractTest --console=plain
It injects fake infrastructure commands. Hosted CI must run only this contract, never the real lab.
The contract executes a copied lifecycle in
src/build/redis-lab-contract/repository, seals PATH to explicit fakes/safe wrappers, and compares
a byte-level snapshot proving it did not modify the real repository's src/build/redis-lab. It
also exercises direct/run signal cleanup, rendered-child symlink rejection, successful and
late-create marker proof, absent/foreign-marker tombstones, second-run state preservation,
CREATED cleanup uncertainty, rejected-run preservation of prior CREATED and RECONCILE state,
the post-up/pre-command signal handoff, the canonical kubeconfig mutation matrix, missing/symlinked
renderer rejection, fail-closed .next/permission/move publication, and infrastructure/user
background-child lock non-inheritance. This is deterministic fake-runtime evidence only; it is not
live Multipass, k3s, kubectl, network, or host-isolation qualification.