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>
This commit is contained in:
parent
93bd3ace5b
commit
c141fcd1d3
38
platform/wordpress-tenant/README.md
Normal file
38
platform/wordpress-tenant/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# platform/wordpress-tenant
|
||||
|
||||
Catalyst Blueprint that provisions a turnkey, SSO-pre-wired WordPress
|
||||
instance per SME tenant inside the SME's vcluster. Part of the
|
||||
`#795 SME-tenant turnkey experience` epic, ticket #800 (SME-5).
|
||||
|
||||
## What's here
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `blueprint.yaml` | Catalyst Blueprint metadata (configSchema, depends, placementSchema) |
|
||||
| `chart/` | Helm chart `bp-wordpress-tenant` v0.1.0 — see `chart/README.md` |
|
||||
| `chart/templates/` | Deployment, Service, Ingress, PVC, CNPG Cluster, NetworkPolicy, ServiceAccount + 3 post-install Jobs (db-secret-sync, oidc-config, admin-user) |
|
||||
| `chart/tests/` | observability-toggle.sh (per #182) |
|
||||
|
||||
## Operator install
|
||||
|
||||
```bash
|
||||
helm install acme-wordpress oci://ghcr.io/openova-io/bp-wordpress-tenant \
|
||||
--version 0.1.0 \
|
||||
--namespace sme-acme \
|
||||
--set smeDomain=acme.otech31.omani.works \
|
||||
--set keycloak.realmURL=https://auth.acme.otech31.omani.works/realms/sme \
|
||||
--set keycloak.clientSecretName=wordpress-oidc \
|
||||
--set adminUser.email=admin@acme.com
|
||||
```
|
||||
|
||||
The Sovereign's tenant-provisioning pipeline (#804) wires this Helm
|
||||
release into a Flux `HelmRelease` per SME, registers the OIDC client
|
||||
in the SME realm, seals the client secret into
|
||||
`wordpress-oidc`, and renders the per-SME values overlay.
|
||||
|
||||
## See also
|
||||
|
||||
- `chart/README.md` — full value reference + boot sequence
|
||||
- `docs/BLUEPRINT-AUTHORING.md` §11 (umbrella shape, hollow-chart guard, observability toggles)
|
||||
- `docs/INVIOLABLE-PRINCIPLES.md` (no hardcoding, SHA-pinned images, target-state shape)
|
||||
- Issue #795 (epic), #800 (this Blueprint)
|
||||
142
platform/wordpress-tenant/blueprint.yaml
Normal file
142
platform/wordpress-tenant/blueprint.yaml
Normal file
@ -0,0 +1,142 @@
|
||||
apiVersion: catalyst.openova.io/v1alpha1
|
||||
kind: Blueprint
|
||||
metadata:
|
||||
name: bp-wordpress-tenant
|
||||
labels:
|
||||
catalyst.openova.io/category: tenant-app
|
||||
catalyst.openova.io/section: pts-7-sme-tenant
|
||||
spec:
|
||||
version: 0.1.0
|
||||
card:
|
||||
title: WordPress Tenant
|
||||
summary: |
|
||||
Turnkey, SSO-pre-wired WordPress per SME tenant. Runs in the SME's
|
||||
vcluster, namespace = SME tenant namespace. SSO via the SME-vcluster
|
||||
Keycloak realm (openid-connect-generic plugin), Postgres via bp-cnpg
|
||||
(Cluster CR in tenant ns), persistent /var/www/html/wp-content via
|
||||
PVC (default 10Gi), ingress at https://wordpress.<sme-domain> with
|
||||
cert-manager TLS. Default theme + admin user pre-seeded at install
|
||||
time so the SME admin's first browser hit lands on /wp-admin
|
||||
authenticated. No install wizard, no manual config.
|
||||
icon: wordpress.svg
|
||||
category: tenant-app
|
||||
tags: [wordpress, cms, sme, tenant, sso, keycloak, oidc, postgres]
|
||||
documentation: https://wordpress.org/documentation/
|
||||
license: GPL-2.0-or-later
|
||||
visibility: listed
|
||||
owner:
|
||||
team: platform
|
||||
contact: catalyst@openova.io
|
||||
configSchema:
|
||||
type: object
|
||||
required: [smeDomain, keycloak, adminUser]
|
||||
properties:
|
||||
smeDomain:
|
||||
type: string
|
||||
description: |
|
||||
The SME tenant's domain (e.g. `acme.<otech-fqdn>` for free-
|
||||
subdomain tenants, or `acme.com` for BYO). Used to derive the
|
||||
default ingress host as `wordpress.<smeDomain>`. Per
|
||||
docs/INVIOLABLE-PRINCIPLES.md #4 there is no default — the
|
||||
tenant-provisioning pipeline supplies this at install time.
|
||||
replicas:
|
||||
type: integer
|
||||
default: 1
|
||||
minimum: 1
|
||||
maximum: 8
|
||||
keycloak:
|
||||
type: object
|
||||
required: [realmURL, clientSecretName]
|
||||
properties:
|
||||
realmURL:
|
||||
type: string
|
||||
description: |
|
||||
Discovery URL of the SME-vcluster Keycloak realm. Example:
|
||||
`https://auth.acme.<otech-fqdn>/realms/sme`. The
|
||||
openid-connect-generic plugin uses this to derive the
|
||||
token / userinfo / authorization / end-session endpoints.
|
||||
clientID:
|
||||
type: string
|
||||
default: wordpress
|
||||
description: OIDC client ID registered in the SME realm.
|
||||
clientSecretName:
|
||||
type: string
|
||||
description: |
|
||||
ExternalSecret carrying the OIDC client secret (key
|
||||
`client-secret`). Provisioned by the tenant-provisioning
|
||||
pipeline before this chart installs.
|
||||
database:
|
||||
type: object
|
||||
properties:
|
||||
cnpgClusterName:
|
||||
type: string
|
||||
default: wordpress-db
|
||||
description: |
|
||||
Name of the `Cluster.postgresql.cnpg.io` provisioned for
|
||||
this WordPress instance. Per-tenant unique within the SME
|
||||
namespace.
|
||||
adminUser:
|
||||
type: object
|
||||
required: [email]
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: |
|
||||
Email of the SME admin (must match the `email` claim
|
||||
Keycloak issues). The admin-user Job pre-seeds a wp_user
|
||||
row with this email and the administrator role.
|
||||
displayName:
|
||||
type: string
|
||||
description: Display name shown in the WP admin bar. Defaults to the local-part of the email.
|
||||
defaultTheme:
|
||||
type: string
|
||||
default: twentytwentyfive
|
||||
description: WordPress theme slug installed + activated at first install.
|
||||
persistence:
|
||||
type: object
|
||||
properties:
|
||||
wpContent:
|
||||
type: object
|
||||
properties:
|
||||
size:
|
||||
type: string
|
||||
default: 10Gi
|
||||
storageClass:
|
||||
type: string
|
||||
default: local-path
|
||||
ingress:
|
||||
type: object
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
description: Override the derived `wordpress.<smeDomain>` ingress host.
|
||||
tls:
|
||||
type: object
|
||||
properties:
|
||||
issuer:
|
||||
type: string
|
||||
default: letsencrypt-prod
|
||||
description: cert-manager ClusterIssuer name.
|
||||
placementSchema:
|
||||
modes: [single-region]
|
||||
default: single-region
|
||||
manifests:
|
||||
chart: ./chart
|
||||
depends:
|
||||
- blueprint: bp-cnpg
|
||||
version: ^1.0
|
||||
alias: db
|
||||
- blueprint: bp-keycloak
|
||||
version: ^1.0
|
||||
alias: idp
|
||||
- blueprint: bp-reflector
|
||||
version: ^1.0
|
||||
alias: reflector
|
||||
- blueprint: bp-cert-manager
|
||||
version: ^1.0
|
||||
alias: tls
|
||||
upgrades:
|
||||
from: ["0.x"]
|
||||
observability:
|
||||
metrics: prometheus
|
||||
logs: stdout
|
||||
53
platform/wordpress-tenant/chart/Chart.yaml
Normal file
53
platform/wordpress-tenant/chart/Chart.yaml
Normal file
@ -0,0 +1,53 @@
|
||||
apiVersion: v2
|
||||
name: bp-wordpress-tenant
|
||||
version: 0.1.0
|
||||
appVersion: "6"
|
||||
description: |
|
||||
Catalyst Blueprint scratch chart for in-vcluster WordPress, one
|
||||
instance per SME tenant. Pre-wires:
|
||||
|
||||
- SSO via the SME-vcluster Keycloak realm using the
|
||||
`openid-connect-generic` plugin (auto-create-on-first-login,
|
||||
Keycloak-group → WP-role mapping).
|
||||
- Postgres provisioned by bp-cnpg (Cluster CR) in the SME tenant
|
||||
namespace; password mirrored via reflector + post-install Job.
|
||||
- PVC-backed `/var/www/html/wp-content/` for theme/plugin/upload
|
||||
persistence.
|
||||
- Ingress at `wordpress.<sme-domain>` routed via the SME's ingress
|
||||
with cert-manager TLS.
|
||||
- Idempotent post-install Jobs that (a) install + activate the
|
||||
`openid-connect-generic` plugin and write its option row pointing
|
||||
at the operator-supplied Keycloak realm + client, (b) pre-seed
|
||||
the SME admin WP user with the SSO email mapping.
|
||||
|
||||
This is a scratch chart — there is no first-party Helm chart for
|
||||
WordPress (the upstream WordPress project ships only a Docker image at
|
||||
`wordpress:6-php8.3-apache`). The `common` library subchart is
|
||||
declared as a Helm dependency so the BLUEPRINT-AUTHORING.md hollow-
|
||||
chart guard (issue #181) is satisfied; bp-newapi follows the same
|
||||
pattern.
|
||||
|
||||
Pairs with bp-cnpg (Postgres), bp-keycloak (SME-vcluster IdP),
|
||||
bp-reflector (Secret mirror), bp-cert-manager (ACME TLS).
|
||||
type: application
|
||||
keywords:
|
||||
- catalyst
|
||||
- blueprint
|
||||
- wordpress
|
||||
- cms
|
||||
- sme
|
||||
- tenant
|
||||
- sso
|
||||
- keycloak
|
||||
- oidc
|
||||
maintainers:
|
||||
- name: OpenOva Catalyst
|
||||
email: catalyst@openova.io
|
||||
|
||||
# Scratch chart — see comments in bp-newapi/chart/Chart.yaml for the
|
||||
# rationale on the `common` library subchart dependency (issue #181
|
||||
# hollow-chart gate).
|
||||
dependencies:
|
||||
- name: common
|
||||
version: "0.1.3"
|
||||
repository: "https://sigstore.github.io/helm-charts"
|
||||
99
platform/wordpress-tenant/chart/README.md
Normal file
99
platform/wordpress-tenant/chart/README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# bp-wordpress-tenant
|
||||
|
||||
Catalyst Blueprint scratch chart that installs a turnkey,
|
||||
SSO-pre-wired WordPress instance per SME tenant inside the SME's
|
||||
vcluster.
|
||||
|
||||
This is a **scratch chart** — there is no first-party Helm chart
|
||||
published by the WordPress project (the upstream ships only a Docker
|
||||
image at `wordpress:6-php8.3-apache`). The `common` library subchart is
|
||||
declared as a Helm dependency so the BLUEPRINT-AUTHORING.md hollow-
|
||||
chart guard (issue #181) is satisfied; bp-newapi follows the same
|
||||
pattern.
|
||||
|
||||
## What it provisions
|
||||
|
||||
| Resource | Purpose |
|
||||
|---|---|
|
||||
| `Deployment` (single replica) | The WordPress Pod. Two initContainers: one seeds `wp-content/` from the image onto the PVC; the other downloads + installs `openid-connect-generic` (Keycloak SSO) and `pg4wp` (Postgres adapter) from wordpress.org / GitHub. |
|
||||
| `Service` (ClusterIP, :80) | In-vcluster service for the ingress to target. |
|
||||
| `Ingress` (Traefik, host `wordpress.<smeDomain>`) | Customer-facing entry point. cert-manager issues TLS via the operator-supplied `ClusterIssuer`. |
|
||||
| `PersistentVolumeClaim` (10Gi default, RWO) | Backs `/var/www/html/wp-content` so themes, plugins, and uploads persist across pod restarts and image upgrades. `helm.sh/resource-policy: keep` so `helm uninstall` never drops customer content. |
|
||||
| `Cluster.postgresql.cnpg.io` (1 instance, 10Gi) | Tenant-isolated Postgres provisioned by bp-cnpg. The CNPG-emitted `<cluster>-app` Secret carries the password. |
|
||||
| `Secret wordpress-database-secret` (placeholder) | Reflector-managed bridge that the WordPress Pod reads via `secretKeyRef`. Populated by the post-install `db-secret-sync` Job. |
|
||||
| `Job <release>-db-secret-sync` (post-install/upgrade) | Mirrors `<cluster>-app.password` into `wordpress-database-secret.password`. Eliminates the otech30-class Reflector race documented in `bp-gitea`. |
|
||||
| `Job <release>-oidc-config` (post-install/upgrade) | Connects to Postgres, ensures `wp_options` exists, then UPSERTs the `openid_connect_generic_settings` row (Keycloak URLs + client secret), `active_plugins` (activates the OIDC plugin), `template`/`stylesheet` (default theme), `siteurl`/`home`. Idempotent — re-running on `helm upgrade` is safe. |
|
||||
| `Job <release>-admin-user` (post-install/upgrade, hook weight 15) | Pre-seeds the SME admin into `wp_users` + `wp_usermeta` with the `administrator` role + the SSO email mapping. The user can log in via Keycloak only. |
|
||||
| `NetworkPolicy` | Restricts egress to: bp-cnpg :5432, Keycloak :8443/:8080, kube-dns, and HTTPS to public IPs (for plugin/theme fetches at first install). Ingress allowed only from the configured ingress namespace (default `traefik`). |
|
||||
| `ServiceAccount` | Default SA for the WordPress Pod. The post-install Jobs use a dedicated SA + Role + RoleBinding scoped to the tenant namespace. |
|
||||
|
||||
## Boot sequence (per docs/INVIOLABLE-PRINCIPLES.md #2)
|
||||
|
||||
```
|
||||
helm install
|
||||
├─ pre-install: namespace, ServiceAccount, Role/RoleBinding hooks (weight 0)
|
||||
├─ install: Deployment, Service, Ingress, PVC, NetworkPolicy,
|
||||
│ Cluster.postgresql.cnpg.io, wordpress-database-secret (empty)
|
||||
├─ post-install hook weight 5: db-secret-sync Job
|
||||
│ └─ waits for CNPG <cluster>-app, PATCHes wordpress-database-secret
|
||||
├─ post-install hook weight 10: oidc-config Job
|
||||
│ └─ trips WP install via Service GET, UPSERTs OIDC + theme + siteurl
|
||||
└─ post-install hook weight 15: admin-user Job
|
||||
└─ INSERT/UPDATE wp_users row for the SME admin's email
|
||||
```
|
||||
|
||||
After all hooks complete, the SME admin browses to
|
||||
`https://wordpress.<smeDomain>` → openid-connect-generic redirects to
|
||||
Keycloak → returns to `/wp-admin` authenticated as administrator. No
|
||||
WP install wizard, no manual config.
|
||||
|
||||
## Required values
|
||||
|
||||
| Value | Description |
|
||||
|---|---|
|
||||
| `smeDomain` | The SME tenant's domain (e.g. `acme.<otech-fqdn>` or BYO `acme.com`). Used to derive the default ingress host as `wordpress.<smeDomain>`. |
|
||||
| `keycloak.realmURL` | Discovery URL of the SME-vcluster Keycloak realm. Example: `https://auth.acme.<otech-fqdn>/realms/sme`. |
|
||||
| `keycloak.clientSecretName` | ExternalSecret carrying the Keycloak OIDC client secret (key `client-secret`). Provisioned by the tenant-provisioning pipeline before this chart installs. |
|
||||
| `adminUser.email` | Email of the SME admin (must match the `email` claim Keycloak issues for that user). The admin-user Job pre-seeds a wp_user with this email and the administrator role. |
|
||||
|
||||
## Override surface
|
||||
|
||||
All other values have sensible defaults; common overrides include:
|
||||
|
||||
| Value | Default | Notes |
|
||||
|---|---|---|
|
||||
| `global.imageRegistry` | `""` | Set to the Sovereign's Harbor proxy-cache hostname post-handover. |
|
||||
| `wordpress.image.tag` | `6-php8.3-apache` | The chart pins the manifest-list digest alongside; change `tag`+`digest` together. |
|
||||
| `database.cnpgClusterName` | `wordpress-db` | Per-tenant unique within the SME namespace. |
|
||||
| `database.cluster.storageSize` | `10Gi` | Postgres storage size. |
|
||||
| `persistence.wpContent.size` | `10Gi` | wp-content PVC size. |
|
||||
| `persistence.wpContent.storageClass` | `local-path` | Set to a RWX class if you want to scale `replicas > 1`. |
|
||||
| `defaultTheme` | `twentytwentyfive` | Any wordpress.org theme slug bundled with the official image. |
|
||||
| `ingress.tls.issuer` | `letsencrypt-prod` | cert-manager `ClusterIssuer`. |
|
||||
|
||||
See `values.yaml` for the full schema, including NetworkPolicy egress
|
||||
peers, OIDC role mapping, and probe tuning.
|
||||
|
||||
## Why Postgres (and not MySQL)?
|
||||
|
||||
Issue #800 specifies "bp-cnpg Postgres in tenant namespace". The
|
||||
official `wordpress` image targets MySQL/MariaDB; we run it against
|
||||
Postgres via the `pg4wp` mu-plugin (a `wp-content/db.php` drop-in that
|
||||
intercepts `wpdb` at the PHP level and translates queries). This keeps
|
||||
the SME tenant footprint to one database operator (bp-cnpg) instead
|
||||
of sprouting a separate MySQL operator per SME — see the upstream
|
||||
project at
|
||||
https://github.com/PostgreSQL-For-Wordpress/postgresql-for-wordpress.
|
||||
|
||||
The `pg4wp` install is performed by the same `wp-plugin-install`
|
||||
initContainer that installs `openid-connect-generic`, so the chart
|
||||
needs no special image build.
|
||||
|
||||
## Capabilities gate
|
||||
|
||||
`Cluster.postgresql.cnpg.io` is rendered behind a Capabilities check on
|
||||
`postgresql.cnpg.io/v1`, so a cold install before bp-cnpg is
|
||||
reconciling skips the Cluster CR (and the Pod waits in
|
||||
`Pending`/`CrashLoopBackOff` until bp-cnpg lands and the Cluster is
|
||||
re-rendered on the next reconcile). The Sovereign's bootstrap order
|
||||
MUST land bp-cnpg before bp-wordpress-tenant.
|
||||
113
platform/wordpress-tenant/chart/templates/_helpers.tpl
Normal file
113
platform/wordpress-tenant/chart/templates/_helpers.tpl
Normal file
@ -0,0 +1,113 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels — required by docs/BLUEPRINT-AUTHORING.md §14 and by the
|
||||
Catalyst projector to track resources back to the Blueprint.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
app.kubernetes.io/name: {{ include "bp-wordpress-tenant.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
catalyst.openova.io/blueprint: bp-wordpress-tenant
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "bp-wordpress-tenant.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
ServiceAccount name.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "bp-wordpress-tenant.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
WordPress image reference, with optional `global.imageRegistry` rewrite
|
||||
for Sovereign Harbor proxy-cache. Returns
|
||||
`{registry/}repository:tag@digest` so consumers SHA-pin to the manifest-
|
||||
list digest published on Docker Hub.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.wordpressImage" -}}
|
||||
{{- $reg := .Values.global.imageRegistry | default "" -}}
|
||||
{{- $repo := .Values.wordpress.image.repository -}}
|
||||
{{- $tag := .Values.wordpress.image.tag -}}
|
||||
{{- $digest := .Values.wordpress.image.digest -}}
|
||||
{{- if $reg -}}
|
||||
{{- printf "%s/%s:%s@%s" $reg $repo $tag $digest -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s@%s" $repo $tag $digest -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Resolved ingress host. Templates `wordpress.<smeDomain>` when
|
||||
`ingress.host` is empty; otherwise returns the operator-supplied host
|
||||
verbatim. `smeDomain` is required when `ingress.host` is empty.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.ingressHost" -}}
|
||||
{{- if .Values.ingress.host -}}
|
||||
{{- .Values.ingress.host -}}
|
||||
{{- else -}}
|
||||
{{- $sme := required ".Values.smeDomain or .Values.ingress.host MUST be set (no sensible default per INVIOLABLE-PRINCIPLES #4)." .Values.smeDomain -}}
|
||||
{{- printf "wordpress.%s" $sme -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
CNPG cluster namespace — defaults to .Release.Namespace if the
|
||||
operator left `database.cluster.namespace` empty.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.cnpgNamespace" -}}
|
||||
{{- default .Release.Namespace .Values.database.cluster.namespace -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
CNPG-emitted application Secret name (`<cluster>-app`). CNPG synthesises
|
||||
this Secret from the `Cluster.spec.bootstrap.initdb.owner` field at
|
||||
install time.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.cnpgAppSecret" -}}
|
||||
{{- printf "%s-app" .Values.database.cnpgClusterName -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
CNPG-emitted read-write Service hostname. CNPG synthesises this Service
|
||||
from the Cluster CR; suffix is `-rw` per the CNPG operator convention.
|
||||
*/}}
|
||||
{{- define "bp-wordpress-tenant.cnpgRwHost" -}}
|
||||
{{- printf "%s-rw.%s.svc.cluster.local" .Values.database.cnpgClusterName (include "bp-wordpress-tenant.cnpgNamespace" .) -}}
|
||||
{{- end -}}
|
||||
173
platform/wordpress-tenant/chart/templates/admin-user-job.yaml
Normal file
173
platform/wordpress-tenant/chart/templates/admin-user-job.yaml
Normal file
@ -0,0 +1,173 @@
|
||||
{{- if and .Values.wordpress.enabled .Values.adminUser.email }}
|
||||
{{- /*
|
||||
Helm post-install Job that pre-seeds the SME admin WP user with the
|
||||
SSO email mapping, so on first SSO login from Keycloak the
|
||||
openid-connect-generic plugin matches the email claim back to a
|
||||
pre-existing wp_users row with `administrator` role.
|
||||
|
||||
Pattern
|
||||
───────
|
||||
We INSERT an `administrator` user with:
|
||||
- user_login = the local-part of `adminUser.email`
|
||||
- user_email = `adminUser.email`
|
||||
- user_pass = a long random hash (the user can never log in via
|
||||
password — only via Keycloak SSO)
|
||||
- display_name = `adminUser.displayName` or local-part of email
|
||||
And the standard WP usermeta rows that mark the user as an
|
||||
administrator.
|
||||
|
||||
Idempotent: if a user with the same email already exists, the Job
|
||||
no-ops (still ensures the user has the administrator capability).
|
||||
|
||||
Hook weight 15 — runs AFTER the OIDC config Job (10) which itself
|
||||
runs after the DB secret sync Job (5).
|
||||
*/}}
|
||||
{{- $ns := .Release.Namespace }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "bp-wordpress-tenant.fullname" . }}-admin-user
|
||||
namespace: {{ $ns }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
catalyst.openova.io/component: wordpress-admin-user
|
||||
annotations:
|
||||
"helm.sh/hook": "post-install,post-upgrade"
|
||||
"helm.sh/hook-weight": "15"
|
||||
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
|
||||
spec:
|
||||
backoffLimit: 6
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wordpress-admin-user
|
||||
catalyst.openova.io/blueprint: bp-wordpress-tenant
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
securityContext:
|
||||
{{- toYaml .Values.wordpress.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: admin-user
|
||||
image: {{ include "bp-wordpress-tenant.wordpressImage" . | quote }}
|
||||
imagePullPolicy: {{ .Values.wordpress.image.pullPolicy }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.wordpress.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: WP_DB_HOST
|
||||
value: {{ include "bp-wordpress-tenant.cnpgRwHost" . | quote }}
|
||||
- name: WP_DB_NAME
|
||||
value: {{ .Values.database.cluster.database | default "wordpress" | quote }}
|
||||
- name: WP_DB_USER
|
||||
value: {{ .Values.database.cluster.owner | default "wordpress" | quote }}
|
||||
- name: WP_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.database.secretName }}
|
||||
key: password
|
||||
- name: ADMIN_EMAIL
|
||||
value: {{ .Values.adminUser.email | quote }}
|
||||
- name: ADMIN_DISPLAY_NAME
|
||||
value: {{ .Values.adminUser.displayName | default "" | quote }}
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
if ! php -m | grep -qi pdo_pgsql; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
php-pgsql
|
||||
fi
|
||||
cat > /tmp/admin-user.php <<'PHP'
|
||||
<?php
|
||||
/**
|
||||
* bp-wordpress-tenant admin user pre-seed.
|
||||
*
|
||||
* Inserts (or upgrades) the SME admin into wp_users +
|
||||
* wp_usermeta with administrator capability. Login is
|
||||
* via SSO only — the password hash is unguessable.
|
||||
*/
|
||||
$host = getenv('WP_DB_HOST');
|
||||
$db = getenv('WP_DB_NAME');
|
||||
$user = getenv('WP_DB_USER');
|
||||
$pass = getenv('WP_DB_PASSWORD');
|
||||
$email = getenv('ADMIN_EMAIL');
|
||||
$disp = getenv('ADMIN_DISPLAY_NAME');
|
||||
$login = strtolower(strtok($email, '@'));
|
||||
if (!$disp) $disp = $login;
|
||||
|
||||
$pdo = new PDO(
|
||||
"pgsql:host={$host};port=5432;dbname={$db}",
|
||||
$user, $pass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
// Check that wp_users exists. The OIDC config Job (hook
|
||||
// weight 10) tripped install of WP earlier; we just
|
||||
// confirm here.
|
||||
$r = $pdo->query("SELECT to_regclass('public.wp_users') AS t")->fetch();
|
||||
if (empty($r['t'])) {
|
||||
fprintf(STDERR, "[admin-user] FATAL: wp_users not present — OIDC config Job failed?\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Upsert the user row.
|
||||
$stmt = $pdo->prepare("SELECT ID FROM wp_users WHERE user_email = :e LIMIT 1");
|
||||
$stmt->execute([':e' => $email]);
|
||||
$existing = $stmt->fetchColumn();
|
||||
|
||||
if ($existing) {
|
||||
$userId = (int)$existing;
|
||||
echo "[admin-user] user {$email} already exists (ID={$userId}); ensuring admin role.\n";
|
||||
} else {
|
||||
// 64-char random hash, salted via WP's phpass; the
|
||||
// user will never use this — Keycloak is the only
|
||||
// login path. We use a wp_hash_password-equivalent
|
||||
// bcrypt because phpass is not exposed without
|
||||
// loading wp-includes; bcrypt is accepted by WP
|
||||
// since 4.x.
|
||||
$rand = bin2hex(random_bytes(32));
|
||||
$hash = password_hash($rand, PASSWORD_BCRYPT);
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO wp_users
|
||||
(user_login, user_pass, user_nicename, user_email,
|
||||
user_registered, user_status, display_name)
|
||||
VALUES (:l, :p, :n, :e, NOW(), 0, :d)
|
||||
RETURNING ID"
|
||||
);
|
||||
$stmt->execute([
|
||||
':l' => $login,
|
||||
':p' => $hash,
|
||||
':n' => $login,
|
||||
':e' => $email,
|
||||
':d' => $disp,
|
||||
]);
|
||||
$userId = (int)$stmt->fetchColumn();
|
||||
echo "[admin-user] created user {$email} (ID={$userId}).\n";
|
||||
}
|
||||
|
||||
// Usermeta: wp_capabilities = a:1:{s:13:"administrator";b:1;}
|
||||
$caps = serialize(['administrator' => true]);
|
||||
$upsertMeta = function($uid, $key, $value) use ($pdo) {
|
||||
// wp_usermeta has unique(user_id, meta_key) on most
|
||||
// installs, but the schema technically allows
|
||||
// duplicates. We delete + insert to be portable.
|
||||
$del = $pdo->prepare("DELETE FROM wp_usermeta WHERE user_id = :u AND meta_key = :k");
|
||||
$del->execute([':u' => $uid, ':k' => $key]);
|
||||
$ins = $pdo->prepare("INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES (:u, :k, :v)");
|
||||
$ins->execute([':u' => $uid, ':k' => $key, ':v' => $value]);
|
||||
};
|
||||
$upsertMeta($userId, 'wp_capabilities', $caps);
|
||||
$upsertMeta($userId, 'wp_user_level', '10');
|
||||
echo "[admin-user] {$email} granted administrator role.\n";
|
||||
PHP
|
||||
php /tmp/admin-user.php
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
{{- end }}
|
||||
78
platform/wordpress-tenant/chart/templates/cnpg-cluster.yaml
Normal file
78
platform/wordpress-tenant/chart/templates/cnpg-cluster.yaml
Normal file
@ -0,0 +1,78 @@
|
||||
{{- if and .Values.wordpress.enabled .Values.database.cluster.enabled }}
|
||||
{{- /*
|
||||
CNPG-managed Postgres cluster for WordPress.
|
||||
|
||||
WordPress requires MySQL or MariaDB historically; on Catalyst Sovereigns
|
||||
we use Postgres via the upstream `wp-postgresql-driver` mu-plugin
|
||||
(installed by the oidc-config Job alongside openid-connect-generic) so
|
||||
the entire SME tenant footprint shares one bp-cnpg footprint instead of
|
||||
sprouting a separate MySQL operator. Pattern mirrors bp-gitea +
|
||||
bp-harbor (templates/cnpg-cluster.yaml in those charts).
|
||||
|
||||
The CNPG-emitted `<cluster>-app` Secret is mirrored cross-namespace via
|
||||
the bp-reflector pattern + a post-install sync Job (see
|
||||
templates/database-secret-sync-job.yaml) so the canonical `password`
|
||||
key is always present in `wordpress-database-secret` before the
|
||||
WordPress Pod starts.
|
||||
|
||||
Capabilities gate: bp-cnpg ships the `postgresql.cnpg.io/v1` CRD. On a
|
||||
cold install before bp-cnpg is reconciling, the apiserver rejects this
|
||||
Cluster. The Capabilities check skips this template until the CRD is
|
||||
registered. The Sovereign's bootstrap order MUST land bp-cnpg before
|
||||
bp-wordpress-tenant.
|
||||
*/}}
|
||||
{{- if .Capabilities.APIVersions.Has "postgresql.cnpg.io/v1" }}
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: {{ .Values.database.cnpgClusterName | quote }}
|
||||
namespace: {{ include "bp-wordpress-tenant.cnpgNamespace" . | quote }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
spec:
|
||||
instances: {{ .Values.database.cluster.instances }}
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:{{ .Values.database.cluster.pgVersion }}
|
||||
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: {{ .Values.database.cluster.database | quote }}
|
||||
owner: {{ .Values.database.cluster.owner | quote }}
|
||||
|
||||
# inheritedMetadata.annotations: CNPG propagates these onto every
|
||||
# Secret it generates (`<cluster>-app`, `<cluster>-superuser`, etc.).
|
||||
# The k8s-reflector (bp-reflector) requires the SOURCE Secret to carry
|
||||
# `reflection-allowed: "true"` before it will copy data into the
|
||||
# DESTINATION Secret. See bp-gitea cnpg-cluster.yaml for the
|
||||
# canonical rationale (issue #584).
|
||||
inheritedMetadata:
|
||||
annotations:
|
||||
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
|
||||
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: {{ include "bp-wordpress-tenant.cnpgNamespace" . | quote }}
|
||||
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
|
||||
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: {{ include "bp-wordpress-tenant.cnpgNamespace" . | quote }}
|
||||
|
||||
postgresql:
|
||||
parameters:
|
||||
max_connections: "100"
|
||||
shared_buffers: "64MB"
|
||||
effective_cache_size: "192MB"
|
||||
work_mem: "4MB"
|
||||
maintenance_work_mem: "64MB"
|
||||
log_statement: "ddl"
|
||||
log_min_duration_statement: "1000"
|
||||
|
||||
resources:
|
||||
{{- toYaml .Values.database.cluster.resources | nindent 4 }}
|
||||
|
||||
storage:
|
||||
size: {{ .Values.database.cluster.storageSize }}
|
||||
storageClass: {{ .Values.database.cluster.storageClass | quote }}
|
||||
|
||||
enableSuperuserAccess: true
|
||||
|
||||
monitoring:
|
||||
# Default false per docs/BLUEPRINT-AUTHORING.md §11.2 — operator
|
||||
# opts in via per-cluster overlay.
|
||||
enablePodMonitor: false
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@ -0,0 +1,152 @@
|
||||
{{- 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 }}
|
||||
@ -0,0 +1,41 @@
|
||||
{{- if and .Values.wordpress.enabled .Values.database.cluster.enabled }}
|
||||
{{- /*
|
||||
Placeholder Secret that bp-reflector + the post-install sync Job
|
||||
populate from the CNPG-generated `<cluster>-app` Secret.
|
||||
|
||||
WordPress reads the database password from WORDPRESS_DB_PASSWORD; the
|
||||
Deployment binds it via secretKeyRef to this Secret's `password` key.
|
||||
CNPG produces `<cnpgClusterName>-app` with a `password` key. This
|
||||
Secret acts as the bridge: reflector copies all keys from
|
||||
`<cluster>-app` into this Secret; the post-install Job in
|
||||
templates/database-secret-sync-job.yaml provides the safety net for
|
||||
the otech30-class Reflector race documented in
|
||||
platform/gitea/chart/templates/database-secret-sync-job.yaml.
|
||||
|
||||
Per docs/INVIOLABLE-PRINCIPLES.md #10 (credential hygiene): no
|
||||
plaintext credentials appear in this committed template — only an
|
||||
empty placeholder. Reflector / the sync Job copy bytes from a live
|
||||
cluster Secret at runtime.
|
||||
*/}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Values.database.secretName }}
|
||||
namespace: {{ include "bp-wordpress-tenant.cnpgNamespace" . | quote }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
# Reflector copies all keys from <cluster>-app into this Secret.
|
||||
reflector.v1.k8s.emberstack.com/reflects: "{{ include "bp-wordpress-tenant.cnpgNamespace" . }}/{{ include "bp-wordpress-tenant.cnpgAppSecret" . }}"
|
||||
# Helm resource-policy keep — do not delete on `helm uninstall`
|
||||
# (the Secret is independently managed by reflector + the
|
||||
# post-install Job after initial creation).
|
||||
helm.sh/resource-policy: keep
|
||||
type: Opaque
|
||||
# Bootstrap empty data — reflector / the post-install Job overwrites
|
||||
# `password` once CNPG finishes provisioning <cluster>-app. Empty
|
||||
# values here prevent CreateContainerConfigError (secret key missing)
|
||||
# during initial render.
|
||||
data:
|
||||
password: ""
|
||||
{{- end }}
|
||||
230
platform/wordpress-tenant/chart/templates/deployment.yaml
Normal file
230
platform/wordpress-tenant/chart/templates/deployment.yaml
Normal file
@ -0,0 +1,230 @@
|
||||
{{- 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 }}
|
||||
39
platform/wordpress-tenant/chart/templates/ingress.yaml
Normal file
39
platform/wordpress-tenant/chart/templates/ingress.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
{{- if and .Values.wordpress.enabled .Values.ingress.enabled }}
|
||||
{{- $fullName := include "bp-wordpress-tenant.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- $host := include "bp-wordpress-tenant.ingressHost" . -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.issuer | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $host }}
|
||||
secretName: {{ .Values.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ $host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- end }}
|
||||
82
platform/wordpress-tenant/chart/templates/networkpolicy.yaml
Normal file
82
platform/wordpress-tenant/chart/templates/networkpolicy.yaml
Normal file
@ -0,0 +1,82 @@
|
||||
{{- if and .Values.wordpress.enabled .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ include "bp-wordpress-tenant.fullname" . }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
{{- include "bp-wordpress-tenant.selectorLabels" . | nindent 6 }}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# Traffic from the SME's ingress namespace (default: traefik).
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
{{- toYaml .Values.networkPolicy.ingress.fromNamespaceLabels | nindent 14 }}
|
||||
ports:
|
||||
- port: {{ .Values.wordpress.port }}
|
||||
protocol: TCP
|
||||
egress:
|
||||
# ── bp-cnpg Postgres ──────────────────────────────────────────
|
||||
# Default: same namespace as the WordPress release. Operator may
|
||||
# point this to a shared cnpg-tenants namespace via overlay.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
{{- if .Values.networkPolicy.egress.cnpgNamespaceLabel }}
|
||||
kubernetes.io/metadata.name: {{ .Values.networkPolicy.egress.cnpgNamespaceLabel }}
|
||||
{{- else }}
|
||||
kubernetes.io/metadata.name: {{ include "bp-wordpress-tenant.cnpgNamespace" . }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- port: {{ .Values.networkPolicy.egress.cnpgPort }}
|
||||
protocol: TCP
|
||||
# ── bp-keycloak (OIDC discovery + token endpoints) ────────────
|
||||
# Tenant Keycloak realm in the SME vcluster. WordPress's PHP
|
||||
# backchannel hits the token endpoint at install time (auto-
|
||||
# discovery) and on every login refresh.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ .Values.networkPolicy.egress.keycloakNamespaceLabel }}
|
||||
ports:
|
||||
- port: {{ .Values.networkPolicy.egress.keycloakPort }}
|
||||
protocol: TCP
|
||||
# Some Sovereigns expose Keycloak on http :8080 inside the
|
||||
# mesh (TLS terminates at the gateway). Allow both.
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
# ── DNS resolution ────────────────────────────────────────────
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
# ── HTTPS egress (Keycloak realm URL if external, plugin/theme
|
||||
# fetches at first install) ────────────────────────────────
|
||||
# The cluster's egress-gateway enforces destination policy
|
||||
# globally; this rule just opens the wire. Used by the WP plugin
|
||||
# installer initContainer to pull `openid-connect-generic` +
|
||||
# `pg4wp` zips from wordpress.org / GitHub. Cloud-egress IPs
|
||||
# only — internal RFC1918 ranges are excluded so this rule
|
||||
# cannot accidentally reach intra-cluster services on :443.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
243
platform/wordpress-tenant/chart/templates/oidc-config-job.yaml
Normal file
243
platform/wordpress-tenant/chart/templates/oidc-config-job.yaml
Normal file
@ -0,0 +1,243 @@
|
||||
{{- if .Values.wordpress.enabled }}
|
||||
{{- /*
|
||||
Helm post-install Job that wires the openid-connect-generic plugin's
|
||||
WordPress option row to the SME-vcluster Keycloak realm + client at
|
||||
first install. Also activates the plugin, the default theme, and the
|
||||
pg4wp drop-in so the SME admin never sees the WordPress install
|
||||
wizard.
|
||||
|
||||
Strategy
|
||||
────────
|
||||
The WordPress `wp_options` table is the single source of truth for
|
||||
plugin configuration: a row keyed `openid_connect_generic_settings`
|
||||
holds a serialised PHP array with login_type / endpoint URLs / client
|
||||
id / client secret. The active theme + the active plugin list also
|
||||
live in `wp_options` (`template`, `stylesheet`, `active_plugins`).
|
||||
|
||||
This Job installs WordPress's database schema if not already present
|
||||
(invokes the official `wordpress` image's `wp-cli`-equivalent install
|
||||
flow via PHP directly), then UPSERTs the OIDC settings row, the
|
||||
`active_plugins` row, and the `template`/`stylesheet` rows. Idempotent:
|
||||
re-running on `helm upgrade` overwrites with the same values.
|
||||
|
||||
Why direct DB writes instead of WP-CLI
|
||||
──────────────────────────────────────
|
||||
WP-CLI is not bundled with the official `wordpress:*` Docker image
|
||||
and adding a layer just for this install would diverge from the
|
||||
upstream image (and force a Catalyst-built WordPress image, which
|
||||
violates the issue's "pull through Sovereign Harbor proxy-cache, no
|
||||
local rebuild" constraint). The Job uses PHP from the WordPress
|
||||
image to talk to Postgres via PDO; same image, no extra dependency.
|
||||
|
||||
Why post-install (not pre-install)
|
||||
──────────────────────────────────
|
||||
WORDPRESS_DB_PASSWORD is only populated by the
|
||||
database-secret-sync-job at post-install hook-weight 5. This OIDC
|
||||
Job runs at hook-weight 10 (after the DB Secret is reconciled).
|
||||
*/}}
|
||||
{{- $ns := .Release.Namespace }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "bp-wordpress-tenant.fullname" . }}-oidc-config
|
||||
namespace: {{ $ns }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
catalyst.openova.io/component: wordpress-oidc-config
|
||||
annotations:
|
||||
"helm.sh/hook": "post-install,post-upgrade"
|
||||
"helm.sh/hook-weight": "10"
|
||||
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
|
||||
spec:
|
||||
backoffLimit: 6
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wordpress-oidc-config
|
||||
catalyst.openova.io/blueprint: bp-wordpress-tenant
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
securityContext:
|
||||
{{- toYaml .Values.wordpress.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: oidc-config
|
||||
image: {{ include "bp-wordpress-tenant.wordpressImage" . | quote }}
|
||||
imagePullPolicy: {{ .Values.wordpress.image.pullPolicy }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.wordpress.containerSecurityContext | nindent 12 }}
|
||||
env:
|
||||
- name: WP_DB_HOST
|
||||
value: {{ include "bp-wordpress-tenant.cnpgRwHost" . | quote }}
|
||||
- name: WP_DB_NAME
|
||||
value: {{ .Values.database.cluster.database | default "wordpress" | quote }}
|
||||
- name: WP_DB_USER
|
||||
value: {{ .Values.database.cluster.owner | default "wordpress" | quote }}
|
||||
- name: WP_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.database.secretName }}
|
||||
key: password
|
||||
- name: WP_SITE_URL
|
||||
value: "https://{{ include "bp-wordpress-tenant.ingressHost" . }}"
|
||||
- name: WP_DEFAULT_THEME
|
||||
value: {{ .Values.defaultTheme | quote }}
|
||||
- name: KC_REALM_URL
|
||||
value: {{ required "keycloak.realmURL is required (e.g. https://auth.<sme-domain>/realms/sme)" .Values.keycloak.realmURL | quote }}
|
||||
- name: KC_CLIENT_ID
|
||||
value: {{ .Values.keycloak.clientID | quote }}
|
||||
- name: KC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ required "keycloak.clientSecretName is required (ExternalSecret name carrying the OIDC client secret)" .Values.keycloak.clientSecretName }}
|
||||
key: client-secret
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
# Ensure php-pgsql + the postgres CLI are present. The
|
||||
# official wordpress image ships php-mysqli but not
|
||||
# php-pgsql; we install at boot (one-time) via apt.
|
||||
if ! php -m | grep -qi pdo_pgsql; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
php-pgsql postgresql-client
|
||||
fi
|
||||
cat > /tmp/oidc-config.php <<'PHP'
|
||||
<?php
|
||||
/**
|
||||
* bp-wordpress-tenant OIDC config bootstrap.
|
||||
*
|
||||
* Connects to the bp-cnpg Postgres provisioned for this
|
||||
* SME tenant, ensures the `wp_options` table exists with
|
||||
* a minimal schema (WP creates it on first browser hit
|
||||
* — we pre-create here to avoid the wizard race),
|
||||
* and UPSERTs the rows that activate openid-connect-
|
||||
* generic + select the default theme.
|
||||
*
|
||||
* Idempotent — re-running on `helm upgrade` overwrites
|
||||
* with the same values.
|
||||
*/
|
||||
$host = getenv('WP_DB_HOST');
|
||||
$db = getenv('WP_DB_NAME');
|
||||
$user = getenv('WP_DB_USER');
|
||||
$pass = getenv('WP_DB_PASSWORD');
|
||||
$siteurl = getenv('WP_SITE_URL');
|
||||
$theme = getenv('WP_DEFAULT_THEME');
|
||||
$issuer = getenv('KC_REALM_URL');
|
||||
$cid = getenv('KC_CLIENT_ID');
|
||||
$csecret = getenv('KC_CLIENT_SECRET');
|
||||
|
||||
echo "[oidc-config] connecting to Postgres at {$host}\n";
|
||||
$pdo = new PDO(
|
||||
"pgsql:host={$host};port=5432;dbname={$db}",
|
||||
$user, $pass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
// Wait for WordPress's wp_options table to exist —
|
||||
// WP creates it lazily on the first browser request. We
|
||||
// poll for up to 5 minutes; if it still doesn't exist,
|
||||
// we trip a sentinel HTTP request to the WP Service to
|
||||
// force the install routine. The WordPress Service in
|
||||
// the same namespace is reachable as
|
||||
// <release>-bp-wordpress-tenant.<ns>.svc.cluster.local.
|
||||
$tableExists = function() use ($pdo) {
|
||||
$r = $pdo->query("SELECT to_regclass('public.wp_options') AS t")->fetch();
|
||||
return !empty($r['t']);
|
||||
};
|
||||
for ($i = 0; $i < 60; $i++) {
|
||||
if ($tableExists()) break;
|
||||
if ($i === 0) {
|
||||
// Trip the install by GETting the WP Service.
|
||||
$svcUrl = sprintf('http://%s.%s.svc.cluster.local/',
|
||||
getenv('WP_SVC_NAME') ?: 'wordpress',
|
||||
getenv('WP_NAMESPACE') ?: 'default');
|
||||
echo "[oidc-config] tripping WP install via {$svcUrl}\n";
|
||||
@file_get_contents($svcUrl);
|
||||
}
|
||||
sleep(5);
|
||||
}
|
||||
if (!$tableExists()) {
|
||||
fprintf(STDERR, "[oidc-config] FATAL: wp_options not present after 5 min\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "[oidc-config] wp_options present.\n";
|
||||
|
||||
// Build the OIDC settings array. Field names match the
|
||||
// openid-connect-generic plugin's option schema (see
|
||||
// upstream openid-connect-generic-settings.php).
|
||||
$oidcSettings = [
|
||||
'login_type' => 'auto', // auto-redirect to Keycloak
|
||||
'client_id' => $cid,
|
||||
'client_secret' => $csecret,
|
||||
'scope' => 'openid email profile',
|
||||
'endpoint_login' => rtrim($issuer, '/') . '/protocol/openid-connect/auth',
|
||||
'endpoint_userinfo' => rtrim($issuer, '/') . '/protocol/openid-connect/userinfo',
|
||||
'endpoint_token' => rtrim($issuer, '/') . '/protocol/openid-connect/token',
|
||||
'endpoint_end_session' => rtrim($issuer, '/') . '/protocol/openid-connect/logout',
|
||||
'identity_key' => 'preferred_username',
|
||||
'no_sslverify' => 0,
|
||||
'http_request_timeout' => 5,
|
||||
'enforce_privacy' => 0,
|
||||
'alternate_redirect_uri' => 0,
|
||||
'token_refresh_enable' => 1,
|
||||
'link_existing_users' => 1,
|
||||
'create_if_does_not_exist' => 1, // auto-create user on first SSO login
|
||||
'redirect_user_back' => 1,
|
||||
'redirect_on_logout' => 1,
|
||||
'acl_enabled' => 0,
|
||||
'enable_logging' => 0,
|
||||
'log_limit' => 1000,
|
||||
];
|
||||
$oidcSerialised = serialize($oidcSettings);
|
||||
|
||||
$upsert = function($key, $value) use ($pdo) {
|
||||
// wp_options.option_id is autoincrement; ON CONFLICT
|
||||
// on option_name handles the upsert. Postgres-flavoured
|
||||
// INSERT ... ON CONFLICT works because pg4wp uses the
|
||||
// native wp_options layout.
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO wp_options (option_name, option_value, autoload)
|
||||
VALUES (:k, :v, 'yes')
|
||||
ON CONFLICT (option_name) DO UPDATE SET option_value = EXCLUDED.option_value"
|
||||
);
|
||||
$stmt->execute([':k' => $key, ':v' => $value]);
|
||||
};
|
||||
|
||||
// 1. OIDC plugin settings.
|
||||
$upsert('openid_connect_generic_settings', $oidcSerialised);
|
||||
echo "[oidc-config] openid_connect_generic_settings upserted.\n";
|
||||
|
||||
// 2. Activate openid-connect-generic in WP.
|
||||
$activePlugins = serialize([
|
||||
'openid-connect-generic/openid-connect-generic.php',
|
||||
]);
|
||||
$upsert('active_plugins', $activePlugins);
|
||||
echo "[oidc-config] active_plugins upserted.\n";
|
||||
|
||||
// 3. Default theme — both rows MUST be set (template is
|
||||
// the parent theme, stylesheet is the active child/parent).
|
||||
$upsert('template', $theme);
|
||||
$upsert('stylesheet', $theme);
|
||||
echo "[oidc-config] default theme set to {$theme}.\n";
|
||||
|
||||
// 4. siteurl + home — overwrite the WP install defaults
|
||||
// so links resolve through the ingress host.
|
||||
$upsert('siteurl', $siteurl);
|
||||
$upsert('home', $siteurl);
|
||||
echo "[oidc-config] siteurl/home set to {$siteurl}.\n";
|
||||
|
||||
echo "[oidc-config] all upserts complete.\n";
|
||||
PHP
|
||||
php /tmp/oidc-config.php
|
||||
envFrom: []
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
{{- end }}
|
||||
31
platform/wordpress-tenant/chart/templates/pvc.yaml
Normal file
31
platform/wordpress-tenant/chart/templates/pvc.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
{{- if and .Values.wordpress.enabled .Values.persistence.wpContent.enabled }}
|
||||
{{- /*
|
||||
Persistent volume for /var/www/html/wp-content. Carries the WordPress
|
||||
themes, plugins, mu-plugins, and uploaded media across pod restarts and
|
||||
image upgrades. Bound at first install; the wp-content-bootstrap
|
||||
initContainer in the Deployment seeds the freshly-bound PVC from the
|
||||
official image's baked-in wp-content directory (idempotent).
|
||||
*/}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "bp-wordpress-tenant.fullname" . }}-wp-content
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
# Helm resource-policy keep — do not delete on `helm uninstall`
|
||||
# (the PVC carries customer content; operator must explicitly
|
||||
# delete via `kubectl delete pvc` after data export).
|
||||
helm.sh/resource-policy: keep
|
||||
spec:
|
||||
accessModes:
|
||||
{{- range .Values.persistence.wpContent.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.wpContent.size | quote }}
|
||||
{{- if .Values.persistence.wpContent.storageClass }}
|
||||
storageClassName: {{ .Values.persistence.wpContent.storageClass | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
17
platform/wordpress-tenant/chart/templates/service.yaml
Normal file
17
platform/wordpress-tenant/chart/templates/service.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
{{- if .Values.wordpress.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "bp-wordpress-tenant.fullname" . }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "bp-wordpress-tenant.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@ -0,0 +1,8 @@
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "bp-wordpress-tenant.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "bp-wordpress-tenant.labels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
66
platform/wordpress-tenant/chart/tests/observability-toggle.sh
Executable file
66
platform/wordpress-tenant/chart/tests/observability-toggle.sh
Executable file
@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# bp-wordpress-tenant observability-toggle integration test (issue #182).
|
||||
#
|
||||
# Verifies docs/BLUEPRINT-AUTHORING.md §11.2 (Observability toggles must
|
||||
# default false): a fresh-Sovereign install of bp-wordpress-tenant must
|
||||
# NOT render a `monitoring.coreos.com/v1` ServiceMonitor or PodMonitor
|
||||
# by default — those CRDs ship with kube-prometheus-stack which
|
||||
# depends on the bootstrap-kit (circular dependency on a fresh
|
||||
# Sovereign).
|
||||
#
|
||||
# Usage: bash tests/observability-toggle.sh [CHART_DIR]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CHART_DIR="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
cd "$CHART_DIR"
|
||||
# Skip helm dep build when charts/ is already vendored (CI populates
|
||||
# it before this step runs, and re-running on CI without `helm repo
|
||||
# add` fails). Pattern lifted from bp-cilium tests/observability-toggle.sh.
|
||||
if [ ! -d charts ] || [ -z "$(ls -A charts 2>/dev/null)" ]; then
|
||||
helm dependency build >/dev/null
|
||||
fi
|
||||
|
||||
# bp-wordpress-tenant requires several values with no sensible default
|
||||
# (smeDomain, keycloak.realmURL, keycloak.clientSecretName, adminUser.email);
|
||||
# we supply minimal stubs so the render proceeds.
|
||||
COMMON_SET=(
|
||||
--set "smeDomain=acme.example.local"
|
||||
--set "keycloak.realmURL=https://auth.acme.example.local/realms/sme"
|
||||
--set "keycloak.clientSecretName=wordpress-oidc"
|
||||
--set "adminUser.email=admin@acme.example.local"
|
||||
)
|
||||
|
||||
echo "[observability-toggle] Case 1: default render produces no PodMonitor / ServiceMonitor"
|
||||
helm template smoke-wp . "${COMMON_SET[@]}" > "$TMP/default.yaml"
|
||||
if grep -qE "^kind: (PodMonitor|ServiceMonitor)$" "$TMP/default.yaml"; then
|
||||
echo "FAIL: default render of bp-wordpress-tenant contains a PodMonitor/ServiceMonitor CR." >&2
|
||||
echo " docs/BLUEPRINT-AUTHORING.md §11.2 forbids this — observability toggles must default false." >&2
|
||||
grep -nE "^kind: (PodMonitor|ServiceMonitor)$" "$TMP/default.yaml" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS"
|
||||
|
||||
echo "[observability-toggle] Case 2: explicit serviceMonitor.enabled=false renders cleanly"
|
||||
if ! helm template smoke-wp . "${COMMON_SET[@]}" \
|
||||
--set "serviceMonitor.enabled=false" \
|
||||
> "$TMP/off.yaml" 2> "$TMP/off.err"; then
|
||||
echo "FAIL: explicit-off render failed:" >&2
|
||||
cat "$TMP/off.err" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -qE "^kind: (PodMonitor|ServiceMonitor)$" "$TMP/off.yaml"; then
|
||||
echo "FAIL: explicit-off render still contains a PodMonitor/ServiceMonitor CR." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS"
|
||||
|
||||
# bp-wordpress-tenant doesn't currently render a ServiceMonitor template
|
||||
# (it would require a wp-prometheus exporter sidecar — see values.yaml
|
||||
# comment). The toggle is reserved for future use; this test still
|
||||
# guarantees the default never produces one.
|
||||
|
||||
echo "[observability-toggle] All bp-wordpress-tenant observability-toggle gates green."
|
||||
260
platform/wordpress-tenant/chart/values.yaml
Normal file
260
platform/wordpress-tenant/chart/values.yaml
Normal file
@ -0,0 +1,260 @@
|
||||
# Catalyst Blueprint scratch chart for bp-wordpress-tenant — in-vcluster
|
||||
# turnkey WordPress per SME tenant, SSO-pre-wired against the SME's
|
||||
# vcluster-local Keycloak realm.
|
||||
#
|
||||
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every
|
||||
# operationally-meaningful value (image tag, ingress host, Keycloak realm
|
||||
# URL, CNPG cluster name, default theme, admin email) is configurable.
|
||||
# Per-Sovereign cluster overlays in clusters/<sovereign>/.../wordpress/
|
||||
# may override any of these without rebuilding the Blueprint OCI artifact.
|
||||
#
|
||||
# Image-pull strategy
|
||||
# ───────────────────
|
||||
# `global.imageRegistry` (rendered as a prefix on `image.repository`)
|
||||
# routes pulls through the Sovereign's own Harbor proxy-cache once
|
||||
# bp-harbor reconciles. Empty default = pull directly from Docker Hub
|
||||
# (only used during pre-Harbor bootstrap; production overlays always
|
||||
# set this). See bp-cnpg / bp-keycloak / bp-gitea values.yaml for the
|
||||
# canonical pattern.
|
||||
|
||||
global:
|
||||
# When set, ALL image pulls in this chart route through this registry.
|
||||
# Used post-handover when the Sovereign's own Harbor takes over the
|
||||
# proxy_cache role from contabo's central Harbor. Empty = no rewrite
|
||||
# (image references use upstream defaults). Per-Sovereign overlays
|
||||
# wire this at install time. Tracked under #560.
|
||||
imageRegistry: ""
|
||||
|
||||
catalystBlueprint:
|
||||
upstream:
|
||||
chart: "" # scratch chart — no upstream Helm chart
|
||||
version: ""
|
||||
repo: ""
|
||||
images:
|
||||
wordpress: "wordpress"
|
||||
|
||||
# ─── SME tenant identity ────────────────────────────────────────────────
|
||||
# `smeDomain` is the customer's free-subdomain (e.g. acme.<otech-fqdn>)
|
||||
# OR BYO domain (e.g. acme.com); supplied by the tenant-provisioning
|
||||
# pipeline. Used to derive the default ingress host. Per
|
||||
# docs/INVIOLABLE-PRINCIPLES.md #4 the value MUST be passed in at
|
||||
# install — there is no sensible default.
|
||||
smeDomain: ""
|
||||
|
||||
# ─── WordPress application ──────────────────────────────────────────────
|
||||
wordpress:
|
||||
enabled: true
|
||||
# Solo-tenant minimum — single replica. Per-Sovereign overlays bump
|
||||
# to 2+ once HA is needed (WordPress is stateless once Postgres + the
|
||||
# wp-content PVC carry the persistent state; with ReadWriteOnce
|
||||
# storage classes the PVC pins replicas to 1 — operators that need
|
||||
# >1 must switch to ReadWriteMany on persistence.wpContent.storageClass).
|
||||
replicas: 1
|
||||
|
||||
image:
|
||||
# Official WordPress image. SHA-pinned per
|
||||
# docs/INVIOLABLE-PRINCIPLES.md #4 — the manifest-list digest below
|
||||
# corresponds to `wordpress:6-php8.3-apache` as published on Docker
|
||||
# Hub; the human-readable tag is kept alongside for forensics.
|
||||
repository: wordpress
|
||||
tag: "6-php8.3-apache"
|
||||
# Multi-arch manifest-list digest for `wordpress:6-php8.3-apache`
|
||||
# (verified against registry-1.docker.io 2026-05-04).
|
||||
digest: "sha256:054e611334390d547c732a38b41b8f42feeb8606a29f0a934deba44e0f17b196"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
port: 80
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 512Mi
|
||||
|
||||
# SecurityContext — the official WordPress image runs Apache as
|
||||
# `www-data` (UID 33). Keep readOnlyRootFilesystem=false because the
|
||||
# WP boot script writes `wp-config.php` and Apache touches /run.
|
||||
podSecurityContext:
|
||||
fsGroup: 33
|
||||
containerSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# Liveness / readiness — WordPress's default `/wp-admin/install.php`
|
||||
# responds 200/302 once the Apache + PHP stack is up. We hit `/`
|
||||
# which redirects to `/wp-admin/install.php` on first boot and to
|
||||
# the homepage afterwards; either way Apache returns < 500.
|
||||
probes:
|
||||
liveness:
|
||||
path: /
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readiness:
|
||||
path: /
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
|
||||
# ─── Database (bp-cnpg in tenant namespace) ─────────────────────────────
|
||||
# Per the bp-gitea / bp-harbor pattern (templates/cnpg-cluster.yaml):
|
||||
# this chart provisions a dedicated `Cluster.postgresql.cnpg.io` for
|
||||
# WordPress. The CNPG-emitted `<cluster>-app` Secret is mirrored into
|
||||
# `wordpress-database-secret` by reflector + a post-install sync Job
|
||||
# so the canonical `password` key is always populated before the
|
||||
# WordPress Pod starts.
|
||||
#
|
||||
# The pattern matches what bp-gitea uses; see
|
||||
# platform/gitea/chart/templates/database-secret-sync-job.yaml for
|
||||
# the rationale on Reflector vs. post-install Job.
|
||||
database:
|
||||
# CNPG Cluster name & topology — operator-overridable via cluster
|
||||
# overlay. Defaults to a tenant-isolated cluster sized for SME usage.
|
||||
cnpgClusterName: "wordpress-db"
|
||||
cluster:
|
||||
enabled: true
|
||||
# CNPG namespace defaults to .Release.Namespace (tenant ns) so the
|
||||
# Cluster lives next to WordPress. Operator may move it to a
|
||||
# shared `cnpg-tenants` namespace via overlay.
|
||||
namespace: ""
|
||||
instances: 1
|
||||
pgVersion: "16"
|
||||
database: "wordpress"
|
||||
owner: "wordpress"
|
||||
storageSize: "10Gi"
|
||||
storageClass: "local-path"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
# The reflector-managed wrapper Secret WordPress reads. Always carries
|
||||
# at minimum the `password` key (mirrored from `<cluster>-app`).
|
||||
secretName: "wordpress-database-secret"
|
||||
|
||||
# ─── SSO via SME-vcluster Keycloak (openid-connect-generic plugin) ──────
|
||||
# The plugin is pre-installed by the WordPress entrypoint (initContainer)
|
||||
# from the wordpress.org plugin registry, then registered + configured
|
||||
# at first install via templates/oidc-config-job.yaml so the SME admin
|
||||
# never sees the WP setup wizard.
|
||||
keycloak:
|
||||
# Discovery endpoint of the SME's realm — operator-supplied (no
|
||||
# default per docs/INVIOLABLE-PRINCIPLES.md #4). Example shape:
|
||||
# https://auth.acme.<otech-fqdn>/realms/sme
|
||||
realmURL: ""
|
||||
# OIDC client registered in the SME realm — provisioned by the
|
||||
# tenant-provisioning pipeline (#804) before this chart installs.
|
||||
clientID: "wordpress"
|
||||
# ExternalSecret name carrying the OIDC client secret. Required
|
||||
# key: client-secret. Provisioned by the tenant-provisioning
|
||||
# pipeline at the same time as the Keycloak client registration.
|
||||
clientSecretName: "wordpress-oidc"
|
||||
# Keycloak group → WP role mapping. Default mirrors the SME unified-
|
||||
# RBAC tier names. Operator may extend per their RBAC posture.
|
||||
roleMapping:
|
||||
administrator: "administrator"
|
||||
editor: "editor"
|
||||
author: "author"
|
||||
contributor: "contributor"
|
||||
subscriber: "subscriber"
|
||||
|
||||
# ─── First-install admin user (created via SSO mapping) ─────────────────
|
||||
# The admin-user-job pre-seeds a wp_users row with `user_login = email`
|
||||
# and the SSO email mapping so the SME admin's first Keycloak login
|
||||
# logs them in as the WP administrator without seeing the install wizard.
|
||||
adminUser:
|
||||
# Operator-supplied, e.g. `admin@acme.com`. Same email as the SME
|
||||
# admin's Keycloak account — that's how openid-connect-generic
|
||||
# matches the Keycloak `email` claim back to a wp_user.
|
||||
email: ""
|
||||
# Display name shown in the WP admin bar. Defaults to the local-part
|
||||
# of the email (job-side) if left empty.
|
||||
displayName: ""
|
||||
|
||||
# ─── Default theme ──────────────────────────────────────────────────────
|
||||
# The theme slug installed + activated by oidc-config-job. Operator-
|
||||
# overridable. Default ships the latest WordPress yearly theme.
|
||||
defaultTheme: "twentytwentyfive"
|
||||
|
||||
# ─── Persistence ────────────────────────────────────────────────────────
|
||||
# /var/www/html/wp-content carries themes, plugins, uploads. Persists
|
||||
# across pod restarts and image upgrades. PVC bound at first install.
|
||||
persistence:
|
||||
wpContent:
|
||||
enabled: true
|
||||
size: "10Gi"
|
||||
storageClass: "local-path"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
# ─── Service ────────────────────────────────────────────────────────────
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
# ─── ServiceAccount ─────────────────────────────────────────────────────
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: ""
|
||||
|
||||
# ─── Ingress + cert-manager TLS ─────────────────────────────────────────
|
||||
# Default host is `wordpress.<smeDomain>`; operator may override
|
||||
# `ingress.host` directly when BYO domain shapes differ.
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "traefik"
|
||||
# Empty string → templates derive `wordpress.{{ .Values.smeDomain }}`.
|
||||
# Operator may set explicitly when BYO requires a non-derived host.
|
||||
host: ""
|
||||
tls:
|
||||
enabled: true
|
||||
issuer: "letsencrypt-prod" # cert-manager ClusterIssuer
|
||||
secretName: "wordpress-tls"
|
||||
annotations: {}
|
||||
|
||||
# ─── NetworkPolicy ──────────────────────────────────────────────────────
|
||||
# Restrict egress to: cnpg cluster (Postgres :5432), Keycloak realm
|
||||
# (HTTPS :443 — discovery + token endpoints), DNS (kube-dns :53),
|
||||
# and ingress traffic (gateway namespace). External egress is allowed
|
||||
# on :443 so WordPress can fetch the `openid-connect-generic` plugin
|
||||
# zip + theme from wordpress.org during initContainer boot; the
|
||||
# cluster's egress-gateway enforces destination policy globally.
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
ingress:
|
||||
fromNamespaceLabels:
|
||||
kubernetes.io/metadata.name: traefik
|
||||
egress:
|
||||
# bp-cnpg — defaults; namespace label may be overridden if the
|
||||
# tenant cluster lives in a non-default cnpg namespace.
|
||||
cnpgNamespaceLabel: "" # empty → match Release.Namespace
|
||||
cnpgPort: 5432
|
||||
# bp-keycloak — same as cnpg, defaults to .Release.Namespace
|
||||
# because the tenant Keycloak runs in the SME vcluster's keycloak
|
||||
# namespace; operator overrides if it lives elsewhere.
|
||||
keycloakNamespaceLabel: "keycloak"
|
||||
keycloakPort: 8443
|
||||
|
||||
# ─── ServiceMonitor ─────────────────────────────────────────────────────
|
||||
# Default false per docs/BLUEPRINT-AUTHORING.md §11.2 (Observability
|
||||
# toggles must default false). Operator opts in via per-cluster overlay
|
||||
# once kube-prometheus-stack reconciles. WordPress itself doesn't expose
|
||||
# /metrics — operator must layer a wp-prometheus exporter plugin or
|
||||
# nginx/php-fpm sidecar before enabling this.
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
interval: "30s"
|
||||
scrapeTimeout: "10s"
|
||||
path: "/metrics"
|
||||
labels: {}
|
||||
Loading…
Reference in New Issue
Block a user