Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 149 additions & 19 deletions kubernetes/kubeconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@
# stuttgart-things/stuttgart-things secrets/kubeconfigs/. Gedacht fuer den
# globalen Taskfile (remote eingebunden als `kubeconfig`), laeuft von ueberall:
#
# task -g kubeconfig:fetch # Quelle + Cluster per gum
# task -g kubeconfig:fetch # Lab, dann Cluster aus allen Quellen per gum
# task -g kubeconfig:fetch LAB=labul # nur LabUL-Cluster zur Auswahl
# task -g kubeconfig:fetch SOURCE=vault CLUSTER=homerun2-dev2
# task -g kubeconfig:sops CLUSTER=labda-machinery OUT=/tmp/kc
#
# Ohne SOURCE zeigt fetch eine Liste aus allen Quellen: Kubeconfigs, die schon
# in ~/.kube liegen (lokal), Vault und SOPS -- jeder Cluster einmal, die Quellen
# daneben. Gibt es ihn in Vault oder SOPS, wird er von dort (neu) geholt, sonst
# wird die lokale Datei genommen. Faellt eine Quelle aus, fehlen nur ihre
# Eintraege.
#
# Jeder Cluster bekommt ein Lab, und vor der Liste wird erst das Lab gewaehlt
# (`alle` zeigt alles, LAB=labul ueberspringt die Frage). Das Lab kommt, in
# dieser Reihenfolge, aus:
# 1. ~/.kube/labels (KUBE_LABELS), Zeilen `<cluster> <lab>`, `#` Kommentar
# 2. dem Netz der server:-Adresse einer lokalen Kubeconfig (LAB_NETS)
# 3. Vault custom_metadata `lab` des Eintrags (kubeconfigs/<cluster>)
# 4. dem Namenspraefix (labda-*, labul-*)
# sonst `-`.
#
# Dateien aus dem Repo (SOPS-Kubeconfigs, Reader-AppRole) kommen aus dem
# Checkout $STHINGS_REPO (Default ~/projects/stuttgart-things); ohne Checkout
# per `gh api`. Vault-Login ueber die Reader-AppRole aus
Expand All @@ -19,12 +35,16 @@ version: 3

vars:
VAULT_MOUNT: '{{ .VAULT_MOUNT | default "kubeconfigs" }}'
# <adress-praefix>=<lab>, leerzeichengetrennt; erster Treffer gewinnt
LAB_NETS: '{{ .LAB_NETS | default "10.31.=labul 10.100.136.=labda" }}'
LIB: |
set -euo pipefail
GH_REPO_SLUG=stuttgart-things/stuttgart-things
REPO="${STHINGS_REPO:-$HOME/projects/stuttgart-things}"
KUBE_DIR="${KUBE_DIR:-$HOME/.kube}"
VAULT_MOUNT="{{ .VAULT_MOUNT }}"
KUBE_LABELS="${KUBE_LABELS:-$KUBE_DIR/labels}"
LAB_NETS="{{ .LAB_NETS }}"

# repo_file <pfad im repo> -> Inhalt auf stdout (Checkout, sonst gh api)
repo_file() {
Expand All @@ -43,7 +63,10 @@ vars:
TMP=$(mktemp)
cat > "$TMP"
if ! grep -q '^apiVersion:' "$TMP"; then rm -f "$TMP"; echo "keine Kubeconfig erhalten" >&2; return 1; fi
if [ -e "$OUT" ] && ! cmp -s "$TMP" "$OUT"; then
if [ -e "$OUT" ] && cmp -s "$TMP" "$OUT"; then
rm -f "$TMP"; echo "unveraendert: $OUT"; echo "export KUBECONFIG=$OUT"; return 0
fi
if [ -e "$OUT" ]; then
gum confirm "$OUT existiert und weicht ab -- ueberschreiben (alte als $OUT.bak)?" || { rm -f "$TMP"; return 1; }
cp -p "$OUT" "$OUT.bak"
fi
Expand All @@ -53,29 +76,55 @@ vars:
echo "export KUBECONFIG=$OUT"
}

fetch_vault() {
local CLUSTER="$1" OUT="$2" ADDR="${VAULT_ADDR:-}" TOKEN="${VAULT_TOKEN:-}" CREDS H DATA KC
# vault_login -> setzt V_ADDR und V_H (einmal pro Lauf)
V_ADDR="" V_H=""
vault_login() {
[ -z "$V_H" ] || return 0
local TOKEN="${VAULT_TOKEN:-}" CREDS
V_ADDR="${VAULT_ADDR:-}"
if [ -z "$TOKEN" ]; then
CREDS=$(repo_file secrets/vault-kubeconfig-reader.enc.yaml \
| sops -d --input-type yaml --output-type json /dev/stdin)
[ -n "$ADDR" ] || ADDR=$(jq -r '.stringData.VAULT_ADDR' <<<"$CREDS")
| sops -d --input-type yaml --output-type json /dev/stdin) || return 1
[ -n "$V_ADDR" ] || V_ADDR=$(jq -r '.stringData.VAULT_ADDR' <<<"$CREDS")
TOKEN=$(jq -n --argjson c "$CREDS" \
'{role_id: $c.stringData.VAULT_ROLE_ID, secret_id: $c.stringData.VAULT_SECRET_ID}' \
| curl -fsS -X POST --data @- "$ADDR/v1/auth/approle/login" \
| jq -r '.auth.client_token')
| curl -fsS --max-time 10 -X POST --data @- "$V_ADDR/v1/auth/approle/login" \
| jq -r '.auth.client_token') || return 1
unset CREDS
fi
[ -n "$ADDR" ] || { echo "VAULT_ADDR fehlt" >&2; return 1; }
H="X-Vault-Token: $TOKEN"
[ -n "$V_ADDR" ] || { echo "VAULT_ADDR fehlt" >&2; return 1; }
[ -n "$TOKEN" ] && [ "$TOKEN" != null ] || { echo "Vault-Login fehlgeschlagen" >&2; return 1; }
V_H="X-Vault-Token: $TOKEN"
}

# LIST nur als GET ?list=true -- `-X LIST` liefert 400 und sieht aus wie leer.
vault_list() {
curl -fsS --max-time 10 -H "$V_H" "$V_ADDR/v1/$VAULT_MOUNT/metadata/?list=true" \
| jq -r '.data.keys[] | select(endswith("/") | not)'
}

sops_list() { repo_ls secrets/kubeconfigs | sed -n 's/\.yaml$//p'; }

# local_list -> <name>\t<pfad> fuer jede Kubeconfig direkt in $KUBE_DIR (ohne Backups)
local_list() {
local f n
for f in "$KUBE_DIR"/*; do
[ -f "$f" ] || continue
n=${f##*/}
case "$n" in *.bak|*.bak-*|*.bak.*) continue ;; esac
grep -q '^clusters:' "$f" 2>/dev/null || continue
printf '%s\t%s\n' "${n%.yaml}" "$f"
done
}

fetch_vault() {
local CLUSTER="$1" OUT="$2" DATA KC
vault_login
if [ -z "$CLUSTER" ]; then
# LIST nur als GET ?list=true -- `-X LIST` liefert 400 und sieht aus wie leer.
CLUSTER=$(curl -fsS -H "$H" "$ADDR/v1/$VAULT_MOUNT/metadata/?list=true" \
| jq -r '.data.keys[] | select(endswith("/") | not)' \
| gum filter --placeholder "Cluster waehlen ($ADDR)")
CLUSTER=$(vault_list | gum filter --placeholder "Cluster waehlen ($V_ADDR)")
fi

DATA=$(curl -fsS -H "$H" "$ADDR/v1/$VAULT_MOUNT/data/$CLUSTER" | jq '.data.data')
DATA=$(curl -fsS -H "$V_H" "$V_ADDR/v1/$VAULT_MOUNT/data/$CLUSTER" | jq '.data.data')
# Schluessel `kubeconfig`, sonst der einzige vorhandene.
KC=$(jq -r 'if has("kubeconfig") then .kubeconfig
elif length == 1 then .[keys[0]]
Expand All @@ -98,9 +147,90 @@ vars:
| write_kubeconfig "${OUT:-$KUBE_DIR/$CLUSTER}"
}

fetch_local() {
local CLUSTER="$1" OUT="$2" P
if [ -z "$CLUSTER" ]; then
CLUSTER=$(local_list | cut -f1 | gum filter --placeholder "Cluster waehlen ($KUBE_DIR)")
fi
P=$(local_list | awk -F'\t' -v n="$CLUSTER" '$1 == n { print $2; exit }')
[ -n "$P" ] || { echo "keine lokale Kubeconfig fuer $CLUSTER in $KUBE_DIR" >&2; return 1; }
if [ -n "$OUT" ] && [ "$OUT" != "$P" ]; then write_kubeconfig "$OUT" <"$P"; return; fi
echo "lokal: $P"
echo "export KUBECONFIG=$P"
}

# lab_of <cluster> <lokaler pfad|""> <in vault: 1|""> -> Lab oder `-`
lab_of() {
local N="$1" P="$2" V="$3" L="" HOST NET
if [ -f "$KUBE_LABELS" ]; then
L=$(awk -v n="$N" '$1 == n { print $2; exit }' "$KUBE_LABELS")
fi
if [ -z "$L" ] && [ -n "$P" ]; then
HOST=$(awk '$1 == "server:" { print $2; exit }' "$P")
HOST=${HOST#*://}; HOST=${HOST%%[:/]*}
for NET in $LAB_NETS; do
case "$HOST" in "${NET%%=*}"*) L=${NET#*=}; break ;; esac
done
fi
if [ -z "$L" ] && [ -n "$V" ]; then
L=$(curl -fsS --max-time 10 -H "$V_H" "$V_ADDR/v1/$VAULT_MOUNT/metadata/$N" \
| jq -r '.data.custom_metadata.lab // empty' 2>/dev/null) || L=""
fi
if [ -z "$L" ]; then
case "$N" in labda-*) L=labda ;; labul-*) L=labul ;; esac
fi
echo "${L:--}"
}

# fetch_any -- alle Quellen in einer Liste, jeder Cluster einmal, erst Lab waehlen.
# Vault/SOPS gehen vor lokal; liegt er schon lokal, wird dieselbe Datei aktualisiert.
fetch_any() {
local CLUSTER="$1" OUT="$2" LAB="$3" SRCLIST LOCALS ROWS="" PICK SRCS P N S V LABS
if ! vault_login; then echo "Vault nicht erreichbar -- Liste ohne Vault" >&2; fi
LOCALS=$(local_list)
SRCLIST=$( {
[ -z "$LOCALS" ] || awk -F'\t' '{ print $1 "\tlokal" }' <<<"$LOCALS"
if [ -n "$V_H" ]; then vault_list | sed 's/$/\tvault/' || echo "Vault-Liste fehlgeschlagen" >&2; fi
sops_list | sed 's/$/\tsops/' || echo "SOPS-Liste fehlgeschlagen" >&2
} | sort -u | awk -F'\t' '{ s[$1] = s[$1] (s[$1] == "" ? "" : ",") $2 }
END { for (n in s) print n "\t" s[n] }' | sort)
[ -n "$SRCLIST" ] || { echo "keine Cluster gefunden" >&2; return 1; }
while IFS=$'\t' read -r N S; do
P=$(awk -F'\t' -v n="$N" '$1 == n { print $2; exit }' <<<"$LOCALS")
case ",$S," in *,vault,*) V=1 ;; *) V="" ;; esac
ROWS+=$(printf '%-40s %-8s %s' "$N" "$(lab_of "$N" "$P" "$V")" "$S")$'\n'
done <<<"$SRCLIST"
ROWS=${ROWS%$'\n'}
if [ -n "$CLUSTER" ]; then
PICK=$(awk -v n="$CLUSTER" '$1 == n' <<<"$ROWS")
[ -n "$PICK" ] || { echo "unbekannter Cluster: $CLUSTER" >&2; return 1; }
else
if [ -z "$LAB" ]; then
LABS=$(awk '{ print $2 }' <<<"$ROWS" | sort -u)
if [ "$(wc -l <<<"$LABS")" -gt 1 ]; then
LAB=$(printf 'alle\n%s\n' "$LABS" | gum choose --header "Lab?")
fi
fi
if [ -n "$LAB" ] && [ "$LAB" != alle ]; then
ROWS=$(awk -v l="$LAB" '$2 == l' <<<"$ROWS")
[ -n "$ROWS" ] || { echo "keine Cluster mit Lab $LAB" >&2; return 1; }
fi
PICK=$(gum filter --placeholder "Cluster waehlen (${LAB:-alle})" --height 25 <<<"$ROWS")
fi
CLUSTER=${PICK%% *}
SRCS=",${PICK##* },"
P=$(local_list | awk -F'\t' -v n="$CLUSTER" '$1 == n { print $2; exit }')
OUT="${OUT:-${P:-$KUBE_DIR/$CLUSTER}}"
case "$SRCS" in
*,vault,*) echo "aus Vault: $CLUSTER" >&2; fetch_vault "$CLUSTER" "$OUT" ;;
*,sops,*) echo "aus SOPS: $CLUSTER" >&2; fetch_sops "$CLUSTER" "$OUT" ;;
*) fetch_local "$CLUSTER" "$OUT" ;;
esac
}

tasks:
fetch:
desc: "kubeconfig: aus Vault oder SOPS waehlen und nach ~/.kube/<cluster> schreiben"
desc: "kubeconfig: Cluster aus einer Liste (lokal, Vault, SOPS) waehlen -> ~/.kube/<cluster>"
aliases: [kc]
interactive: true
silent: true
Expand All @@ -110,11 +240,11 @@ tasks:
cmds:
- |
{{ .LIB }}
SOURCE="{{ .SOURCE }}"
[ -n "$SOURCE" ] || SOURCE=$(gum choose --header "Quelle?" vault sops)
case "$SOURCE" in
case "{{ .SOURCE }}" in
"") fetch_any "{{ .CLUSTER }}" "{{ .OUT }}" "{{ .LAB }}" ;;
vault) fetch_vault "{{ .CLUSTER }}" "{{ .OUT }}" ;;
sops) fetch_sops "{{ .CLUSTER }}" "{{ .OUT }}" ;;
lokal|local) fetch_local "{{ .CLUSTER }}" "{{ .OUT }}" ;;
*) echo "unbekannte Quelle: $SOURCE" >&2; exit 1 ;;
esac

Expand Down
Loading