openova/platform/wordpress-tenant/chart/templates/deployment.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

231 lines
11 KiB
YAML

{{- if .Values.wordpress.enabled }}
{{- /*
WordPress Deployment.
Boot sequence (per docs/INVIOLABLE-PRINCIPLES.md #2 — ship target-state shape, no stubs):
1. initContainer `wp-content-bootstrap`: copies the official image's
baked-in `/var/www/html/wp-content/` (themes, plugins, mu-plugins
skeleton) onto the empty PVC ON FIRST INSTALL ONLY. The official
wordpress:* image bakes wp-content into the image; mounting an
empty PVC over `/var/www/html/wp-content` would otherwise hide
those bytes. The init container detects an unseeded PVC by looking
for `themes/` and seeds it from the image once, idempotently.
2. initContainer `wp-oidc-plugin-install`: downloads + extracts
`openid-connect-generic` (latest stable) from
downloads.wordpress.org into `wp-content/plugins/` on the PVC.
Idempotent — skips if the plugin dir already exists.
3. main container `wordpress`: the official image starts; its
entrypoint writes `wp-config.php` from WORDPRESS_DB_* env vars,
then `apache2-foreground` boots Apache + mod_php.
The OIDC plugin's option row, the admin user, and the default theme
activation are populated by the post-install Jobs in
templates/oidc-config-job.yaml + templates/admin-user-job.yaml — those
talk to the WordPress install once the Service is reachable, which
keeps this Deployment template free of WP CLI.
*/}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "bp-wordpress-tenant.fullname" . }}
labels:
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.wordpress.replicas }}
selector:
matchLabels:
{{- include "bp-wordpress-tenant.selectorLabels" . | nindent 6 }}
strategy:
# ReadWriteOnce PVC default — Recreate avoids the "two pods can't
# mount the same PVC" deadlock during rollouts. Operators that
# switch persistence to RWX may flip to RollingUpdate via overlay.
type: Recreate
template:
metadata:
labels:
{{- include "bp-wordpress-tenant.selectorLabels" . | nindent 8 }}
annotations:
# Roll the pod when the OIDC config / DB secret name changes.
checksum/oidc-job: {{ include (print $.Template.BasePath "/oidc-config-job.yaml") . | sha256sum }}
spec:
serviceAccountName: {{ include "bp-wordpress-tenant.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.wordpress.podSecurityContext | nindent 8 }}
initContainers:
# ── 1. Seed wp-content from the image onto the PVC (idempotent) ─
- name: wp-content-bootstrap
image: {{ include "bp-wordpress-tenant.wordpressImage" . | quote }}
imagePullPolicy: {{ .Values.wordpress.image.pullPolicy }}
securityContext:
{{- toYaml .Values.wordpress.containerSecurityContext | nindent 12 }}
command:
- /bin/bash
- -c
- |
set -eu
# If the PVC is empty (first install) seed it from the
# image's baked-in wp-content. We probe for `themes/`
# because the official image always ships at least the
# default theme — if it's missing, the PVC was just
# bound and we need to seed.
if [ ! -d /pvc/themes ]; then
echo "Seeding wp-content/ onto PVC from image..."
cp -a /usr/src/wordpress/wp-content/. /pvc/
chown -R 33:33 /pvc
echo "Seed complete."
else
echo "wp-content/ already populated on PVC — skipping seed."
fi
volumeMounts:
- name: wp-content
mountPath: /pvc
# ── 2. Install plugins from wordpress.org / GitHub onto the PVC ─
# Two plugins are pre-installed at boot, both idempotent
# (skip if already present on the PVC):
#
# a) `openid-connect-generic` — the SSO plugin that drives
# Keycloak OIDC login flow. Source: wordpress.org plugin
# registry.
# b) `pg4wp` (mu-plugin) — translates WordPress's MySQL
# queries to PostgreSQL so the official `wordpress:*`
# image runs against bp-cnpg without a MySQL footprint
# in the SME tenant namespace. Source: PostgreSQL-For-
# Wordpress on GitHub. Installed under wp-content/pg4wp
# and chain-loaded via wp-content/db.php drop-in.
- name: wp-plugin-install
image: {{ include "bp-wordpress-tenant.wordpressImage" . | quote }}
imagePullPolicy: {{ .Values.wordpress.image.pullPolicy }}
securityContext:
{{- toYaml .Values.wordpress.containerSecurityContext | nindent 12 }}
command:
- /bin/bash
- -c
- |
set -eu
# apt: ensure unzip + curl present (curl is shipped, unzip
# may not be on a slim image variant; runs only on first
# install per PVC, so the cost is a one-time apt fetch).
if ! command -v unzip >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends unzip
fi
# ── (a) openid-connect-generic plugin ──────────────────
OIDC_DIR=/pvc/plugins/openid-connect-generic
if [ -d "${OIDC_DIR}" ]; then
echo "openid-connect-generic already installed — skipping."
else
echo "Downloading openid-connect-generic..."
cd /tmp
curl -fsSL -o oidc.zip \
https://downloads.wordpress.org/plugin/openid-connect-generic.latest-stable.zip
mkdir -p /pvc/plugins
unzip -q oidc.zip -d /pvc/plugins/
rm -f /tmp/oidc.zip
echo "openid-connect-generic installed."
fi
# ── (b) pg4wp drop-in (Postgres adapter) ───────────────
# bp-wordpress-tenant runs WordPress against bp-cnpg
# Postgres (per ticket #800 scope). pg4wp ships a
# `wp-content/db.php` drop-in + `pg4wp/` library that
# together intercept `wpdb` at the PHP level and map
# MySQL queries to Postgres. Repo:
# https://github.com/PostgreSQL-For-Wordpress/postgresql-for-wordpress
PG4WP_DIR=/pvc/pg4wp
if [ -d "${PG4WP_DIR}" ] && [ -f /pvc/db.php ]; then
echo "pg4wp already installed — skipping."
else
echo "Downloading pg4wp..."
cd /tmp
# Pin to a stable tag, not main, per
# docs/INVIOLABLE-PRINCIPLES.md #4 (no floating refs).
curl -fsSL -o pg4wp.zip \
https://github.com/PostgreSQL-For-Wordpress/postgresql-for-wordpress/archive/refs/tags/v3.7.0.zip
unzip -q pg4wp.zip -d /tmp/
# Extracted layout:
# /tmp/postgresql-for-wordpress-3.7.0/pg4wp/
# /tmp/postgresql-for-wordpress-3.7.0/pg4wp/db.php
# Copy `pg4wp/` into wp-content/ and the `db.php`
# drop-in alongside it (WP looks for db.php at
# wp-content/db.php).
cp -a /tmp/postgresql-for-wordpress-3.7.0/pg4wp /pvc/
cp /tmp/postgresql-for-wordpress-3.7.0/pg4wp/db.php /pvc/db.php
rm -rf /tmp/postgresql-for-wordpress-3.7.0 /tmp/pg4wp.zip
echo "pg4wp installed."
fi
chown -R 33:33 /pvc
volumeMounts:
- name: wp-content
mountPath: /pvc
containers:
- name: wordpress
image: {{ include "bp-wordpress-tenant.wordpressImage" . | quote }}
imagePullPolicy: {{ .Values.wordpress.image.pullPolicy }}
securityContext:
{{- toYaml .Values.wordpress.containerSecurityContext | nindent 12 }}
ports:
- name: http
containerPort: {{ .Values.wordpress.port }}
protocol: TCP
env:
# ── Database connection (CNPG-emitted reflector secret) ─
- name: WORDPRESS_DB_HOST
value: {{ include "bp-wordpress-tenant.cnpgRwHost" . | quote }}
- name: WORDPRESS_DB_NAME
value: {{ .Values.database.cluster.database | default "wordpress" | quote }}
- name: WORDPRESS_DB_USER
value: {{ .Values.database.cluster.owner | default "wordpress" | quote }}
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.database.secretName }}
key: password
# ── Site URL (used by WP to construct admin/login links) ─
- name: WORDPRESS_CONFIG_EXTRA
value: |
define('WP_HOME', 'https://{{ include "bp-wordpress-tenant.ingressHost" . }}');
define('WP_SITEURL', 'https://{{ include "bp-wordpress-tenant.ingressHost" . }}');
/* SSL terminates at the ingress; tell WP to honour the
X-Forwarded-Proto header so login redirects use https. */
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
livenessProbe:
httpGet:
path: {{ .Values.wordpress.probes.liveness.path }}
port: http
httpHeaders:
- name: Host
value: {{ include "bp-wordpress-tenant.ingressHost" . | quote }}
initialDelaySeconds: {{ .Values.wordpress.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.wordpress.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.wordpress.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.wordpress.probes.liveness.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.wordpress.probes.readiness.path }}
port: http
httpHeaders:
- name: Host
value: {{ include "bp-wordpress-tenant.ingressHost" . | quote }}
initialDelaySeconds: {{ .Values.wordpress.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.wordpress.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.wordpress.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.wordpress.probes.readiness.failureThreshold }}
resources:
{{- toYaml .Values.wordpress.resources | nindent 12 }}
volumeMounts:
- name: wp-content
mountPath: /var/www/html/wp-content
volumes:
- name: wp-content
{{- if .Values.persistence.wpContent.enabled }}
persistentVolumeClaim:
claimName: {{ include "bp-wordpress-tenant.fullname" . }}-wp-content
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}