openova/platform/wordpress-tenant/chart/templates/oidc-config-job.yaml
e3mrah 3fe27f625f
feat(bp-wordpress-tenant): wp-cli OIDC bootstrap + oidc.* canonical block (0.2.0, #915) (#927)
Umbrella issue #915 (D1 sub-task). Aligns the chart's post-install OIDC
config Job with the canonical wp-cli flow and the bp-keycloak tenant-
realm contract C1's PR #918 ships.

Chart 0.2.0
-----------
- templates/oidc-config-job.yaml rewritten to use the official
  wordpress:cli-2.12.0-php8.3 image (manifest-list digest pinned per
  Inviolable Principle #4). Replaces direct PHP/SQL UPSERTs against
  wp_options with:
    * wp core install (idempotent: wp core is-installed)
    * wp plugin install openid-connect-generic --activate (idempotent:
      wp plugin is-installed)
    * wp option update openid_connect_generic_settings <json>
    * wp option update default_role
    * wp theme install/activate
    * wp option update siteurl/home
  Going through wp-cli (i.e. WordPress core's own PHP API) is more
  resilient than schema-shape-dependent INSERT statements and survives
  WordPress minor upgrades.

- values.yaml: new canonical oidc.* block —
    oidc.{enabled, issuerURL, clientId, clientSecretName, defaultRole,
          identityKey, roleMapping, cliImage}.
  Default oidc.clientSecretName = "wordpress-oidc-client-secret"
  matches the K8s Secret bp-keycloak's PR #918 emits alongside the
  realm import ConfigMap (so the realm JSON's `secret` field and the
  Secret bytes never drift).

- Legacy keycloak.{realmURL, clientID, clientSecretName} kept as a
  back-compat alias. _helpers.tpl folds it into oidc.* when the
  modern keys are at their values.yaml defaults so chart 0.1.x
  clusters keep reconciling. Removed in chart 0.3.0.

- oidc.defaultRole=subscriber — newly auto-created SSO users land
  with subscriber capability (operator overrides via overlay).

- Redirect URIs: the openid-connect-generic plugin's default callback
  is /wp-admin/admin-ajax.php?action=openid-connect-authorize when
  alternate_redirect_uri=0 (we set 0). bp-keycloak (PR #918)
  registers the same URL plus /wp-login.php and a /* wildcard, so the
  client's allowed-redirect-URI list aligns with what the plugin
  actually issues.

Orchestrator emit
-----------------
- products/catalyst/bootstrap/api/internal/handler/sme_tenant_gitops.go
  smeTenantBPWordPress now emits the canonical oidc.* block AND the
  legacy keycloak.* alias (for chart 0.1.x clusters mid-upgrade).

Tests
-----
- chart/tests/oidc-config.sh — 7 helm-template assertions:
    1. Canonical oidc.* render produces a Job with the required
       wp-cli command flow + wordpress:cli-2.12.0-php8.3 image.
    2. Legacy keycloak.* fold path (chart 0.1.x compat).
    3. oidc.enabled=false short-circuits the Job.
    4. alternate_redirect_uri=0 (so plugin URL matches the realm-
       registered redirect URI from PR #918).
    5. defaultRole rendered + propagated.
    6. Render YAML is parseable and contains all required kinds.
    7. wp-content PVC mounted in the Job (so pg4wp's db.php drop-in
       loads — failure here would silently fall back to mysqli).

- internal/handler/sme_tenant_test.go:
    * TestRenderSMETenantOverlay_WordPressEmitsOIDC — pins the
      canonical oidc.* block + legacy keycloak.* alias the
      orchestrator emits for the alice@omantel test fixture.
    * TestRenderSMETenantOverlay_WordPressOIDC_BYOMode — BYO domain
      mode renders wordpress.<byo-domain> as the ingress host.

Verification
------------
- helm lint clean
- helm template smoke green for: oidc.* canonical, keycloak.* legacy
  fold, oidc.enabled=false short-circuit
- chart/tests/oidc-config.sh: 7/7 PASS
- chart/tests/observability-toggle.sh: 2/2 PASS (regression)
- go test ./internal/handler/ -run "SMETenant|TestRenderSME": all
  green (TestAuthHandover_HappyPath failure is pre-existing on main,
  unrelated to this change)

Closes (D1 sub-task) of #915.

Co-authored-by: hatiyildiz <hatice@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:10:41 +04:00

286 lines
14 KiB
YAML

{{- if and .Values.wordpress.enabled .Values.oidc.enabled }}
{{- /*
Helm post-install / post-upgrade Job that:
1. Waits for the WordPress Pod to be reachable (its initContainers
have seeded /var/www/html/wp-content with pg4wp + the
openid-connect-generic plugin from the wordpress.org plugin
registry — see deployment.yaml).
2. Runs `wp core install` (idempotent — `wp core is-installed` first).
This populates the wp_* tables via WordPress's own install code,
which goes through pg4wp and is therefore Postgres-safe.
3. Runs `wp plugin install openid-connect-generic --activate` —
idempotent: skips when the plugin is already present.
4. Runs `wp option update openid_connect_generic_settings <json>`
with the per-tenant Keycloak realm + client + secret from the
chart values. Idempotent — overwrites with the same values on
`helm upgrade`.
5. Runs `wp theme activate <defaultTheme>` if the theme is
installed; otherwise installs + activates it.
Why wp-cli (not direct PHP/SQL writes)
──────────────────────────────────────
- WordPress's table schema, option-row serialisation format, and the
`active_plugins` / `template` / `stylesheet` semantics evolve
between minor releases. wp-cli sits ON TOP of WordPress core's
public PHP API and is the only stable contract.
- pg4wp's drop-in handles MySQL→Postgres translation transparently
for any code that goes through `wpdb` — wp-cli uses `wpdb`, so it
Just Works against bp-cnpg.
- The official `wordpress:cli-*` image bundles the same WordPress
core layout as the runtime image, so the wp-content PVC mount is
byte-for-byte interchangeable.
Why post-install / weight 10
────────────────────────────
- weight 5: database-secret-sync-job populates `wordpress-database-
secret` from the CNPG-emitted `<cluster>-app` Secret.
- weight 10: this Job — DB Secret is now real, WordPress Pod is up,
PVC initContainers seeded wp-content + db.php drop-in.
- weight 15: admin-user-job pre-seeds the SME admin's wp_users row.
Canonical seam — see umbrella issue #915 (D1 sub-task) and the
matching tenant-realm Keycloak client registration in
platform/keycloak/chart/templates/configmap-tenant-realm.yaml
(PR #918) which emits `wordpress-oidc-client-secret` carrying the
same client_secret bytes the realm JSON registers.
*/}}
{{- $ns := .Release.Namespace }}
{{- $issuer := include "bp-wordpress-tenant.oidcIssuerURL" . }}
{{- $clientId := include "bp-wordpress-tenant.oidcClientId" . }}
{{- $secretName := include "bp-wordpress-tenant.oidcClientSecretName" . }}
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.wpCliImage" . | quote }}
imagePullPolicy: {{ .Values.oidc.cliImage.pullPolicy }}
securityContext:
{{- toYaml .Values.wordpress.containerSecurityContext | nindent 12 }}
env:
# WordPress DB connection — wp-cli reads these via the
# wp-config.php we materialise inline below. Identical
# variable names to the runtime container so the same
# values.yaml block drives both.
- 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
- name: WP_SITE_URL
value: "https://{{ include "bp-wordpress-tenant.ingressHost" . }}"
- name: WP_DEFAULT_THEME
value: {{ .Values.defaultTheme | quote }}
# OIDC inputs — folded from oidc.* / keycloak.* (legacy).
- name: KC_ISSUER_URL
value: {{ $issuer | quote }}
- name: KC_CLIENT_ID
value: {{ $clientId | quote }}
- name: KC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: {{ $secretName }}
key: client-secret
- name: OIDC_DEFAULT_ROLE
value: {{ .Values.oidc.defaultRole | quote }}
- name: OIDC_IDENTITY_KEY
value: {{ .Values.oidc.identityKey | quote }}
command:
- /bin/sh
- -c
- |
set -eu
# The wordpress:cli image's default WORKDIR is /var/www/html;
# wp-cli expects to find wp-config.php and the WordPress
# core files there. We materialise wp-config.php inline so
# `wp` boots WordPress and chain-loads pg4wp from the
# mounted /var/www/html/wp-content PVC (db.php drop-in).
cd /var/www/html
# 1. Materialise wp-config.php — same contract as the
# runtime container's WORDPRESS_CONFIG_EXTRA so the
# site URL + reverse-proxy headers are consistent.
if [ ! -f wp-config.php ]; then
cat > wp-config.php <<'PHPCFG'
<?php
define('DB_NAME', getenv('WORDPRESS_DB_NAME'));
define('DB_USER', getenv('WORDPRESS_DB_USER'));
define('DB_PASSWORD', getenv('WORDPRESS_DB_PASSWORD'));
define('DB_HOST', getenv('WORDPRESS_DB_HOST'));
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
$table_prefix = 'wp_';
define('WP_DEBUG', false);
define('WP_HOME', getenv('WP_SITE_URL'));
define('WP_SITEURL', getenv('WP_SITE_URL'));
if (!defined('ABSPATH')) {
define('ABSPATH', __DIR__ . '/');
}
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
require_once ABSPATH . 'wp-settings.php';
PHPCFG
echo "[oidc-config] wrote wp-config.php";
else
echo "[oidc-config] wp-config.php already present";
fi
# 2. Wait for the database to be reachable. wp-cli
# `wp db check` returns non-zero when the DB isn't up;
# we poll for up to 5 minutes. pg4wp's drop-in will
# automatically run from wp-content/db.php, having been
# seeded by the wp-plugin-install initContainer.
for i in $(seq 1 60); do
if wp db check --allow-root >/dev/null 2>&1; then
echo "[oidc-config] database reachable"
break
fi
if [ "$i" = "60" ]; then
echo "[oidc-config] FATAL: database not reachable after 5 min" >&2
exit 1
fi
sleep 5
done
# 3. Run `wp core install` if WordPress isn't installed
# yet. Idempotent — `wp core is-installed` exits 0
# when the install row in wp_options is present.
if ! wp core is-installed --allow-root >/dev/null 2>&1; then
echo "[oidc-config] wp_options/install row absent — running wp core install"
# Generate a strong throwaway admin password — the SME
# admin will only ever log in via Keycloak SSO; this
# password is never used. NEVER printed (Inviolable
# Principle #10).
ADMIN_PASS="$(head -c 48 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 32)"
wp core install \
--allow-root \
--url="${WP_SITE_URL}" \
--title="WordPress" \
--admin_user="wp-bootstrap" \
--admin_password="${ADMIN_PASS}" \
--admin_email="bootstrap@${WP_SITE_URL##https://}" \
--skip-email
unset ADMIN_PASS
echo "[oidc-config] wp core install complete"
else
echo "[oidc-config] WordPress already installed — skipping core install"
fi
# 4. Install + activate openid-connect-generic. wp-cli
# deduplicates: --activate is a no-op when already
# active; `wp plugin is-installed` returns 0 when the
# plugin directory exists on the PVC (seeded by the
# wp-plugin-install initContainer in deployment.yaml,
# so wp-cli skips the wordpress.org download).
if wp plugin is-installed openid-connect-generic --allow-root >/dev/null 2>&1; then
echo "[oidc-config] openid-connect-generic already installed"
wp plugin activate openid-connect-generic --allow-root >/dev/null
else
echo "[oidc-config] installing openid-connect-generic from wordpress.org"
wp plugin install openid-connect-generic --activate --allow-root
fi
echo "[oidc-config] openid-connect-generic activated"
# 5. Compose the OIDC settings option row. Fields match
# the openid-connect-generic plugin's option schema —
# see openid-connect-generic-settings.php upstream.
# `endpoint_*` URLs are derived from the issuer URL
# (Keycloak's standard /protocol/openid-connect/* paths).
ISSUER="${KC_ISSUER_URL%/}"
wp option update openid_connect_generic_settings --format=json --allow-root <<EOF
{
"login_type": "auto",
"client_id": "${KC_CLIENT_ID}",
"client_secret": "${KC_CLIENT_SECRET}",
"scope": "openid email profile",
"endpoint_login": "${ISSUER}/protocol/openid-connect/auth",
"endpoint_userinfo": "${ISSUER}/protocol/openid-connect/userinfo",
"endpoint_token": "${ISSUER}/protocol/openid-connect/token",
"endpoint_end_session": "${ISSUER}/protocol/openid-connect/logout",
"identity_key": "${OIDC_IDENTITY_KEY}",
"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,
"redirect_user_back": 1,
"redirect_on_logout": 1,
"acl_enabled": 0,
"enable_logging": 0,
"log_limit": 1000
}
EOF
echo "[oidc-config] openid_connect_generic_settings written"
# 6. Default WordPress role for newly-created SSO users —
# plugin reads this from the standard `default_role`
# option.
wp option update default_role "${OIDC_DEFAULT_ROLE}" --allow-root >/dev/null
echo "[oidc-config] default_role set to ${OIDC_DEFAULT_ROLE}"
# 7. Theme activation — install + activate idempotently.
if wp theme is-installed "${WP_DEFAULT_THEME}" --allow-root >/dev/null 2>&1; then
wp theme activate "${WP_DEFAULT_THEME}" --allow-root >/dev/null
else
wp theme install "${WP_DEFAULT_THEME}" --activate --allow-root
fi
echo "[oidc-config] theme ${WP_DEFAULT_THEME} active"
# 8. siteurl + home — overwrite the WP install defaults so
# links resolve through the ingress host.
wp option update siteurl "${WP_SITE_URL}" --allow-root >/dev/null
wp option update home "${WP_SITE_URL}" --allow-root >/dev/null
echo "[oidc-config] siteurl/home set to ${WP_SITE_URL}"
echo "[oidc-config] all OIDC bootstrap steps complete"
volumeMounts:
- name: wp-content
mountPath: /var/www/html/wp-content
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: wp-content
{{- if .Values.persistence.wpContent.enabled }}
persistentVolumeClaim:
claimName: {{ include "bp-wordpress-tenant.fullname" . }}-wp-content
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}