rig major updates
This commit is contained in:
@@ -6,6 +6,11 @@
|
||||
# need no profile at all. Copy an env.d/*.env.example to <name>.env to add one.
|
||||
PROFILE=
|
||||
|
||||
# The overlay: one folder, outside rig's version control, holding what runs —
|
||||
# its settings (rig.env), manifests, addons, Tiltfile. Relative to rig's folder,
|
||||
# or absolute. Unset: rig's own examples/starter. See docs/notes/overlay.md.
|
||||
# OVERLAY=local/<name>
|
||||
|
||||
# Cluster name; the kubectl context becomes kind-<CLUSTER>.
|
||||
# LEAVE UNSET: it defaults to this folder's name, which keeps the folder copyable.
|
||||
# CLUSTER=
|
||||
@@ -17,9 +22,8 @@ PROFILE=
|
||||
# TILT_PORT=
|
||||
# REGISTRY_PORT=
|
||||
|
||||
# Where the application manifests live; repoint at their own repo, e.g.
|
||||
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
MANIFESTS_DIR=ctrl/k8s/overlays/dev
|
||||
# Where the manifests live. Leave unset: the overlay's k8s/overlays/dev.
|
||||
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
|
||||
# Where the installer fetches the pinned binaries from:
|
||||
# upstream (needs internet) | artifactory (generic repo) | baked (in the image)
|
||||
|
||||
@@ -27,8 +27,10 @@ CMD ["install"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# deps-full — same image, binaries baked in, works with no network at all.
|
||||
# deps-full — same image, binaries and the addons' manifests baked in, works with no network.
|
||||
FROM deps AS deps-full
|
||||
RUN /work/rigdeps.sh fetch --to /opt/rig/bin
|
||||
RUN /work/rigdeps.sh fetch --to /opt/rig/bin \
|
||||
&& /work/rigdeps.sh manifests --to /opt/rig/manifests
|
||||
ENV DEPS_SOURCE=baked \
|
||||
BAKED_BIN=/opt/rig/bin
|
||||
BAKED_BIN=/opt/rig/bin \
|
||||
BAKED_MANIFESTS=/opt/rig/manifests
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# EXAMPLE component image. Copy, rename, replace:
|
||||
# ctrl/Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
||||
# COPY paths are relative to the REPO ROOT (Tilt context='..'), not this directory.
|
||||
# Notes: docs/notes/Dockerfile.example.md
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies first, in their own layer, so a source edit does not reinstall them.
|
||||
COPY api/requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Repo-root relative — see above.
|
||||
COPY api/ ./api/
|
||||
|
||||
# Match this with the containerPort in the manifest and the target of the
|
||||
# Service in front of it.
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python", "-m", "api"]
|
||||
|
||||
# live_update: Tiltfile's sync('../api', '/app/api') must match COPY api/ + WORKDIR /app,
|
||||
# or edits silently do nothing.
|
||||
@@ -1,6 +1,6 @@
|
||||
# The dev loop. `make tilt` from the project root, or `cd ctrl && tilt up`.
|
||||
# Works unedited; replace the two EXAMPLES and add yours in the marked sections.
|
||||
# rig supplies this file but does not own it, and nothing here names this directory.
|
||||
# The dev loop, rig's half. `make tilt` from rig's folder, or from an overlay's forwarder.
|
||||
# rig owns this file: who we are, the context guard, the registry, the overlay's manifests.
|
||||
# The workload's half is the overlay's own Tiltfile, included at the end; edit that one.
|
||||
# Notes: docs/notes/Tiltfile.md
|
||||
|
||||
# ── who we are, and on which ports ─────────────────────────────────────────
|
||||
@@ -12,10 +12,9 @@ HTTP = _facts[2]
|
||||
HTTPS = _facts[3]
|
||||
TILT = _facts[4]
|
||||
REGISTRY = _facts[5]
|
||||
|
||||
# Where the manifests live (MANIFESTS_DIR, see k8s/README.md).
|
||||
# The value is REPO-ROOT relative and this file runs in ctrl/, so prefix '../'.
|
||||
MANIFESTS = '../' + _facts[6]
|
||||
# Absolute paths, or '-' when there is none.
|
||||
MANIFESTS = '' if _facts[6] == '-' else _facts[6]
|
||||
OVERLAY = '' if _facts[7] == '-' else _facts[7]
|
||||
|
||||
# ── refuse to deploy into the wrong cluster ────────────────────────────────
|
||||
# Tilt fixes the context before parsing this file, so it can only be refused here.
|
||||
@@ -25,73 +24,44 @@ if k8s_context() != CTX:
|
||||
fail("Wrong kubectl context: '%s'. This is %s — run: make tilt, or tilt up --context %s"
|
||||
% (k8s_context(), CLUSTER, CTX))
|
||||
|
||||
# The namespace has to exist before anything lands in it, and kustomize does not
|
||||
# guarantee ordering across resources. Creating it here is idempotent.
|
||||
local('kubectl --context %s create namespace %s --dry-run=client -o yaml | kubectl --context %s apply -f -'
|
||||
% (CTX, CLUSTER, CTX), quiet=True)
|
||||
|
||||
# ── images go to this environment's own registry ───────────────────────────
|
||||
# Fail closed: name the registry rather than let Tilt infer it, or a miss pushes
|
||||
# an unqualified image to docker.io.
|
||||
default_registry('localhost:' + REGISTRY)
|
||||
|
||||
k8s_yaml(kustomize(MANIFESTS))
|
||||
|
||||
# ── Images ─────────────────────────────────────────────────────────────────
|
||||
# (nothing yet — rig's examples run upstream images. Add docker_build calls here.)
|
||||
|
||||
|
||||
# ── Resources ──────────────────────────────────────────────────────────────
|
||||
# (nothing yet — add k8s_resource calls here to name and order what you deploy.)
|
||||
|
||||
|
||||
# Everything with no dev loop of its own, gathered so it does not clutter the UI.
|
||||
k8s_resource(
|
||||
objects=[CLUSTER + ':namespace'],
|
||||
new_name='infra',
|
||||
)
|
||||
# ── the overlay's manifests ────────────────────────────────────────────────
|
||||
# Every namespace they use must exist before anything lands in it, and kustomize
|
||||
# does not order resources, so create them here (idempotent). The Namespaces they
|
||||
# declare are grouped as 'infra', whatever they are named.
|
||||
if MANIFESTS:
|
||||
_yaml = kustomize(MANIFESTS)
|
||||
k8s_yaml(_yaml)
|
||||
_declared = []
|
||||
_namespaces = {}
|
||||
for _o in decode_yaml_stream(_yaml):
|
||||
if not _o:
|
||||
continue
|
||||
_md = _o.get('metadata') or {}
|
||||
if _o.get('kind') == 'Namespace':
|
||||
_declared.append(_md.get('name'))
|
||||
_namespaces[_md.get('name')] = True
|
||||
elif _md.get('namespace'):
|
||||
_namespaces[_md.get('namespace')] = True
|
||||
for _ns in sorted(_namespaces.keys()):
|
||||
local('kubectl --context %s create namespace %s --dry-run=client -o yaml | kubectl --context %s apply -f -'
|
||||
% (CTX, _ns, CTX), quiet=True)
|
||||
if _declared:
|
||||
k8s_resource(objects=[_n + ':namespace' for _n in _declared], new_name='infra')
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Catalogue — paste what you need, delete the rest.
|
||||
# Commented out so this file runs as-is.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# ── build an image ─────────────────────────────────────────────────────────
|
||||
# context= is the REPO ROOT ('..'); dockerfile= is relative to THIS file (ctrl/).
|
||||
# So every COPY is repo-root relative, even for files beside the Dockerfile.
|
||||
#
|
||||
# docker_build(
|
||||
# CLUSTER + '-api', # must match `image:` in the manifest —
|
||||
# context='..', # that string is the only thing
|
||||
# dockerfile='Dockerfile.api', # connecting the two
|
||||
# ignore=['.git', 'def', '.venv', 'node_modules', '__pycache__'],
|
||||
# live_update=[sync('../api', '/app/api')],
|
||||
# )
|
||||
#
|
||||
# ── name and order a resource ──────────────────────────────────────────────
|
||||
# k8s_resource('api', resource_deps=['postgres'], labels=['app'])
|
||||
# k8s_resource('gateway', resource_deps=['api', 'ui'], labels=['app'])
|
||||
#
|
||||
# ── reload the gateway when its config changes ─────────────────────────────
|
||||
# A hash-less configMapGenerator ConfigMap never changes name, so edits do NOT
|
||||
# roll the pod on their own.
|
||||
#
|
||||
# local_resource(
|
||||
# 'gateway-reload',
|
||||
# cmd='kubectl --context %s -n %s rollout restart deployment/gateway' % (CTX, CLUSTER),
|
||||
# deps=['k8s/base/Caddyfile'],
|
||||
# resource_deps=['gateway'],
|
||||
# auto_init=False,
|
||||
# )
|
||||
#
|
||||
# ── an overlay whose secretGenerator reads outside its own directory ───────
|
||||
# kustomize refuses to read above the kustomization root unless told to. Only
|
||||
# add this if you actually have such a generator; it loosens a safety check.
|
||||
#
|
||||
# k8s_yaml(kustomize(MANIFESTS, flags=['--load-restrictor=LoadRestrictionsNone']))
|
||||
#
|
||||
# ── reach a service directly, bypassing the gateway ────────────────────────
|
||||
# Prefer the gateway; host ports are shared machine-wide. If you need one, take it
|
||||
# from this environment's own port block.
|
||||
#
|
||||
# k8s_resource('postgres', port_forwards=[str(int(HTTP) + 5) + ':5432'])
|
||||
# ── the workload's half: the overlay's Tiltfile ────────────────────────────
|
||||
# Included, so its relative paths resolve from the overlay's own folder. It reads
|
||||
# these facts with os.getenv and never needs a path back into rig.
|
||||
os.putenv('RIG_CLUSTER', CLUSTER)
|
||||
os.putenv('RIG_CONTEXT', CTX)
|
||||
os.putenv('RIG_HTTP_PORT', HTTP)
|
||||
os.putenv('RIG_HTTPS_PORT', HTTPS)
|
||||
os.putenv('RIG_TILT_PORT', TILT)
|
||||
os.putenv('RIG_REGISTRY', 'localhost:' + REGISTRY)
|
||||
os.putenv('RIG_OVERLAY_DIR', OVERLAY)
|
||||
if OVERLAY and os.path.exists(OVERLAY + '/Tiltfile'):
|
||||
include(OVERLAY + '/Tiltfile')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the addons the active profile asked for, in the order listed.
|
||||
# One idempotent script per addon in ctrl/addons/.
|
||||
# Install the addons the configuration asks for (ADDONS), in the order listed.
|
||||
# One idempotent script per addon: the overlay's addons/<name>.sh first, then rig's ctrl/addons/.
|
||||
# Usage: addons.sh install | list
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
@@ -9,26 +9,51 @@ cd "$(dirname "$0")"
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
# Every addon runs from here, wherever its file lives, so it can source ./lib/config.sh.
|
||||
export RIG_CTRL="$PWD"
|
||||
|
||||
# The script for one addon name: the overlay's, else rig's; empty if neither.
|
||||
addon_path() {
|
||||
local ov=""
|
||||
if [ -n "$OVERLAY_DIR" ]; then ov="$(_from_ctrl "$OVERLAY_DIR")/addons/$1.sh"; fi
|
||||
if [ -n "$ov" ] && [ -f "$ov" ]; then
|
||||
echo "$ov"
|
||||
elif [ -f "addons/$1.sh" ]; then
|
||||
echo "addons/$1.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
if [ -z "${ADDONS// /}" ]; then
|
||||
echo "no addons in profile '$PROFILE_NAME'"
|
||||
echo "no addons asked for (ADDONS is empty)"
|
||||
return
|
||||
fi
|
||||
local a
|
||||
local a p
|
||||
for a in $ADDONS; do
|
||||
if [ ! -f "addons/${a}.sh" ]; then
|
||||
echo "no such addon: addons/${a}.sh" >&2
|
||||
p=$(addon_path "$a")
|
||||
if [ -z "$p" ]; then
|
||||
echo "no such addon: $a (looked in the overlay's addons/ and ctrl/addons/)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "addon: $a"
|
||||
bash "addons/${a}.sh"
|
||||
bash "$p"
|
||||
done
|
||||
}
|
||||
|
||||
list() {
|
||||
echo "profile '$PROFILE_NAME' wants: ${ADDONS:-none}"
|
||||
echo "wanted: ${ADDONS:-none}"
|
||||
echo "available:"
|
||||
ls addons/*.sh 2>/dev/null | xargs -n1 basename | sed 's/\.sh$//' | sed 's/^/ /'
|
||||
local f
|
||||
if [ -n "$OVERLAY_DIR" ]; then
|
||||
for f in "$(_from_ctrl "$OVERLAY_DIR")"/addons/*.sh; do
|
||||
[ -e "$f" ] || continue
|
||||
printf ' %-16s overlay\n' "$(basename "$f" .sh)"
|
||||
done
|
||||
fi
|
||||
for f in addons/*.sh; do
|
||||
[ -e "$f" ] || continue
|
||||
printf ' %-16s rig\n' "$(basename "$f" .sh)"
|
||||
done
|
||||
}
|
||||
|
||||
case "${1:-list}" in
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apache Airflow — the cluster half of the airflow cabinet (one `standalone` pod).
|
||||
# Requires the postgres addon; refuses to install without it.
|
||||
# Notes: docs/notes/addons.md
|
||||
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"
|
||||
@@ -2,7 +2,7 @@
|
||||
# cert-manager plus a self-signed cluster issuer (offline local CA).
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
@@ -12,7 +12,9 @@ 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"
|
||||
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||
manifest=$(bash ./deps.sh manifest CERT_MANAGER)
|
||||
$K apply -f "$manifest"
|
||||
fi
|
||||
|
||||
echo " waiting for cert-manager..."
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# The pool is derived from the kind Docker network at install time.
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
@@ -46,7 +46,9 @@ echo " kind network $subnet → pool ${pool_start}-${pool_end}"
|
||||
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"
|
||||
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||
manifest=$(bash ./deps.sh manifest METALLB)
|
||||
$K apply -f "$manifest"
|
||||
fi
|
||||
|
||||
# `rollout status`, not `kubectl wait`: wait errors out while the pod doesn't exist yet.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# metrics-server — makes `kubectl top` work (patched with --kubelet-insecure-tls for kind).
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
@@ -10,7 +10,9 @@ 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"
|
||||
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||
manifest=$(bash ./deps.sh manifest METRICS_SERVER)
|
||||
$K apply -f "$manifest"
|
||||
fi
|
||||
|
||||
$K patch deployment metrics-server -n kube-system --type=json \
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL — the cluster half of the postgres cabinet: plain manifests, one replica on a PVC.
|
||||
# Notes: docs/notes/addons.md
|
||||
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:-postgres}" \
|
||||
--from-literal=POSTGRES_USER="${POSTGRES_USER:-postgres}" \
|
||||
--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"
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Redis — the cluster half of the redis cabinet: cache/broker, no persistence.
|
||||
# Notes: docs/notes/addons.md
|
||||
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"
|
||||
@@ -69,10 +69,34 @@ port_busy() {
|
||||
echo
|
||||
echo "rig"
|
||||
echo " cluster ${CLUSTER} (${KUBECONTEXT}) profile ${PROFILE_NAME}, ${NODES} node(s), registry ${REGISTRY_MODE}"
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
echo " overlay $(basename "$(_abs_from_ctrl "$OVERLAY_DIR")") ($(_abs_from_ctrl "$OVERLAY_DIR"))"
|
||||
elif [ -n "$OVERLAY_DIR" ]; then
|
||||
fact " overlay none named — rig's own ${OVERLAY_DIR}"
|
||||
fi
|
||||
if [ -n "$VERBOSE" ] && [ -n "$OVERLAY_DIR" ]; then
|
||||
ov=$(_from_ctrl "$OVERLAY_DIR") pieces=""
|
||||
for piece in rig.env k8s/overlays/dev kind-config.yaml.tpl addons Tiltfile; do
|
||||
[ -e "$ov/$piece" ] && pieces+="$piece "
|
||||
done
|
||||
echo " provides: ${pieces:-nothing rig reads}"
|
||||
fi
|
||||
fact " manifests ${MANIFESTS_DIR:-none}"
|
||||
fact " kind config ${KIND_CONFIG}"
|
||||
fact " ingress ${INGRESS_MODE}"
|
||||
if [ ! -f ./.env ]; then
|
||||
fact " .env none — built-in defaults (cp ctrl/.env.example ctrl/.env to set values)"
|
||||
fi
|
||||
if [ -n "${STALE_MANIFESTS_DIR:-}" ]; then
|
||||
echo " ! .env MANIFESTS_DIR=${STALE_MANIFESTS_DIR} is the old default; rig's examples moved"
|
||||
echo " to examples/ — delete that line from ctrl/.env (ignored until then)"
|
||||
fi
|
||||
# registry.sh points containerd at certs.d, which only works if the kind config says so,
|
||||
# and a kind config is fixed at creation: a project's own file that drops it fails silently.
|
||||
if [ "$REGISTRY_MODE" != none ] && ! grep -q 'config_path *= *"/etc/containerd/certs.d"' "$KIND_CONFIG"; then
|
||||
echo " ! kind ${KIND_CONFIG} lacks the containerd config_path patch that registry mode"
|
||||
echo " '${REGISTRY_MODE}' needs — copy it from ctrl/k8s/kind-config.yaml.tpl"
|
||||
fi
|
||||
|
||||
# ── memory: does this cluster fit right now? Warns; never blocks. ──────────
|
||||
total_mb=$(mb_of MemTotal)
|
||||
@@ -157,7 +181,11 @@ elif [ "$mine" -eq 1 ]; then
|
||||
else
|
||||
echo " ports ${list% } free"
|
||||
fi
|
||||
fact " derived from the directory name; pin them: bash ctrl/ports.sh persist"
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
fact " derived from the overlay's folder name"
|
||||
else
|
||||
fact " derived from the directory name; pin them: bash ctrl/ports.sh persist"
|
||||
fi
|
||||
|
||||
# ── what `make cluster up` wires in beside the cluster ─────────────────────
|
||||
REG_NAME="${CLUSTER}-registry"
|
||||
@@ -167,7 +195,9 @@ else
|
||||
fact " registry no container yet — 'make cluster up' starts it"
|
||||
fi
|
||||
echo " addons ${ADDONS:-none}"
|
||||
fact " available: $(ls addons/*.sh 2>/dev/null | xargs -n1 basename | sed 's/\.sh$//' | tr '\n' ' ')"
|
||||
if [ -n "$VERBOSE" ]; then
|
||||
bash ./addons.sh list | sed -n '3,$p' | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# The CA reaches three places and only one of them is ours. Report the other two.
|
||||
if [ -n "${REGISTRY_CA_FILE:-}" ]; then
|
||||
|
||||
@@ -15,6 +15,7 @@ up() {
|
||||
# Say what this profile locks in BEFORE spending minutes building it:
|
||||
# the kind config is fixed at creation and cannot be changed later.
|
||||
echo "creating cluster '$CLUSTER' from profile '$PROFILE_NAME'"
|
||||
echo " overlay ${OVERLAY_DIR:-none}"
|
||||
echo " kind config ${KIND_CONFIG}"
|
||||
echo " nodes $NODES"
|
||||
echo " image $NODE_IMAGE"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# rig:standalone rigdeps detect
|
||||
# Toolchain installer: detect the host, install pinned tools into $OUT_BIN, report
|
||||
# host actions it will not perform (no sudo, no apt). Usually via `make deps`.
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]]
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]
|
||||
# | manifest NAME | manifests [--to DIR]]
|
||||
# Notes: docs/notes/deps.md
|
||||
|
||||
set -euo pipefail
|
||||
@@ -719,6 +720,51 @@ require_linux
|
||||
cmd="${1:-install}"
|
||||
[ $# -gt 0 ] && shift
|
||||
|
||||
# ── manifests rig's own addons apply ───────────────────────────────────────
|
||||
# Pinned (URL + SHA256), fetched through the same DEPS_SOURCE resolver as the
|
||||
# binaries and verified, then applied from disk: an offline machine needs no
|
||||
# network for them. Default home: vendor/manifests/ in rig's folder (gitignored).
|
||||
MANIFESTS_HOME="${MANIFESTS_HOME:-$(cd .. && pwd)/vendor/manifests}"
|
||||
BAKED_MANIFESTS="${BAKED_MANIFESTS:-/opt/rig/manifests}"
|
||||
MANIFEST_NAMES="METALLB CERT_MANAGER METRICS_SERVER"
|
||||
|
||||
manifest_path() { # NAME dir
|
||||
local v="${1}_VERSION"
|
||||
echo "$2/$(echo "$1" | tr 'A-Z_' 'a-z-')-${!v}.yaml"
|
||||
}
|
||||
|
||||
# Make one pinned manifest present and verified in dir; print only its path.
|
||||
fetch_manifest() { # NAME dir
|
||||
local name="$1" dir="$2" url_var="${1}_MANIFEST_URL" sha_var="${1}_MANIFEST_SHA256" file
|
||||
if [ -z "${!url_var:-}" ] || [ -z "${!sha_var:-}" ]; then
|
||||
echo "no pinned manifest for $name (${url_var} / ${sha_var} unset)" >&2
|
||||
exit 1
|
||||
fi
|
||||
file=$(manifest_path "$name" "$dir")
|
||||
if [ -f "$file" ] && [ "$($SHA "$file" | awk '{print $1}')" = "${!sha_var}" ]; then
|
||||
echo "$file"
|
||||
return
|
||||
fi
|
||||
mkdir -p "$dir"
|
||||
if [ "$DEPS_SOURCE" = baked ]; then
|
||||
cp "$(manifest_path "$name" "$BAKED_MANIFESTS")" "$file.tmp"
|
||||
else
|
||||
download "$(resolve_url "${!url_var}")" "$file.tmp"
|
||||
fi
|
||||
verify "$file.tmp" "${!sha_var}" "$name manifest"
|
||||
mv "$file.tmp" "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
fetch_manifests() { # [--to DIR]
|
||||
local dest="$MANIFESTS_HOME" n
|
||||
if [ "${1:-}" = --to ]; then dest="$(abspath "${2:?--to needs a directory}")"; fi
|
||||
echo "fetching the addons' manifests into $dest (source: $DEPS_SOURCE)"
|
||||
for n in $MANIFEST_NAMES; do
|
||||
echo " $n $(fetch_manifest "$n" "$dest")"
|
||||
done
|
||||
}
|
||||
|
||||
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||
need_downloads() {
|
||||
require_amd64
|
||||
@@ -732,9 +778,13 @@ case "$cmd" in
|
||||
verify) verify_tools "${1:-dev}" ;;
|
||||
fetch) need_downloads; fetch "$@" ;;
|
||||
install) need_downloads; install "${1:-dev}" ;;
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install]" >&2
|
||||
manifest) need_downloads
|
||||
fetch_manifest "${1:?usage: $0 manifest <METALLB|CERT_MANAGER|METRICS_SERVER>}" "$MANIFESTS_HOME" ;;
|
||||
manifests) need_downloads; fetch_manifests "$@" ;;
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install|manifest NAME|manifests]" >&2
|
||||
echo " install [core|dev] (default dev)" >&2
|
||||
echo " fetch [core|dev] [--to DIR]" >&2
|
||||
echo " manifests [--to DIR] the addons' pinned manifests, verified" >&2
|
||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# EXAMPLE PROFILE (optional): copy to data.env, then PROFILE=data; overlays the defaults.
|
||||
# data — postgres, redis and airflow (upstream images) in the `data` namespace.
|
||||
# Notes: docs/notes/env.md
|
||||
|
||||
PROFILE_NAME=data
|
||||
K8S_VERSION=v1_36
|
||||
# Order matters: addons.sh installs in the order listed, and airflow refuses to
|
||||
# start without a metadata database, so postgres comes first.
|
||||
ADDONS="metallb postgres redis airflow"
|
||||
# local, not none — see the defaults in lib/config.sh: `none` has no outward-push guard.
|
||||
REGISTRY_MODE=local
|
||||
INGRESS_MODE=hostport
|
||||
DNS_MODE=hosts
|
||||
|
||||
# Namespace for the dependency containers.
|
||||
DATA_NAMESPACE=data
|
||||
|
||||
# Postgres identity. The password is generated once by postgres.sh and kept.
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=app
|
||||
POSTGRES_STORAGE=2Gi
|
||||
|
||||
AIRFLOW_ADMIN_USER=admin
|
||||
|
||||
# Reach the databases with port-forward, e.g.
|
||||
# kubectl -n data port-forward svc/postgres 5432:5432
|
||||
@@ -1,8 +1,9 @@
|
||||
# EXAMPLE PROFILE (optional): copy to client.env, then PROFILE=client; overlays the defaults.
|
||||
# client — images via a pull-through cache of the corporate registry, TLS and metrics addons.
|
||||
# EXAMPLE PROFILE (optional): copy to mirror.env, then PROFILE=mirror; overlays the defaults.
|
||||
# mirror — images via a pull-through cache of an internal registry, TLS and metrics addons.
|
||||
# A profile says how this machine reaches the world; what runs is an overlay's business.
|
||||
# Notes: docs/notes/env.md
|
||||
|
||||
PROFILE_NAME=client
|
||||
PROFILE_NAME=mirror
|
||||
K8S_VERSION=v1_36
|
||||
ADDONS="metallb cert-manager metrics-server"
|
||||
REGISTRY_MODE=mirror
|
||||
@@ -15,6 +16,6 @@ DNS_MODE=hosts
|
||||
# HTTPS_PORT=443
|
||||
|
||||
# Set these in ctrl/.env (gitignored), not here:
|
||||
# REGISTRY_REMOTE_URL=https://artifactory.corp.example/artifactory/api/docker/docker-virtual
|
||||
# REGISTRY_REMOTE_URL=https://registry.internal.example/api/docker/docker-virtual
|
||||
# REGISTRY_USER / REGISTRY_PASSWORD
|
||||
# REGISTRY_CA_FILE=/path/to/corp-root-ca.crt
|
||||
# REGISTRY_CA_FILE=/path/to/internal-root-ca.crt
|
||||
@@ -1,60 +0,0 @@
|
||||
# `ctrl/k8s` — the cluster, and what runs on it
|
||||
|
||||
Same layout as every other project here: a kind config, a kustomize `base/`,
|
||||
and an `overlays/dev/` that patches it.
|
||||
|
||||
```
|
||||
kind-config.yaml.tpl the cluster itself — nodes, ports
|
||||
base/ the components, as plain manifests
|
||||
overlays/dev/ how this rig differs from the base
|
||||
```
|
||||
|
||||
## Why the cluster config is a template
|
||||
|
||||
Every other project checks in a literal `kind-config.yaml`, because there is
|
||||
exactly one `unt` and one `nvi`. A rig is copied and renamed to make a second
|
||||
environment, and both the cluster name and the host port block follow the
|
||||
directory name — so a literal would make every copy collide on both.
|
||||
|
||||
`ctrl/cluster.sh` renders it with `sed`, substituting `${CLUSTER}`,
|
||||
`${NODE_IMAGE}`, `${HTTP_PORT}` and `${HOST_WORKDIR}`. Not `envsubst`: that is
|
||||
`gettext-base`, which a minimal Debian does not have, and Docker being the only
|
||||
prerequisite is the one promise rig makes.
|
||||
|
||||
**To change the cluster, edit this file** — more nodes, other port mappings —
|
||||
then `make cluster reset`: a kind config is fixed at creation, not re-applied.
|
||||
`lib/config.sh` reads the node count back out of it, so nothing restates it.
|
||||
|
||||
A project that builds its own cluster through rig passes its own file as
|
||||
`KIND_CONFIG=<path>`; it is rendered the same way.
|
||||
|
||||
## `base/` — replace these
|
||||
|
||||
**The two components in `base/` are examples, not the system.** They exist so
|
||||
the real manifests have a shape to be written against.
|
||||
|
||||
The real ones are expected to be versioned **separately from the installer** —
|
||||
they change on a different cadence, by different people, under different review.
|
||||
Point `MANIFESTS_DIR` in `ctrl/.env` at their overlay and rig stops owning them:
|
||||
|
||||
```
|
||||
MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
```
|
||||
|
||||
Until then it defaults to `ctrl/k8s/overlays/dev`.
|
||||
|
||||
### The three states a component can be in
|
||||
|
||||
Switching between them should be a one-line change, never a rewrite. The DNS
|
||||
name stays the same in every case, so callers never know the difference:
|
||||
|
||||
| state | what exists | when |
|
||||
| --- | --- | --- |
|
||||
| **real** | an image built from source, hot-reloaded | the one thing you are working on |
|
||||
| **mock** | a stub returning canned responses (`example-mock.yaml`) | everything else — most of the estate |
|
||||
| **remote** | no pod at all, just a Service (`example-remote.yaml`) | when the real system is reachable and you want it |
|
||||
|
||||
Most components should be **mock**. What has to be faithful is the topology —
|
||||
names, ports, dependency order, who can reach whom, how it fails. The workloads
|
||||
are noise, and mocking them is what makes several copies of a large estate fit
|
||||
on one laptop.
|
||||
@@ -1,104 +0,0 @@
|
||||
# EXAMPLE — a mocked component. Copy, rename, replace.
|
||||
#
|
||||
# A stub that answers on the right name and port with canned responses. No image
|
||||
# to build: the script is mounted from the ConfigMap, so changing the behaviour
|
||||
# is a kubectl apply, not a rebuild.
|
||||
#
|
||||
# Deliberately boring and readable. This is onboarding material — someone should
|
||||
# be able to read the generated object and recognise what it is.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: example-service-stub
|
||||
data:
|
||||
# Canned responses by path. Add entries as the contract becomes clear;
|
||||
# anything unmatched returns 404 so a missing route is visible, not silent.
|
||||
routes.json: |
|
||||
{
|
||||
"/health": {"status": 200, "body": {"status": "ok"}},
|
||||
"/v1/example": {"status": 200, "body": {"items": [], "mocked": true}}
|
||||
}
|
||||
serve.py: |
|
||||
import json, os
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
ROUTES = json.load(open("/etc/stub/routes.json"))
|
||||
NAME = os.environ.get("STUB_NAME", "stub")
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
r = ROUTES.get(self.path)
|
||||
if r is None:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
# Say which stub rejected it — with everything mocked, "404"
|
||||
# alone tells you nothing about where the call actually landed.
|
||||
self.wfile.write(json.dumps(
|
||||
{"error": "no canned route", "stub": NAME, "path": self.path}
|
||||
).encode())
|
||||
return
|
||||
body = json.dumps(r["body"]).encode()
|
||||
self.send_response(r["status"])
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("X-Mocked-By", NAME)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print("%s %s" % (NAME, fmt % args), flush=True)
|
||||
|
||||
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: example-service
|
||||
labels:
|
||||
app: example-service
|
||||
rig.component/impl: mock # so `kubectl get deploy -L rig.component/impl`
|
||||
# shows at a glance what is real and what is not
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example-service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example-service
|
||||
spec:
|
||||
containers:
|
||||
- name: stub
|
||||
image: python:3.12-slim
|
||||
command: ["python3", "/etc/stub/serve.py"]
|
||||
env:
|
||||
- name: STUB_NAME
|
||||
value: example-service
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: stub
|
||||
mountPath: /etc/stub
|
||||
readinessProbe:
|
||||
httpGet: { path: /health, port: 8080 }
|
||||
initialDelaySeconds: 2
|
||||
# Small enough that a whole estate of these fits alongside the real
|
||||
# thing you are working on.
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi }
|
||||
volumes:
|
||||
- name: stub
|
||||
configMap:
|
||||
name: example-service-stub
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-service
|
||||
spec:
|
||||
selector:
|
||||
app: example-service
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
@@ -1,46 +0,0 @@
|
||||
# EXAMPLE — a component that is NOT simulated, pointed at the real system.
|
||||
#
|
||||
# This is the payoff of keeping the topology honest: there is no pod here at
|
||||
# all, yet `example-remote.<namespace>.svc.cluster.local` resolves exactly as it
|
||||
# does when the same component is mocked. Callers are identical in both cases,
|
||||
# so moving a dependency from mocked to real is a one-line change and nothing
|
||||
# downstream is touched.
|
||||
#
|
||||
# Use this when the real system is reachable and you want it in the loop.
|
||||
# Note that reachability depends on where you are running: systems restricted to
|
||||
# a managed workspace will not resolve from a laptop at all, which is the whole
|
||||
# reason most components should stay mocked.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-remote
|
||||
labels:
|
||||
rig.component/impl: remote
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: real-system.internal.example.com
|
||||
---
|
||||
# If the real system has no DNS name — only an IP, which is common for legacy
|
||||
# hosts — ExternalName cannot express it. Use a bare Service plus manual
|
||||
# Endpoints instead, and delete the block above.
|
||||
#
|
||||
# apiVersion: v1
|
||||
# kind: Service
|
||||
# metadata:
|
||||
# name: example-remote
|
||||
# labels:
|
||||
# rig.component/impl: remote
|
||||
# spec:
|
||||
# ports:
|
||||
# - port: 80
|
||||
# targetPort: 8080
|
||||
# ---
|
||||
# apiVersion: v1
|
||||
# kind: Endpoints
|
||||
# metadata:
|
||||
# name: example-remote # must match the Service name exactly
|
||||
# subsets:
|
||||
# - addresses:
|
||||
# - ip: 10.0.0.42
|
||||
# ports:
|
||||
# - port: 8080
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# The namespace every component lands in. The overlay overrides it, so a rig
|
||||
# modelling two estates can apply the same base twice under different names.
|
||||
namespace: rig
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- example-mock.yaml
|
||||
- example-remote.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: rig
|
||||
@@ -1,5 +1,5 @@
|
||||
# The cluster. Add nodes or port mappings here, then `make cluster reset`.
|
||||
# ctrl/cluster.sh substitutes (sed): CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
||||
# ctrl/cluster.sh substitutes (sed): CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR, OVERLAY_DIR
|
||||
# lib/config.sh reads the node count back from this file.
|
||||
# Notes: docs/notes/kind-config.md
|
||||
kind: Cluster
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
# The dev overlay is where a rig says how its estate differs from the base —
|
||||
# which components are real, which are mocked, which point at a live system.
|
||||
# Kept empty on purpose: the base already boots, and an overlay full of examples
|
||||
# is harder to read than one that starts blank.
|
||||
#
|
||||
# The shape a patch takes, for when the first one is needed:
|
||||
#
|
||||
# patches:
|
||||
# - target: {kind: Service, name: example-service}
|
||||
# patch: |
|
||||
# - op: replace
|
||||
# path: /spec/type
|
||||
# value: NodePort
|
||||
# - op: add
|
||||
# path: /spec/ports/0/nodePort
|
||||
# value: 30080
|
||||
@@ -1,19 +1,34 @@
|
||||
# Shared config loading: how the config layers compose. Sourced, never executed.
|
||||
# Precedence, weakest first: defaults < versions.env < env.d/<profile> < .env < caller's env.
|
||||
# Precedence, weakest first:
|
||||
# defaults < versions.env < env.d/<profile> < <overlay>/rig.env < .env < caller's env.
|
||||
# Run from ctrl/.
|
||||
# Notes: docs/notes/config.md
|
||||
|
||||
# Per-invocation overrides: restored after the files are read, so the caller wins.
|
||||
# NODES is deliberately not here (read from the kind config).
|
||||
CONFIG_OVERRIDABLE="PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||
CONFIG_OVERRIDABLE="PROFILE OVERLAY CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
||||
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT
|
||||
REGISTRY_PORT MANIFESTS_DIR"
|
||||
|
||||
# The repo folder's name, reduced to a DNS label kind accepts as a cluster name.
|
||||
# rig's own example, used when no overlay is named (relative to rig's root).
|
||||
DEFAULT_OVERLAY=examples/starter
|
||||
|
||||
# A rig-root-relative path as seen from ctrl/; absolute paths pass through.
|
||||
_from_ctrl() { case "$1" in /*) echo "$1" ;; *) echo "../$1" ;; esac; }
|
||||
|
||||
# The same, absolute. Empty if it does not exist.
|
||||
_abs_from_ctrl() { (cd "$(_from_ctrl "$1")" 2>/dev/null && pwd); }
|
||||
|
||||
# The environment's folder — the overlay's when one is named, else rig's —
|
||||
# reduced to a DNS label kind accepts as a cluster name.
|
||||
default_cluster_name() {
|
||||
local n
|
||||
n=$(basename "$(cd .. && pwd)")
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
n=$(basename "$(_abs_from_ctrl "$OVERLAY_DIR")")
|
||||
else
|
||||
n=$(basename "$(cd .. && pwd)")
|
||||
fi
|
||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
||||
echo "${n:-rig}"
|
||||
@@ -45,15 +60,59 @@ load_config() {
|
||||
_config_restore "$saved"
|
||||
|
||||
# A profile is optional; naming one that does not exist is an error.
|
||||
local profile="${PROFILE:-}"
|
||||
local profile="${PROFILE:-}" layered=""
|
||||
if [ -n "$profile" ] && [ "$profile" != default ]; then
|
||||
if [ ! -f "./env.d/${profile}.env" ]; then
|
||||
echo "no such profile: env.d/${profile}.env" >&2
|
||||
if [ -d "../examples/${profile}" ]; then
|
||||
echo " it is an example overlay now: OVERLAY=examples/${profile}" >&2
|
||||
fi
|
||||
echo "available: $(config_profiles | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
source "./env.d/${profile}.env"
|
||||
set +a
|
||||
layered=1
|
||||
fi
|
||||
|
||||
# The overlay: one folder, outside rig, holding a use case (docs/notes/overlay.md).
|
||||
# Named ones must exist; with none named, rig's own example is used if present.
|
||||
OVERLAY_DIR=""
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
OVERLAY_DIR="${OVERLAY%/}"
|
||||
if [ ! -d "$(_from_ctrl "$OVERLAY_DIR")" ]; then
|
||||
echo "no overlay at OVERLAY=${OVERLAY} (relative to rig's folder, or absolute)" >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ -d "../${DEFAULT_OVERLAY}" ]; then
|
||||
OVERLAY_DIR="$DEFAULT_OVERLAY"
|
||||
fi
|
||||
# Its rig.env may not choose the profile or the overlay (both are chosen before
|
||||
# it loads), and the paths it sets are relative to the overlay.
|
||||
local ov_env="" m_before k_before
|
||||
if [ -n "$OVERLAY_DIR" ]; then ov_env="$(_from_ctrl "$OVERLAY_DIR")/rig.env"; fi
|
||||
if [ -n "$ov_env" ] && [ -f "$ov_env" ]; then
|
||||
if grep -qE '^[[:space:]]*(export[[:space:]]+)?(PROFILE|OVERLAY)=' "$ov_env"; then
|
||||
echo "$ov_env: an overlay's rig.env cannot set PROFILE or OVERLAY (they choose it)" >&2
|
||||
exit 1
|
||||
fi
|
||||
m_before="${MANIFESTS_DIR-}" k_before="${KIND_CONFIG-}"
|
||||
set -a
|
||||
source "$ov_env"
|
||||
set +a
|
||||
if [ "${MANIFESTS_DIR-}" != "$m_before" ]; then
|
||||
case "$MANIFESTS_DIR" in /*|none|"") ;; *) MANIFESTS_DIR="${OVERLAY_DIR}/${MANIFESTS_DIR}" ;; esac
|
||||
fi
|
||||
if [ "${KIND_CONFIG-}" != "$k_before" ]; then
|
||||
case "$KIND_CONFIG" in /*|"") ;; *) KIND_CONFIG="$(dirname "$ov_env")/${KIND_CONFIG}" ;; esac
|
||||
fi
|
||||
layered=1
|
||||
fi
|
||||
|
||||
# The machine and the caller still win over both.
|
||||
if [ -n "$layered" ]; then
|
||||
set -a
|
||||
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
||||
set +a
|
||||
_config_restore "$saved"
|
||||
@@ -86,9 +145,25 @@ load_config() {
|
||||
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
||||
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
||||
|
||||
# Where the workload's manifests live, repo-root relative; always resolved.
|
||||
# See k8s/README.md.
|
||||
MANIFESTS_DIR="${MANIFESTS_DIR:-ctrl/k8s/overlays/dev}"
|
||||
# Where the workload's manifests live, relative to rig's folder (or absolute):
|
||||
# the overlay's k8s/overlays/dev unless something names another. `none`: rig
|
||||
# applies none (the overlay's Tiltfile does). A named folder must exist.
|
||||
if [ "${MANIFESTS_DIR:-}" = ctrl/k8s/overlays/dev ] && [ ! -d ../ctrl/k8s/overlays/dev ]; then
|
||||
# The old default, pinned by an older .env.example; rig's examples moved.
|
||||
STALE_MANIFESTS_DIR="$MANIFESTS_DIR"
|
||||
MANIFESTS_DIR=""
|
||||
fi
|
||||
if [ -z "${MANIFESTS_DIR:-}" ] && [ -n "$OVERLAY_DIR" ] \
|
||||
&& [ -d "$(_from_ctrl "$OVERLAY_DIR")/k8s/overlays/dev" ]; then
|
||||
MANIFESTS_DIR="$OVERLAY_DIR/k8s/overlays/dev"
|
||||
fi
|
||||
MANIFESTS_DIR="${MANIFESTS_DIR:-}"
|
||||
if [ "$MANIFESTS_DIR" = none ]; then
|
||||
MANIFESTS_DIR=""
|
||||
elif [ -n "$MANIFESTS_DIR" ] && [ ! -d "$(_from_ctrl "$MANIFESTS_DIR")" ]; then
|
||||
echo "no manifests at MANIFESTS_DIR=${MANIFESTS_DIR} (relative to rig's folder, or absolute)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
||||
local var="NODE_IMAGE_${K8S_VERSION}"
|
||||
@@ -98,9 +173,13 @@ load_config() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The cluster is one file: k8s/kind-config.yaml.tpl. To change it, edit it.
|
||||
# KIND_CONFIG is only "use this file instead", for a project that builds its
|
||||
# own cluster through rig (a path relative to ctrl/, or absolute).
|
||||
# The cluster is one file: the overlay's kind-config.yaml.tpl if it has one,
|
||||
# else rig's k8s/kind-config.yaml.tpl. KIND_CONFIG is "use this file instead",
|
||||
# for a project that builds its own cluster through rig (relative to ctrl/, or absolute).
|
||||
if [ -z "${KIND_CONFIG:-}" ] && [ -n "$OVERLAY_DIR" ] \
|
||||
&& [ -f "$(_from_ctrl "$OVERLAY_DIR")/kind-config.yaml.tpl" ]; then
|
||||
KIND_CONFIG="$(_from_ctrl "$OVERLAY_DIR")/kind-config.yaml.tpl"
|
||||
fi
|
||||
KIND_CONFIG="${KIND_CONFIG:-./k8s/kind-config.yaml.tpl}"
|
||||
if [ ! -f "$KIND_CONFIG" ]; then
|
||||
echo "no kind config at KIND_CONFIG=${KIND_CONFIG}" >&2
|
||||
@@ -117,13 +196,15 @@ load_config() {
|
||||
}
|
||||
|
||||
# Render the kind config to stdout with sed (not envsubst) over an explicit variable list.
|
||||
# HOST_WORKDIR must be a host path: the host dockerd resolves hostPath entries.
|
||||
# HOST_WORKDIR and OVERLAY_DIR must be host paths: the host dockerd resolves hostPath entries.
|
||||
render_kind_config() {
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}" overlay_dir=""
|
||||
if [ -n "$OVERLAY_DIR" ]; then overlay_dir=$(_abs_from_ctrl "$OVERLAY_DIR"); fi
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" \
|
||||
-e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" \
|
||||
-e "s|\${HTTP_PORT}|${HTTP_PORT}|g" \
|
||||
-e "s|\${HOST_WORKDIR}|${host_workdir}|g" \
|
||||
-e "s|\${OVERLAY_DIR}|${overlay_dir}|g" \
|
||||
"$KIND_CONFIG"
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,19 @@ derive() {
|
||||
}
|
||||
|
||||
# Resolved facts for consumers outside bash, space-separated, positional:
|
||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||
# Read this, not `derive` (which ignores ctrl/.env).
|
||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
|
||||
# The two paths are absolute, or - when there is none. Read this, not `derive`.
|
||||
active() {
|
||||
load_config
|
||||
echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $MANIFESTS_DIR"
|
||||
local m="-" o="-"
|
||||
if [ -n "$MANIFESTS_DIR" ]; then m=$(_abs_from_ctrl "$MANIFESTS_DIR"); fi
|
||||
if [ -n "$OVERLAY_DIR" ]; then o=$(_abs_from_ctrl "$OVERLAY_DIR"); fi
|
||||
case "$m$o" in
|
||||
*[[:space:]]*)
|
||||
echo "a path here holds whitespace, and these facts are split on spaces: $m $o" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $m $o"
|
||||
}
|
||||
|
||||
show() {
|
||||
@@ -55,6 +63,13 @@ _row() {
|
||||
# rewritten — an override stays an override.
|
||||
persist() {
|
||||
derive
|
||||
# ctrl/.env belongs to this rig, not to an overlay: a pin written now would
|
||||
# follow every overlay this rig later runs, and two of them would collide.
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
echo "not pinning: OVERLAY is set, and ctrl/.env would carry this block to every overlay" >&2
|
||||
echo " its ports stay derived from its folder name ($CLUSTER): $DERIVED_HTTP-$DERIVED_REGISTRY" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -f ./.env ] || cp ./.env.example ./.env
|
||||
|
||||
local wrote=0 key val
|
||||
|
||||
@@ -23,6 +23,17 @@ check() { # name, expected, actual
|
||||
|
||||
note() { printf '\n%s\n' "$1"; }
|
||||
|
||||
# A scratch copy of rig for a check to change freely. local/ (overlays, possibly
|
||||
# someone else's) and def/ (scratch) never ride along, and neither do this
|
||||
# machine's PROFILE/OVERLAY/CLUSTER choices: a check sets what it tests.
|
||||
copy_rig() { # dest-dir
|
||||
mkdir -p "$1"
|
||||
tar -C .. --exclude=./local --exclude=./def -cf - . | tar -C "$1" -xf -
|
||||
if [ -f "$1/ctrl/.env" ]; then
|
||||
sed -i '/^PROFILE=/d; /^OVERLAY=/d; /^CLUSTER=/d; /^MANIFESTS_DIR=/d' "$1/ctrl/.env"
|
||||
fi
|
||||
}
|
||||
|
||||
# Resolve one key the way every rig script does, in a clean shell so the
|
||||
# caller's exported value is the only thing in play.
|
||||
resolved() {
|
||||
@@ -33,12 +44,12 @@ resolved() {
|
||||
note "rig needs no profile"
|
||||
# No env.d/ must still resolve and generate a kit; an unknown profile stays an error.
|
||||
NP="$(mktemp -d)"
|
||||
cp -r .. "$NP/rig"; rm -rf "$NP/rig/ctrl/env.d"; sed -i '/^PROFILE=/d' "$NP/rig/ctrl/.env" 2>/dev/null
|
||||
copy_rig "$NP/rig"; rm -rf "$NP/rig/ctrl/env.d"
|
||||
check "no env.d: config resolves" "default" \
|
||||
"$(cd "$NP/rig/ctrl" && bash -c 'source ./lib/config.sh; load_config >/dev/null && echo "$PROFILE_NAME"' 2>&1)"
|
||||
check "no env.d: the k8s version comes from the pins" "yes" \
|
||||
"$(cd "$NP/rig/ctrl" && bash -c 'source ./lib/config.sh; load_config >/dev/null && [ -n "$NODE_IMAGE" ] && echo yes' 2>&1)"
|
||||
check "no env.d: ports.sh active works" "7" \
|
||||
check "no env.d: ports.sh active works" "8" \
|
||||
"$(cd "$NP/rig/ctrl" && bash ports.sh active 2>/dev/null | wc -w)"
|
||||
check "no env.d: a kit is generated for the defaults" "yes" \
|
||||
"$( (cd "$NP/rig/ctrl" && rm -rf ../standalone/*/ && bash standalone.sh write >/dev/null 2>&1) && [ -f "$NP/rig/standalone/default/rigdeps.sh" ] && echo yes || echo no)"
|
||||
@@ -50,13 +61,14 @@ rm -rf "$NP"
|
||||
note "the ports.sh active contract"
|
||||
# ports.sh active is read positionally by the Makefile and Tiltfile: pin field count and order.
|
||||
FACTS="$(bash ports.sh active)"
|
||||
check "active: exactly 7 fields" "7" "$(printf '%s' "$FACTS" | wc -w)"
|
||||
read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS <<< "$FACTS"
|
||||
check "active: exactly 8 fields" "8" "$(printf '%s' "$FACTS" | wc -w)"
|
||||
read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS F_OVERLAY <<< "$FACTS"
|
||||
check "active: field 2 is kind-<cluster>" "kind-$F_CLUSTER" "$F_CTX"
|
||||
check "active: fields 3-6 are numeric" "yes" \
|
||||
"$([[ "$F_HTTP$F_HTTPS$F_TILT$F_REG" =~ ^[0-9]+$ ]] && echo yes || echo no)"
|
||||
check "active: field 7 is a path" "yes" \
|
||||
"$([ -n "$F_MANIFESTS" ] && [ "${F_MANIFESTS#-}" = "$F_MANIFESTS" ] && echo yes || echo no)"
|
||||
# Absolute, or - when there is none: an empty field would shift every later one.
|
||||
check "active: fields 7-8 are absolute paths or -" "yes" \
|
||||
"$(for f in "$F_MANIFESTS" "$F_OVERLAY"; do case "$f" in -|/*) ;; *) echo no; exit; esac; done; echo yes)"
|
||||
# derive answers a different question and must keep its own shape: it reports
|
||||
# what the directory name implies, ignoring ctrl/.env, so nothing should
|
||||
# configure itself from it.
|
||||
@@ -76,7 +88,9 @@ test_value() {
|
||||
KIND_CONFIG) echo "$PWD/k8s/kind-config.yaml.tpl" ;;
|
||||
*_PORT) echo "19999" ;;
|
||||
CLUSTER) echo "selftest-name" ;;
|
||||
MANIFESTS_DIR) echo "../elsewhere/overlays/dev" ;;
|
||||
# Named folders must exist, and must not be the default.
|
||||
MANIFESTS_DIR) echo "examples/starter/k8s/base" ;;
|
||||
OVERLAY) echo "examples/data" ;;
|
||||
ADDONS) echo "metallb" ;;
|
||||
*) echo "selftest-sentinel" ;;
|
||||
esac
|
||||
@@ -110,8 +124,8 @@ trap 'rm -rf "$TMP"' EXIT
|
||||
mkdir -p "$TMP/My_Proj"
|
||||
cp -r . "$TMP/My_Proj/ctrl"
|
||||
# A pinned CLUSTER in .env would be an override, not a derivation, and this
|
||||
# check is about the derivation.
|
||||
sed -i '/^CLUSTER=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
||||
# check is about the derivation. (An OVERLAY would be another derivation.)
|
||||
sed -i '/^CLUSTER=/d; /^OVERLAY=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
||||
COPY="$(cd "$TMP/My_Proj/ctrl" && bash ports.sh active)"
|
||||
check "a dir named My_Proj derives a DNS label" "my-proj" "$(awk '{print $1}' <<< "$COPY")"
|
||||
check "and a context to match" "kind-my-proj" "$(awk '{print $2}' <<< "$COPY")"
|
||||
@@ -129,9 +143,88 @@ check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
||||
note "rig stays standalone"
|
||||
# rig must be copyable out of its host project: no references to the host.
|
||||
# The pattern is assembled from fragments so this file does not match itself.
|
||||
HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b')"
|
||||
# The host project's word for a backing service counts too: rig described its
|
||||
# workload addons with it until they left. local/ holds overlays, which may say anything.
|
||||
HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b' '|' 'cab' 'inet')"
|
||||
check "no host-project references" "0" \
|
||||
"$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def 2>/dev/null | wc -l)"
|
||||
"$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def --exclude-dir=local 2>/dev/null | wc -l)"
|
||||
|
||||
|
||||
note "what runs is an overlay; rig only reads it"
|
||||
# docs/notes/overlay.md. Every check runs in a scratch copy with its own overlay.
|
||||
OV="$TMP/overlay-proof"; copy_rig "$OV/rig"
|
||||
OVR="$OV/rig"
|
||||
mkdir -p "$OVR/local/My_Env/addons" "$OVR/local/My_Env/k8s/prod" "$OVR/ctrl/env.d"
|
||||
printf 'ADDONS="from-profile"\nDATA_NAMESPACE=from-profile\n' > "$OVR/ctrl/env.d/selftest.env"
|
||||
cat > "$OVR/local/My_Env/rig.env" <<'EOF'
|
||||
ADDONS="metallb"
|
||||
DATA_NAMESPACE=from-overlay
|
||||
MANIFESTS_DIR=k8s/prod
|
||||
SELFTEST_SENTINEL=selftest-overlay-sentinel
|
||||
EOF
|
||||
printf 'resources: []\n' > "$OVR/local/My_Env/k8s/prod/kustomization.yaml"
|
||||
printf '#!/usr/bin/env bash\necho "overlay-metallb from $PWD with ${RIG_CTRL:-no RIG_CTRL}"\n' \
|
||||
> "$OVR/local/My_Env/addons/metallb.sh"
|
||||
in_ov() { (cd "$OVR/ctrl" && "$@"); }
|
||||
ov_key() { # key [env assignments...]
|
||||
local k="$1"; shift
|
||||
in_ov env "$@" bash -c 'source ./lib/config.sh; load_config >/dev/null 2>&1; printf "%s" "${!1}"' _ "$k"
|
||||
}
|
||||
|
||||
# With nothing named, rig behaves as it did before overlays: same name, ports, addons, nodes.
|
||||
check "no overlay: the cluster, ports and addons of before" "rig kind-rig 20310 20311 20312 20313" \
|
||||
"$(in_ov bash ports.sh active | awk '{print $1, $2, $3, $4, $5, $6}')"
|
||||
check "no overlay: no addons, one node, rig's own kind config" "|1|./k8s/kind-config.yaml.tpl" \
|
||||
"$(ov_key ADDONS)|$(ov_key NODES)|$(ov_key KIND_CONFIG)"
|
||||
|
||||
# The overlay's rig.env sits between the profile and ctrl/.env; the caller beats all.
|
||||
check "rig.env beats the profile" "from-overlay" \
|
||||
"$(ov_key DATA_NAMESPACE PROFILE=selftest OVERLAY=local/My_Env)"
|
||||
echo 'DATA_NAMESPACE=from-dotenv' >> "$OVR/ctrl/.env"
|
||||
check "ctrl/.env beats rig.env" "from-dotenv" \
|
||||
"$(ov_key DATA_NAMESPACE PROFILE=selftest OVERLAY=local/My_Env)"
|
||||
sed -i '/^DATA_NAMESPACE=from-dotenv$/d' "$OVR/ctrl/.env"
|
||||
check "the caller beats rig.env" "from-caller" \
|
||||
"$(ov_key ADDONS OVERLAY=local/My_Env ADDONS=from-caller)"
|
||||
|
||||
# Identity follows the overlay's folder, so one rig serves several without collisions.
|
||||
check "identity follows the overlay's folder" "my-env kind-my-env" \
|
||||
"$(in_ov env OVERLAY=local/My_Env bash ports.sh active | awk '{print $1, $2}')"
|
||||
check "paths in rig.env are relative to the overlay" "$OVR/local/My_Env/k8s/prod" \
|
||||
"$(in_ov env OVERLAY=local/My_Env bash ports.sh active | awk '{print $7}')"
|
||||
check "a named overlay that does not exist is an error" "yes" \
|
||||
"$(in_ov env OVERLAY=local/nope bash ports.sh active >/dev/null 2>&1 && echo no || echo yes)"
|
||||
printf 'PROFILE=x\n' > "$OV/bad-rig.env"; mkdir -p "$OVR/local/bad"; cp "$OV/bad-rig.env" "$OVR/local/bad/rig.env"
|
||||
check "rig.env may not choose the profile or the overlay" "yes" \
|
||||
"$(in_ov env OVERLAY=local/bad bash ports.sh active >/dev/null 2>&1 && echo no || echo yes)"
|
||||
|
||||
# Addons: the overlay's is found before rig's own, and runs from rig's ctrl/.
|
||||
check "an overlay's addon comes before rig's of the same name" \
|
||||
"overlay-metallb from $OVR/ctrl with $OVR/ctrl" \
|
||||
"$(in_ov env OVERLAY=local/My_Env ADDONS=metallb bash addons.sh install 2>&1 | grep '^overlay-metallb')"
|
||||
|
||||
# ctrl/.env is this rig's: a pinned block would follow every overlay.
|
||||
check "ports.sh persist refuses while an overlay is set" "yes" \
|
||||
"$(in_ov env OVERLAY=local/My_Env bash ports.sh persist >/dev/null 2>&1 && echo no || echo yes)"
|
||||
|
||||
# make's $(shell) must see an OVERLAY given as a make argument (make < 4.4 does not pass it).
|
||||
check "make -n tilt OVERLAY=... asks for the overlay's context" "kind-data" \
|
||||
"$(cd .. && make --no-print-directory -n tilt OVERLAY=examples/data 2>/dev/null | grep -m1 'tilt ' | sed -n 's/.*--context \([^ ]*\).*/\1/p')"
|
||||
|
||||
# rig reads an overlay and never writes into it; its values never reach a committed kit.
|
||||
sum_ov() { (cd "$OVR/local/My_Env" && find . -type f | sort | xargs sha256sum | sha256sum); }
|
||||
before=$(sum_ov)
|
||||
echo 'OVERLAY=local/My_Env' >> "$OVR/ctrl/.env"
|
||||
in_ov bash ports.sh active >/dev/null 2>&1
|
||||
in_ov bash addons.sh list >/dev/null 2>&1
|
||||
in_ov bash -c 'source ./lib/config.sh; load_config >/dev/null; render_kind_config >/dev/null' 2>/dev/null
|
||||
in_ov bash standalone.sh write >/dev/null 2>&1
|
||||
in_ov bash standalone.sh export "$OV/export" >/dev/null 2>&1
|
||||
check "rig writes nothing into an overlay" "$before" "$(sum_ov)"
|
||||
check "an overlay's values never reach a committed kit" "0" \
|
||||
"$(grep -rlE 'selftest-overlay-sentinel|local/My_Env' "$OVR/standalone" 2>/dev/null | wc -l)"
|
||||
check "a committed kit holds no path of this machine" "0" \
|
||||
"$(grep -rlF "$OVR" "$OVR/standalone" 2>/dev/null | wc -l)"
|
||||
|
||||
|
||||
note "the Tiltfile hardcodes nothing"
|
||||
@@ -139,6 +232,7 @@ note "the Tiltfile hardcodes nothing"
|
||||
check "no literal kind-<name>" "0" "$(grep -cE "['\"]kind-[a-z0-9]" Tiltfile)"
|
||||
check "guards on the variable" "1" "$(grep -c 'allow_k8s_contexts(CTX)' Tiltfile)"
|
||||
check "asks ports.sh for facts" "1" "$(grep -c "local('bash ports.sh active'" Tiltfile)"
|
||||
check "hands over to the overlay's Tiltfile" "1" "$(grep -c "include(OVERLAY + '/Tiltfile')" Tiltfile)"
|
||||
|
||||
|
||||
note "standalone kits are generated, current, and call only real verbs"
|
||||
@@ -168,7 +262,8 @@ check "there is a kit for every profile" "$(config_profiles | wc -l)" "$kits"
|
||||
|
||||
# An export carries this machine's choices but never its credentials; committed kits carry neither.
|
||||
# Proven with sentinel values in a scratch copy, since the real ctrl/.env may leave them empty.
|
||||
SX="$TMP/export-proof"; mkdir -p "$SX"; cp -r .. "$SX/rig"
|
||||
SX="$TMP/export-proof"; copy_rig "$SX/rig"
|
||||
mkdir -p "$SX/selftest-sentinel-choice/overlays/dev" # a named MANIFESTS_DIR must exist
|
||||
cat >> "$SX/rig/ctrl/.env" <<'EOF'
|
||||
REGISTRY_USER=selftest-sentinel-user
|
||||
REGISTRY_PASSWORD=selftest-sentinel-password
|
||||
@@ -187,16 +282,87 @@ check "export: refuses to write inside the repository" "yes" \
|
||||
"$( (bash standalone.sh export ../standalone/selftest-mine >/dev/null 2>&1) && echo no || echo yes)"
|
||||
|
||||
|
||||
note "optional — needs tilt and this rig's cluster"
|
||||
# Tilt needs a cluster context to parse the Tiltfile, so this is skipped without one.
|
||||
if ! command -v tilt >/dev/null; then
|
||||
printf ' skip tilt is not installed\n'
|
||||
elif ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$F_CTX"; then
|
||||
printf " skip no %s context — run 'make cluster up' to include this\n" "$F_CTX"
|
||||
note "rig's addons apply verified files, never URLs"
|
||||
# The offline profile must need no network for manifests: each addon asks deps.sh for a
|
||||
# pinned manifest, verified on disk (versions.md). Their images still need preloading.
|
||||
check "no rig addon applies a URL" "0" \
|
||||
"$(cat addons/*.sh | grep -cE 'apply -f "?https?://')"
|
||||
check "every manifest an addon asks for is pinned with a sum" "" \
|
||||
"$(for n in $(grep -ohE 'deps\.sh manifest [A-Z_]+' addons/*.sh | awk '{print $3}' | sort -u); do
|
||||
grep -q "^${n}_MANIFEST_URL=" versions.env && grep -q "^${n}_MANIFEST_SHA256=[0-9a-f]\{64\}$" versions.env \
|
||||
|| printf '%s ' "$n"; done)"
|
||||
|
||||
|
||||
note "withdrawn stays withdrawn (STALE.md)"
|
||||
# One check per entry; the reasoning is in STALE.md, not here.
|
||||
check "✖ S1 rig's Tiltfile has no Images section of its own" "0" "$(grep -c '^# ── Images' Tiltfile)"
|
||||
check "✖ S2 local/ is where overlays live, and ignored" "yes" \
|
||||
"$(grep -qx '/local/' ../.gitignore && echo yes || echo no)"
|
||||
# Patterns assembled from fragments so this file does not match itself.
|
||||
COPIES_PAT="$(printf '%s' 'ac' 'me-rig|ac' 'mebank')"
|
||||
HOUSE_PAT="$(printf '%s' 'semes' 'ter|local' '\.ar\b')"
|
||||
check "✖ S2 no example environment name from the copies era" "0" \
|
||||
"$(cd .. && grep -rIlE "$COPIES_PAT" . --exclude-dir=def --exclude-dir=local --exclude=STALE.md 2>/dev/null | wc -l)"
|
||||
check "✖ S3 ctrl/addons makes the cluster work, nothing more" "cert-manager metallb metrics-server" \
|
||||
"$(ls addons/ | sed 's/\.sh$//' | sort | xargs)"
|
||||
check "✖ S3 versions.env pins no workload image" "0" \
|
||||
"$(grep -cE '^(POSTGRES|REDIS|AIRFLOW)_IMAGE=' versions.env)"
|
||||
check "✖ S4 no namespace named after the cluster" "0" "$(grep -c "CLUSTER + ':namespace'" Tiltfile)"
|
||||
check "✖ S5 rig's examples left ctrl/k8s" "no" "$([ -d k8s/overlays ] && echo yes || echo no)"
|
||||
check "✖ S5 .env.example does not pin MANIFESTS_DIR" "0" "$(grep -c '^MANIFESTS_DIR=' .env.example)"
|
||||
check "✖ S6 no client or data example profile" "0" \
|
||||
"$(ls env.d/ | grep -cE '^(client|data)\.')"
|
||||
check "✖ S7 no house path or host name in rig" "0" \
|
||||
"$(cd .. && grep -rIlE "$HOUSE_PAT" . --exclude-dir=def --exclude-dir=local --exclude=STALE.md 2>/dev/null | wc -l)"
|
||||
|
||||
|
||||
note "the dev loop parses — needs tilt and kubectl, not a cluster"
|
||||
# A throwaway kubeconfig with kind-named entries (Tilt trusts kind contexts) and a
|
||||
# kubectl that swallows `apply`: the Tiltfile evaluates for real, nothing is contacted.
|
||||
# The second run is a copy under another name, the case that once failed at load.
|
||||
if ! command -v tilt >/dev/null || ! command -v kubectl >/dev/null; then
|
||||
printf ' skip tilt or kubectl is not installed\n'
|
||||
else
|
||||
FK="$TMP/fake-kube"; mkdir -p "$FK"
|
||||
real_kubectl=$(command -v kubectl)
|
||||
printf '#!/usr/bin/env bash\nfor a in "$@"; do [ "$a" = apply ] && { cat >/dev/null; exit 0; }; done\nexec %q "$@"\n' \
|
||||
"$real_kubectl" > "$FK/kubectl"
|
||||
chmod +x "$FK/kubectl"
|
||||
parses() { # cluster-name [env...] -> the manifests Tilt would deploy, or the error
|
||||
local name="$1"; shift
|
||||
cat > "$FK/kubeconfig" <<EOF
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters: [{name: kind-$name, cluster: {server: "https://127.0.0.1:9"}}]
|
||||
contexts: [{name: kind-$name, context: {cluster: kind-$name, user: kind-$name}}]
|
||||
users: [{name: kind-$name, user: {token: selftest}}]
|
||||
current-context: kind-$name
|
||||
EOF
|
||||
env "$@" KUBECONFIG="$FK/kubeconfig" PATH="$FK:$PATH" \
|
||||
timeout 120 tilt alpha tiltfile-result --context "kind-$name" > "$FK/out.json" 2> "$FK/err" \
|
||||
&& grep -o '"Name": *"[^"]*"' "$FK/out.json" | sed 's/.*"\([^"]*\)"$/\1/' | sort -u | xargs \
|
||||
|| grep -m1 -iE 'error|no object' "$FK/err"
|
||||
}
|
||||
check "the starter overlay parses" "example-service infra uncategorized" "$(parses rig)"
|
||||
check "and under another name" "example-service infra uncategorized" \
|
||||
"$(parses selftest-copy CLUSTER=selftest-copy)"
|
||||
check "the data overlay parses" "items-api uncategorized" "$(parses data OVERLAY=examples/data)"
|
||||
fi
|
||||
|
||||
|
||||
note "the examples are overlays that work as shipped"
|
||||
# They are what a real overlay is copied from, so they must at least parse.
|
||||
bad=""
|
||||
for f in ../examples/*/addons/*.sh; do [ -e "$f" ] && { bash -n "$f" 2>/dev/null || bad+="$f "; }; done
|
||||
check "every example addon parses" "" "$bad"
|
||||
if command -v python3 >/dev/null; then
|
||||
bad=""
|
||||
for f in ../examples/*/dags/*.py; do
|
||||
[ -e "$f" ] && { python3 -c 'import ast, sys; ast.parse(open(sys.argv[1]).read())' "$f" 2>/dev/null || bad+="$f "; }
|
||||
done
|
||||
check "every example DAG parses" "" "$bad"
|
||||
else
|
||||
out="$(tilt alpha tiltfile-result --context "$F_CTX" 2>&1)"
|
||||
check "Tiltfile evaluates" "yes" \
|
||||
"$(printf '%s' "$out" | grep -q '"Manifests"' && echo yes || echo "no: $(printf '%s' "$out" | tail -1)")"
|
||||
printf ' skip python3 is not installed\n'
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -30,25 +30,28 @@ COMPOSE_VERSION=5.5.1
|
||||
COMPOSE_SHA256=db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576
|
||||
COMPOSE_URL=https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64
|
||||
|
||||
# Node images for KIND_VERSION, pinned by digest; profiles pick one via K8S_VERSION.
|
||||
# Older entries are kept deliberately (legacy-estate simulation).
|
||||
# Node images for KIND_VERSION, pinned by digest; K8S_VERSION picks one (default: the newest).
|
||||
# Older entries are kept deliberately, for targets that run an older Kubernetes.
|
||||
NODE_IMAGE_v1_36=kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5
|
||||
NODE_IMAGE_v1_35=kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||
NODE_IMAGE_v1_34=kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256
|
||||
NODE_IMAGE_v1_33=kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4
|
||||
|
||||
# Images pulled at runtime (registry, mocks). Pinned by tag; the registry mode
|
||||
# decides where they are pulled FROM.
|
||||
# Images pulled at runtime. Pinned by tag; the registry mode decides where they are pulled FROM.
|
||||
REGISTRY_IMAGE=registry:2
|
||||
STUB_IMAGE=python:3.12-slim
|
||||
|
||||
# Addons, installed by ctrl/addons/<name>.sh when listed in a profile's ADDONS.
|
||||
# rig's own addons (ctrl/addons/<name>.sh), installed when ADDONS names them.
|
||||
CERT_MANAGER_VERSION=v1.21.1
|
||||
METRICS_SERVER_VERSION=v0.9.0
|
||||
METALLB_VERSION=v0.16.0
|
||||
|
||||
# Cabinets — unmodified upstream images, usable on compose or in the cluster.
|
||||
# Pinned by tag; bump freely, and preload them for the offline profile.
|
||||
POSTGRES_IMAGE=postgres:16-alpine
|
||||
REDIS_IMAGE=redis:7-alpine
|
||||
AIRFLOW_IMAGE=apache/airflow:2.10.4
|
||||
# The manifests those addons apply, fetched and verified like the binaries
|
||||
# (`deps.sh manifest <NAME>`), so an offline machine needs no network for them.
|
||||
# Sums from the release's own asset digest; metallb publishes none, see versions.md.
|
||||
CERT_MANAGER_MANIFEST_URL=https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml
|
||||
CERT_MANAGER_MANIFEST_SHA256=5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408
|
||||
METRICS_SERVER_MANIFEST_URL=https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml
|
||||
METRICS_SERVER_MANIFEST_SHA256=1cec29a5267809306a2c6ec74a3e449abbb705b4a8beed0c8a1963910f72c79b
|
||||
METALLB_MANIFEST_URL=https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml
|
||||
METALLB_MANIFEST_SHA256=b0b9be2802f10aa32d45308b4457d06cde0c70544712c8d0cf5511657ffd2b69
|
||||
METALLB_MANIFEST_GIT_BLOB=7fbda334cc3ac0aaabdcb081af4f543feb3c2f9f
|
||||
|
||||
Reference in New Issue
Block a user