openova/platform/harbor/chart/templates/database-secret-sync-job.yaml
e3mrah 74d23ab3dc
fix(charts): explicit harbor.openova.io/proxy-dockerhub prefix on all chart-hook images (#163) (#1367)
Per CLAUDE.md MIRROR-EVERYTHING inviolable rule: every chart-hook
image reference (pre/post-install Jobs, helper Pods) must use the
explicit Harbor proxy-cache form. Fix #158's bitnami → bitnamilegacy
swap was a band-aid; the architecturally correct fix is to defeat
upstream-deletion blast radius entirely by routing through Harbor.

The node-level containerd mirror in infra/hetzner/cloudinit-control-
plane.tftpl (line 706) already redirects docker.io/* →
harbor.openova.io/proxy-dockerhub/* implicitly, but implicit routing:
  - Hides the routing from SBOM scans
  - Bypasses the Kyverno harbor-proxy-pull ClusterPolicy
  - Means a chart audit (`grep docker.io`) misses a real dependency
  - Was the proximate cause of prov #27 wedging when Bitnami deleted
    docker.io/bitnami/kubectl:1.30.4 (Fix #158 had to chase the
    deletion mid-flight instead of being insulated by Harbor cache)

19 chart-hook image: refs + 5 chart values.yaml repository: defaults
now carry the explicit harbor.openova.io/proxy-dockerhub prefix.
Application/subchart images (keycloak, postgresql, mongodb in
keycloak+litmus subcharts) are intentionally out of scope for this
PR — those go through the node-level containerd mirror still.

Affected blueprints + chart version bumps:
  bp-cert-manager            1.2.1  -> 1.2.2
  bp-external-secrets-stores 1.0.4  -> 1.0.5
  bp-crossplane-claims       1.1.4  -> 1.1.5
  bp-flux                    1.2.1  -> 1.2.2
  bp-guacamole               0.1.16 -> 0.1.17
  bp-self-sovereign-cutover  0.1.28 -> 0.1.29
  bp-k8s-ws-proxy            0.1.9  -> 0.1.10
  bp-harbor                  1.2.15 -> 1.2.16
  bp-gitea                   1.2.5  -> 1.2.6
  bp-newapi                  1.4.5  -> 1.4.6
  bp-wordpress-tenant        0.2.0  -> 0.2.1
  catalyst-platform          1.4.138 -> 1.4.139

Co-authored-by: e3mrah <1234567+e3mrah@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:32:21 +04:00

192 lines
7.7 KiB
YAML

{{- /*
Helm post-install Job that copies the CNPG-emitted `harbor-pg-app` Secret
into `harbor-database-secret` so Harbor core's env binding finds the
expected `password` key.
WHY A JOB INSTEAD OF REFLECTOR
--------------------------------
The earlier Reflector-based path relied on the Reflector daemon refiring
when CNPG creates `harbor-pg-app` AFTER `harbor-database-secret` was
processed. In practice (caught live on otech30) Reflector logs:
"Could not update harbor/harbor-database-secret —
Source harbor/harbor-pg-app could not be found"
once at install time and never retries. Even with `auto-enabled: true`
on the source's inherited annotations, Reflector's auto-reflect path
copies the SOURCE name (harbor-pg-app) — it does NOT update the
explicit `reflects:` destination (harbor-database-secret). So the
destination stays empty and harbor-core stalls forever with
`couldn't find key password in Secret harbor/harbor-database-secret`.
A post-install Job avoids the watcher race entirely:
1. Polls until CNPG has provisioned harbor-pg-app
2. Reads `password` from it
3. kubectl-applies harbor-database-secret with both the original
`password` key (used by harbor-core) and `HARBOR_DATABASE_PASSWORD`
(used by upstream Harbor charts that bind via envFrom)
4. Exits 0; Helm marks the post-install hook complete
The Job is idempotent — re-running it overwrites with the same data,
which CNPG never rotates without operator action.
WHY NOT INIT-CONTAINER ON HARBOR-CORE
--------------------------------------
Harbor's upstream chart's deployment template doesn't expose an
init-containers extension point per service, and patching it via a
post-render kustomize patch would diverge from the upstream chart's
contract. A separate Helm hook keeps the override surgical.
NB: still keep `templates/database-secret.yaml` as a pre-install
placeholder (so `kubectl get secret harbor-database-secret -n harbor`
returns Found before this Job populates it; some tooling assumes
the Secret is named for the lifetime of the release). Reflector
annotations are kept harmless — they just become a redundant
no-op once this Job has populated the Secret.
*/}}
{{- $ns := .Values.postgres.cluster.namespace | default .Release.Namespace }}
{{- $clusterName := .Values.postgres.cluster.name | default "harbor-pg" }}
apiVersion: batch/v1
kind: Job
metadata:
name: harbor-database-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-harbor.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": "post-install,post-upgrade"
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
spec:
backoffLimit: 6
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app.kubernetes.io/name: harbor-database-secret-sync
spec:
restartPolicy: OnFailure
serviceAccountName: harbor-database-secret-sync
imagePullSecrets:
- name: ghcr-pull
containers:
- name: sync
# curlimages/curl is a tiny alpine-based image with curl + sh,
# avoiding kubectl entirely. We talk to the k8s API directly via
# the in-pod ServiceAccount token. Earlier attempts:
# - bitnami/kubectl:1.31.4 — bitnami moved to sha256-only tags
# - rancher/kubectl:v1.34.6 — distroless, no /bin/sh; container
# can't run our inline shell script.
# Fix #163 (2026-05-11, MIRROR-EVERYTHING): explicit
# harbor.openova.io/proxy-dockerhub prefix per CLAUDE.md
# inviolable rule.
image: harbor.openova.io/proxy-dockerhub/curlimages/curl:8.10.1
imagePullPolicy: IfNotPresent
env:
- name: SOURCE_SECRET
value: {{ printf "%s-app" $clusterName | quote }}
- name: DEST_SECRET
value: "harbor-database-secret"
- name: NAMESPACE
value: {{ $ns | quote }}
command:
- /bin/sh
- -c
- |
set -eu
# In-cluster k8s API access via projected ServiceAccount token.
APISERVER="https://kubernetes.default.svc"
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
echo "Waiting for ${SOURCE_SECRET} in ${NAMESPACE}…"
for i in $(seq 1 120); do
code=$(curl -s -o /tmp/src.json -w '%{http_code}' --cacert "${CA}" \
-H "Authorization: Bearer ${TOKEN}" \
"${APISERVER}/api/v1/namespaces/${NAMESPACE}/secrets/${SOURCE_SECRET}")
if [ "${code}" = "200" ]; then
echo "Found ${SOURCE_SECRET}"; break
fi
if [ "$i" = "120" ]; then
echo "Timeout: ${SOURCE_SECRET} not present after 10 min" >&2
exit 1
fi
sleep 5
done
# Extract base64 password from possibly multi-line JSON.
# k8s apiserver doesn't pretty-print but other proxies might;
# tr -d '\n' makes the next grep robust.
PWD_B64=$(tr -d '\n' < /tmp/src.json | grep -oE '"password"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
if [ -z "${PWD_B64}" ]; then
echo "Source ${SOURCE_SECRET} has empty password" >&2
exit 1
fi
# PATCH the destination Secret merging both keys. Strategic
# merge tolerates the existing pre-install hook annotations.
PATCH=$(printf '{"data":{"password":"%s","HARBOR_DATABASE_PASSWORD":"%s"}}' "${PWD_B64}" "${PWD_B64}")
code=$(curl -s -o /tmp/patch.json -w '%{http_code}' --cacert "${CA}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/strategic-merge-patch+json" \
-X PATCH --data "${PATCH}" \
"${APISERVER}/api/v1/namespaces/${NAMESPACE}/secrets/${DEST_SECRET}")
if [ "${code}" != "200" ] && [ "${code}" != "201" ]; then
echo "PATCH ${DEST_SECRET} returned HTTP ${code}:" >&2
cat /tmp/patch.json >&2
exit 1
fi
echo "Synced ${DEST_SECRET} from ${SOURCE_SECRET}"
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: harbor-database-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-harbor.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": "post-install,post-upgrade"
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": "before-hook-creation"
imagePullSecrets:
- name: ghcr-pull
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: harbor-database-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-harbor.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": "post-install,post-upgrade"
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": "before-hook-creation"
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update", "patch", "apply"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: harbor-database-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-harbor.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": "post-install,post-upgrade"
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": "before-hook-creation"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: harbor-database-secret-sync
subjects:
- kind: ServiceAccount
name: harbor-database-secret-sync
namespace: {{ $ns }}