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 and grants follow within a minute and the claim becomes Ready. ArgoCD treats the claim as Progressing until then and, because the claim runs in sync wave -1, does not 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 (or delete the whole XPlatformApp) and re-apply with ./argocd/sync-apps.sh; the composition removes the ArgoCD Application, which prunes the MySQLApp claim. Deleting the Application directly doesn't work — Crossplane recreates it. Then wait until the claim, the XMySQLApp, and the user.mysql.sql.crossplane.io/<short-env>-<user> resource are gone before deleting the namespace:

kubectl -n argocd get application <project-slug>-<env> # should be gone
kubectl get xmysqlapp -l crossplane.io/claim-namespace=<project-slug>-<env>
kubectl get user.mysql.sql.crossplane.io <short-env>-<user>
kubectl delete namespace <project-slug>-<env>

provider-sql reads the password secret on every reconcile, including the one that runs DROP USER. If the namespace (and with it the secret) is deleted first, the User resource can never be observed again, its finalizer is never removed, and the MySQL user stays behind. A later deployment with the same name then collides with the stuck resource.

Stuck User after a namespace wipe

Symptom: kubectl get user.mysql.sql.crossplane.io shows SYNCED=False with cannot get password secret: Secret "app-secrets" not found and a deletionTimestamp that never clears.

Give provider-sql a secret to read so the deletion can finish:

NS=<project-slug>-<env> # from the User's spec.forProvider.passwordSecretRef.namespace
KEY=<passwordField> # from spec.forProvider.passwordSecretRef.key
kubectl create namespace "$NS"
kubectl -n "$NS" create secret generic app-secrets --from-literal="$KEY=unused"
kubectl wait --for=delete user.mysql.sql.crossplane.io/<short-env>-<user> --timeout=5m
kubectl delete namespace "$NS"

Do one at a time. Each one runs a DROP USER on the cluster, and bursts of ACL DDL are what wedge Galera. Removing the finalizer instead unblocks the resource but leaves the MySQL user in place.