feat: add production capability foundations
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# 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-kubeconfig` and use its unchanged original context;
|
||||
- lab read-only queries use `src/build/redis-lab/kubeconfig` and exact context `ca-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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
Executable
+1540
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
#cloud-config
|
||||
package_update: false
|
||||
package_upgrade: false
|
||||
ssh_pwauth: false
|
||||
disable_root: true
|
||||
write_files:
|
||||
- path: /etc/sysctl.d/90-ca-redis-lab.conf
|
||||
owner: root:root
|
||||
permissions: "0644"
|
||||
content: |
|
||||
net.ipv4.ip_forward=1
|
||||
runcmd:
|
||||
- [mkdir, -p, /etc/rancher/k3s]
|
||||
- [sysctl, --system]
|
||||
@@ -0,0 +1,194 @@
|
||||
BEGIN {
|
||||
state = "start"
|
||||
invalid = 0
|
||||
output_count = 0
|
||||
|
||||
if (target != "ca-redis-lab") {
|
||||
invalid = 1
|
||||
}
|
||||
}
|
||||
|
||||
function remember(line) {
|
||||
output[++output_count] = line
|
||||
}
|
||||
|
||||
function is_credential_line(line, prefix, value) {
|
||||
if (index(line, prefix) != 1) {
|
||||
return 0
|
||||
}
|
||||
value = substr(line, length(prefix) + 1)
|
||||
return value ~ /^[A-Za-z0-9+\/=_-]+$/
|
||||
}
|
||||
|
||||
function is_namespace_line(line, value) {
|
||||
if (index(line, " namespace: ") != 1) {
|
||||
return 0
|
||||
}
|
||||
value = substr(line, length(" namespace: ") + 1)
|
||||
return value ~ /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/
|
||||
}
|
||||
|
||||
{
|
||||
if (invalid) {
|
||||
next
|
||||
}
|
||||
if (index($0, "\t") != 0 || index($0, "\r") != 0 ||
|
||||
$0 ~ /^[[:space:]]*(---|\.\.\.)([[:space:]]|$)/) {
|
||||
invalid = 1
|
||||
next
|
||||
}
|
||||
if ($0 != "preferences: {}" &&
|
||||
(index($0, "{") != 0 || index($0, "}") != 0 ||
|
||||
index($0, "[") != 0 || index($0, "]") != 0)) {
|
||||
invalid = 1
|
||||
next
|
||||
}
|
||||
|
||||
if (state == "start" && $0 == "apiVersion: v1") {
|
||||
api_version_count += 1
|
||||
state = "apiVersion"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "apiVersion" && $0 == "clusters:") {
|
||||
clusters_count += 1
|
||||
state = "clusters"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "clusters" && $0 == "- cluster:") {
|
||||
cluster_item_count += 1
|
||||
state = "cluster-item"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "cluster-item" &&
|
||||
is_credential_line($0, " certificate-authority-data: ")) {
|
||||
ca_data_count += 1
|
||||
state = "ca-data"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "ca-data" &&
|
||||
$0 == " server: https://127.0.0.1:6443") {
|
||||
server_count += 1
|
||||
state = "server"
|
||||
remember(" server: https://" address ":6443")
|
||||
next
|
||||
}
|
||||
if (state == "server" && $0 == " name: default") {
|
||||
cluster_name_count += 1
|
||||
state = "cluster-name"
|
||||
remember(" name: " target)
|
||||
next
|
||||
}
|
||||
if (state == "cluster-name" && $0 == "contexts:") {
|
||||
contexts_count += 1
|
||||
state = "contexts"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "contexts" && $0 == "- context:") {
|
||||
context_item_count += 1
|
||||
state = "context-item"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "context-item" && $0 == " cluster: default") {
|
||||
context_cluster_count += 1
|
||||
state = "context-cluster"
|
||||
remember(" cluster: " target)
|
||||
next
|
||||
}
|
||||
if (state == "context-cluster" && is_namespace_line($0)) {
|
||||
namespace_count += 1
|
||||
state = "optional-namespace"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if ((state == "context-cluster" || state == "optional-namespace") &&
|
||||
$0 == " user: default") {
|
||||
context_user_count += 1
|
||||
state = "context-user"
|
||||
remember(" user: " target)
|
||||
next
|
||||
}
|
||||
if (state == "context-user" && $0 == " name: default") {
|
||||
context_name_count += 1
|
||||
state = "context-name"
|
||||
remember(" name: " target)
|
||||
next
|
||||
}
|
||||
if (state == "context-name" && $0 == "current-context: default") {
|
||||
current_context_count += 1
|
||||
state = "current-context"
|
||||
remember("current-context: " target)
|
||||
next
|
||||
}
|
||||
if (state == "current-context" && $0 == "kind: Config") {
|
||||
kind_count += 1
|
||||
state = "kind"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "kind" && $0 == "preferences: {}") {
|
||||
preferences_count += 1
|
||||
state = "preferences"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "preferences" && $0 == "users:") {
|
||||
users_count += 1
|
||||
state = "users"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "users" && $0 == "- name: default") {
|
||||
user_name_count += 1
|
||||
state = "user-name"
|
||||
remember("- name: " target)
|
||||
next
|
||||
}
|
||||
if (state == "user-name" && $0 == " user:") {
|
||||
user_body_count += 1
|
||||
state = "user-body"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "user-body" &&
|
||||
is_credential_line($0, " client-certificate-data: ")) {
|
||||
client_cert_count += 1
|
||||
state = "client-cert"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
if (state == "client-cert" &&
|
||||
is_credential_line($0, " client-key-data: ")) {
|
||||
client_key_count += 1
|
||||
state = "client-key"
|
||||
remember($0)
|
||||
next
|
||||
}
|
||||
|
||||
invalid = 1
|
||||
}
|
||||
|
||||
END {
|
||||
if (invalid || state != "client-key" ||
|
||||
api_version_count != 1 || clusters_count != 1 ||
|
||||
cluster_item_count != 1 || ca_data_count != 1 ||
|
||||
server_count != 1 || cluster_name_count != 1 ||
|
||||
contexts_count != 1 || context_item_count != 1 ||
|
||||
context_cluster_count != 1 || namespace_count > 1 ||
|
||||
context_user_count != 1 || context_name_count != 1 ||
|
||||
current_context_count != 1 || kind_count != 1 ||
|
||||
preferences_count != 1 || users_count != 1 ||
|
||||
user_name_count != 1 || user_body_count != 1 ||
|
||||
client_cert_count != 1 || client_key_count != 1) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
for (line_number = 1; line_number <= output_count; line_number += 1) {
|
||||
print output[line_number]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: preserve-default-ca-canary
|
||||
server: https://192.0.2.10:6443
|
||||
name: ca-redis-lab
|
||||
contexts:
|
||||
- context:
|
||||
cluster: ca-redis-lab
|
||||
namespace: team-default
|
||||
user: ca-redis-lab
|
||||
name: ca-redis-lab
|
||||
current-context: ca-redis-lab
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: ca-redis-lab
|
||||
user:
|
||||
client-certificate-data: preserve-default-client-cert-canary
|
||||
client-key-data: preserve-default-client-key-canary
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: preserve-default-ca-canary
|
||||
server: https://192.0.2.10:6443
|
||||
name: ca-redis-lab
|
||||
contexts:
|
||||
- context:
|
||||
cluster: ca-redis-lab
|
||||
user: ca-redis-lab
|
||||
name: ca-redis-lab
|
||||
current-context: ca-redis-lab
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: ca-redis-lab
|
||||
user:
|
||||
client-certificate-data: preserve-default-client-cert-canary
|
||||
client-key-data: preserve-default-client-key-canary
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: preserve-default-ca-canary
|
||||
server: https://127.0.0.1:6443
|
||||
name: default
|
||||
contexts:
|
||||
- context:
|
||||
cluster: default
|
||||
user: default
|
||||
name: default
|
||||
current-context: default
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: default
|
||||
user:
|
||||
client-certificate-data: preserve-default-client-cert-canary
|
||||
client-key-data: preserve-default-client-key-canary
|
||||
Executable
+1885
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
K3S_VERSION=v1.33.3+k3s1
|
||||
MULTIPASS_IMAGE=24.04
|
||||
K3S_AMD64_URL=https://github.com/k3s-io/k3s/releases/download/v1.33.3%2Bk3s1/k3s
|
||||
K3S_AMD64_SHA256=f03cad6610cf5b2903d8a9ac3d6716690e53dab461b09c07b0c913a262166abc
|
||||
Reference in New Issue
Block a user