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>
231 lines
11 KiB
YAML
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 }}
|