gitea-repo-template/scripts/reconcile-cluster-inject.sh
Hermes Agent 883e264c42
Some checks failed
build-and-deploy / test (push) Failing after 2m36s
build-and-deploy / build-deploy (push) Has been skipped
chore: seed gitea-repo-template cookie-cutter
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.
2026-09-24 11:45:57 +10:00

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