# Shared config loading: how the config layers compose. Sourced, never executed. # Precedence, weakest first: defaults < versions.env < env.d/ < .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 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. 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 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:-}" if [ -n "$profile" ] && [ "$profile" != default ]; then if [ ! -f "./env.d/${profile}.env" ]; then echo "no such profile: env.d/${profile}.env" >&2 echo "available: $(config_profiles | 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" 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/, 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, repo-root relative; always resolved. # 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 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). 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 must be a host path: the host dockerd resolves hostPath entries. 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" } _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 | --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 }