diff --git a/briefings/wordpress.md b/briefings/wordpress.md new file mode 100644 index 00000000..fb742877 --- /dev/null +++ b/briefings/wordpress.md @@ -0,0 +1,162 @@ +# wordpress — maintainer briefing + +## What it is + +- **WordPress** — the dominant open-source content management system; this template runs a full + site plus its database on Control Plane. +- **GPLv2 or later** (strong copyleft: modifications you distribute must stay open — it does not + attach to us, since we deploy the unmodified upstream image onto the user's own + infrastructure). Free, nothing to register, no gated edition. + +## Common use cases + +- A company blog, marketing site or documentation site the marketing team edits themselves. +- A customer-facing site that must sit inside the user's own org and network boundary rather + than on a third-party host. +- Migrating an existing WordPress site in: bring the database dump (set `tablePrefix` to match) + and copy the media library onto the volume. +- A WooCommerce/plugin-driven app where the plugin ecosystem, not the CMS, is the point. + +## Architecture on cpln + +| Resource | Purpose | +|---|---| +| `{release}-wordpress` workload (`standard`) | Apache + mod_php + WordPress on :80, **pinned to the single `location`** | +| `{release}-wordpress-vs` volumeset (**`shared`**, RWX) | `/var/www/html` — core, plugins, themes, uploads, `wp-config.php` | +| `{release}-wordpress-start` / `-install` / `-php-ini` secrets | boot wrapper, first-run installer, PHP limits | +| `{release}-wordpress-identity` + 2 policies | `reveal` on its 5 secrets; `view` on the one GVC (boot location guard) | +| DB credentials (dictionary) + root password (opaque) secrets | created by **this** chart, read by the MariaDB subchart | +| `{release}-maria` workload + `{release}-maria-vs` (`ext4`) | bundled MariaDB 11 (subchart `mariadb` 1.4.1) — **not** pinned; a parent cannot template a subchart's workload | + +- The volumeset is **`shared` (read-write-many), not block** — that is the whole reason + multi-replica is possible: a block volumeset gives each replica its **own** volume, so two + replicas would mean two divergent media libraries and two sets of auth salts. +- The admin account is created **before Apache binds**, so there is never a reachable + `/wp-admin/install.php` for a stranger to claim; that is why public access can default on. + +## Key knobs + +| Knob | Default | Note | +|---|---|---| +| `location` | `aws-us-east-1` | the ONE GVC location WordPress runs in; a location the GVC lacks starts nothing, silently | +| `wordpress.image` | `wordpress:7.1.0-php8.4-apache` | seeds the docroot on **first boot only** | +| `wordpress.replicas` | `1` | ≥2 survives a replica loss; does NOT help with the MariaDB bounce below | +| `wordpress.siteUrl` | `""` | empty = the auto-assigned `*.cpln.app` URL | +| `wordpress.siteTitle` / `tablePrefix` / `debug` | `My WordPress Site` / `wp_` / `false` | title and prefix are first-boot only | +| `php.uploadMaxSize` / `php.memoryLimit` | `64M` / `256M` | stock php.ini caps uploads at 2M | +| `admin.secretName` | `my-wordpress-admin` | **prerequisite** dictionary secret: `username`, `password`, `email` | +| `volumeset.capacity` | `10` (GiB) | minimum is 10 | +| `publicAccess.enabled` / `internalAccess.type` | `true` / `same-gvc` | firewall changes take ~30 s to ~10 min | +| `mariadb.*` | bundled MariaDB 11 | full pass-through, incl. `mariadb.backup.enabled: true` | + +## Troubleshooting / considerations + +- **Bumping `wordpress.image` does NOT upgrade WordPress core on an existing install.** The image + seeds the docroot only when the volume is empty; after that WordPress owns its files and the + site owner updates core from the dashboard (upstream's own model). A tag bump changes PHP and + Apache only. "I updated the template and WordPress still says 7.1" is working as designed. + Note the image tag reads `7.1.0` while `$wp_version` reads `7.1` — expected, not a wrong image. +- **The media library has no backup.** The bundled database can be backed up on a schedule + (`mariadb.backup.enabled: true`), but a `shared` volumeset is expand-only with no verified + snapshot/restore path, so uploads exist in exactly one place. The README says so plainly + rather than documenting a restore we have not proven. +- **A missing `admin.secretName` secret wedges the install almost silently** — `cpln logs` + returns *zero* lines because the container never starts. The only diagnostic is + `status.versions[].message` from `cpln workload get-deployments`. It self-heals in roughly + 5–10 minutes after the secret is created, or immediately with a forced redeployment. +- **The admin password is applied on first boot only** — measured, not assumed: after a restart + with the ORIGINAL password still in the container env, a password changed in between still + worked. WordPress is in the `keycloak`/`metabase` class, not the `n8n` class. Lost-password + recovery is the documented `php -r 'wp_set_password(…, 1)'` exec (verified verbatim, with a + negative control), not email. +- **Outbound email does not work out of the box.** The container has no mail transport, so + `wp_mail()` — including password reset and invites — fails silently until an SMTP plugin is + installed. Most likely "WordPress is broken" report. +- **Single location, TWO layers, only one of which is a guarantee.** A workload runs in every + location its GVC has, so a second location means a second everything. + - **Structural (holds by construction):** the WordPress workload sets `defaultOptions` + `minScale/maxScale: 0` and supplies the real count via a `localOptions` entry for + `.Values.location`. An undeclared location gets `desiredScale: 0` and reports `This workload + location is deactivated because maxScale is set to 0`. + - **Runtime (best-effort):** the boot-time GVC read still refuses a **fresh** docroot on a + multi-location GVC and only warns on an initialised one. It is needed because the **bundled + MariaDB cannot be pinned** — a parent cannot template a subchart's workload spec, so MariaDB + still runs in every location with its own volume and its own database behind one DNS name. + - **Say "best-effort", never "guaranteed", about the runtime layer.** Measured 2026-08-31: it + **failed open on 1 boot in 5** (`WARNING: could not read GVC … guard SKIPPED`, most likely the + very start of a replica's life or the ~4-minute authorization cache window), and it **passed + during a location-removal drain**, which let `aws-us-east-2` replicas seed and run a **second + complete independent WordPress install**. The split it prevents is total and measured: + `aws-us-east-1` held 7 posts including the test post and its attachment, `aws-us-east-2` held + the bare 3-post fresh-install set. **Never change a GVC's locations under a live release.** + - The guard now logs the **HTTP status** (`403` / `000` / `200`-but-unparseable) — previously a + 403, a timeout and a bad body all produced one identical line and a maintainer had nothing to + go on. + - Residual hole with no in-container fix: a `location` the GVC LACKS is accepted, stored and + inert, so nothing boots and no check can fire. Diagnose with `get-deployments` showing no + replicas. +- **Cold start elects one replica.** With several replicas on one RWX docroot, a naive cold start + would have every replica seed and install at once, so `start.sh` takes an atomic `mkdir` lock; + the loser waits for a done-marker (bounded, then releases a stale lock and exits so the + platform restarts it). Verified with two replicas racing on one volume: one seeded, one waited + 5 s, exactly one admin user was created. +- **`cpln workload exec … php -r` does not see `WP_SITE_URL`.** It is exported by `start.sh`, so + only Apache's process tree has it; an `exec` gets a fresh process and `WP_HOME` reads as + undefined there. Harmless for the documented password reset, but do not diagnose the site URL + that way — check it over HTTP instead. +- **The first `helm upgrade` after an install may bounce the bundled MariaDB** (probabilistic + across the catalog) — measured at **95–107 s of site outage** (503 → WordPress's own + `Database Error` page at 500 → 200). **It is the DATABASE, not the web tier, and + `wordpress.replicas` does not mitigate it:** a WordPress-only roll was transparent at both + replica counts, 200/200 and 300/300, each with a control proving the replicas were genuinely + replaced. The platform surges a `standard` workload even at `maxScale: 1`. Check whether the + database is restarting before diagnosing a credential problem. During the bounce + `/cpln-health.php` returns 503 while `?shallow=1` returns 200 — the deep/shallow split working + as designed. +- **A private install needs `wordpress.siteUrl` to match the port-forward address.** WordPress + rewrites every URL from its stored site URL, so `port-forward` alone against the default + `siteUrl` gives `301 → http://localhost/` and `302 →` an unresolvable `*.cpln.local`; only + `/cpln-health.php` answers. The tunnel is fine (health returned 200 through it while the + canonical endpoint returned 403). Pair it with `siteUrl: http://localhost:8080` on the same + port — measured working: front page 200, login 302, wp-admin 200. Set `siteUrl` back before + going public, or visitors get redirected to `localhost`. +- **Availability posture, plainly:** `replicas: 2` removes the web tier as a single point of + failure — replicas are independent PHP servers sharing one database and one filesystem, with + cookie-based auth signed by salts in the shared `wp-config.php`, so no session affinity or peer + discovery is needed. The **bundled MariaDB remains single-instance** and is the availability + ceiling; there is no HA MariaDB template to depend on yet. +- **Plugin installs write to the shared volume** and are picked up by other replicas within + ~2 seconds (PHP opcache `revalidate_freq=2`). If a plugin install ever asks for **FTP + credentials**, `FS_METHOD = 'direct'` is not reaching WordPress — check `WORDPRESS_CONFIG_EXTRA` + on the container. + +## Platform facts measured while building this (worth reusing) + +- **A `standard` workload MAY mount a `shared` volumeset** — it reached `ready: true`. The + `stateful`/`vm` restriction applies to block (`ext4`/`xfs`) volumesets only. No template had + done this before. +- **A `shared` volumeset is JuiceFS, mounted `drwxrwxrwx` root:root, and `chown` works on it.** + So the upstream entrypoint's unguarded `chown .` would *not* have crashed, and no + `filesystemGroupId` is needed. The spec's `--no-same-owner` mitigation was actively harmful: + it leaves root-owned directories that `www-data` cannot write into, which would have broken + plugin installs and media uploads. The chart seeds with `--owner www-data --group www-data`. +- **The API backfills `autoscaling.target: 95`**, not 100, plus `maxConcurrency: 0` and + `scaleToZeroDelay: 300`, on a partial `defaultOptions.autoscaling` block. Omitting + `rolloutOptions` entirely stored no `rolloutOptions` at all. +- The image has `curl` but **no `wget`**, and `/usr/src/wordpress` is 117 MB (not ~250 MB). +- **A partial `firewallConfig.external` block is COMPLETED by the API.** Sending only + `inbound/outboundAllowCIDR` had it backfill `inboundBlockedCIDR`, `outboundBlockedCIDR` and + `outboundAllowHostname` as `[]`, and an undeclared `supportDynamicTags` stored as `false` — + four permanent rendered-vs-stored differences from creation, now declared explicitly. + `outboundAllowPort` was **not** backfilled, so it is deliberately still absent: declaring a + field the API does not store creates the drift it was meant to remove. +- **A `helm upgrade` drift gate cannot see that class.** Two no-op upgrades were fully green + (14/14 `Unchanged` on the second) while four fields differed from creation, because they are + written once and never churn. Only the render-vs-stored gate finds it — both gates are needed. +- **The bundled `mariadb` 1.4.1 subchart contributes 11 more render-vs-stored differences** and + they are NOT fixable from here: a parent cannot template a subchart's workload spec. They are a + partial `defaultOptions` (`maxConcurrency`, `scaleToZeroDelay`, `target`, `debug`, `suspend`), a + partial `firewallConfig.external` (the same three lists plus `inboundAllowCIDR`), a missing + `inboundAllowWorkload` on the non-list branch, and a missing `supportDynamicTags`. Every + `mariadb` install in the catalog carries this drift, and every parent that vendors it inherits + it — worth a tracking issue against the `mariadb` template, not against this one. diff --git a/wordpress/icon.png b/wordpress/icon.png new file mode 100644 index 00000000..45e8961d Binary files /dev/null and b/wordpress/icon.png differ diff --git a/wordpress/versions/1.0.0/Chart.yaml b/wordpress/versions/1.0.0/Chart.yaml new file mode 100644 index 00000000..f10563ac --- /dev/null +++ b/wordpress/versions/1.0.0/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: wordpress +description: WordPress content management system with bundled MariaDB and persistent media storage +type: application +version: 1.0.0 +appVersion: "7.1.0" + +annotations: + created: "2026-08-31" + lastModified: "2026-08-31" + category: "cms" + createsGvc: false + +dependencies: + - name: cpln-common + version: 1.0.0 + repository: "oci://ghcr.io/controlplane-com/templates" + - name: mariadb + version: 1.4.1 + repository: "oci://ghcr.io/controlplane-com/templates" diff --git a/wordpress/versions/1.0.0/README.md b/wordpress/versions/1.0.0/README.md new file mode 100644 index 00000000..3a40789b --- /dev/null +++ b/wordpress/versions/1.0.0/README.md @@ -0,0 +1,246 @@ +# WordPress + +[WordPress](https://wordpress.org/) is the open-source content management system behind a large share of the web. This template deploys a complete site — Apache with mod_php, a bundled MariaDB, and a read-write-many volume for your media library — with the administrator account created before the site ever accepts a request. + +## Architecture + +- **WordPress workload** (`standard`) — Apache + mod_php serving the site and `wp-admin` on port 80, pinned to the single `location`. +- **Docroot volumeset** (`shared`, read-write-many) — `/var/www/html`: WordPress core, plugins, themes, uploads and the generated `wp-config.php`. One volume in `location`, mounted by every replica. +- **MariaDB workload + volumeset** (subchart `mariadb`, `stateful`) — posts, pages, users and settings. Not pinned: it runs in every location the GVC has. +- **Three chart secrets** — the boot wrapper, the first-run installer, and a PHP limits overlay. +- **Two chart-created database secrets** — the bundled MariaDB's application credentials and its root password, kept separate so the application credential never carries root. +- **Identity + two policies** — `reveal` on exactly the five secrets the container reads, and `view` on the single install GVC for the boot-time location guard. +- *Optional (subchart pass-through)* — scheduled database backups and a phpMyAdmin console. + +## Prerequisites + +- **An admin `dictionary` secret, created BEFORE you install.** It holds exactly three keys — `username`, `password`, `email` — and the site's administrator is created from it on first boot: + + ```bash + cpln secret create-dictionary --name my-wordpress-admin \ + --entry username=wpadmin \ + --entry password='YOUR-STRONG-PASSWORD' \ + --entry email=admin@example.com + ``` + + If the secret does not exist the deployment **wedges silently** — the container never starts, so `cpln logs` returns *zero lines*. The only diagnostic is `status.versions[].message`: + + ```bash + cpln workload get-deployments RELEASE-wordpress --gvc YOUR-GVC -o yaml + ``` + + Create the secret and the deployment recovers on its own within roughly 5–10 minutes, or immediately with `cpln workload force-redeployment RELEASE-wordpress --gvc YOUR-GVC`. Allow about three more minutes after that for the first-boot seed — from creating the secret late to a working site is roughly 11 minutes. + +- **A single-location GVC**, and `location` set to that location. Every workload runs in *every* location its GVC has, so a second location would mean a second WordPress and a second MariaDB, each on its own volume, behind one hostname. Measured: one location held the live site's 7 posts and its uploads while the other held nothing but the bare 3-post fresh-install set, with every status surface green. + + Two things guard that, and only one of them is a guarantee: + + - **WordPress itself is pinned** to `location` (`maxScale: 0` everywhere else), so no other location of the GVC can start one. That holds by construction. + - **The bundled MariaDB is not pinned** — it comes from the `mariadb` subchart, whose workload spec this chart cannot template, so it still starts in every location the GVC has. The container reads the GVC at boot and refuses a *fresh* install when it sees more than one location. Treat that as a **safety net, not a guarantee**: it fails open when the GVC cannot be read (observed on 1 boot in 5, and on any boot inside the ~4-minute window after an authorization change), and it passes during a location add/remove transition. + + **Do not change a GVC's locations under a live WordPress release.** During a location *removal*, replicas in the departing location read the GVC as single-location, passed the check, and installed a second, completely independent WordPress site. + +## Configuration + +### Location + +```yaml +# ─── Location ───────────────────────────────────────────────────────────────── +# The ONE location of your GVC that WordPress runs in. Every replica is pinned +# here, and no other location of the GVC starts one: an undeclared location gets +# maxScale 0 and starts nothing. Without that pin an extra GVC location silently +# runs a SECOND, complete, independent WordPress on its own volume. +# It must be a location your GVC actually has — the platform accepts one the GVC +# lacks without any error and then runs nothing, with no failed deployment to see. +location: aws-us-east-1 + +``` + +### WordPress + +```yaml +# ─── WordPress ──────────────────────────────────────────────────────────────── +wordpress: + # Official image (library/wordpress); the tag pins WordPress AND PHP. + # It seeds the docroot on FIRST BOOT ONLY — after that WordPress owns its own + # files and you update core from the dashboard. Bumping this tag later changes + # PHP and Apache, not WordPress core. + image: wordpress:7.1.0-php8.4-apache + replicas: 1 # >=2 share one uploads volume and one database; see README "Availability" + siteTitle: My WordPress Site # set on first boot only; change later in Settings → General + siteUrl: "" # public base URL; empty = the auto-assigned *.cpln.app endpoint + tablePrefix: wp_ # change only when importing an existing WordPress database + debug: false # WP_DEBUG — PHP notices to the container log; leave off in production + resources: + minCpu: 250m + maxCpu: 1000m + minMemory: 512Mi + maxMemory: 1Gi + +``` + +### PHP limits + +```yaml +# ─── PHP limits ─────────────────────────────────────────────────────────────── +# Stock php.ini caps uploads at 2M, which blocks most media uploads. +php: + uploadMaxSize: 64M # upload_max_filesize and post_max_size + memoryLimit: 256M # PHP memory_limit; some plugins want 256M in wp-admin + +``` + +### Admin account + +```yaml +# ─── Admin account (REQUIRED PREREQUISITE SECRET) ───────────────────────────── +# CREATE IT BEFORE YOU INSTALL. A `dictionary` secret with exactly three keys: +# `username`, `password`, `email`. Applied on FIRST BOOT ONLY — rotating it later +# does NOT change your login. If it does not exist the deployment WEDGES silently +# and `cpln logs` returns nothing; the only diagnostic is status.versions[].message +# from `cpln workload get-deployments`. +admin: + secretName: my-wordpress-admin + +``` + +### Content storage + +```yaml +# ─── Content storage ────────────────────────────────────────────────────────── +# One read-write-many volume per location, mounted at /var/www/html by every +# replica: core, plugins, themes, uploads and wp-config.php. Shared volumes can be +# expanded but NOT snapshotted — the media library has no backup, keep your own copy. +volumeset: + capacity: 10 # initial capacity in GiB (minimum is 10) + +``` + +### Access + +```yaml +# ─── Access ─────────────────────────────────────────────────────────────────── +# A firewall change takes ~30 s to ~10 min to propagate. +publicAccess: + enabled: true # HTTPS site on the auto-assigned *.cpln.app endpoint +internalAccess: + type: same-gvc # none | same-gvc | same-org | workload-list + workloads: [] # used only with workload-list + # workloads: + # - //gvc/GVC_NAME/workload/WORKLOAD_NAME + +``` + +### MariaDB (bundled database) + +```yaml +# ─── MariaDB (subchart: mariadb) — posts, pages, users, settings ────────────── +# Every knob of the `mariadb` template is available under this key — including its +# native scheduled backups and phpMyAdmin console, with nothing extra to install: +# mariadb.backup.enabled: true +# mariadb.backup.provider: aws | gcp +# mariadb.backup.aws.bucket / .region / .cloudAccountName / .policyName +# mariadb.phpMyAdmin.enabled: true +# See "Backing up the bundled database" in the README. +mariadb: + image: mariadb:11 + # Credentials for the bundled database — internal plumbing no human types + # elsewhere. This chart CREATES both secrets named below out of these values, + # so there is nothing for you to create before installing. + credentials: + username: wordpress + password: change-me-wordpress-db # change before installing + database: wordpress + rootPassword: change-me-wordpress-db-root # change before installing + # Names of the two secrets this chart creates and the bundled MariaDB reads. + # Secret names are org-wide: give each wordpress release its own names. A second + # release left on these names is REFUSED at install (they are owned by the first + # release) — nothing is shared, overwritten or deleted. + credentialsSecretName: my-wordpress-db-credentials + rootPasswordSecretName: my-wordpress-db-root-password + resources: + minCpu: 150m # keeps cpu:minCpu under the stateful 4:1 cap (500/150 = 3.3:1) + maxCpu: 500m + minMemory: 256Mi + maxMemory: 1Gi + volumeset: + capacity: 10 # initial capacity in GiB (minimum is 10) + internalAccess: + type: same-gvc # WordPress must be able to reach the database +``` + +## Connecting + +| What | Where | +|---|---| +| Public site | The auto-assigned `*.cpln.app` endpoint (`publicAccess.enabled: true`). Read the exact URL from `status.canonicalEndpoint` of `cpln workload get RELEASE-wordpress --gvc YOUR-GVC -o yaml`. | +| Admin dashboard | `/wp-admin` | +| Admin credentials | The `username` / `password` you put in the `admin.secretName` dictionary secret | +| From another workload in the GVC | `http://RELEASE-wordpress.YOUR-GVC.cpln.local` (port 80) | +| Health endpoint | `/cpln-health.php` — returns `ok` when PHP, the database and the install are all good | +| Database (internal only) | `RELEASE-maria.YOUR-GVC.cpln.local:3306`, credentials in the `mariadb.credentialsSecretName` secret | + +With `publicAccess.enabled: false` the site is still reachable in a browser through a tunnel — **but the tunnel address and `wordpress.siteUrl` have to agree.** WordPress rewrites every URL from its stored site URL, so a tunnel opened while `siteUrl` is empty bounces the browser somewhere it cannot reach (`/` redirects to `http://localhost/`, `/wp-admin/` to the internal `*.cpln.local` name) and only `/cpln-health.php` answers. Point the site URL at the tunnel, on the same port: + +```bash +# 1. in your values — the site URL is the address you will actually browse: +# publicAccess: +# enabled: false +# wordpress: +# siteUrl: http://localhost:8080 +# +# 2. open the tunnel on the SAME port (a mismatch reproduces the redirect): +cpln port-forward RELEASE-wordpress 8080:80 --gvc YOUR-GVC +# +# 3. browse http://localhost:8080/ and http://localhost:8080/wp-admin/ +``` + +`wordpress.siteUrl` takes effect on an already-installed site, so this works as an upgrade rather than a reinstall. Set it back to `""` (or to your real public URL) before turning `publicAccess.enabled` on again, otherwise the public site redirects your visitors to `localhost`. + +## Availability + +`wordpress.replicas: 2` or more removes the web tier as a single point of failure: losing a replica was absorbed with zero failed requests. Replicas are independent PHP servers sharing one database and one filesystem; authentication is cookie-based and signed with the salts in the shared `wp-config.php`, so any replica accepts any other's cookie and no session affinity is needed. All replicas run in the single `location` — this is not a multi-region deployment. + +**A rolling restart is already transparent at `replicas: 1`.** The platform surges a new replica before retiring the old one, so an upgrade that touches only WordPress serves without interruption at any replica count. + +**The bundled MariaDB stays single-instance and is the availability ceiling** — and it is where the outage you are most likely to actually see comes from. The first `helm upgrade` after an install may bounce the bundled database, and the site then returns errors for roughly 95–107 seconds while it comes back. **More `wordpress.replicas` does not help with that**: every replica depends on the one database. Later upgrades do not repeat it. There is no HA MariaDB template to depend on yet. + +## Backups + +**The database can be backed up on a schedule. The media library cannot.** Be clear on this before you put a real site here: + +- **Database — supported.** Every knob of the `mariadb` template is available under the `mariadb` key, including its native scheduled backups to AWS S3 or GCS. Set `mariadb.backup.enabled: true`, pick `mariadb.backup.provider`, and fill in the bucket, region, [cloud account](https://docs.controlplane.com/guides/create-cloud-account) and IAM policy under `mariadb.backup.aws` or `mariadb.backup.gcp`. The `mariadb` template's own README carries the per-provider bucket and IAM setup steps. +- **Media library — not backed up, and there is no snapshot path we can vouch for.** The docroot is a `shared` volumeset, which can be expanded but not snapshotted. Uploads, installed plugins and themes therefore exist in exactly one place. Keep your own copy, and do not assume a reinstall can recover it. + +A database backup on its own restores your posts, pages, users and settings, but the images those posts reference will be missing unless you kept the media library yourself. + +## Recovering a lost admin password + +The admin account is created on **first boot only**, so rotating the `admin.secretName` secret does not change anyone's login. Reset the password directly instead (user ID `1` is the administrator created at install): + +```bash +cpln workload exec RELEASE-wordpress --gvc YOUR-GVC --container wordpress -- \ + php -r 'require "/var/www/html/wp-load.php"; wp_set_password("YOUR-NEW-PASSWORD", 1);' +``` + +WordPress's own "lost password" email cannot help here — see the note on outbound email below. + +## Important Notes + +- **Create the `admin.secretName` dictionary secret before installing.** A missing secret wedges the deployment with zero log output; read `status.versions[].message` from `cpln workload get-deployments` to see which secret is missing. +- **Change `mariadb.credentials.password` and `mariadb.credentials.rootPassword` before installing.** They ship as obviously-invalid `change-me-…` placeholders and are used as-is. +- **Bumping `wordpress.image` does NOT upgrade WordPress core on an existing install.** The image seeds the docroot only while it is empty; after that WordPress owns its files and you update core from the dashboard. A tag bump changes PHP and Apache only. +- **Outbound email does not work out of the box.** The container has no mail transport, so `wp_mail()` — including password reset and user invites — fails silently. Install an SMTP plugin before you rely on any email. +- **Install into a single-location GVC, set `location` to that location, and do not change the GVC's locations afterwards.** WordPress is pinned to `location`, but the bundled MariaDB is a subchart workload this chart cannot pin and still runs in every location. The boot-time check that catches that is a best-effort safety net, not a guarantee: it fails open if it cannot read the GVC and does not hold during a location add/remove. Data split across locations cannot be merged back. +- **A `location` your GVC does not have starts nothing, silently.** The platform accepts it without any error and there is no failed deployment to see — the workload simply has no replicas. Check `cpln workload get-deployments RELEASE-wordpress` if an install appears to do nothing. +- **`wordpress.siteTitle` and `wordpress.tablePrefix` apply on first boot only.** Change the title later in Settings → General; the table prefix cannot be changed after install. +- **Give each release its own `mariadb.credentialsSecretName` and `mariadb.rootPasswordSecretName`.** Secret names are org-wide, and a second release left on the defaults is refused at install. +- **A firewall change takes ~30 s to ~10 min to propagate.** After flipping `publicAccess.enabled` or `internalAccess.type`, keep re-polling rather than concluding the knob is broken. + +## Links + +- [WordPress documentation](https://wordpress.org/documentation/) +- [Advanced administration handbook](https://developer.wordpress.org/advanced-administration/) +- [WordPress requirements](https://wordpress.org/about/requirements/) +- [`wp-config.php` reference](https://developer.wordpress.org/apis/wp-config-php/) +- [Official WordPress Docker image](https://hub.docker.com/_/wordpress) diff --git a/wordpress/versions/1.0.0/templates/_helpers.tpl b/wordpress/versions/1.0.0/templates/_helpers.tpl new file mode 100644 index 00000000..2c95f409 --- /dev/null +++ b/wordpress/versions/1.0.0/templates/_helpers.tpl @@ -0,0 +1,204 @@ +{{/* Resource Naming */}} + +{{- define "wordpress.name" -}} +{{- printf "%s-wordpress" .Release.Name }} +{{- end }} + +{{- define "wordpress.volume.name" -}} +{{- printf "%s-wordpress-vs" .Release.Name }} +{{- end }} + +{{- define "wordpress.identity.name" -}} +{{- printf "%s-wordpress-identity" .Release.Name }} +{{- end }} + +{{- define "wordpress.policy.name" -}} +{{- printf "%s-wordpress-policy" .Release.Name }} +{{- end }} + +{{- define "wordpress.policy.gvc.name" -}} +{{- printf "%s-wordpress-gvc-policy" .Release.Name }} +{{- end }} + +{{/* Boot wrapper: GVC guard -> docroot seed -> wp-config -> install -> Apache. */}} +{{- define "wordpress.secret.start.name" -}} +{{- printf "%s-wordpress-start" .Release.Name }} +{{- end }} + +{{/* First-run installer, run before Apache binds. */}} +{{- define "wordpress.secret.install.name" -}} +{{- printf "%s-wordpress-install" .Release.Name }} +{{- end }} + +{{/* PHP ini overlay (upload size, memory limit). */}} +{{- define "wordpress.secret.phpini.name" -}} +{{- printf "%s-wordpress-php-ini" .Release.Name }} +{{- end }} + +{{/* +Names of the two secrets this chart CREATES for the bundled mariadb subchart. +mariadb 1.4.0 stopped creating its own secrets and now takes only their NAMES, +and a parent cannot template a subchart value — so the names are plain values +that both sides read. +*/}} +{{- define "wordpress.secret.db.name" -}} +{{- .Values.mariadb.credentialsSecretName }} +{{- end }} + +{{- define "wordpress.secret.dbRoot.name" -}} +{{- .Values.mariadb.rootPasswordSecretName }} +{{- end }} + + +{{/* Dependency Helpers (deterministic on .Release.Name — mirrors the subchart helper) */}} + +{{/* +Hostname of the bundled MariaDB (the mariadb subchart's `maria.name` helper → +{release}-maria), on port 3306. Always the FQDN: the bare short name is not +reliable and is workload-type dependent. +*/}} +{{- define "wordpress.mariadb.host" -}} +{{- printf "%s-maria.%s.cpln.local" .Release.Name .Values.global.cpln.gvc }} +{{- end }} + +{{/* +Every workload this release creates, as workload links. + +Defined ONCE and appended at every `workload-list` call site, so an +`internalAccess.type: workload-list` can never silently cut this release off +from itself. Each member is gated on the toggle that creates it, so the list +never names a workload that was not rendered. + +Nothing in this release currently calls INTO WordPress (the traffic runs the +other way, WordPress → MariaDB, and is governed by `mariadb.internalAccess`), so +these entries are defensive rather than load-bearing today. They are here +because a hand-maintained list is exactly what drifts when that changes. +*/}} +{{- define "wordpress.ownWorkloadLinks" -}} +{{- $gvc := .Values.global.cpln.gvc -}} +- //gvc/{{ $gvc }}/workload/{{ include "wordpress.name" . }} +- //gvc/{{ $gvc }}/workload/{{ .Release.Name }}-maria +{{- if .Values.mariadb.phpMyAdmin }}{{- if .Values.mariadb.phpMyAdmin.enabled }} +- //gvc/{{ $gvc }}/workload/{{ .Release.Name }}-phpmyadmin +{{- end }}{{- end }} +{{- if .Values.mariadb.backup }}{{- if .Values.mariadb.backup.enabled }} +- //gvc/{{ $gvc }}/workload/{{ .Release.Name }}-maria-backup +{{- end }}{{- end }} +{{- end }} + + +{{/* Labeling */}} + +{{- define "wordpress.tags" -}} +{{- include "cpln-common.tags" . }} +{{- end }} + + +{{/* Validation */}} + +{{- define "wordpress.validate" -}} + +{{- /* Admin account — a REQUIRED prerequisite secret, never a values default. */ -}} +{{- if not .Values.admin.secretName -}} +{{- fail "wordpress: admin.secretName is required — the name of a prerequisite `dictionary` secret holding exactly the keys `username`, `password` and `email`. It MUST exist BEFORE install: the admin account is created before Apache binds, and a missing secret wedges the deployment silently (cpln logs returns nothing — read status.versions[].message from `cpln workload get-deployments`)" -}} +{{- end -}} + +{{- /* Bundled-database plumbing this chart creates. */ -}} +{{- if not .Values.mariadb.credentialsSecretName -}} +{{- fail "wordpress: mariadb.credentialsSecretName is required — this chart CREATES that dictionary secret from mariadb.credentials.*, and the bundled MariaDB reads it by name. Secret names are org-wide, so give each wordpress release its own name" -}} +{{- end -}} +{{- if not .Values.mariadb.rootPasswordSecretName -}} +{{- fail "wordpress: mariadb.rootPasswordSecretName is required — this chart CREATES that opaque secret from mariadb.credentials.rootPassword, and the bundled MariaDB reads it by name. Secret names are org-wide, so give each wordpress release its own name" -}} +{{- end -}} +{{- if not .Values.mariadb.credentials.username -}} +{{- fail "wordpress: mariadb.credentials.username is required" -}} +{{- end -}} +{{- if not .Values.mariadb.credentials.password -}} +{{- fail "wordpress: mariadb.credentials.password is required" -}} +{{- end -}} +{{- if not .Values.mariadb.credentials.database -}} +{{- fail "wordpress: mariadb.credentials.database is required" -}} +{{- end -}} +{{- if not .Values.mariadb.credentials.rootPassword -}} +{{- fail "wordpress: mariadb.credentials.rootPassword is required" -}} +{{- end -}} +{{- if eq .Values.mariadb.credentialsSecretName .Values.mariadb.rootPasswordSecretName -}} +{{- fail "wordpress: mariadb.credentialsSecretName and mariadb.rootPasswordSecretName must be DIFFERENT secrets — the application credential is deliberately separate from root, so sharing the first does not hand out the second" -}} +{{- end -}} + +{{- /* + Location — the ONE GVC location this release runs in. It is what CONFINES the + WordPress workload: `defaultOptions` scales to 0 everywhere and `localOptions` + supplies the real count here. The platform does NOT validate this direction — + a localOptions entry naming a location the GVC lacks is accepted, stored, and + simply inert — so the render-time checks below catch the shapes we can see and + the boot-time GVC read in start.sh catches the rest, best-effort. +*/ -}} +{{- if hasKey .Values "locations" -}} +{{- fail "wordpress: `locations` (plural) is not a key of this chart. WordPress runs in exactly ONE location — one docroot volume and one bundled database — so use the singular `location`, e.g. `location: aws-us-east-1`" -}} +{{- end -}} +{{- if not .Values.location -}} +{{- fail "wordpress: `location` is required — it names the ONE location of your GVC that WordPress runs in, e.g. `location: aws-us-east-1`. Every replica is pinned there and no other location of the GVC starts one" -}} +{{- end -}} +{{- if not (kindIs "string" .Values.location) -}} +{{- fail "wordpress: `location` must be a single location NAME, e.g. `location: aws-us-east-1`. WordPress runs in exactly one location: the docroot is one volume and the bundled database is one instance" -}} +{{- end -}} +{{- if not (regexMatch "^[a-z0-9]+(-[a-z0-9]+)*$" .Values.location) -}} +{{- fail (printf "wordpress: `location` must be a bare location NAME like `aws-us-east-1`, got '%s'. A link form (`//location/x`, which is what `spec.staticPlacement.locationLinks` contains) or a comma-separated list is accepted by Helm AND by the API, and stores a placement that matches no real location — so WordPress starts NOWHERE, every location reports `deactivated because maxScale is set to 0`, and the logs are silent." .Values.location) -}} +{{- end -}} + +{{- /* Replicas. */ -}} +{{- if lt (int .Values.wordpress.replicas) 1 -}} +{{- fail (printf "wordpress: wordpress.replicas must be at least 1, got '%v'" .Values.wordpress.replicas) -}} +{{- end -}} + +{{- /* Table prefix: WordPress requires a non-empty, [A-Za-z0-9_] prefix. */ -}} +{{- if not .Values.wordpress.tablePrefix -}} +{{- fail "wordpress: wordpress.tablePrefix must not be empty (WordPress default is 'wp_')" -}} +{{- end -}} +{{- if not (regexMatch "^[A-Za-z0-9_]+$" .Values.wordpress.tablePrefix) -}} +{{- fail (printf "wordpress: wordpress.tablePrefix may contain only letters, numbers and underscores, got '%s'" .Values.wordpress.tablePrefix) -}} +{{- end -}} + +{{- /* Site URL must carry a scheme — it becomes WP_HOME/WP_SITEURL verbatim. */ -}} +{{- if .Values.wordpress.siteUrl -}} +{{- if not (or (hasPrefix "http://" .Values.wordpress.siteUrl) (hasPrefix "https://" .Values.wordpress.siteUrl)) -}} +{{- fail (printf "wordpress: wordpress.siteUrl must start with http:// or https:// — it is used verbatim as WP_HOME and WP_SITEURL, got '%s'" .Values.wordpress.siteUrl) -}} +{{- end -}} +{{- if hasSuffix "/" .Values.wordpress.siteUrl -}} +{{- fail (printf "wordpress: wordpress.siteUrl must not end with a trailing slash, got '%s'" .Values.wordpress.siteUrl) -}} +{{- end -}} +{{- end -}} + +{{- /* Volume capacity. */ -}} +{{- if lt (int .Values.volumeset.capacity) 10 -}} +{{- fail (printf "wordpress: volumeset.capacity must be at least 10 (GiB), got '%v'" .Values.volumeset.capacity) -}} +{{- end -}} + +{{- /* Access. */ -}} +{{- if not (has .Values.internalAccess.type (list "none" "same-gvc" "same-org" "workload-list")) -}} +{{- fail (printf "wordpress: internalAccess.type must be 'none', 'same-gvc', 'same-org' or 'workload-list', got '%s'" .Values.internalAccess.type) -}} +{{- end -}} +{{- if and (eq .Values.internalAccess.type "workload-list") (not .Values.internalAccess.workloads) -}} +{{- fail "wordpress: internalAccess.workloads must list at least one workload link when internalAccess.type is 'workload-list', e.g. //gvc/GVC_NAME/workload/WORKLOAD_NAME (this release's own workloads are added automatically)" -}} +{{- end -}} + +{{- /* + The database is reached over the GVC's internal network. If the user narrows + the bundled MariaDB to a workload list that omits this WordPress workload, the + site cannot reach its own database — and it fails as a boot hang, not as an + error, so catch it at render instead. +*/ -}} +{{- if .Values.mariadb.internalAccess -}} +{{- if eq (.Values.mariadb.internalAccess.type | default "") "workload-list" -}} +{{- $self := printf "//gvc/%s/workload/%s" .Values.global.cpln.gvc (include "wordpress.name" .) -}} +{{- if not (has $self (.Values.mariadb.internalAccess.workloads | default list)) -}} +{{- fail (printf "wordpress: mariadb.internalAccess.type is 'workload-list' but the list does not include this release's WordPress workload — add '%s', or the site cannot reach its own database" $self) -}} +{{- end -}} +{{- end -}} +{{- if eq (.Values.mariadb.internalAccess.type | default "") "none" -}} +{{- fail "wordpress: mariadb.internalAccess.type must not be 'none' — WordPress reaches the bundled database over the GVC internal network. Use 'same-gvc' (default) or 'workload-list' including this release's WordPress workload" -}} +{{- end -}} +{{- end -}} + +{{- end }} diff --git a/wordpress/versions/1.0.0/templates/identity.yaml b/wordpress/versions/1.0.0/templates/identity.yaml new file mode 100644 index 00000000..5e1b3a5e --- /dev/null +++ b/wordpress/versions/1.0.0/templates/identity.yaml @@ -0,0 +1,5 @@ +kind: identity +name: {{ include "wordpress.identity.name" . }} +description: WordPress workload identity +gvc: {{ .Values.global.cpln.gvc }} +tags: {{- include "wordpress.tags" . | nindent 4 }} diff --git a/wordpress/versions/1.0.0/templates/policy-gvc.yaml b/wordpress/versions/1.0.0/templates/policy-gvc.yaml new file mode 100644 index 00000000..00a8937e --- /dev/null +++ b/wordpress/versions/1.0.0/templates/policy-gvc.yaml @@ -0,0 +1,19 @@ +{{- /* + The container reads its OWN GVC at boot to confirm it has exactly one location. + On a multi-location GVC the platform would run one WordPress AND one MariaDB + per location, each with its own volume — the media library and the database + split silently. Scoped to the ONE install GVC; never `target: all`, which would + grant view on every GVC in the org. +*/}} +kind: policy +name: {{ include "wordpress.policy.gvc.name" . }} +description: WordPress GVC read policy (single-location boot guard) +tags: {{- include "wordpress.tags" . | nindent 4 }} +bindings: + - permissions: + - view + principalLinks: + - //gvc/{{ .Values.global.cpln.gvc }}/identity/{{ include "wordpress.identity.name" . }} +targetKind: gvc +targetLinks: + - //gvc/{{ .Values.global.cpln.gvc }} diff --git a/wordpress/versions/1.0.0/templates/policy.yaml b/wordpress/versions/1.0.0/templates/policy.yaml new file mode 100644 index 00000000..36a773c5 --- /dev/null +++ b/wordpress/versions/1.0.0/templates/policy.yaml @@ -0,0 +1,22 @@ +{{- /* + `reveal` on exactly the secrets the WordPress container reads — and no others. + The bundled database's ROOT password is deliberately absent: the application + connects as the unprivileged user, and the MariaDB subchart's own identity + already covers root. +*/}} +kind: policy +name: {{ include "wordpress.policy.name" . }} +description: WordPress secret access +tags: {{- include "wordpress.tags" . | nindent 4 }} +bindings: + - permissions: + - reveal + principalLinks: + - //gvc/{{ .Values.global.cpln.gvc }}/identity/{{ include "wordpress.identity.name" . }} +targetKind: secret +targetLinks: + - //secret/{{ include "wordpress.secret.start.name" . }} + - //secret/{{ include "wordpress.secret.install.name" . }} + - //secret/{{ include "wordpress.secret.phpini.name" . }} + - //secret/{{ include "wordpress.secret.db.name" . }} + - //secret/{{ .Values.admin.secretName }} diff --git a/wordpress/versions/1.0.0/templates/secret-db-root.yaml b/wordpress/versions/1.0.0/templates/secret-db-root.yaml new file mode 100644 index 00000000..1819a5cb --- /dev/null +++ b/wordpress/versions/1.0.0/templates/secret-db-root.yaml @@ -0,0 +1,14 @@ +{{- /* + The bundled database's root password, kept in a SEPARATE secret from the + application credentials so the WordPress policy can grant `reveal` on the + application credential without also handing out root. Only the MariaDB + subchart's own identity can read this one. +*/}} +kind: secret +name: {{ include "wordpress.secret.dbRoot.name" . }} +description: WordPress bundled MariaDB root password +tags: {{- include "wordpress.tags" . | nindent 4 }} +type: opaque +data: + encoding: plain + payload: {{ .Values.mariadb.credentials.rootPassword | quote }} diff --git a/wordpress/versions/1.0.0/templates/secret-db.yaml b/wordpress/versions/1.0.0/templates/secret-db.yaml new file mode 100644 index 00000000..bdaed43d --- /dev/null +++ b/wordpress/versions/1.0.0/templates/secret-db.yaml @@ -0,0 +1,17 @@ +{{- /* + Credentials for the BUNDLED database, in the shape the mariadb subchart reads: + exactly the keys username, password and database. Internal plumbing — no human + ever types this password — so the bundled-plumbing exception applies and it + stays a template-created secret rather than a prerequisite the user creates. + mariadb 1.4.0 stopped creating this secret and now takes only its NAME, which + is why the chart on this side of the dependency owns it. +*/}} +kind: secret +name: {{ include "wordpress.secret.db.name" . }} +description: WordPress bundled MariaDB application credentials +tags: {{- include "wordpress.tags" . | nindent 4 }} +type: dictionary +data: + username: {{ .Values.mariadb.credentials.username | quote }} + password: {{ .Values.mariadb.credentials.password | quote }} + database: {{ .Values.mariadb.credentials.database | quote }} diff --git a/wordpress/versions/1.0.0/templates/secret-install.yaml b/wordpress/versions/1.0.0/templates/secret-install.yaml new file mode 100644 index 00000000..689d0ff9 --- /dev/null +++ b/wordpress/versions/1.0.0/templates/secret-install.yaml @@ -0,0 +1,56 @@ +{{- /* + First-run installer, run by start.sh BEFORE Apache binds — so there is never a + reachable /wp-admin/install.php for a stranger to claim, which is what lets + publicAccess default to true. + + Credentials arrive as ENV (resolved by the platform from the prerequisite + admin secret) and never appear on a command line or in a log. + + wp_install() does NOT guard itself, so the is_blog_installed() check is + mandatory: without it a restart on an installed site would attempt a second + install. +*/}} +kind: secret +name: {{ include "wordpress.secret.install.name" . }} +description: WordPress first-run installer +tags: {{- include "wordpress.tags" . | nindent 4 }} +type: opaque +data: + encoding: plain + payload: |- + $user, 'WP_ADMIN_PASSWORD' => $pass, 'WP_ADMIN_EMAIL' => $mail] as $k => $v) { + if ($v === false || $v === '') { + fwrite(STDERR, "cpln-bootstrap: $k is empty - the admin secret must hold the keys username, password and email\n"); + exit(1); + } + } + + $result = wp_install(getenv('WP_SITE_TITLE') ?: 'WordPress', $user, $mail, true, '', $pass); + + if (is_wp_error($result)) { + fwrite(STDERR, "cpln-bootstrap: install failed: " . $result->get_error_message() . "\n"); + exit(1); + } + fwrite(STDERR, "cpln-bootstrap: WordPress installed; admin user '" . $user . "' created\n"); diff --git a/wordpress/versions/1.0.0/templates/secret-php-ini.yaml b/wordpress/versions/1.0.0/templates/secret-php-ini.yaml new file mode 100644 index 00000000..77b8effd --- /dev/null +++ b/wordpress/versions/1.0.0/templates/secret-php-ini.yaml @@ -0,0 +1,16 @@ +{{- /* + Mounted at /usr/local/etc/php/conf.d/zz-cpln-wordpress.ini. The `zz-` prefix + sorts it after the image's own opcache-recommended.ini and error-logging.ini, + so these win. Without it the stock php.ini-production caps uploads at 2M. +*/}} +kind: secret +name: {{ include "wordpress.secret.phpini.name" . }} +description: WordPress PHP limits (upload size, memory limit) +tags: {{- include "wordpress.tags" . | nindent 4 }} +type: opaque +data: + encoding: plain + payload: |- + upload_max_filesize = {{ .Values.php.uploadMaxSize }} + post_max_size = {{ .Values.php.uploadMaxSize }} + memory_limit = {{ .Values.php.memoryLimit }} diff --git a/wordpress/versions/1.0.0/templates/secret-start.yaml b/wordpress/versions/1.0.0/templates/secret-start.yaml new file mode 100644 index 00000000..05fc814c --- /dev/null +++ b/wordpress/versions/1.0.0/templates/secret-start.yaml @@ -0,0 +1,244 @@ +{{- /* + Boot wrapper, run as `bash /cpln/start.sh`. Ordered contract: + 1. single-location GVC guard 2. docroot seed (one replica only) + 3. wp-config.php generation 4. wait for MariaDB + 5. first-run install 6. health endpoint 7. exec Apache + + Why a wrapper at all: with several replicas sharing one read-write-many + docroot, a cold start would have every replica seed and install at once. The + image's own entrypoint has no election, so this script performs one. +*/}} +kind: secret +name: {{ include "wordpress.secret.start.name" . }} +description: WordPress boot wrapper (GVC guard, docroot seed, first-run install) +tags: {{- include "wordpress.tags" . | nindent 4 }} +type: opaque +data: + encoding: plain + payload: |- + #!/bin/bash + set -Eeuo pipefail + + DOCROOT=/var/www/html + LOCK="$DOCROOT/.cpln-bootstrap.lock" + DONE="$DOCROOT/.cpln-bootstrap-done" + cd "$DOCROOT" + + log() { echo "cpln-bootstrap: $*" >&2; } + + # ── 0. Site URL ────────────────────────────────────────────────────────────── + # WP_HOME/WP_SITEURL are derived from this via WORDPRESS_CONFIG_EXTRA, which is + # read on EVERY request -- so a later `helm upgrade` changes the site URL even + # though wp-config.php was generated on first boot. +{{- if .Values.wordpress.siteUrl }} + export WP_SITE_URL="{{ .Values.wordpress.siteUrl }}" +{{- else if .Values.publicAccess.enabled }} + # CPLN_GLOBAL_ENDPOINT is already a full https:// URL, NOT a bare host -- + # use it verbatim, never prepend a second scheme, never assemble it from parts. + export WP_SITE_URL="${CPLN_GLOBAL_ENDPOINT:-}" +{{- else }} + # No public endpoint -- links point at the internal in-GVC address. + export WP_SITE_URL="http://{{ include "wordpress.name" . }}.{{ .Values.global.cpln.gvc }}.cpln.local" +{{- end }} + + # ── 1. Single-location GVC guard ───────────────────────────────────────────── + # SECOND layer, not the only one. The workload itself is CONFINED to the one + # configured location by defaultOptions maxScale 0 + localOptions, so an extra + # GVC location can no longer start a second WordPress. What confinement does + # NOT cover is the bundled MariaDB: a subchart's workload spec cannot be + # templated by the parent, so MariaDB still runs in EVERY location the GVC has, + # each with its own volume and its own database, behind one DNS name. That is + # what this check is for now. + # + # It is BEST-EFFORT and must be described that way everywhere: it fails open + # when the GVC cannot be read (measured on 1 boot in 5, and on any boot inside + # the ~4-minute authorization cache window after a policy change), and a + # location add/remove transition can make a multi-location GVC read as one. + # A FRESH docroot refuses; an already-initialised one only warns, so the check + # can never take a live site down. + FRESH=no + if [ ! -e wp-includes/version.php ] && [ ! -e index.php ]; then FRESH=yes; fi + + CONFIGURED_LOCATION="{{ .Values.location }}" + THIS_LOCATION="$(basename "${CPLN_LOCATION:-unknown}")" + + LOCATIONS="" + GVC_HTTP="000" + GVC_ERR="" + if [ -n "${WP_GVC_LOCATIONS_OVERRIDE:-}" ]; then + # Test hook: lets the >1-location disposition be exercised on a + # single-location GVC. The chart never sets this. + LOCATIONS="$WP_GVC_LOCATIONS_OVERRIDE" + GVC_HTTP="override" + log "GVC location list overridden for testing: $LOCATIONS" + elif [ -n "${CPLN_TOKEN:-}" ] && [ -n "${CPLN_ORG:-}" ] && [ -n "${CPLN_GVC:-}" ]; then + # curl defaults to HTTP/1.1; $CPLN_ENDPOINT sits behind istio-envoy, which + # answers HTTP/1.0 with 426. --max-time bounds the whole call, not just connect. + # + # The status code is captured and logged: a 403 (no `view` on the GVC), a + # timeout and an unparseable 200 are three different problems, and this + # script used to render all three as one identical WARNING line -- which was + # the only diagnostic a maintainer got. + # Fixed $$-suffixed paths rather than mktemp: every other external command + # this script uses is already proven present in the pinned image, and this + # avoids adding one more that is only exercised at boot. + GVC_BODY="/tmp/cpln-gvc-body.$$"; GVC_ERRF="/tmp/cpln-gvc-err.$$" + GVC_HTTP="$(curl -sS -o "$GVC_BODY" -w '%{http_code}' \ + --connect-timeout 5 --max-time 15 --retry 2 --retry-delay 3 \ + -H "Authorization: $CPLN_TOKEN" \ + "${CPLN_ENDPOINT:-http://api.cpln.io}/org/$CPLN_ORG/gvc/$CPLN_GVC" 2>"$GVC_ERRF" || echo "000")" + GVC_ERR="$(tr '\n' ' ' < "$GVC_ERRF" | cut -c1-200)" + if [ "$GVC_HTTP" = "200" ]; then + LOCATIONS="$(php -r ' + $d = json_decode(stream_get_contents(STDIN), true); + $l = $d["spec"]["staticPlacement"]["locationLinks"] ?? null; + if (!is_array($l) || !count($l)) { exit(1); } + echo implode(",", array_map(function ($x) { return basename($x); }, $l)); + ' < "$GVC_BODY" 2>/dev/null || true)" + fi + rm -f "$GVC_BODY" "$GVC_ERRF" + else + GVC_HTTP="no-env" + GVC_ERR="CPLN_TOKEN, CPLN_ORG or CPLN_GVC missing from the environment" + fi + + if [ -z "$LOCATIONS" ]; then + # A control-plane hiccup or a missing GVC policy must not stop a site booting. + log "WARNING: could not read GVC '${CPLN_GVC:-?}' (HTTP $GVC_HTTP)${GVC_ERR:+ - $GVC_ERR}" + case "$GVC_HTTP" in + 403) log "WARNING: 403 = this workload's identity has no 'view' on the GVC; check this chart's GVC policy" ;; + 200) log "WARNING: the GVC read succeeded but spec.staticPlacement.locationLinks could not be parsed" ;; + 000) log "WARNING: no HTTP response (connect/timeout); this is common on the first seconds of a replica's life" ;; + esac + log "WARNING: GVC location check SKIPPED - it is a best-effort safety net, not a guarantee" + else + LOCATION_COUNT="$(printf '%s' "$LOCATIONS" | awk -F, '{print NF}')" + + # Opposite direction: the platform accepts a localOptions entry naming a + # location the GVC LACKS, stores it, and runs nothing -- with no failed + # deployment to observe. Warn only: reaching this line at all means a replica + # DID start, so the pin cannot be entirely wrong; and refusing here would + # take down a site over a control-plane reading. + case ",$LOCATIONS," in + *",$CONFIGURED_LOCATION,"*) : ;; + *) log "WARNING: this release is pinned to location '$CONFIGURED_LOCATION', which GVC '${CPLN_GVC:-?}' does not list ($LOCATIONS). Set 'location' to one of the GVC's own locations." ;; + esac + if [ "$THIS_LOCATION" != "unknown" ] && [ "$THIS_LOCATION" != "$CONFIGURED_LOCATION" ]; then + log "WARNING: this replica is running in '$THIS_LOCATION' but the release is configured for '$CONFIGURED_LOCATION'" + fi + + if [ "$LOCATION_COUNT" -gt 1 ]; then + if [ "$FRESH" = yes ]; then + log "FATAL: GVC '${CPLN_GVC:-?}' has $LOCATION_COUNT locations ($LOCATIONS)." + log "FATAL: This template supports a SINGLE-location GVC only. WordPress itself is" + log "FATAL: pinned to '$CONFIGURED_LOCATION', but the bundled MariaDB is a subchart" + log "FATAL: workload this chart cannot pin - it runs in EVERY location, each with its" + log "FATAL: own volume and its own database, behind one DNS name." + log "FATAL: Install into a single-location GVC instead." + exit 1 + fi + log "WARNING: GVC '${CPLN_GVC:-?}' has $LOCATION_COUNT locations ($LOCATIONS) - the bundled" + log "WARNING: MariaDB runs in each of them with its own database. Not failing: the docroot" + log "WARNING: is already initialised, and this check must never take a live site down." + else + log "single-location GVC ok ($LOCATIONS)" + fi + fi + + # ── 2. Bootstrap election ──────────────────────────────────────────────────── + # The docroot is read-write-many, so on a cold start every replica would seed and + # install at once. mkdir is atomic on POSIX and on network filesystems, so + # exactly one replica wins. Keyed on .cpln-bootstrap-done rather than on an empty + # docroot, so a crash between "seeded" and "installed" is still repaired. + HOLD=no + if [ ! -e "$DONE" ]; then + if mkdir "$LOCK" 2>/dev/null; then + HOLD=yes + log "this replica is bootstrapping the site" + else + log "another replica is bootstrapping; waiting..." + WAITED=0 + while [ ! -e "$DONE" ]; do + if [ "$WAITED" -ge 600 ]; then + # The holder died mid-bootstrap. Clear the lock and exit non-zero so the + # platform restarts us and this replica can take the lock next time, + # rather than wedging on an orphaned lock forever. + log "ERROR: timed out after ${WAITED}s waiting for the bootstrap replica; releasing the stale lock and restarting" + rmdir "$LOCK" 2>/dev/null || true + exit 1 + fi + sleep 5 + WAITED=$((WAITED + 5)) + done + log "bootstrap completed elsewhere after ${WAITED}s" + fi + fi + + # ── 3. Seed the docroot ────────────────────────────────────────────────────── + # We seed rather than letting the image entrypoint do it, for one reason the + # entrypoint cannot solve: with several replicas sharing one volume, only the + # lock holder above may write. Ownership is set the way the entrypoint sets it + # (tar --owner/--group www-data) -- measured working on the platform's shared + # volumeset. Do NOT use --no-same-owner: it leaves root-owned directories that + # www-data cannot write into, which breaks plugin installs and media uploads. + if [ "$HOLD" = yes ] && [ "$FRESH" = yes ]; then + log "seeding WordPress into $DOCROOT ..." + tar --create --file - --directory /usr/src/wordpress --owner www-data --group www-data . \ + | tar --extract --file - --directory "$DOCROOT" + if ! chown -R www-data:www-data "$DOCROOT" 2>/dev/null; then + log "WARNING: chown failed on this storage - falling back to a permissive mode" + chmod -R a+rwX "$DOCROOT" 2>/dev/null || log "WARNING: chmod fallback also failed; plugin installs and uploads may fail" + fi + log "seed complete ($(du -sh "$DOCROOT" 2>/dev/null | cut -f1))" + fi + + # ── 4. wp-config.php ───────────────────────────────────────────────────────── + # The image's own alias runs the setup for ANY command. It generates + # wp-config.php with eight unique auth salts (once, on the shared volume, so + # every replica signs cookies identically) and is a no-op once it exists. + docker-ensure-installed.sh true + + # ── 5. Wait for the database ───────────────────────────────────────────────── + log "waiting for the database at ${WORDPRESS_DB_HOST:-?} ..." + WAITED=0 + until php -r ' + $hp = getenv("WORDPRESS_DB_HOST"); + $port = 3306; + if (strpos($hp, ":") !== false) { list($hp, $port) = explode(":", $hp, 2); $port = (int) $port; } + mysqli_report(MYSQLI_REPORT_OFF); + $c = @mysqli_connect($hp, getenv("WORDPRESS_DB_USER"), getenv("WORDPRESS_DB_PASSWORD"), getenv("WORDPRESS_DB_NAME"), $port); + exit($c ? 0 : 1); + ' 2>/dev/null; do + if [ "$WAITED" -ge 300 ]; then + log "FATAL: database ${WORDPRESS_DB_HOST:-?} not reachable after ${WAITED}s" + exit 1 + fi + sleep 5 + WAITED=$((WAITED + 5)) + done + log "database reachable after ${WAITED}s" + + # ── 6. First-run install ───────────────────────────────────────────────────── + if [ "$HOLD" = yes ]; then + php /cpln/install.php + : > "$DONE" + rmdir "$LOCK" 2>/dev/null || true + fi + + # ── 7. Health endpoint ─────────────────────────────────────────────────────── + # Rewritten every boot so an upgrade refreshes it. Written via a temp file so a + # concurrent replica never serves a half-written file. + cat > "$DOCROOT/.cpln-health.php.tmp" <<'HEALTH' + /dev/null || true + + log "handing off to Apache" + exec apache2-foreground diff --git a/wordpress/versions/1.0.0/templates/volumeset.yaml b/wordpress/versions/1.0.0/templates/volumeset.yaml new file mode 100644 index 00000000..039c54d5 --- /dev/null +++ b/wordpress/versions/1.0.0/templates/volumeset.yaml @@ -0,0 +1,23 @@ +{{- /* + READ-WRITE-MANY on purpose. A block volumeset (ext4/xfs) gives every replica its + OWN volume, so two WordPress replicas would mean two media libraries, two plugin + sets and two wp-config.php files with two different sets of auth salts — every + user logged out on every request that landed on the "other" replica. A `shared` + volumeset is one volume per location, mounted by every replica. + + `performanceClass: shared` is declared explicitly: omitting it makes the API + backfill exactly that value, which is permanent drift from creation. + + No `snapshots` block. The API accepts one, but shared volumes are expand-only + with no restore path we have verified — see "Backups" in the README rather than + a recovery procedure we cannot stand behind. +*/}} +kind: volumeset +name: {{ include "wordpress.volume.name" . }} +description: WordPress docroot — core, plugins, themes, uploads and wp-config.php +gvc: {{ .Values.global.cpln.gvc }} +tags: {{- include "wordpress.tags" . | nindent 4 }} +spec: + fileSystemType: shared + initialCapacity: {{ .Values.volumeset.capacity }} + performanceClass: shared diff --git a/wordpress/versions/1.0.0/templates/workload.yaml b/wordpress/versions/1.0.0/templates/workload.yaml new file mode 100644 index 00000000..d2fed756 --- /dev/null +++ b/wordpress/versions/1.0.0/templates/workload.yaml @@ -0,0 +1,218 @@ +{{- include "wordpress.validate" . }} +kind: workload +name: {{ include "wordpress.name" . }} +description: WordPress — Apache + mod_php serving the site and wp-admin +gvc: {{ .Values.global.cpln.gvc }} +tags: {{- include "wordpress.tags" . | nindent 4 }} +spec: + # `standard`, not `stateful`: this tier is a stateless PHP server. All state is + # in the database and on the read-write-many docroot volume. (Measured: a + # standard workload MAY mount a `shared` volumeset — the stateful/vm + # restriction applies to block ext4/xfs volumesets only.) + type: standard + identityLink: //gvc/{{ .Values.global.cpln.gvc }}/identity/{{ include "wordpress.identity.name" . }} + containers: + - name: wordpress + image: {{ .Values.wordpress.image }} + # Boot goes through the mounted wrapper: it elects one replica to seed the + # shared docroot and create the admin account BEFORE Apache binds, then + # execs the image's own apache2-foreground. + command: /bin/bash + args: + - /cpln/start.sh + inheritEnv: false + minCpu: {{ .Values.wordpress.resources.minCpu | quote }} + minMemory: {{ .Values.wordpress.resources.minMemory | quote }} + cpu: {{ .Values.wordpress.resources.maxCpu | quote }} + memory: {{ .Values.wordpress.resources.maxMemory | quote }} + env: + # ── Database (mariadb subchart) ── + # wp-config-docker.php resolves these with getenv() on every request, so + # they stay platform-resolved secret references and are never + # interpolated into a string at render time. + - name: WORDPRESS_DB_HOST + value: {{ include "wordpress.mariadb.host" . }} + - name: WORDPRESS_DB_USER + value: 'cpln://secret/{{ include "wordpress.secret.db.name" . }}.username' + - name: WORDPRESS_DB_PASSWORD + value: 'cpln://secret/{{ include "wordpress.secret.db.name" . }}.password' + - name: WORDPRESS_DB_NAME + value: 'cpln://secret/{{ include "wordpress.secret.db.name" . }}.database' + - name: WORDPRESS_TABLE_PREFIX + value: {{ .Values.wordpress.tablePrefix | quote }} + # Any non-empty value is truthy to wp-config-docker.php, so `false` must + # render as an EMPTY string, never the word "false". + - name: WORDPRESS_DEBUG + value: {{ if .Values.wordpress.debug }}"1"{{ else }}""{{ end }} + # eval()-ed by wp-config.php on every request. + - name: WORDPRESS_CONFIG_EXTRA + value: |- + // The platform injects X-Forwarded-Proto on INTERNAL GVC hops too, not + // only at the public edge. Without this WordPress builds http:// URLs + // behind the TLS edge and redirect-loops. Deliberately not gated on + // publicAccess. + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; } + // WordPress decides it may write files by comparing getmyuid() with + // fileowner(). On a group-owned network mount that comparison fails and + // the admin is asked for FTP credentials on every plugin install. + define('FS_METHOD', 'direct'); + $cpln_site_url = getenv('WP_SITE_URL'); + if ($cpln_site_url) { define('WP_HOME', $cpln_site_url); define('WP_SITEURL', $cpln_site_url); } + # ── First-run bootstrap (read by /cpln/install.php only) ── + - name: WP_SITE_TITLE + value: {{ .Values.wordpress.siteTitle | quote }} + - name: WP_ADMIN_USERNAME + value: 'cpln://secret/{{ .Values.admin.secretName }}.username' + - name: WP_ADMIN_PASSWORD + value: 'cpln://secret/{{ .Values.admin.secretName }}.password' + - name: WP_ADMIN_EMAIL + value: 'cpln://secret/{{ .Values.admin.secretName }}.email' + ports: + - number: 80 + protocol: http + volumes: + - path: /var/www/html + recoveryPolicy: retain + uri: cpln://volumeset/{{ include "wordpress.volume.name" . }} + - path: /cpln/start.sh + recoveryPolicy: retain + uri: cpln://secret/{{ include "wordpress.secret.start.name" . }}.payload + - path: /cpln/install.php + recoveryPolicy: retain + uri: cpln://secret/{{ include "wordpress.secret.install.name" . }}.payload + - path: /usr/local/etc/php/conf.d/zz-cpln-wordpress.ini + recoveryPolicy: retain + uri: cpln://secret/{{ include "wordpress.secret.phpini.name" . }}.payload + # WordPress ships no health endpoint: / and /wp-login.php redirect before + # install and on host/scheme mismatch, and a static asset would prove Apache + # is up while PHP or the database is down. start.sh writes cpln-health.php + # into the docroot instead. + # + # readiness runs the DEEP check (PHP + database + is_blog_installed). + # Budget = 20 + 20*15 = 320 s, which has to cover seeding a 117 MB docroot + # onto network storage, the database wait and the install. + readinessProbe: + httpGet: + path: /cpln-health.php + port: 80 + scheme: HTTP + initialDelaySeconds: 20 + periodSeconds: 15 + failureThreshold: 20 + successThreshold: 1 + timeoutSeconds: 5 + # liveness runs the SHALLOW check, deliberately database-independent: a + # database blip must stop traffic to the replica (readiness) without + # restarting it in a loop. initialDelaySeconds is past the readiness budget + # so liveness never kills a container that is legitimately still seeding. + livenessProbe: + httpGet: + path: /cpln-health.php?shallow=1 + port: 80 + scheme: HTTP + initialDelaySeconds: 360 + periodSeconds: 30 + failureThreshold: 10 + successThreshold: 1 + timeoutSeconds: 5 + defaultOptions: + # Sent as a COMPLETE block: the API backfills PARTIAL blocks from its own + # platform defaults, not from this workload's settings. + # + # minScale/maxScale 0 is the STRUCTURAL half of the single-location rule, and + # it is the half that holds by construction. A workload runs in EVERY location + # its GVC has, so without this an extra GVC location starts a second, complete, + # independent WordPress on its own volume -- measured on 2026-08-31, where a + # second location seeded its own docroot, ran its own installer and created its + # own admin user while every status surface stayed green. `localOptions` below + # supplies the real count for the ONE configured location; any other location + # gets desiredScale 0 and reports `This workload location is deactivated + # because maxScale is set to 0`. + autoscaling: + metric: disabled + minScale: 0 + maxScale: 0 + maxConcurrency: 0 + target: 95 + scaleToZeroDelay: 300 + capacityAI: false + debug: false + suspend: false + timeoutSeconds: 30 + localOptions: + # A localOptions entry is a FULL override, not a patch: every field + # defaultOptions carries is repeated here, because the API backfills its OWN + # platform defaults for anything left out -- not this workload's defaults. + - autoscaling: + metric: disabled + minScale: {{ .Values.wordpress.replicas | int }} + maxScale: {{ .Values.wordpress.replicas | int }} + maxConcurrency: 0 + target: 95 + scaleToZeroDelay: 300 + capacityAI: false + debug: false + location: //location/{{ .Values.location }} + suspend: false + timeoutSeconds: 30 + # rolloutOptions is omitted ENTIRELY rather than partially: the API backfills a + # partial block, and maxUnavailableReplicas is silently dropped on some + # workload types, which would imply a rollout limit that is not in force. + firewallConfig: + external: + {{- if .Values.publicAccess.enabled }} + inboundAllowCIDR: + - 0.0.0.0/0 + {{- else }} + inboundAllowCIDR: [] + {{- end }} + {{- /* + The three empty lists below are NOT decoration. The API COMPLETES a + PARTIAL block from its own defaults -- send the whole block or none of + it. Sending only inbound/outboundAllowCIDR made the API backfill + inboundBlockedCIDR, outboundBlockedCIDR and outboundAllowHostname, so + the stored spec disagreed with the rendered manifest from creation + (measured 2026-08-31 on release test-wp). A `helm upgrade` drift gate + CANNOT see this class -- it was fully green -- because the fields are + written once at creation and then never churn. `outboundAllowPort` is + deliberately absent: the API did NOT backfill it, and declaring a field + the API does not store would create the drift this block removes. + */}} + inboundBlockedCIDR: [] + outboundAllowCIDR: + - 0.0.0.0/0 + outboundAllowHostname: [] + outboundBlockedCIDR: [] + internal: + inboundAllowType: {{ .Values.internalAccess.type }} + {{- if eq .Values.internalAccess.type "workload-list" }} + {{- /* + This release's own workloads are merged in unconditionally: a + workload-list naming only the user's clients would otherwise deny the + release its own internal paths. A link the user also listed is emitted + ONCE — a duplicate array entry is a drift candidate. Same shape as + calcom.internalFirewall and documenso's workload. + */}} + {{- $own := splitList "\n" (trim (include "wordpress.ownWorkloadLinks" .)) }} + inboundAllowWorkload: + {{- include "wordpress.ownWorkloadLinks" . | nindent 8 }} + {{- range .Values.internalAccess.workloads }} + {{- if not (has (printf "- %s" .) $own) }} + - {{ . }} + {{- end }} + {{- end }} + {{- else }} + {{- /* + Emitted as [] on the non-list types too. Sending a PARTIAL `internal` + block makes the API complete it: grafana-multi-location 2.0.0 emitted + this key only under `workload-list`, and both its workloads drifted from + their own manifest from creation on the DEFAULT type. A `helm upgrade` + drift gate CANNOT see that class -- it reported `Unchanged` throughout, + because the field is backfilled once at creation and then never churns. + Only render-vs-stored finds it. + */}} + inboundAllowWorkload: [] + {{- end }} + # Declared rather than left to the API, which stores `false` either way: an + # undeclared field is a permanent rendered-vs-stored difference. + supportDynamicTags: false diff --git a/wordpress/versions/1.0.0/values.yaml b/wordpress/versions/1.0.0/values.yaml new file mode 100644 index 00000000..be13f060 --- /dev/null +++ b/wordpress/versions/1.0.0/values.yaml @@ -0,0 +1,92 @@ +# ─── Location ───────────────────────────────────────────────────────────────── +# The ONE location of your GVC that WordPress runs in. Every replica is pinned +# here, and no other location of the GVC starts one: an undeclared location gets +# maxScale 0 and starts nothing. Without that pin an extra GVC location silently +# runs a SECOND, complete, independent WordPress on its own volume. +# It must be a location your GVC actually has — the platform accepts one the GVC +# lacks without any error and then runs nothing, with no failed deployment to see. +location: aws-us-east-1 + +# ─── WordPress ──────────────────────────────────────────────────────────────── +wordpress: + # Official image (library/wordpress); the tag pins WordPress AND PHP. + # It seeds the docroot on FIRST BOOT ONLY — after that WordPress owns its own + # files and you update core from the dashboard. Bumping this tag later changes + # PHP and Apache, not WordPress core. + image: wordpress:7.1.0-php8.4-apache + replicas: 1 # >=2 share one uploads volume and one database; see README "Availability" + siteTitle: My WordPress Site # set on first boot only; change later in Settings → General + siteUrl: "" # public base URL; empty = the auto-assigned *.cpln.app endpoint + tablePrefix: wp_ # change only when importing an existing WordPress database + debug: false # WP_DEBUG — PHP notices to the container log; leave off in production + resources: + minCpu: 250m + maxCpu: 1000m + minMemory: 512Mi + maxMemory: 1Gi + +# ─── PHP limits ─────────────────────────────────────────────────────────────── +# Stock php.ini caps uploads at 2M, which blocks most media uploads. +php: + uploadMaxSize: 64M # upload_max_filesize and post_max_size + memoryLimit: 256M # PHP memory_limit; some plugins want 256M in wp-admin + +# ─── Admin account (REQUIRED PREREQUISITE SECRET) ───────────────────────────── +# CREATE IT BEFORE YOU INSTALL. A `dictionary` secret with exactly three keys: +# `username`, `password`, `email`. Applied on FIRST BOOT ONLY — rotating it later +# does NOT change your login. If it does not exist the deployment WEDGES silently +# and `cpln logs` returns nothing; the only diagnostic is status.versions[].message +# from `cpln workload get-deployments`. +admin: + secretName: my-wordpress-admin + +# ─── Content storage ────────────────────────────────────────────────────────── +# One read-write-many volume per location, mounted at /var/www/html by every +# replica: core, plugins, themes, uploads and wp-config.php. Shared volumes can be +# expanded but NOT snapshotted — the media library has no backup, keep your own copy. +volumeset: + capacity: 10 # initial capacity in GiB (minimum is 10) + +# ─── Access ─────────────────────────────────────────────────────────────────── +# A firewall change takes ~30 s to ~10 min to propagate. +publicAccess: + enabled: true # HTTPS site on the auto-assigned *.cpln.app endpoint +internalAccess: + type: same-gvc # none | same-gvc | same-org | workload-list + workloads: [] # used only with workload-list + # workloads: + # - //gvc/GVC_NAME/workload/WORKLOAD_NAME + +# ─── MariaDB (subchart: mariadb) — posts, pages, users, settings ────────────── +# Every knob of the `mariadb` template is available under this key — including its +# native scheduled backups and phpMyAdmin console, with nothing extra to install: +# mariadb.backup.enabled: true +# mariadb.backup.provider: aws | gcp +# mariadb.backup.aws.bucket / .region / .cloudAccountName / .policyName +# mariadb.phpMyAdmin.enabled: true +# See "Backing up the bundled database" in the README. +mariadb: + image: mariadb:11 + # Credentials for the bundled database — internal plumbing no human types + # elsewhere. This chart CREATES both secrets named below out of these values, + # so there is nothing for you to create before installing. + credentials: + username: wordpress + password: change-me-wordpress-db # change before installing + database: wordpress + rootPassword: change-me-wordpress-db-root # change before installing + # Names of the two secrets this chart creates and the bundled MariaDB reads. + # Secret names are org-wide: give each wordpress release its own names. A second + # release left on these names is REFUSED at install (they are owned by the first + # release) — nothing is shared, overwritten or deleted. + credentialsSecretName: my-wordpress-db-credentials + rootPasswordSecretName: my-wordpress-db-root-password + resources: + minCpu: 150m # keeps cpu:minCpu under the stateful 4:1 cap (500/150 = 3.3:1) + maxCpu: 500m + minMemory: 256Mi + maxMemory: 1Gi + volumeset: + capacity: 10 # initial capacity in GiB (minimum is 10) + internalAccess: + type: same-gvc # WordPress must be able to reach the database