#!/usr/bin/env bash # Reconciles the live 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=( []=:placeholder []=:"" ) # key -> Gitea-var-env:placeholder (placeholders apply to the ConfigMap) # EDIT ME: add every ConfigMap key your deployments reference. declare -A CM_KEYS=( []=:"" ) 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 "keyvalue" 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