Files
soleprint/rig/ctrl/lib/config.sh
2026-09-22 05:15:49 -03:00

306 lines
12 KiB
Bash

# Shared config loading: how the config layers compose. Sourced, never executed.
# 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 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"
# 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
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}"
}
# Base of this environment's 10-port block; cksum so it is the same on every machine.
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 .env (set by config_snapshot for kits).
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"
# A profile is optional; naming one that does not exist is an error.
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"
fi
# The defaults a profile would otherwise have to supply. Weakest of all: a
# profile, ctrl/.env and the caller each override them.
PROFILE_NAME="${PROFILE_NAME:-default}"
ADDONS="${ADDONS-}"
# local, not none: with no registry an unqualified image name means
# docker.io/library/<name>, and a default must not make that disclosure.
REGISTRY_MODE="${REGISTRY_MODE:-local}"
INGRESS_MODE="${INGRESS_MODE:-hostport}"
DNS_MODE="${DNS_MODE:-hosts}"
# The newest node image versions.env pins, found rather than restated, so
# bumping the pins moves the default with them.
if [ -z "${K8S_VERSION:-}" ]; then
K8S_VERSION=$(compgen -v NODE_IMAGE_v | sort -V | tail -1)
K8S_VERSION="${K8S_VERSION#NODE_IMAGE_}"
fi
# Identity follows the folder, so a renamed copy is a distinct environment.
CLUSTER="${CLUSTER:-$(default_cluster_name)}"
KUBECONTEXT="kind-${CLUSTER}"
# Host ports: fill only the gaps from the derived block; anything already set wins.
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, 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}"
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 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
exit 1
fi
# Read the node count back out of the file rather than restating it:
# check.sh and the memory tool size their budget on NODES.
NODES=$(grep -c '^ - role:' "$KIND_CONFIG")
# Measured MB per node (cluster alone, errs high for workers); shared by
# check.sh, the memory tool and standalone kits.
NODE_MB=800
}
# Render the kind config to stdout with sed (not envsubst) over an explicit variable list.
# 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)}" 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"
}
_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 ────────────────────────────────────
# The questions ctrl/standalone.sh asks, so it never knows how config is stored.
# Every configuration rig can run as, one per line: each profile, or `default`
# when there are none. Never empty.
config_profiles() {
local f found=""
for f in ./env.d/*.env; do
[ -e "$f" ] || continue
f=${f##*/}; echo "${f%.env}"; found=1
done
[ -n "$found" ] || echo default
}
# What load_config sets, minus the machine-local layer, as `declare -p` lines.
# Usage: config_snapshot <profile> | --current (found by difference, not a list)
config_snapshot() {
local _rig_snap_choices
if [ "$1" = --current ]; then
_rig_snap_choices=$( (
load_config >/dev/null || exit 1
for _rig_snap_n in $CONFIG_OVERRIDABLE; do
if [ -n "${!_rig_snap_n+x}" ]; then printf 'export %s=%q\n' "$_rig_snap_n" "${!_rig_snap_n}"; fi
done
) ) || return 1
else
_rig_snap_choices="export PROFILE=$(printf '%q' "$1")"
fi
(
# 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
eval "$_rig_snap_choices"
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
)
}
# The profile this machine runs, as load_config resolves it here.
config_current_profile() { ( load_config >/dev/null && echo "$PROFILE_NAME" ); }
# Names (never values) of .env keys an export does not carry, e.g. credentials.
config_left_out() {
[ -f ./.env ] || return 0
local k
for k in $(sed -nE 's/^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=.*/\2/p' ./.env | sort -u); do
case " $(echo $CONFIG_OVERRIDABLE) " in
*" $k "*) ;;
*) echo "$k" ;;
esac
done
}
# Print a load_config with a resolution frozen in, for a standalone kit to carry.
# The caller's env still wins; derived values (e.g. ports) stay fixed.
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
}