Reusable repo template for homelab services on git.aridgwayweb.com. Bakes in (all verified live on armistace/wedding-photos): - Hard commit guard: shared pre-commit hook (git config core.hooksPath ~/dev/git-hooks) + master branch protection (push whitelist [armistace], merge whitelist [hermes, armistace]). - Gitea Actions CI (.gitea/workflows/build_push.yml): test + build-deploy, persistent remote buildkit cache, registry push, idempotent deploy that preserves hand-provisioned Secrets, cluster injection from repo secrets/vars via scripts/reconcile-cluster-inject.sh. - Persistent buildkit cache (ci/buildkit/): single-replica Longhorn backing. - scripts/reconcile-cluster-inject.sh: reconcile live Secret/ConfigMap from Gitea secrets/vars without clobbering hand-provisioned values. - RUNBOOK.md: handoff-complete ops doc. Placeholders (<APP> <OWNER> <NS> <KEY_*>) are filled per-service on repo creation.
104 lines
3.6 KiB
Bash
104 lines
3.6 KiB
Bash
#!/usr/bin/env bash
|
|
# Reconciles the live <NS> Secret and ConfigMap from Gitea repo secrets/vars,
|
|
# WITHOUT ever clobbering hand-provisioned real credentials.
|
|
#
|
|
# Design rationale (post CreateContainerConfigError lesson):
|
|
# The deployments reference DB creds / JWT / etc. unconditionally. If any key
|
|
# is missing from the live Secret, the pod fails to start (rollout timeout in
|
|
# CI). The Secret has historically been provisioned by hand (RUNBOOK §4.2);
|
|
# git holds placeholders only.
|
|
#
|
|
# This script makes CI the reconciler. For each managed key it applies EXACTLY
|
|
# one rule (priority order):
|
|
# 1. Gitea supplies a NON-EMPTY value -> that value wins (seed / rotate).
|
|
# 2. The key MISSING from the live resource -> write the placeholder so the
|
|
# app's env refs always resolve and the rollout never breaks.
|
|
# 3. Otherwise (key present, Gitea empty or absent) -> PRESERVE the live
|
|
# value untouched. A hand-provisioned credential is never overwritten by
|
|
# an empty Gitea secret.
|
|
#
|
|
# The patch payload is built by Python's json module (no shell interpolation),
|
|
# so arbitrary values — quotes, newlines, unicode — cannot corrupt the JSON or
|
|
# inject flags into kubectl.
|
|
|
|
set -euo pipefail
|
|
|
|
SECRET="${1:?secret name}"
|
|
CM="${2:?configmap name}"
|
|
NS="${3:?namespace}"
|
|
KUBECTL="${KUBECTL:-kubectl}"
|
|
|
|
# key -> Gitea-var-env:placeholder (placeholder applies to the Secret)
|
|
# EDIT ME: add every Secret key your deployments reference.
|
|
declare -A SECRET_KEYS=(
|
|
[<KEY_1>]=<KEY_1>:placeholder<KEY_1>
|
|
[<KEY_2>]=<KEY_2>:""
|
|
)
|
|
|
|
# key -> Gitea-var-env:placeholder (placeholders apply to the ConfigMap)
|
|
# EDIT ME: add every ConfigMap key your deployments reference.
|
|
declare -A CM_KEYS=(
|
|
[<CONFIG_KEY_1>]=<CONFIG_KEY_1>:""
|
|
)
|
|
|
|
live_has_key() { # kind name key -> 0 if the resource has the key
|
|
local v
|
|
v="$($KUBECTL get "$1" "$2" -n "$NS" -o "jsonpath={.data.$3}" 2>/dev/null || true)"
|
|
[[ -n "$v" ]]
|
|
}
|
|
|
|
# Build a JSON patch body {"data": {...}} safely via Python json.
|
|
# Args: kind newline-joined "key<TAB>value" lines.
|
|
build_patch() {
|
|
python3 -c '
|
|
import sys, json, base64
|
|
kind = sys.argv[1]
|
|
data = {}
|
|
for line in sys.argv[2].split("\n"):
|
|
if not line:
|
|
continue
|
|
key, _, val = line.partition("\t")
|
|
data[key] = base64.b64encode(val.encode("utf-8")).decode("ascii") if kind == "secret" else val
|
|
print(json.dumps({"data": data}, ensure_ascii=False))
|
|
' "$1" "$2"
|
|
}
|
|
|
|
# emit_patch KIND assoc-name resource-kind resource-name
|
|
emit_patch() {
|
|
local kind="$1" declare_var="$2" reskind="$3" resname="$4"
|
|
local -n MAP="$declare_var"
|
|
local lines=() key envvar placeholder val
|
|
for key in "${!MAP[@]}"; do
|
|
envvar="${MAP[$key]%%:*}"
|
|
placeholder="${MAP[$key]#*:}"
|
|
val="${!envvar:-}"
|
|
if [[ -n "$val" ]]; then
|
|
lines+=("$key"$'\t'"$val")
|
|
elif ! live_has_key "$reskind" "$resname" "$key"; then
|
|
lines+=("$key"$'\t'"$placeholder")
|
|
fi
|
|
done
|
|
if [[ ${#lines[@]} -gt 0 ]]; then
|
|
build_patch "$kind" "$(printf '%s\n' "${lines[@]}")"
|
|
else
|
|
echo '{}'
|
|
fi
|
|
}
|
|
|
|
apply_secret() {
|
|
local body
|
|
body="$(emit_patch secret SECRET_KEYS secret "$SECRET")"
|
|
[[ "$body" == '{}' ]] || $KUBECTL patch secret "$SECRET" -n "$NS" --type merge -p "$body" >/dev/null
|
|
echo "reconciled secret $SECRET ($NS): non-empty Gitea applied, missing seeded, present preserved"
|
|
}
|
|
|
|
apply_cm() {
|
|
local body
|
|
body="$(emit_patch configmap CM_KEYS configmap "$CM")"
|
|
[[ "$body" == '{}' ]] || $KUBECTL patch configmap "$CM" -n "$NS" --type merge -p "$body" >/dev/null
|
|
echo "reconciled configmap $CM ($NS): non-empty Gitea applied, missing seeded, present preserved"
|
|
}
|
|
|
|
apply_secret
|
|
apply_cm
|