Add coulomb-social-env secret create/patch script
Builds SECRET_KEY, optional URL-encoded DATABASE_URL from app DB credentials, and USER_ENGINE_PROXY_SECRET from user-engine-runtime. Never prints values; Makefile dry-run and apply targets.
This commit is contained in:
parent
32610e9090
commit
c79f07aadf
3 changed files with 321 additions and 5 deletions
273
tools/create-coulomb-social-env-secret.sh
Executable file
273
tools/create-coulomb-social-env-secret.sh
Executable file
|
|
@ -0,0 +1,273 @@
|
|||
#!/usr/bin/env bash
|
||||
# Create or patch the coulomb-social-env Opaque Secret used by the Helm chart
|
||||
# (envFrom). Never prints secret values to stdout/stderr.
|
||||
#
|
||||
# Required keys for a full production deploy:
|
||||
# SECRET_KEY
|
||||
# DATABASE_URL
|
||||
# USER_ENGINE_PROXY_SECRET
|
||||
#
|
||||
# Usage:
|
||||
# make coulomb-social-env-secret
|
||||
# ./tools/create-coulomb-social-env-secret.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --dry-run Plan only (list keys, no apply)
|
||||
# --rotate-secret-key Always generate a new Django SECRET_KEY
|
||||
# --skip-db Do not set DATABASE_URL
|
||||
# --skip-user-engine Do not copy USER_ENGINE_PROXY_SECRET
|
||||
#
|
||||
# Environment overrides (defaults match Makefile / chart):
|
||||
# COULOMB_SOCIAL_NAMESPACE, COULOMB_SOCIAL_ENV_SECRET
|
||||
# COULOMB_SOCIAL_DB_SECRET, COULOMB_SOCIAL_DB_USER, COULOMB_SOCIAL_DB_HOST,
|
||||
# COULOMB_SOCIAL_DB_PORT, COULOMB_SOCIAL_DB_NAME, COULOMB_SOCIAL_DB_PASSWORD_KEY
|
||||
# USER_ENGINE_NAMESPACE, USER_ENGINE_RUNTIME_SECRET, USER_ENGINE_PROXY_SECRET_KEY
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
sed -n '2,28p' "$0" | sed 's/^# \?//'
|
||||
}
|
||||
|
||||
DRY_RUN=0
|
||||
ROTATE_SECRET_KEY=0
|
||||
SKIP_DB=0
|
||||
SKIP_USER_ENGINE=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--rotate-secret-key) ROTATE_SECRET_KEY=1; shift ;;
|
||||
--skip-db) SKIP_DB=1; shift ;;
|
||||
--skip-user-engine) SKIP_USER_ENGINE=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
NS="${COULOMB_SOCIAL_NAMESPACE:-coulomb-social}"
|
||||
ENV_SECRET="${COULOMB_SOCIAL_ENV_SECRET:-coulomb-social-env}"
|
||||
|
||||
DB_SECRET="${COULOMB_SOCIAL_DB_SECRET:-coulomb-social-app-credentials}"
|
||||
DB_USER="${COULOMB_SOCIAL_DB_USER:-coulomb_social}"
|
||||
DB_HOST="${COULOMB_SOCIAL_DB_HOST:-apps-pg-rw.databases}"
|
||||
DB_PORT="${COULOMB_SOCIAL_DB_PORT:-5432}"
|
||||
DB_NAME="${COULOMB_SOCIAL_DB_NAME:-coulomb_social_db}"
|
||||
DB_PASSWORD_KEY="${COULOMB_SOCIAL_DB_PASSWORD_KEY:-password}"
|
||||
DB_SCHEME="${COULOMB_SOCIAL_DB_SCHEME:-postgresql}"
|
||||
|
||||
UE_NS="${USER_ENGINE_NAMESPACE:-user-engine}"
|
||||
UE_RUNTIME_SECRET="${USER_ENGINE_RUNTIME_SECRET:-user-engine-runtime}"
|
||||
UE_PROXY_KEY="${USER_ENGINE_PROXY_SECRET_KEY:-proxy-secret}"
|
||||
|
||||
for cmd in kubectl python3 openssl base64 mktemp; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: missing required command: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
secret_exists() {
|
||||
kubectl get secret "$2" -n "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
secret_has_key() {
|
||||
local b64
|
||||
b64="$(kubectl get secret "$2" -n "$1" -o "jsonpath={.data.${3}}" 2>/dev/null || true)"
|
||||
[[ -n "$b64" ]]
|
||||
}
|
||||
|
||||
read_secret_plain() {
|
||||
local b64
|
||||
b64="$(kubectl get secret "$2" -n "$1" -o "jsonpath={.data.${3}}" 2>/dev/null || true)"
|
||||
[[ -n "$b64" ]] || return 1
|
||||
printf '%s' "$b64" | base64 -d
|
||||
}
|
||||
|
||||
gen_secret_key() {
|
||||
openssl rand -base64 48 | tr -d '\n' | tr '/+=' 'xyz'
|
||||
}
|
||||
|
||||
# Staging file: JSON object of stringData keys (mode 0600).
|
||||
STAGE="$(mktemp)"
|
||||
chmod 600 "$STAGE"
|
||||
echo '{}' >"$STAGE"
|
||||
trap 'rm -f "$STAGE"' EXIT
|
||||
|
||||
stage_put() {
|
||||
local key="$1" value="$2"
|
||||
KEY="$key" VALUE="$value" STAGE="$STAGE" python3 - <<'PY'
|
||||
import json, os
|
||||
path = os.environ["STAGE"]
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data[os.environ["KEY"]] = os.environ["VALUE"]
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, separators=(",", ":"))
|
||||
PY
|
||||
}
|
||||
|
||||
PLANNED=()
|
||||
SKIPPED=()
|
||||
WARNINGS=()
|
||||
|
||||
plan() { PLANNED+=("$1"); }
|
||||
skip() { SKIPPED+=("$1"); }
|
||||
warn() { WARNINGS+=("$1"); }
|
||||
|
||||
# ── SECRET_KEY ──────────────────────────────────────────────────────────────
|
||||
if [[ "$ROTATE_SECRET_KEY" -eq 1 ]]; then
|
||||
stage_put SECRET_KEY "$(gen_secret_key)"
|
||||
plan "SECRET_KEY (rotated)"
|
||||
elif secret_exists "$NS" "$ENV_SECRET" && secret_has_key "$NS" "$ENV_SECRET" SECRET_KEY; then
|
||||
skip "SECRET_KEY (kept existing)"
|
||||
else
|
||||
stage_put SECRET_KEY "$(gen_secret_key)"
|
||||
plan "SECRET_KEY (generated)"
|
||||
fi
|
||||
|
||||
# ── DATABASE_URL ────────────────────────────────────────────────────────────
|
||||
if [[ "$SKIP_DB" -eq 1 ]]; then
|
||||
skip "DATABASE_URL (--skip-db)"
|
||||
elif ! secret_exists "$NS" "$DB_SECRET"; then
|
||||
warn "DB secret $NS/$DB_SECRET missing — skip DATABASE_URL (create app DB credentials first)"
|
||||
elif ! secret_has_key "$NS" "$DB_SECRET" "$DB_PASSWORD_KEY"; then
|
||||
warn "DB secret key '$DB_PASSWORD_KEY' missing — skip DATABASE_URL"
|
||||
else
|
||||
raw_password="$(read_secret_plain "$NS" "$DB_SECRET" "$DB_PASSWORD_KEY")"
|
||||
if [[ -z "${raw_password}" ]]; then
|
||||
warn "DB password empty — skip DATABASE_URL"
|
||||
else
|
||||
encoded_password="$(
|
||||
RAW_PASSWORD="$raw_password" python3 -c \
|
||||
'import os, urllib.parse; print(urllib.parse.quote(os.environ["RAW_PASSWORD"], safe=""))'
|
||||
)"
|
||||
unset raw_password
|
||||
stage_put DATABASE_URL \
|
||||
"${DB_SCHEME}://${DB_USER}:${encoded_password}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
||||
unset encoded_password
|
||||
plan "DATABASE_URL (from $NS/$DB_SECRET)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── USER_ENGINE_PROXY_SECRET ────────────────────────────────────────────────
|
||||
if [[ "$SKIP_USER_ENGINE" -eq 1 ]]; then
|
||||
skip "USER_ENGINE_PROXY_SECRET (--skip-user-engine)"
|
||||
elif ! secret_exists "$UE_NS" "$UE_RUNTIME_SECRET"; then
|
||||
warn "user-engine secret $UE_NS/$UE_RUNTIME_SECRET missing — skip USER_ENGINE_PROXY_SECRET"
|
||||
elif ! secret_has_key "$UE_NS" "$UE_RUNTIME_SECRET" "$UE_PROXY_KEY"; then
|
||||
warn "user-engine secret key '$UE_PROXY_KEY' missing — skip USER_ENGINE_PROXY_SECRET"
|
||||
else
|
||||
proxy="$(read_secret_plain "$UE_NS" "$UE_RUNTIME_SECRET" "$UE_PROXY_KEY")"
|
||||
if [[ -z "${proxy}" ]]; then
|
||||
warn "USER_ENGINE_PROXY_SECRET empty after decode — skip"
|
||||
else
|
||||
stage_put USER_ENGINE_PROXY_SECRET "$proxy"
|
||||
unset proxy
|
||||
plan "USER_ENGINE_PROXY_SECRET (from $UE_NS/$UE_RUNTIME_SECRET)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── report ──────────────────────────────────────────────────────────────────
|
||||
echo "==> coulomb-social env secret plan"
|
||||
echo " namespace: $NS"
|
||||
echo " secret: $ENV_SECRET"
|
||||
if [[ ${#PLANNED[@]} -gt 0 ]]; then
|
||||
echo " set/update:"
|
||||
for k in "${PLANNED[@]}"; do echo " + $k"; done
|
||||
else
|
||||
echo " set/update: (none)"
|
||||
fi
|
||||
if [[ ${#SKIPPED[@]} -gt 0 ]]; then
|
||||
echo " skipped:"
|
||||
for k in "${SKIPPED[@]}"; do echo " · $k"; done
|
||||
fi
|
||||
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
|
||||
echo " warnings:"
|
||||
for k in "${WARNINGS[@]}"; do echo " ! $k"; done
|
||||
fi
|
||||
|
||||
KEY_COUNT="$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))))' "$STAGE")"
|
||||
if [[ "$KEY_COUNT" -eq 0 ]]; then
|
||||
echo "Nothing to apply."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "Dry-run: no changes applied ($KEY_COUNT key(s) would be written)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── apply ───────────────────────────────────────────────────────────────────
|
||||
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||
|
||||
STAGE="$STAGE" NS="$NS" ENV_SECRET="$ENV_SECRET" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
stage = os.environ["STAGE"]
|
||||
ns = os.environ["NS"]
|
||||
name = os.environ["ENV_SECRET"]
|
||||
with open(stage, encoding="utf-8") as f:
|
||||
string_data = json.load(f)
|
||||
|
||||
exists = (
|
||||
subprocess.call(
|
||||
["kubectl", "get", "secret", name, "-n", ns],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
if exists:
|
||||
body = json.dumps({"stringData": string_data})
|
||||
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tf:
|
||||
tf.write(body)
|
||||
path = tf.name
|
||||
try:
|
||||
os.chmod(path, 0o600)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"kubectl",
|
||||
"patch",
|
||||
"secret",
|
||||
name,
|
||||
"-n",
|
||||
ns,
|
||||
"--type=merge",
|
||||
f"--patch-file={path}",
|
||||
]
|
||||
)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
print(f"Patched secret {ns}/{name} ({len(string_data)} key(s)).")
|
||||
else:
|
||||
args = ["kubectl", "create", "secret", "generic", name, "-n", ns]
|
||||
files = []
|
||||
try:
|
||||
for key, value in string_data.items():
|
||||
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tf:
|
||||
tf.write(value)
|
||||
path = tf.name
|
||||
os.chmod(path, 0o600)
|
||||
files.append(path)
|
||||
args.append(f"--from-file={key}={path}")
|
||||
subprocess.check_call(args)
|
||||
print(f"Created secret {ns}/{name} ({len(string_data)} key(s)).")
|
||||
finally:
|
||||
for path in files:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
PY
|
||||
|
||||
echo "Keys now present (names only):"
|
||||
kubectl get secret "$ENV_SECRET" -n "$NS" -o json \
|
||||
| python3 -c 'import sys,json; d=json.load(sys.stdin).get("data") or {}; print(" ", ", ".join(sorted(d.keys())) or "(none)")'
|
||||
echo
|
||||
echo "Next:"
|
||||
echo " COULOMB_SOCIAL_IMAGE_TAG=<sha> make coulomb-social-deploy"
|
||||
echo " kubectl -n $NS rollout restart deploy/coulomb-social # if secret updated under a running deploy"
|
||||
Loading…
Add table
Add a link
Reference in a new issue