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:
DongHyeonka
2026-08-11 16:48:43 +09:00
co-authored by Claude Opus 5
parent 1a3b560678
commit 5f10b791d3
1857 changed files with 130925 additions and 72491 deletions
@@ -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
+61
View File
@@ -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
+67
View File
@@ -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;
}
}
}
+12
View File
@@ -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 |
+12
View File
@@ -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.
+12
View File
@@ -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.
+16
View File
@@ -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.
+17
View File
@@ -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
-134
View File
@@ -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
-14
View File
@@ -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]
-194
View File
@@ -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
-4
View File
@@ -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
+17
View File
@@ -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 |
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
user ca-skeleton-raw-gateway on nopass sanitize-payload ~prod:* resetchannels -@all +smembers +sort +sort_ro
+26
View File
@@ -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
+78
View File
@@ -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: