Skip to main content

component-database

Provisions a MySQL database for the project via the in-cluster MySQLApp CRD (Percona). The actual DB pods come from the Percona operator; this component just defines a MySQLApp resource that the operator picks up.

Enable it​

component-database:
enabled: true

That's usually enough — the chart fills the rest from global.projectSlug, global.environmentSlug, and global.secretName.

Defaults​

parameters:
user: "" # falls back to global.projectSlug
secretName: "" # falls back to global.secretName
privilegeTier: admin
provider: percona-mysql-config
passwordField: DATABASE_PASSWORD
additionalDatabases: []

Custom user name​

The MySQL user and database are named <short-env>-<user> (e.g. stg-my-project). MySQL limits user names to 32 characters, so projects with a long global.projectSlug must set a shorter parameters.user:

component-database:
enabled: true
parameters:
user: my-shorter-name

This only affects the database and user name. The password is always read from the secret in the namespace where the MySQLApp claim lives.

Additional databases​

Some projects need more than one database (e.g. for migrations or sidecars):

component-database:
enabled: true
parameters:
additionalDatabases:
- my-project-migration

Environment naming quirk​

If global.environmentSlug is prod, the rendered environment becomes "production". Anything else passes through verbatim (e.g. staging, sprint).

When the user is created​

The MySQLApp composition only creates the MySQL user and grants once the password secret exists in the claim's namespace and contains the passwordField key. Until then only the database is created and the claim reports why:

kubectl -n <project-slug>-<env> get mysqlapp database
# NAME SYNCED READY PASSWORD-SECRET
# database True False waiting for my-project-production/app-secrets key DATABASE_PASSWORD

Once the key lands (ESO syncs it from Infisical) the user follows within a minute, and the grants once the user reports Ready; then the claim becomes Ready. Granting before the user exists fails, and each failed GRANT is a cluster-wide vote, so the composition waits rather than letting provider-sql retry. ArgoCD treats the claim as Progressing until then and, because the claim runs in sync wave -2, does not start the migrations Job (wave -1) or roll out the app pods before the database user is ready.

The user's password is kept in sync with the secret: changing the value in Infisical results in an ALTER USER once ESO has refreshed the secret. A user that already exists in MySQL (for example after a namespace was recreated) is adopted and its password reset to the secret value.

Removing an app​

Remove the environment from environments: in the project's definition/xplatformapp.yaml and push (to remove the whole app, kubectl delete xplatformapp <slug> and delete the directory; cluster-apps does not prune); the composition removes the ArgoCD Application, which prunes the MySQLApp claim. Deleting the Application directly doesn't work — Crossplane recreates it. The namespace can be deleted whenever it suits.

The composition gives its Database, User and Grant resources deletionPolicy: Orphan, so removing the claim never touches MySQL: the database, its data and the user stay. A renamed claim, a pruned Application or a composition change therefore cannot drop a production database, and a redeploy under the same name adopts what is already there.

Dropping them is a separate, deliberate step, once nothing needs the data:

scripts/db-drop stg-my-project # database and user
scripts/db-drop prod-my-project --force # prod-* needs --force
scripts/db-drop prod-my-project-migration --force # an additional database (no user of its own)

db-drop refuses while a Crossplane resource still points at the name, while anything is connected to it on any node, or while the cluster is not 3 nodes Synced. It writes <name>-<timestamp>.sql.gz and <name>-user-<timestamp>.sql (the account's password hash and grants) to the current directory, asks for the name to be typed, then drops the user and the database one statement at a time, waiting for every node to apply each one. Keep the backup files until you are sure; db-import and db-shell < <name>-user-….sql restore them.

Run it for one name at a time. Each drop is cluster-wide DDL, and bursts of account DDL are what wedge Galera.

Stuck User from before the orphan policy​

Resources deleted before the composition switched to deletionPolicy: Orphan (2026-10) still try to run DROP USER and need the password secret to do it. If their namespace is gone, they hang with SYNCED=False, cannot get password secret: Secret "app-secrets" not found and a deletionTimestamp that never clears. Switch them to orphan, which releases them without reading the secret, then drop the account with db-drop:

kubectl patch user.mysql.sql.crossplane.io/<short-env>-<user> --type merge -p '{"spec":{"deletionPolicy":"Orphan"}}'
kubectl wait --for=delete user.mysql.sql.crossplane.io/<short-env>-<user> --timeout=2m
scripts/db-drop <short-env>-<user>