259 lines
11 KiB
Bash
259 lines
11 KiB
Bash
# Shared config loading. Sourced, never executed.
|
|
#
|
|
# The ecosystem convention is that scripts are standalone with no shared log
|
|
# library — that still holds. This file is not a logging lib; it is the single
|
|
# definition of how the config layers compose, which every script has to agree
|
|
# on exactly. Precedence, weakest first:
|
|
#
|
|
# ctrl/versions.env pinned toolchain + image digests (committed)
|
|
# ctrl/env.d/<profile> cluster shape (committed)
|
|
# ctrl/.env machine-local values and secrets (gitignored)
|
|
# the caller's env `make cluster up PROFILE=client` (always wins)
|
|
#
|
|
# That last rule is why this is more than a few `source` lines: .env sets
|
|
# PROFILE, so without snapshotting it would silently override the PROFILE the
|
|
# user just typed on the command line.
|
|
#
|
|
# Run from ctrl/.
|
|
|
|
# Values a user can reasonably override per-invocation. Anything set in the
|
|
# environment when load_config runs is restored after the files are read.
|
|
# NODES and AUDIT are deliberately NOT here: they are properties of the chosen
|
|
# ctrl/k8s/kind-config*.yaml.tpl and are read back out of it below, so there is
|
|
# one place that decides the shape of the cluster rather than two that can drift.
|
|
#
|
|
# REGISTRY_PORT and MANIFESTS_DIR were missing here while ctrl/.env set them, so
|
|
# the caller's env silently LOST to the file for those two — the one precedence
|
|
# rule this header states. Both are now listed; the other twelve are unchanged.
|
|
CONFIG_OVERRIDABLE="PROFILE 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 containing folder's name, reduced to something kind accepts as a cluster
|
|
# name (a DNS label: lowercase alphanumerics and dashes). Run from ctrl/, so the
|
|
# repo root is the parent.
|
|
default_cluster_name() {
|
|
local n
|
|
n=$(basename "$(cd .. && pwd)")
|
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
|
echo "${n:-rig}"
|
|
}
|
|
|
|
# Base of this environment's 10-port block. cksum is used rather than $RANDOM or
|
|
# bash hashing because it is POSIX and returns the same value on every machine,
|
|
# which is what makes the block reproducible instead of merely unique.
|
|
derive_port_base() {
|
|
local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}')
|
|
echo $((20000 + (h % 200) * 10))
|
|
}
|
|
|
|
load_config() {
|
|
local k saved=""
|
|
for k in $CONFIG_OVERRIDABLE; do
|
|
# ${!k+x} distinguishes "set but empty" from "unset" — an explicit
|
|
# FOO= on the command line is a real choice and must survive.
|
|
if [ -n "${!k+x}" ]; then
|
|
saved+="$k=$(printf '%q' "${!k}")"$'\n'
|
|
fi
|
|
done
|
|
|
|
set -a
|
|
source ./versions.env
|
|
# RIG_PORTABLE skips the machine-local layer. config_snapshot sets it, so a
|
|
# generated standalone kit never carries this machine's .env — which holds
|
|
# local values and, by its own description, secrets.
|
|
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
|
set +a
|
|
|
|
# Re-apply overrides now so PROFILE is the caller's before we pick the file.
|
|
_config_restore "$saved"
|
|
|
|
local profile="${PROFILE:-minimal}"
|
|
if [ ! -f "./env.d/${profile}.env" ]; then
|
|
echo "no such profile: env.d/${profile}.env" >&2
|
|
echo "available: $(ls env.d/*.env 2>/dev/null | xargs -n1 basename | sed 's/\.env$//' | tr '\n' ' ')" >&2
|
|
exit 1
|
|
fi
|
|
|
|
set -a
|
|
source "./env.d/${profile}.env"
|
|
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
|
set +a
|
|
|
|
_config_restore "$saved"
|
|
|
|
# Identity follows the FOLDER, so copying this directory somewhere else and
|
|
# renaming it yields a distinct environment with no further edits. Without
|
|
# this, two copies would share one cluster and `make cluster down` in either
|
|
# would destroy the other's.
|
|
CLUSTER="${CLUSTER:-$(default_cluster_name)}"
|
|
KUBECONTEXT="kind-${CLUSTER}"
|
|
|
|
# Host ports are a single shared namespace, so unlike the cluster name they
|
|
# cannot just follow the directory — they have to be spread out. Anything
|
|
# already set (ctrl/.env, a profile, the command line) wins; only the gaps
|
|
# are filled. See ports.sh for the reasoning.
|
|
local base; base=$(derive_port_base "$CLUSTER")
|
|
HTTP_PORT="${HTTP_PORT:-$base}"
|
|
HTTPS_PORT="${HTTPS_PORT:-$((base + 1))}"
|
|
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
|
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
|
|
|
# Where the workload's manifests live, repo-root relative. Defaulted here so
|
|
# it is always resolved rather than sometimes-set: it is the seam that lets
|
|
# the real manifests be versioned away from the installer, and a consumer
|
|
# should not have to know whether anyone filled it in. See k8s/README.md.
|
|
MANIFESTS_DIR="${MANIFESTS_DIR:-ctrl/k8s/overlays/dev}"
|
|
|
|
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
|
local var="NODE_IMAGE_${K8S_VERSION}"
|
|
NODE_IMAGE="${!var:-}"
|
|
if [ -z "$NODE_IMAGE" ]; then
|
|
echo "K8S_VERSION='${K8S_VERSION}' has no NODE_IMAGE_${K8S_VERSION} in versions.env" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The cluster's shape is a file in ctrl/k8s/, named by the profile. Adding a
|
|
# shape is adding a file; there is no dispatcher to edit.
|
|
#
|
|
# A host that needs its own shape — extra port mappings, more nodes — passes
|
|
# an absolute path instead, and rig renders it exactly like one of its own:
|
|
# ${CLUSTER} and ${NODE_IMAGE} are substituted either way. The shape stays in
|
|
# the host's tree, because what a host's cluster needs is the host's business;
|
|
# rig only knows how to build whatever it is handed.
|
|
KIND_CONFIG="${KIND_CONFIG:-kind-config.yaml.tpl}"
|
|
case "$KIND_CONFIG" in
|
|
/*) KIND_CONFIG_PATH="$KIND_CONFIG"; KIND_CONFIG_SHOWN="$KIND_CONFIG" ;;
|
|
*) KIND_CONFIG_PATH="./k8s/${KIND_CONFIG}"; KIND_CONFIG_SHOWN="ctrl/k8s/${KIND_CONFIG}" ;;
|
|
esac
|
|
if [ ! -f "$KIND_CONFIG_PATH" ]; then
|
|
echo "no such cluster shape: ${KIND_CONFIG_SHOWN}" >&2
|
|
echo "rig's own: $(ls k8s/kind-config*.yaml.tpl 2>/dev/null | xargs -n1 basename | tr '\n' ' ')" >&2
|
|
echo "or pass an absolute path to a shape of your own" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Read the shape back out of the YAML rather than trusting a profile to
|
|
# restate it. check.sh sizes the memory warning on NODES, and cluster.sh
|
|
# prints AUDIT before spending minutes building something that cannot be
|
|
# changed afterwards — both would mislead if the numbers drifted.
|
|
NODES=$(grep -c '^ - role:' "$KIND_CONFIG_PATH")
|
|
if grep -q 'audit-policy-file' "$KIND_CONFIG_PATH"; then AUDIT=on; else AUDIT=off; fi
|
|
|
|
# What one node costs, measured rather than guessed. On 2026-09-11 a minimal
|
|
# control-plane node ran at 620 MiB idle and ~728 MiB with a small mock, plus
|
|
# 16 MiB for the local registry — ~745 MiB of working set. 800 rounds that up,
|
|
# and agrees with the 800 MB observed independently on a larger rig. Worker
|
|
# nodes carry no etcd or apiserver and are lighter, so for a multi-node shape
|
|
# this errs high. It is the cluster alone: whatever you deploy comes on top.
|
|
#
|
|
# Here rather than in check.sh because the memory tool and every standalone
|
|
# kit need the same figure.
|
|
NODE_MB=800
|
|
}
|
|
|
|
# Render a cluster shape to stdout. sed rather than envsubst: envsubst is
|
|
# gettext-base, absent from a minimal Debian, and Docker is meant to be the only
|
|
# prerequisite. The variable list is explicit so a template cannot quietly start
|
|
# depending on something the caller does not set.
|
|
#
|
|
# hostPath entries are resolved by the HOST dockerd, so HOST_WORKDIR must stay a
|
|
# host path even when this runs inside the installer container.
|
|
render_kind_config() {
|
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"
|
|
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" \
|
|
"$KIND_CONFIG_PATH"
|
|
}
|
|
|
|
_config_restore() {
|
|
local line
|
|
while IFS= read -r line; do
|
|
if [ -n "$line" ]; then
|
|
eval "export $line"
|
|
fi
|
|
done <<< "$1"
|
|
# A while loop returns its last body command's status; the trailing empty
|
|
# line would otherwise make this return 1 and trip `set -e` in the caller.
|
|
return 0
|
|
}
|
|
|
|
# ── what a standalone kit needs to know ────────────────────────────────────
|
|
# Two questions the kit generator (ctrl/standalone.sh) asks, so that it never has
|
|
# to know how configuration is stored. Where profiles live, which files are
|
|
# layered and what is derived are this file's business and can change freely;
|
|
# the generator only calls these.
|
|
|
|
# Every profile rig can be run as, one per line.
|
|
config_profiles() {
|
|
local f
|
|
for f in ./env.d/*.env; do
|
|
[ -e "$f" ] || continue
|
|
f=${f##*/}; echo "${f%.env}"
|
|
done
|
|
}
|
|
|
|
# The resolved configuration for one profile, as `declare -p` lines — exactly
|
|
# what load_config leaves behind, minus the machine-local layer. A kit freezes
|
|
# this in place of load_config, so it carries rig's decisions for that profile
|
|
# and nothing about the machine it was generated on.
|
|
#
|
|
# Found by difference, not by a list: whatever load_config sets today, it sets.
|
|
# A list here would be one more place to forget a variable.
|
|
config_snapshot() {
|
|
local _rig_snap_profile="$1"
|
|
(
|
|
# Nothing from the caller's shell may leak into a kit.
|
|
for _rig_snap_n in $CONFIG_OVERRIDABLE; do unset "$_rig_snap_n"; done
|
|
declare -A _rig_snap_was=()
|
|
for _rig_snap_n in $(compgen -v); do
|
|
_rig_snap_was[$_rig_snap_n]="${!_rig_snap_n-}"
|
|
done
|
|
PROFILE="$_rig_snap_profile" RIG_PORTABLE=1 load_config >/dev/null
|
|
for _rig_snap_n in $(compgen -v); do
|
|
case "$_rig_snap_n" in
|
|
_rig_snap_*|RIG_PORTABLE|BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|\
|
|
SECONDS|EPOCH*|HISTCMD|COLUMNS|LINES|PWD|OLDPWD|_|SHLVL|OPTIND|OPTERR) continue ;;
|
|
esac
|
|
if [ -z "${_rig_snap_was[$_rig_snap_n]+x}" ] \
|
|
|| [ "${_rig_snap_was[$_rig_snap_n]}" != "${!_rig_snap_n-}" ]; then
|
|
declare -p "$_rig_snap_n"
|
|
fi
|
|
done
|
|
)
|
|
}
|
|
|
|
# A replacement for load_config with one profile's resolution frozen in, printed
|
|
# as a function definition for a standalone kit to carry. The generator embeds
|
|
# whatever this prints and interprets none of it, so what "frozen" means stays
|
|
# rig's decision.
|
|
#
|
|
# It keeps load_config's one stated rule: the caller's env wins for anything in
|
|
# CONFIG_OVERRIDABLE. A kit therefore behaves like rig — `OUT_BIN=... rigdeps.sh`
|
|
# still works — rather than like a copy with everything pinned.
|
|
#
|
|
# What freezing does give up, knowingly: values DERIVED from an overridable one
|
|
# are fixed at generation. Override CLUSTER and the ports stay the ones derived
|
|
# for the original name. Re-deriving would mean carrying the layering itself,
|
|
# which is exactly what a kit exists not to need.
|
|
config_freeze() {
|
|
local snap
|
|
snap=$(config_snapshot "$1") || return 1
|
|
cat <<'EOF'
|
|
load_config() {
|
|
local k saved=""
|
|
for k in $CONFIG_OVERRIDABLE; do
|
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
done
|
|
EOF
|
|
printf '%s\n' "$snap" | sed -E 's/^declare --* / declare -g /; s/^declare -([a-zA-Z]+) / declare -g\1 /'
|
|
cat <<'EOF'
|
|
_config_restore "$saved"
|
|
}
|
|
EOF
|
|
}
|