openova/platform/wordpress-tenant/chart/templates/database-secret-sync-job.yaml
e3mrah c141fcd1d3
feat(bp-wordpress-tenant): turnkey SSO-wired WordPress per SME (#800) (#811)
New scratch Blueprint chart `bp-wordpress-tenant` v0.1.0 that
provisions a turnkey, SSO-pre-wired WordPress instance per SME tenant
inside the SME's vcluster, satisfying ticket #800 (SME-5) of the #795
SME-tenant turnkey experience epic.

What it provisions:

  - Deployment of `wordpress:6-php8.3-apache` (manifest-list digest
    sha256:054e611...196), pulled through the Sovereign Harbor
    proxy-cache when `global.imageRegistry` is set (per
    INVIOLABLE-PRINCIPLES #4).
  - Two initContainers seed wp-content/ from the image onto the PVC
    and install the openid-connect-generic plugin + pg4wp Postgres
    drop-in from wordpress.org / GitHub. Idempotent, runs only once
    per PVC.
  - Postgres provisioned in-tenant via a `Cluster.postgresql.cnpg.io`
    (default `wordpress-db`, 1 instance, 10Gi, pg16). The CNPG-emitted
    `<cluster>-app` Secret is mirrored into `wordpress-database-secret`
    by Reflector + a post-install sync Job (otech30 race fix carried
    forward from bp-gitea).
  - PVC for `/var/www/html/wp-content/` (default 10Gi, RWO,
    helm.sh/resource-policy: keep so customer content survives
    `helm uninstall`).
  - Ingress at `wordpress.<smeDomain>` with cert-manager TLS via
    operator-supplied ClusterIssuer (default `letsencrypt-prod`).
  - NetworkPolicy restricting egress to bp-cnpg :5432, Keycloak
    :8443/:8080, kube-dns, and HTTPS to public IPs (for plugin/theme
    fetches).
  - Three post-install Jobs:
      hook weight 5  — db-secret-sync (PATCHes wordpress-database-
                       secret.password from CNPG <cluster>-app)
      hook weight 10 — oidc-config (UPSERTs openid_connect_generic_
                       settings, active_plugins, template/stylesheet,
                       siteurl/home rows in wp_options via PHP+PDO)
      hook weight 15 — admin-user (INSERT/UPDATE wp_users +
                       wp_usermeta for SME admin's email with
                       administrator role)

After all hooks complete, the SME admin's first browser hit lands on
/wp-admin authenticated via Keycloak SSO — no install wizard, no
manual config.

Hollow-chart guard (issue #181) satisfied via the `common` library
subchart from sigstore, matching bp-newapi's pattern for scratch
charts (no first-party WordPress Helm chart exists upstream).

Tests:
  - chart/tests/observability-toggle.sh verifies BLUEPRINT-AUTHORING
    §11.2 (default render produces no PodMonitor/ServiceMonitor).
  - `helm template` smoke render with required values produces 11 K8s
    resources cleanly; `helm lint` zero-failure.

Refs: #800, #795

Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
2026-05-04 22:13:32 +04:00

153 lines
6.0 KiB
YAML

{{- if and .Values.wordpress.enabled .Values.database.cluster.enabled }}
{{- /*
Helm post-install Job that copies the CNPG-emitted `<cluster>-app`
Secret into `wordpress-database-secret` so the WordPress Pod's
WORDPRESS_DB_PASSWORD env binding finds the expected `password` key.
WHY A JOB INSTEAD OF RELYING ON RELFLECTOR ALONE
────────────────────────────────────────────────
The Reflector-based path can race when CNPG creates
`<cluster>-app` AFTER `wordpress-database-secret` is processed; on
otech30 (caught live, see bp-gitea database-secret-sync-job.yaml)
Reflector logged "Source could not be found" once and never retried.
The post-install Job waits for CNPG to provision the source Secret,
then PATCHes the destination, eliminating the watcher race.
Pattern lifted from platform/gitea/chart/templates/database-secret-
sync-job.yaml (canonical seam — see ADR-0001 §11.3 anti-duplication).
*/}}
{{- $ns := include "bp-wordpress-tenant.cnpgNamespace" . }}
{{- $clusterName := .Values.database.cnpgClusterName }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
catalyst.openova.io/component: wordpress-database-secret-sync
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: wordpress-database-secret-sync
catalyst.openova.io/blueprint: bp-wordpress-tenant
spec:
restartPolicy: OnFailure
serviceAccountName: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
containers:
- name: sync
# curlimages/curl matches bp-gitea (no kubectl image
# required — we talk to the apiserver via the in-pod
# ServiceAccount token).
image: curlimages/curl:8.10.1
imagePullPolicy: IfNotPresent
env:
- name: SOURCE_SECRET
value: {{ printf "%s-app" $clusterName | quote }}
- name: DEST_SECRET
value: {{ .Values.database.secretName | quote }}
- name: NAMESPACE
value: {{ $ns | quote }}
command:
- /bin/sh
- -c
- |
set -eu
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
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=$(printf '{"data":{"password":"%s"}}' "${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: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": "post-install,post-upgrade"
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": "before-hook-creation"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-wordpress-tenant.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: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
namespace: {{ $ns }}
labels:
{{- include "bp-wordpress-tenant.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: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
subjects:
- kind: ServiceAccount
name: {{ include "bp-wordpress-tenant.fullname" . }}-db-secret-sync
namespace: {{ $ns }}
{{- end }}