This commit is contained in:
2026-08-20 11:24:42 -03:00
parent a65c92257d
commit 83b6cbebe3
64 changed files with 7688 additions and 0 deletions

115
rig/ctrl/addons/airflow.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# Apache Airflow — the cluster half of soleprint's airflow cabinet.
#
# Airflow needs a metadata database before it will start at all, so this refuses
# rather than rolls a pod that will CrashLoopBackOff while the real problem
# (postgres missing from ADDONS) stays invisible in the logs.
#
# One pod on `standalone`, matching the compose cabinet: migration, admin user,
# scheduler and webserver in a single container. The official chart's five
# deployments model an installation; a room switching this on wants pipelines.
set -euo pipefail
cd "$(dirname "$0")/.."
source ./lib/config.sh
load_config
K="kubectl --context ${KUBECONTEXT}"
NS="${DATA_NAMESPACE:-data}"
if ! $K get deployment -n "$NS" postgres >/dev/null 2>&1; then
echo " ! airflow needs the postgres addon, and it is not installed" >&2
echo " add it before airflow in the profile's ADDONS:" >&2
echo " ADDONS=\"... postgres airflow\"" >&2
exit 1
fi
# Reuse the credential postgres generated rather than storing a second copy.
db_user=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_USER}' | base64 -d)
db_pass=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)
db_name=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_DB}' | base64 -d)
if $K get secret -n "$NS" airflow >/dev/null 2>&1; then
echo " secret exists, keeping the current admin password and fernet key"
else
admin_password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
# Airflow requires a 32-byte urlsafe-base64 key; without a fixed one every
# restart invalidates every stored connection.
fernet_key=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_')
$K create secret generic airflow -n "$NS" \
--from-literal=ADMIN_USER="${AIRFLOW_ADMIN_USER:-admin}" \
--from-literal=ADMIN_PASSWORD="$admin_password" \
--from-literal=FERNET_KEY="$fernet_key" \
--from-literal=SQL_ALCHEMY_CONN="postgresql+psycopg2://${db_user}:${db_pass}@postgres:5432/${db_name}" \
>/dev/null
echo " generated an admin password (read it back with the command below)"
fi
echo " applying manifests"
$K apply -n "$NS" -f - >/dev/null <<YAML
apiVersion: v1
kind: Service
metadata:
name: airflow
spec:
selector:
app: airflow
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: airflow
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: airflow
template:
metadata:
labels:
app: airflow
spec:
containers:
- name: airflow
image: ${AIRFLOW_IMAGE}
args: ["standalone"]
env:
- name: AIRFLOW__CORE__EXECUTOR
value: LocalExecutor
- name: AIRFLOW__CORE__LOAD_EXAMPLES
value: "false"
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
valueFrom:
secretKeyRef: {name: airflow, key: SQL_ALCHEMY_CONN}
- name: AIRFLOW__CORE__FERNET_KEY
valueFrom:
secretKeyRef: {name: airflow, key: FERNET_KEY}
- name: _AIRFLOW_WWW_USER_USERNAME
valueFrom:
secretKeyRef: {name: airflow, key: ADMIN_USER}
- name: _AIRFLOW_WWW_USER_PASSWORD
valueFrom:
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
# First boot runs the whole migration before it serves anything.
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 20
YAML
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
$K rollout status deployment/airflow -n "$NS" --timeout=600s
echo " in-cluster: http://airflow.${NS}.svc.cluster.local:8080"
echo " reach it: kubectl --context ${KUBECONTEXT} -n ${NS} port-forward svc/airflow 8080:8080"
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret airflow -o jsonpath='{.data.ADMIN_PASSWORD}' | base64 -d"

65
rig/ctrl/addons/cert-manager.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# cert-manager plus a self-signed cluster issuer.
#
# In a regulated estate almost everything is TLS, so the interesting question
# during onboarding is "does this service present a cert my client trusts" — not
# "can I reach a public ACME server". A local CA answers that offline, which is
# also what makes the air-gapped profile usable.
set -euo pipefail
cd "$(dirname "$0")/.."
source ./lib/config.sh
load_config
K="kubectl --context ${KUBECONTEXT}"
if $K get deployment -n cert-manager cert-manager >/dev/null 2>&1; then
echo " already installed"
else
$K apply -f "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml"
fi
echo " waiting for cert-manager..."
$K wait --namespace cert-manager \
--for=condition=ready pod --selector=app.kubernetes.io/instance=cert-manager \
--timeout=240s
# A self-signed root, then a CA issuer chained off it. Workloads reference
# ClusterIssuer/local-ca and get a cert from a CA you can actually distribute.
echo " creating local CA issuer"
$K apply -f - <<'YAML' >/dev/null
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-root
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: local-ca
namespace: cert-manager
spec:
isCA: true
commonName: rig-local-ca
secretName: local-ca-key-pair
duration: 87600h
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-root
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: local-ca
spec:
ca:
secretName: local-ca-key-pair
YAML
echo " export the CA for your browser/client with:"
echo " kubectl --context ${KUBECONTEXT} -n cert-manager get secret local-ca-key-pair -o jsonpath='{.data.tls\\.crt}' | base64 -d"

103
rig/ctrl/addons/metallb.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# MetalLB — makes `Service type: LoadBalancer` actually get an address.
#
# Why it matters here: real manifests use LoadBalancer, because a real cluster
# has one. On a bare kind cluster those Services sit at EXTERNAL-IP <pending>
# forever with no error anywhere — the deployment looks fine and simply is not
# reachable. Without this, every such Service has to be edited to NodePort,
# which means the local manifests stop matching the ones being modelled.
#
# The address pool is derived from the kind Docker network at install time, not
# hardcoded: Docker picks that subnet, it differs between machines, and a pool
# outside it is silently unroutable.
set -euo pipefail
cd "$(dirname "$0")/.."
source ./lib/config.sh
load_config
K="kubectl --context ${KUBECONTEXT}"
# ── work out an address range ──────────────────────────────────────────────
# kind hands node addresses out from the bottom of the subnet, so the top is
# free. Taking a slice there avoids collisions with current and future nodes.
subnet=$(docker network inspect kind \
-f '{{range .IPAM.Config}}{{.Subnet}} {{end}}' 2>/dev/null \
| tr ' ' '\n' | grep -E '^[0-9]+\.' | head -1)
if [ -z "$subnet" ]; then
echo " ! could not read the kind Docker network subnet" >&2
echo " (is the cluster up? MetalLB needs the network to exist first)" >&2
exit 1
fi
base="${subnet%/*}"; prefix="${subnet#*/}"
o1=$(echo "$base" | cut -d. -f1); o2=$(echo "$base" | cut -d. -f2)
o3=$(echo "$base" | cut -d. -f3)
case "$prefix" in
16) pool_start="${o1}.${o2}.255.200"; pool_end="${o1}.${o2}.255.250" ;;
24) pool_start="${o1}.${o2}.${o3}.200"; pool_end="${o1}.${o2}.${o3}.250" ;;
*)
# Guessing a range inside an unexpected prefix risks handing out
# addresses that belong to something else. Say so instead.
echo " ! kind network is $subnet — only /16 and /24 are handled" >&2
echo " set the pool by hand in ctrl/addons/metallb.sh" >&2
exit 1
;;
esac
echo " kind network $subnet → pool ${pool_start}-${pool_end}"
# ── install ────────────────────────────────────────────────────────────────
if $K get deployment -n metallb-system controller >/dev/null 2>&1; then
echo " already installed"
else
$K apply -f "https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml"
fi
# `kubectl wait` on a selector errors out immediately when nothing matches yet,
# and right after apply the ReplicaSet has not created the pod — so it loses a
# race it looks like it should win. `rollout status` waits for the Deployment
# itself and handles the not-yet-created case.
echo " waiting for the controller..."
$K rollout status deployment/controller -n metallb-system --timeout=240s
$K rollout status daemonset/speaker -n metallb-system --timeout=240s
# The webhook rejects IPAddressPools until it is actually serving, and it comes
# up a moment after the pod is Ready — so retry rather than fail the whole run
# on a race that resolves itself in seconds.
echo " configuring the address pool"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if $K apply -f - >/dev/null 2>&1 <<YAML
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- ${pool_start}-${pool_end}
---
# Layer 2 mode: one node answers ARP for each address. No BGP peer needed, which
# is what makes this work on a laptop.
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default
YAML
then
echo " pool ready: ${pool_start}-${pool_end}"
exit 0
fi
sleep 3
done
echo " ! the pool was rejected after 10 attempts — is the webhook up?" >&2
$K get pods -n metallb-system >&2
exit 1

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# metrics-server — makes `kubectl top` work.
#
# kind nodes serve kubelet metrics over a self-signed cert, so the standard
# manifest never becomes ready without --kubelet-insecure-tls. That is fine here
# (it is a local cluster) and is the single most common reason metrics-server
# sits at 0/1 on kind.
set -euo pipefail
cd "$(dirname "$0")/.."
source ./lib/config.sh
load_config
K="kubectl --context ${KUBECONTEXT}"
if ! $K get deployment -n kube-system metrics-server >/dev/null 2>&1; then
$K apply -f "https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml"
fi
$K patch deployment metrics-server -n kube-system --type=json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' \
>/dev/null 2>&1 || true
echo " waiting for metrics-server..."
$K rollout status deployment/metrics-server -n kube-system --timeout=180s

118
rig/ctrl/addons/postgres.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# PostgreSQL — the cluster half of soleprint's postgres cabinet.
#
# A room declares the dependency once, in cfg/<room>/data/cabinets.json. On a
# laptop `build.py` composes it into docker-compose.yml; here it becomes a pod,
# so the same declaration works either way and nothing has to be remembered
# twice.
#
# Plain manifests rather than a helm chart, matching the other addons: a chart
# repo is a network dependency, and the offline profile exists precisely so
# there is a path with none. The image is pinned in ctrl/versions.env and can be
# preloaded into a local registry like every other image here.
#
# One replica on a PVC. This models a dependency for local work, not a
# highly-available database, and pretending otherwise on a kind node would be a
# more elaborate lie rather than a more useful one.
set -euo pipefail
cd "$(dirname "$0")/.."
source ./lib/config.sh
load_config
K="kubectl --context ${KUBECONTEXT}"
NS="${DATA_NAMESPACE:-data}"
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
# The password is generated once and then left alone, so re-running this does
# not rotate the credential out from under whatever is already connected.
if $K get secret -n "$NS" postgres >/dev/null 2>&1; then
echo " secret exists, keeping the current password"
else
password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
$K create secret generic postgres -n "$NS" \
--from-literal=POSTGRES_DB="${POSTGRES_DB:-soleprint}" \
--from-literal=POSTGRES_USER="${POSTGRES_USER:-soleprint}" \
--from-literal=POSTGRES_PASSWORD="$password" >/dev/null
echo " generated a password (read it back with the command printed below)"
fi
echo " applying manifests"
$K apply -n "$NS" -f - >/dev/null <<YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: ${POSTGRES_STORAGE:-2Gi}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
# One volume, one writer. Rolling would start a second pod against the same
# PVC before the first exits, and Postgres refuses to share a data directory.
strategy:
type: Recreate
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: ${POSTGRES_IMAGE}
envFrom:
- secretRef:
name: postgres
env:
# The image initialises into the volume root otherwise, and a
# lost+found from the PVC makes it refuse to initdb.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
initialDelaySeconds: 30
periodSeconds: 15
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
YAML
echo " waiting for postgres..."
$K rollout status deployment/postgres -n "$NS" --timeout=240s
echo " in-cluster: postgres.${NS}.svc.cluster.local:5432"
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d"

60
rig/ctrl/addons/redis.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Redis — the cluster half of soleprint's redis cabinet.
#
# Cache, and the broker anything queue-shaped runs on. No persistence: a broker
# that loses its queue on restart is the honest local model, and a PVC here buys
# nothing but a volume to clean up.
set -euo pipefail
cd "$(dirname "$0")/.."
source ./lib/config.sh
load_config
K="kubectl --context ${KUBECONTEXT}"
NS="${DATA_NAMESPACE:-data}"
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
echo " applying manifests"
$K apply -n "$NS" -f - >/dev/null <<YAML
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: ${REDIS_IMAGE}
ports:
- containerPort: 6379
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 3
periodSeconds: 5
YAML
echo " waiting for redis..."
$K rollout status deployment/redis -n "$NS" --timeout=180s
echo " in-cluster: redis://redis.${NS}.svc.cluster.local:6379/0"