{{- if .Values.bootstrap.enabled }} --- apiVersion: batch/v1 kind: Job metadata: name: {{ include "terdut-server.fullname" . }}-bootstrap namespace: {{ .Release.Namespace }} labels: {{- include "terdut-server.labels" . | nindent 4 }} annotations: helm.sh/hook: post-install,post-upgrade helm.sh/hook-weight: "0" helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded spec: backoffLimit: 3 # Bounds the whole Job, retries included, so a database that never comes up ends the # hook instead of leaving it pending. Each attempt waits up to bootstrap.waitSeconds # for the server, and the allowance covers every attempt plus the pod backoff between. activeDeadlineSeconds: {{ add (mul (int .Values.bootstrap.waitSeconds) 4) 300 }} template: metadata: labels: {{- include "terdut-server.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: bootstrap spec: restartPolicy: OnFailure serviceAccountName: {{ include "terdut-server.fullname" . }}-bootstrap containers: - name: bootstrap # alpine/curl, not alpine:3 + `apk add curl`. Installing the binary at run time # writes it into the container's writable upper layer, which is exactly the # signature Falco's `Drop and execute new binary in container` (MITRE TA0003) # exists to catch -- this hook emitted two Critical events on every single # upgrade. See Ryuvia/charts#100. It also made `helm upgrade` depend on the # Alpine CDN answering, since this runs as a post-upgrade hook and a failed # hook fails the release. # # Still a full Alpine underneath, so sh, cat, sleep, grep, cut, head and tail # are all present (verified in-cluster 2026-09-04). The image declares # ENTRYPOINT ["/entrypoint.sh"], which `command:` below overrides -- do not # change `command:` to `args:`. image: alpine/curl:8.21.0@sha256:a1c44bab54d88e18ea9a6a4ecefab7f2d230b968567b78960fcaff8d51b7f067 command: - /bin/sh - -c - | SERVICE_URL="http://{{ include "terdut-server.fullname" . }}:{{ .Values.service.port }}" SECRET_NAME="{{ include "terdut-server.bootstrapSecretName" . }}" USERNAME="{{ .Values.bootstrap.username }}" EMAIL="{{ .Values.bootstrap.email }}" K8S_API="https://kubernetes.default.svc" SA_TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" CA_CERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" SECRETS="$K8S_API/api/v1/namespaces/$NAMESPACE/secrets" # kapi METHOD URL [BODY]: sets CODE and BODY from the Kubernetes API. kapi() { _ctype="application/json" [ "$1" = "PATCH" ] && _ctype="application/merge-patch+json" if [ -n "$3" ]; then _resp=$(printf '%s' "$3" | curl -s -w "\n%{http_code}" -X "$1" "$2" \ --cacert "$CA_CERT" -H "Authorization: Bearer $SA_TOKEN" \ -H "Content-Type: $_ctype" -d @-) else _resp=$(curl -s -w "\n%{http_code}" -X "$1" "$2" \ --cacert "$CA_CERT" -H "Authorization: Bearer $SA_TOKEN") fi CODE=$(echo "$_resp" | tail -1) BODY=$(echo "$_resp" | sed '$d') } # The server exits when the database does not answer within ~10 seconds, so # while the database is down the pod crash-loops, with a restart backoff that # grows to five minutes, and /healthz only answers once it has connected. Wait # for that for as long as bootstrap.waitSeconds allows, not a fixed two minutes: # a failed hook fails the release and nothing runs it again until the next # upgrade, so a database that was merely slow would leave no administrator. WAIT={{ int .Values.bootstrap.waitSeconds }} echo "Waiting up to ${WAIT}s for terdut-server to be ready (it needs the database)..." WAITED=0 while ! curl -sf "$SERVICE_URL/healthz" > /dev/null 2>&1; do if [ "$WAITED" -ge "$WAIT" ]; then echo "Timed out after ${WAITED}s waiting for the server to be ready." echo "Check the database and the terdut-server pod's logs, then run helm upgrade again." exit 1 fi [ $((WAITED % 30)) -eq 0 ] && [ "$WAITED" -gt 0 ] && echo "Still waiting (${WAITED}s)..." sleep 2 WAITED=$((WAITED + 2)) done echo "Server is ready after ${WAITED}s." # The Secret is the source of truth for the administrator's password: reuse # the one it holds, so recreating the database brings the same account back. PASSWORD="" EXISTS=0 kapi GET "$SECRETS/$SECRET_NAME" if [ "$CODE" = "200" ]; then EXISTS=1 PASSWORD=$(echo "$BODY" | grep -o '"password": *"[^"]*"' | head -1 | cut -d'"' -f4 | base64 -d) elif [ "$CODE" != "404" ]; then echo "Failed to read secret '$SECRET_NAME' (HTTP $CODE)." exit 1 fi # Written BEFORE the server is asked to create the account, so a crash in # between cannot leave an administrator whose password nobody has. GENERATED=0 if [ -z "$PASSWORD" ]; then PASSWORD=$(head -c 256 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 32) if [ "${#PASSWORD}" -lt 32 ]; then echo "Failed to generate a password." exit 1 fi GENERATED=1 DATA=$(printf '"username":"%s","password":"%s"' "$USERNAME" "$PASSWORD") if [ "$EXISTS" = "1" ]; then kapi PATCH "$SECRETS/$SECRET_NAME" "{\"stringData\":{$DATA}}" else kapi POST "$SECRETS" "{\"apiVersion\":\"v1\",\"kind\":\"Secret\",\"metadata\":{\"name\":\"$SECRET_NAME\"},\"stringData\":{$DATA}}" fi case "$CODE" in 200|201) ;; *) echo "Failed to store the password in secret '$SECRET_NAME' (HTTP $CODE)." exit 1 ;; esac fi RESPONSE=$(printf '{"username":"%s","email":"%s","password":"%s"}' "$USERNAME" "$EMAIL" "$PASSWORD" \ | curl -s -w "\n%{http_code}" -X POST "$SERVICE_URL/api/bootstrap" \ -H "Content-Type: application/json" -d @-) HTTP_CODE=$(echo "$RESPONSE" | tail -1) RESP_BODY=$(echo "$RESPONSE" | head -1) if [ "$HTTP_CODE" = "403" ]; then # Somebody else made the first account. The Secret is only worth keeping # if its password signs in. LOGIN=$(printf '{"username":"%s","password":"%s"}' "$USERNAME" "$PASSWORD" \ | curl -s -o /dev/null -w "%{http_code}" -X POST "$SERVICE_URL/api/login" \ -H "Content-Type: application/json" -d @-) if [ "$LOGIN" = "200" ]; then echo "Server already bootstrapped; the password in '$SECRET_NAME' signs in." exit 0 fi echo "Server already bootstrapped by something else; '$USERNAME' does not sign in with the password in '$SECRET_NAME'." if [ "$GENERATED" = "1" ]; then if [ "$EXISTS" = "1" ]; then kapi PATCH "$SECRETS/$SECRET_NAME" '{"data":{"username":null,"password":null}}' else kapi DELETE "$SECRETS/$SECRET_NAME" fi echo "Removed the password this run generated." fi exit 0 fi if [ "$HTTP_CODE" != "201" ]; then echo "Bootstrap failed (HTTP $HTTP_CODE): $RESP_BODY" exit 1 fi API_KEY=$(echo "$RESP_BODY" | grep -o '"key":"[^"]*"' | cut -d'"' -f4) if [ -z "$API_KEY" ]; then echo "Failed to extract API key from response." exit 1 fi echo "Bootstrap succeeded. Storing the API key in secret '$SECRET_NAME'." kapi PATCH "$SECRETS/$SECRET_NAME" "{\"stringData\":{\"api-key\":\"$API_KEY\"}}" if [ "$CODE" != "200" ]; then echo "Failed to store the API key (HTTP $CODE)." exit 1 fi echo "Secret '$SECRET_NAME' holds username, password and api-key." {{- end }}