chore: record pre-existing uncommitted repository state
Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1a3b560678
commit
5f10b791d3
@@ -0,0 +1,127 @@
|
||||
# Storage certification job.
|
||||
#
|
||||
# A PersistentVolumeClaim is not a filesystem contract. Whether an atomic rename, a same-file-store
|
||||
# guarantee, or symlink refusal actually holds depends on the CSI driver, the StorageClass, the
|
||||
# access mode, the backend, and the mount options — so this job records all five alongside the probe
|
||||
# result. A certification without that tuple is not transferable to another cluster.
|
||||
#
|
||||
# The job writes a machine-readable result to the claim itself so the evidence lives with the volume
|
||||
# it describes.
|
||||
#
|
||||
# kubectl apply -f infra/fileserver/kubernetes/pvc-certification-job.yaml
|
||||
# kubectl logs job/fileserver-pvc-certification
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: fileserver-certification
|
||||
labels:
|
||||
app.kubernetes.io/name: fileserver
|
||||
app.kubernetes.io/component: certification
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
# Left unset on purpose: the certification is only meaningful for the class it actually ran on,
|
||||
# so the operator names it explicitly rather than inheriting a cluster default.
|
||||
storageClassName: ""
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: fileserver-pvc-certification
|
||||
labels:
|
||||
app.kubernetes.io/name: fileserver
|
||||
app.kubernetes.io/component: certification
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: fileserver
|
||||
app.kubernetes.io/component: certification
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
fsGroup: 10001
|
||||
containers:
|
||||
- name: certify
|
||||
image: eclipse-temurin:21-jdk
|
||||
env:
|
||||
- name: FILESERVER_STORAGE_ROOT
|
||||
value: /var/lib/backend/files
|
||||
- name: KUBERNETES_VERSION
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.annotations['certification.fileserver/kubernetes-version']
|
||||
- name: CSI_DRIVER
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.annotations['certification.fileserver/csi-driver']
|
||||
- name: STORAGE_CLASS
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.annotations['certification.fileserver/storage-class']
|
||||
- name: ACCESS_MODE
|
||||
value: ReadWriteOnce
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -euo pipefail
|
||||
ROOT="${FILESERVER_STORAGE_ROOT}"
|
||||
mkdir -p "${ROOT}/staging" "${ROOT}/content"
|
||||
|
||||
# Atomic rename within one file store is the property the publish path depends on.
|
||||
echo probe > "${ROOT}/staging/probe"
|
||||
if mv "${ROOT}/staging/probe" "${ROOT}/content/probe" 2>/dev/null; then
|
||||
ATOMIC_MOVE=true
|
||||
else
|
||||
ATOMIC_MOVE=false
|
||||
fi
|
||||
|
||||
# Same device means a rename is a metadata operation rather than a copy.
|
||||
STAGING_DEV=$(stat -c %d "${ROOT}/staging")
|
||||
CONTENT_DEV=$(stat -c %d "${ROOT}/content")
|
||||
[ "${STAGING_DEV}" = "${CONTENT_DEV}" ] && SAME_STORE=true || SAME_STORE=false
|
||||
|
||||
# O_EXCL create is what makes a publish create-only rather than an overwrite.
|
||||
if (set -o noclobber; echo x > "${ROOT}/content/excl") 2>/dev/null; then
|
||||
ATOMIC_CREATE=true
|
||||
else
|
||||
ATOMIC_CREATE=false
|
||||
fi
|
||||
|
||||
cat > "${ROOT}/certification-result.json" <<RESULT
|
||||
{
|
||||
"kubernetesVersion": "${KUBERNETES_VERSION:-unknown}",
|
||||
"csiDriver": "${CSI_DRIVER:-unknown}",
|
||||
"storageClass": "${STORAGE_CLASS:-unknown}",
|
||||
"accessMode": "${ACCESS_MODE}",
|
||||
"backend": "$(stat -f -c %T "${ROOT}")",
|
||||
"mountOptions": "$(findmnt -no OPTIONS --target "${ROOT}" || echo unknown)",
|
||||
"atomicMove": ${ATOMIC_MOVE},
|
||||
"sameFileStore": ${SAME_STORE},
|
||||
"atomicCreate": ${ATOMIC_CREATE}
|
||||
}
|
||||
RESULT
|
||||
cat "${ROOT}/certification-result.json"
|
||||
|
||||
# Fail closed: a volume that cannot publish atomically must not be certified silently.
|
||||
[ "${SAME_STORE}" = "true" ] || { echo "staging and content are on different stores"; exit 1; }
|
||||
volumeMounts:
|
||||
- name: storage
|
||||
mountPath: /var/lib/backend/files
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: fileserver-certification
|
||||
@@ -0,0 +1,61 @@
|
||||
# Network-filesystem certification environment.
|
||||
#
|
||||
# A local filesystem cannot reproduce the failures this environment exists to test: a rename whose
|
||||
# acknowledgement is lost, a stale file handle after the server restarts, and a client that keeps
|
||||
# writing across a network cut. Those are precisely the cases where "the write failed, retry it" is
|
||||
# the wrong conclusion, so they are certified against a real NFS server rather than a mock.
|
||||
#
|
||||
# Opt in with FILESERVER_NFS_TESTS=true; the default test run does not start this.
|
||||
#
|
||||
# docker compose -f infra/fileserver/nfs/compose.yml up -d
|
||||
# FILESERVER_NFS_TESTS=true ./gradlew :adapter:outbound:fileserver:test
|
||||
#
|
||||
# To exercise the ambiguity paths:
|
||||
# docker compose -f infra/fileserver/nfs/compose.yml restart nfs-server # stale handles
|
||||
# docker network disconnect fileserver-nfs <client> # lost responses
|
||||
|
||||
services:
|
||||
nfs-server:
|
||||
image: erichough/nfs-server:2.2.1
|
||||
container_name: fileserver-nfs-server
|
||||
privileged: true
|
||||
environment:
|
||||
NFS_EXPORT_0: "/exports *(rw,sync,no_subtree_check,no_root_squash,fsid=0)"
|
||||
NFS_VERSION: "4.2"
|
||||
NFS_LOG_LEVEL: DEBUG
|
||||
volumes:
|
||||
- nfs-exports:/exports
|
||||
ports:
|
||||
- "2049:2049"
|
||||
networks:
|
||||
- fileserver-nfs
|
||||
healthcheck:
|
||||
test: ["CMD", "rpcinfo", "-t", "localhost", "nfs", "4"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
nfs-client:
|
||||
image: eclipse-temurin:21-jdk
|
||||
container_name: fileserver-nfs-client
|
||||
privileged: true
|
||||
depends_on:
|
||||
nfs-server:
|
||||
condition: service_healthy
|
||||
# hard,intr is the correct production mount: a soft mount turns a slow server into a silent
|
||||
# short write, which is exactly the corruption the design refuses to accept.
|
||||
command: >
|
||||
bash -c "mkdir -p /mnt/fileserver &&
|
||||
mount -t nfs4 -o hard,timeo=50,retrans=2 nfs-server:/ /mnt/fileserver &&
|
||||
tail -f /dev/null"
|
||||
volumes:
|
||||
- ../../..:/workspace:ro
|
||||
networks:
|
||||
- fileserver-nfs
|
||||
|
||||
volumes:
|
||||
nfs-exports:
|
||||
|
||||
networks:
|
||||
fileserver-nfs:
|
||||
name: fileserver-nfs
|
||||
@@ -0,0 +1,67 @@
|
||||
# Fileserver front-proxy configuration.
|
||||
#
|
||||
# The application authorizes every download and then hands the transfer to Nginx with
|
||||
# X-Accel-Redirect. Two properties make that safe, and both are enforced here rather than assumed:
|
||||
#
|
||||
# 1. /__files/ is `internal`, so it is reachable ONLY through an internal redirect the application
|
||||
# issued. A direct request from a client returns 404 and never touches the content root.
|
||||
# 2. The application never emits an absolute path. It emits a relative URI below /__files/, and
|
||||
# the alias below is the only place that prefix becomes a filesystem location.
|
||||
#
|
||||
# Keep `alias` in sync with the storage root's content directory. A mismatch is a startup
|
||||
# misconfiguration, not a runtime fallback: the application's startup validator checks that the
|
||||
# internal mapping was proven before it accepts traffic.
|
||||
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
sendfile_max_chunk 2m;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Uploads stream through to the application; buffering a large body to disk here would double
|
||||
# the write and defeat the streaming upload path.
|
||||
proxy_request_buffering off;
|
||||
client_max_body_size 0;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
|
||||
# Public API. Everything, including download authorization, is decided by the application.
|
||||
location / {
|
||||
proxy_pass http://app:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# The application must never see a client-supplied delegation header: it would let a
|
||||
# caller name an arbitrary internal object.
|
||||
proxy_set_header X-Accel-Redirect "";
|
||||
}
|
||||
|
||||
# Internal transfer location. Not reachable from outside; see property (1) above.
|
||||
location /__files/ {
|
||||
internal;
|
||||
alias /srv/files/content/;
|
||||
|
||||
sendfile on;
|
||||
sendfile_max_chunk 2m;
|
||||
|
||||
# Uploaded content is never trusted to describe itself.
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Content-Disposition $upstream_http_content_disposition always;
|
||||
add_header Cache-Control $upstream_http_cache_control always;
|
||||
add_header ETag $upstream_http_etag always;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# HTTP Client Platform — local test topology
|
||||
|
||||
The suites drive these dependencies through Testcontainers and in-process fixtures, so nothing here
|
||||
is required to run `./gradlew :adapter:outbound:httpclient:test`. These files exist for the nightly
|
||||
lane and for reproducing a failure locally with the same images and ports CI uses.
|
||||
|
||||
| Directory | Purpose | Used by |
|
||||
|---|---|---|
|
||||
| `toxiproxy/` | TCP fault injection (latency, reset, bandwidth) | `httpClientFailureInjectionTest` |
|
||||
| `tls/` | how the TLS and mTLS material is produced | TLS and mTLS suites |
|
||||
| `proxy/` | forward proxy with CONNECT and proxy authentication | proxy contract suite |
|
||||
| `oauth2/` | token endpoint behaviour under contention | OAuth2 suites |
|
||||
@@ -0,0 +1,12 @@
|
||||
# OAuth2 fixture
|
||||
|
||||
`OAuth2Fixture` exposes a token endpoint backed by the same deterministic fixture server as the rest
|
||||
of the suite.
|
||||
|
||||
It counts token requests, which is what makes design §20.3's single-flight guarantee provable rather
|
||||
than assumed: a hundred genuinely concurrent callers must produce exactly one token request. It can
|
||||
also issue rotating token values, so a stale cached token is detectable, and queue a failure status
|
||||
to exercise the refresh-failure path.
|
||||
|
||||
The token endpoint is configured as its own Named Client Profile, separate from the upstream it
|
||||
issues tokens for.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Forward proxy fixture
|
||||
|
||||
`ProxyFixture` runs an in-process forward proxy so the proxy lane needs no external service.
|
||||
|
||||
| Factory | Behaviour |
|
||||
|---|---|
|
||||
| `ProxyFixture.openProxy()` | accepts CONNECT and tunnels to the target |
|
||||
| `ProxyFixture.authenticatingProxy(user, password)` | answers `407` until `Proxy-Authorization` matches |
|
||||
|
||||
The fixture records every request line and every `Proxy-Authorization` value it saw, which is what
|
||||
lets the suite prove design §24.3: proxy credentials never appear on the target request, and a proxy
|
||||
CONNECT failure is reported as `HttpProxyException` rather than as a target TLS failure.
|
||||
@@ -0,0 +1,16 @@
|
||||
# TLS fixtures
|
||||
|
||||
Certificates are generated **in process** by `TlsFixture`, not checked in. A committed private key
|
||||
is a private key that leaks, and design §21.2 forbids key material in the repository.
|
||||
|
||||
`TlsFixture` produces, from a throwaway CA created per test run:
|
||||
|
||||
| Fixture | Purpose |
|
||||
|---|---|
|
||||
| `TlsFixture.trusted()` | a server certificate valid for the loopback host |
|
||||
| `TlsFixture.hostnameMismatch()` | a certificate whose SAN does not match the connection host |
|
||||
| `TlsFixture.expired()` | an already-expired certificate |
|
||||
| `clientHandshake(true)` | client key material for the mTLS lane |
|
||||
|
||||
All three failure cases must classify as permanent (design §21.3) — never retried, never downgraded
|
||||
to plaintext.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Fault-injection topology for the HTTP Client Platform failure suite (design §28.1, §28.3).
|
||||
#
|
||||
# The suite normally drives Toxiproxy through Testcontainers. This compose file exists for the
|
||||
# nightly lane and for reproducing a failure locally with the exact same image and ports.
|
||||
services:
|
||||
toxiproxy:
|
||||
image: ghcr.io/shopify/toxiproxy:2.9.0
|
||||
container_name: httpclient-toxiproxy
|
||||
ports:
|
||||
- "8474:8474" # control API
|
||||
- "18080:18080" # proxied upstream: plaintext
|
||||
- "18443:18443" # proxied upstream: TLS
|
||||
healthcheck:
|
||||
test: ["CMD", "/toxiproxy-cli", "list"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
@@ -1,134 +0,0 @@
|
||||
# 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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
#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]
|
||||
@@ -1,194 +0,0 @@
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
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
|
||||
@@ -0,0 +1,17 @@
|
||||
# ACL accounts
|
||||
|
||||
One file per `CommandAccess` level. Each account is deliberately narrower than the SDK's own rules:
|
||||
the account is the last enforcement boundary and a permit issued inside the process never widens it,
|
||||
so a mistake in the SDK is still refused by the server.
|
||||
|
||||
A Redis ACL file accepts nothing but complete `user` lines — no comments and no line continuations —
|
||||
which is why the rationale lives here instead of inline. Password material is supplied at deploy
|
||||
time; none of these files carries one.
|
||||
|
||||
| File | Account | Grants |
|
||||
| --- | --- | --- |
|
||||
| `application.acl` | `CommandAccess.APPLICATION` | the typed data-structure commands over `prod:*`, with the dangerous and deprecated names denied |
|
||||
| `application-advanced.acl` | `APPLICATION_ADVANCED` | the above plus pub/sub, transactions, and the registered-script path. `EVAL` is absent: only `EVALSHA` of an already loaded digest is reachable |
|
||||
| `raw-gateway.acl` | `RAW_GATEWAY` | exactly the commands the catalog classifies `RAW_ONLY`, and nothing else |
|
||||
| `all-accounts.acl` | – | the four accounts concatenated; Redis takes one `aclfile`, so this is what a deployment loads |
|
||||
| `admin-readonly.acl` | `ADMIN_READONLY` | read-only diagnostics. Every destructive counterpart is denied here and blocked in the command policy catalog — two independent controls for the same rule |
|
||||
@@ -0,0 +1 @@
|
||||
user ca-skeleton-admin-readonly on nopass ~* resetchannels -@all +info +dbsize +time +lastsave +memory|usage +memory|stats +slowlog|get +slowlog|len +latency|latest +latency|history +client|list +client|info +command|info +command|docs +command|count +command|getkeysandflags +config|get +acl|dryrun +acl|whoami +cluster|info +cluster|slots +cluster|shards +cluster|nodes +object|encoding +object|freq +object|idletime +pubsub|channels +pubsub|numsub +pubsub|shardchannels +xinfo|stream +xinfo|groups +xinfo|consumers +function|list +function|stats +cluster|keyslot
|
||||
@@ -0,0 +1 @@
|
||||
user ca-skeleton-application-advanced on nopass sanitize-payload ~prod:* resetchannels &prod:* -@all +@read +@write +@string +@hash +@list +@set +@sortedset +@bitmap +@hyperloglog +@geo +@stream +@pubsub +@transaction +evalsha +evalsha_ro +script|load +script|exists +fcall +fcall_ro -keys -flushdb -flushall -shutdown -debug -eval -eval_ro -smembers -sort -sort_ro -randomkey -migrate -swapdb -select
|
||||
@@ -0,0 +1 @@
|
||||
user ca-skeleton-application on nopass sanitize-payload ~prod:* resetchannels &prod:* -@all +@connection +@pubsub +@transaction +@read +@write +@string +@hash +@list +@set +@sortedset +@bitmap +@hyperloglog +@geo +@stream -keys -flushdb -flushall -shutdown -debug -sort -sort_ro -smembers -randomkey -migrate -swapdb -select
|
||||
@@ -0,0 +1 @@
|
||||
user ca-skeleton-raw-gateway on nopass sanitize-payload ~prod:* resetchannels -@all +smembers +sort +sort_ro
|
||||
@@ -0,0 +1,26 @@
|
||||
# Standalone lane for the Redis SDK topology tests.
|
||||
#
|
||||
# The version is a build argument rather than a pinned image so the same file serves every row of
|
||||
# the support matrix. Nothing here is a production topology: no persistence, no TLS, no replication.
|
||||
# It exists to answer "does the driver behave the way the SDK claims", which is a question the
|
||||
# in-memory fixture cannot answer at all.
|
||||
services:
|
||||
redis:
|
||||
image: "redis:${REDIS_VERSION:-7.4}"
|
||||
command:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- "no"
|
||||
- --save
|
||||
- ""
|
||||
- --aclfile
|
||||
- /etc/redis/acl/all-accounts.acl
|
||||
volumes:
|
||||
- ../acl:/etc/redis/acl:ro
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "[ \"$$(redis-cli --user ca-skeleton-application --pass fixture-application --no-auth-warning ping)\" = PONG ]"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 15
|
||||
@@ -0,0 +1,78 @@
|
||||
# TLS lane for the Redis SDK topology tests.
|
||||
#
|
||||
# Standalone in shape, but the point is the transport: the SDK's TLS settings — enabled, hostname
|
||||
# verification, trust material, client certificate — are configuration nothing else exercises, and
|
||||
# a TLS path that has never carried a command is a claim rather than a capability.
|
||||
#
|
||||
# The certificates are generated at start-up rather than checked in. A checked-in key is a secret in
|
||||
# the repository however loudly the file is named "test", and a lane that regenerates its own
|
||||
# material also proves the trust configuration actually matters: point the client at the wrong CA
|
||||
# and it fails, which is what the qualification has to show.
|
||||
services:
|
||||
certs:
|
||||
# The redis image carries no openssl, so certificate generation gets an image that does. The
|
||||
# alternative — checking the material in — puts a private key in the repository.
|
||||
image: alpine/openssl:latest
|
||||
user: root
|
||||
volumes:
|
||||
- tls:/tls
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
if [ -f /tls/redis.crt ]; then exit 0; fi
|
||||
openssl genrsa -out /tls/ca.key 2048
|
||||
openssl req -x509 -new -nodes -key /tls/ca.key -sha256 -days 1 \
|
||||
-subj "/CN=ca-skeleton-test-ca" -out /tls/ca.crt
|
||||
openssl genrsa -out /tls/redis.key 2048
|
||||
openssl req -new -key /tls/redis.key -subj "/CN=localhost" -out /tls/redis.csr
|
||||
printf 'subjectAltName=DNS:localhost,IP:127.0.0.1' > /tls/redis.ext
|
||||
openssl x509 -req -in /tls/redis.csr -CA /tls/ca.crt -CAkey /tls/ca.key \
|
||||
-CAcreateserial -out /tls/redis.crt -days 1 -sha256 -extfile /tls/redis.ext
|
||||
chmod 644 /tls/redis.key /tls/ca.key
|
||||
|
||||
redis:
|
||||
image: "redis:${REDIS_VERSION:-7.4}"
|
||||
depends_on:
|
||||
certs:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- ../acl:/etc/redis/acl:ro
|
||||
- tls:/tls:ro
|
||||
command:
|
||||
- redis-server
|
||||
# Plaintext is off entirely. A lane that accepts both proves nothing about the TLS path,
|
||||
# because a misconfigured client would quietly fall back and still pass.
|
||||
- --port
|
||||
- "0"
|
||||
- --tls-port
|
||||
- "6379"
|
||||
- --tls-cert-file
|
||||
- /tls/redis.crt
|
||||
- --tls-key-file
|
||||
- /tls/redis.key
|
||||
- --tls-ca-cert-file
|
||||
- /tls/ca.crt
|
||||
- --tls-auth-clients
|
||||
- "no"
|
||||
- --appendonly
|
||||
- "no"
|
||||
- --save
|
||||
- ""
|
||||
- --aclfile
|
||||
- /etc/redis/acl/all-accounts.acl
|
||||
ports:
|
||||
- "${REDIS_PORT:-6390}:6379"
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- >-
|
||||
[ "$$(redis-cli --tls --cacert /tls/ca.crt
|
||||
--user ca-skeleton-application --pass fixture-application --no-auth-warning ping)" = PONG ]
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
tls:
|
||||
Reference in New Issue
Block a user