#!/usr/bin/env bash # Give each environment its own block of host ports. # # New versions of a system mean new clusters on ONE machine, not new machines. # Cluster name, kubectl context, registry container and image tag already derive # from the directory name, so two copies never collide there — but host ports are # a single shared namespace and would. # # The block is derived from the directory name: stateless, stable, and requiring # no coordination between copies that know nothing about each other. # # base = 20000 + (hash(slug) % 200) * 10 # +0 HTTP +1 HTTPS +2 TILT +3 REGISTRY (+4..9 reserved) # # 20000+ deliberately avoids the ports something is already likely to hold: 80, # 443, 3000, 5432, 8000, 8080. # # Derivation is a default, not a decision. On first use the resolved block is # written into ctrl/.env, so it becomes pinned, visible and editable rather than # a number that appears from nowhere. Anything already in ctrl/.env wins. # # Usage: ports.sh show | active | derive | persist set -euo pipefail cd "$(dirname "$0")" source ./lib/config.sh # derive_port_base lives in lib/config.sh so every script resolves the same block # without going through this one. derive_base() { derive_port_base "$1"; } derive() { load_config local base; base=$(derive_base "$CLUSTER") DERIVED_HTTP=$base DERIVED_HTTPS=$((base + 1)) DERIVED_TILT=$((base + 2)) DERIVED_REGISTRY=$((base + 3)) } # The resolved facts a consumer outside bash needs, machine-readable: # # CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR # # Identity and ports together, because they are one fact set — the header above # says so: both derive from the directory name so that copies never collide. A # consumer needs all of them or none, and fetching them separately is how two # end up disagreeing. MANIFESTS_DIR rides along because the one consumer that # needs the addressing is the one that needs to know what to deploy. # # Space-separated, so MANIFESTS_DIR must not contain spaces. Everything else in # rig already assumes that of paths — kind, docker and kubectl all do. # # `derive` answers a DIFFERENT question — what the directory name alone implies # — and deliberately ignores ctrl/.env. Configuring anything from it would # silently contradict this file's own rule that "anything already in ctrl/.env # wins". `active` is what anything downstream should read. # # Why this exists at all: the cluster name is not the bare directory name. # default_cluster_name() lowercases it and replaces every character outside # [a-z0-9-], because it has to be a DNS label. Re-deriving that in another # language is how a copy in `My_Project/` ends up guarding the wrong context. active() { load_config echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $MANIFESTS_DIR" } show() { derive echo "environment $CLUSTER" echo "derived base $(derive_base "$CLUSTER")" echo printf " %-14s %-8s %-8s %s\n" KEY DERIVED ACTIVE SOURCE _row HTTP_PORT "$DERIVED_HTTP" _row HTTPS_PORT "$DERIVED_HTTPS" _row TILT_PORT "$DERIVED_TILT" _row REGISTRY_PORT "$DERIVED_REGISTRY" } _row() { local key="$1" derived="$2" active="${!1:-}" src="derived" if [ -n "$active" ] && [ "$active" != "$derived" ]; then src="override" elif [ -z "$active" ]; then active="$derived" fi printf " %-14s %-8s %-8s %s\n" "$key" "$derived" "$active" "$src" } # Write the derived block into ctrl/.env, once. Existing keys are never # rewritten — an override stays an override. persist() { derive [ -f ./.env ] || cp ./.env.example ./.env local wrote=0 key val for key in HTTP_PORT:$DERIVED_HTTP \ HTTPS_PORT:$DERIVED_HTTPS \ TILT_PORT:$DERIVED_TILT \ REGISTRY_PORT:$DERIVED_REGISTRY; do val="${key#*:}"; key="${key%%:*}" if grep -qE "^${key}=[0-9]" ./.env 2>/dev/null; then continue fi if [ "$wrote" -eq 0 ]; then { echo "" echo "# Port block for this environment, derived from the directory name" echo "# so copies never collide. Pinned here on first use — edit freely." } >> ./.env wrote=1 fi # Replace a commented/empty placeholder if present, else append. if grep -qE "^#?\s*${key}=" ./.env 2>/dev/null; then sed -i "s|^#\?\s*${key}=.*|${key}=${val}|" ./.env else echo "${key}=${val}" >> ./.env fi done [ "$wrote" -eq 1 ] && echo "pinned port block into ctrl/.env" || echo "ports already set in ctrl/.env" return 0 } case "${1:-show}" in show) show ;; derive) derive; echo "$DERIVED_HTTP $DERIVED_HTTPS $DERIVED_TILT $DERIVED_REGISTRY" ;; active) active ;; persist) persist ;; *) echo "usage: $0 [show|active|derive|persist]" >&2; exit 1 ;; esac