Operator upgrades
The operator and instance manager are the same binary. The
bootstrap-controller init container copies /manager from the operator image
into each instance Pod, so every instance runs the same manager version as the
operator. When you upgrade the operator Deployment, the new image lands first in
the operator Pod. Instance Pods keep running their current manager until the
operator detects the mismatch and rolls the upgrade out to them.
The operator computes a SHA-256 hash of its own binary at startup and reports it
in status.operatorExecutableHash. Instance managers report their own hash in
the /status control endpoint on every health check. When the operator sees an
instance hash that differs from the target hash, it marks that instance as stale
and begins the upgrade rollout.
Two rollout modes are available: the rolling upgrade (default), which deletes and recreates Pods one at a time, and the in-place upgrade, which streams the new binary to each instance through its control API so the manager re-execs without restarting mysqld.
Rolling upgrade (default)​
When spec.inPlaceInstanceManagerUpdates is false or unset, the operator
rolls the upgrade by deleting and recreating each instance Pod one at a time.
The rollout is serialized. Only one replica is ever down at a time, and the operator waits for each recreated instance to report Ready before moving on to the next one. Replicas are upgraded first, in ascending ordinal order. The primary is upgraded last.
Fenced instances are skipped during the rollout. They are not deleted and their hash mismatch is ignored until they are unfenced.
Primary handling. When the primary becomes stale on a multi-instance cluster
with spec.primaryUpdateMethod set to switchover (the default), the operator
triggers a planned switchover to a healthy replica first, then deletes the old
primary Pod. If no healthy replica is available to switch to, the operator falls
back to deleting the primary Pod in place. With a single-instance cluster, the
primary is always deleted in place because there is no replica to switch to.
With spec.primaryUpdateMethod set to restart, the primary Pod is deleted
without any switchover regardless of instance count.
Supervised strategy. When spec.primaryUpdateStrategy is supervised and
the primary is stale, the operator stops the entire rollout and waits. The
cluster enters the WaitingForUser phase with a status message asking for
manual intervention. No replicas are upgraded while the operator waits, not even
ones that are already stale. This gives you control over when the primary is
switched over. To proceed, trigger a manual switchover to a replica (see
Planned switchover). Once a new primary is
in place, the operator resumes the rollout automatically on the next reconcile.
With spec.primaryUpdateStrategy set to unsupervised (the default), the
rollout proceeds without waiting.
In-place upgrade​
When spec.inPlaceInstanceManagerUpdates is set to true, the operator streams
the new manager binary to each stale instance through its control API at
POST /instance/manager/upgrade. The instance manager validates the binary
against the SHA-256 hash sent in the X-CNMSQL-Manager-Hash header, writes it
to disk atomically, and re-execs itself in place.
mysqld stays running throughout the swap. The re-exec'd manager inherits the
existing mysqld process instead of starting a new one, so the server never stops
accepting queries. The Pod's restart count stays flat and status.uptimeSeconds
keeps climbing.
The in-place path treats the primary the same as any replica. No switchover is
triggered, no Pod is deleted, and the primaryUpdateMethod and
primaryUpdateStrategy fields are ignored for in-place upgrades.
The rollout remains serialized: one instance per reconcile, replicas first,
primary last. Fenced instances are skipped, and so are instances whose Pod is
about to be recreated anyway (its Pod template changed, or it is already
terminating): they get the new binary from the recreated Pod. An instance
manager that has started shutting down refuses the swap and cancels one still
pending, so a Pod deletion's SIGTERM is never lost to a re-exec.
Under the hood. The operator opens its own executable and streams it over
mTLS to the instance. The instance manager writes the binary to a temp file,
verifies the hash, makes it executable, and atomically renames it over
/controller/manager. A 250 ms delay gives the HTTP response time to flush
before syscall.Exec replaces the process image. The new image reads
CNMYSQL_ADOPT_MYSQLD_PID from the environment (set by the old image before
the exec) and adopts the running mysqld. It also re-attaches to mysqld's stdout
FIFO through a file descriptor inherited across the exec, so structured logging
continues without interruption.
If the exec fails, the old manager continues supervising mysqld. The operator retries on the next reconcile.
Manual in-place restart​
The kubectl cnmsql restart-inplace command triggers a byte-identical re-exec
of an instance manager without a version upgrade. It calls the
POST /instance/manager/restart-inplace endpoint, which re-execs the current
binary from /proc/self/exe instead of streaming a new one:
kubectl cnmsql restart-inplace cluster-sample cluster-sample-2
This is useful for verifying that mysqld survives a manager swap. After the
command returns, confirm the Pod's restart count did not change and that
status.uptimeSeconds has not reset.
Status fields​
During an upgrade, the cluster status.phase reports Upgrading with a reason
like Upgrading instance manager on cluster-sample-2 (2/3 remaining). When the
supervised strategy blocks the rollout, the phase is WaitingForUser.
Two new status fields track hash state:
status.operatorExecutableHashis the SHA-256 of the running operator binary, reported at startup and used as the target hash for all instances.status.executableHashByInstancemaps each instance name to the hash reported by its manager on the last/statuspoll.
Compare these two fields to see which instances are current and which are stale:
kubectl get cluster cluster-sample -o json | \
jq '{operator: .status.operatorExecutableHash, instances: .status.executableHashByInstance}'
Configuring in-place upgrades​
Enable in-place upgrades on an existing cluster:
kubectl patch cluster cluster-sample --type merge \
-p '{"spec":{"inPlaceInstanceManagerUpdates":true}}'
Or set it at creation time in the Cluster spec:
spec:
inPlaceInstanceManagerUpdates: true
When you then update the operator Deployment to a new image, the operator streams the new binary to each instance with no Pod restarts and no switchover of the primary.
The setting has no effect until the operator executable hash changes, meaning the operator Deployment has been updated to a new image.
Version-specific upgrade notes​
Binlogs are retained locally after archiving​
Clusters with spec.backup.continuousArchiving.enabled: true change their disk
profile on upgrade. Previously the archiver purged each binary log from the data
volume as soon as it reached the object store, so local binlogs stayed near
zero. They are now kept until binlogExpireSeconds (default 604800, seven days)
so a lagged or returning replica can catch up from the primary instead of being
re-cloned from a backup.
Action required. The data volume must now hold the dataset plus roughly
write throughput × binlogExpireSeconds of binlogs. Before upgrading, check the
headroom on your archiving clusters:
kubectl exec <instance> -c mysql -- df -h /var/lib/mysql
kubectl exec <instance> -c mysql -- \
sh -c 'du -sh /var/lib/mysql/binlog.* 2>/dev/null | tail -1'
Then pick one:
- Grow the volume. Raise
spec.storage.size; see Storage. This is the option that preserves the new catch-up behaviour. - Shorten the window. Lower
spec.backup.continuousArchiving.binlogExpireSecondsto fit the disk you have. A shorter window still beats purge-on-archive for replica catch-up. - Keep the old behaviour. Set
spec.backup.continuousArchiving.purgeAfterArchive: trueto restore purge-on-archive exactly as it worked before.
spec:
backup:
continuousArchiving:
enabled: true
purgeAfterArchive: true # pre-upgrade behaviour
PITR is unaffected either way: recovery replays from the object store, not from local binlogs. See Local binlog retention for the sizing model and the trade-off in full.
Single-instance clusters restart once​
Instance Pods now carry the switchover-on-drain preStop hook whatever the
instance count. Previously a single-instance cluster's Pod had no hook, so its
Pod template changed whenever the cluster was scaled to or from one instance.
That rolled the primary without a handoff and, on a scale-up, failed it over
instead of switching over. The hook now returns immediately when no replica is
streaming from the primary, so a single-instance teardown is not delayed.
Expect one restart. Each single-instance cluster with switchover-on-drain enabled (the default) gets a new Pod template, so its only instance is restarted once during the operator upgrade. Multi-instance clusters already carry the hook and are not rolled for this change. Plan the operator upgrade for a window in which a brief outage of your single-instance clusters is acceptable.
Instances roll once: passwords move from env vars to the API​
Instance managers now read their MySQL account passwords from the cluster's
credential Secrets through the Kubernetes API, instead of from MYSQL_*_PASSWORD
environment variables. The instance Pods no longer carry those variables, so the
Pod template changes once: after upgrading the operator, every instance restarts
once through the normal rolling update (replicas first, then a switchover, then
the primary), even with inPlaceInstanceManagerUpdates enabled.
The same roll removes the bootstrap and import init containers from the Pod
template: an instance's data volume is now bootstrapped by a one-shot Job
(<instance>-initdb, <instance>-restore, <instance>-join,
<instance>-import) that runs before the Pod exists (see
Cluster lifecycle). Both changes
land in the same single restart per instance.
Each instance's ServiceAccount can get and watch only its own cluster's
credential Secrets, by name, and cannot list Secrets.
A changed credential Secret is now picked up within minutes, without a restart.
That includes a Secret regenerated wholesale by a GitOps or External Secrets
tool: replacing the Secret is a change, and the instance managers follow it.
Changing the Secret does not change the MySQL account, and no rotation path
runs ALTER USER for you (only the dump account is re-applied from its
Secret): run ALTER USER to the new password first, then update the Secret. A
Secret that no longer matches its MySQL account now fails every instance at
once instead of waiting for the next restart, and the instances report a
credential mismatch rather than looking unreachable; see Upgrade procedure:
0.7.x to 0.8.0 for the check to run before
upgrading.
A credential Secret that is deleted on a cluster with bootstrapped instances is
no longer regenerated, because a fresh random password would match no account.
The running instances keep the password they last read and the operator keeps
reconciling (failover included), but the Cluster reports Degraded with a
CredentialSecretMissing warning event until the Secret is restored with its
previous password: until then no instance can bootstrap, and an instance that
restarts cannot start. Clusters that have not bootstrapped any instance yet
still get their Secrets generated.
Upgrade when no instance is initialising. An instance whose old Pod is
still running its bootstrap init container when the operator upgrades keeps a
volume without the bootstrap annotation, which counts as bootstrapped. If the
roll recreates that Pod before the init container has finished, the new Pod
starts mysqld on an unfinished data directory. On an established cluster
auto-reinit re-clones a replica that ends up there; a primary must be
re-initialised by hand.
Recovered clusters no longer need their source Backup. A recovery source is resolved only until the bootstrap primary's volume is bootstrapped. Clusters that were blocked on every reconcile because their source Backup was deleted recover by themselves after the upgrade, and the Backup, its object store and its Secrets can go.
spec.backup.jobTemplate now reaches the bootstrap Jobs. Its
priorityClassName (over the instance's) and tolerations (added to the
instance's) also apply to bootstrap Jobs; resources keep sizing the restore
only; the template's nodeSelector and affinity are not applied, because
bootstrap Jobs follow the instance's scheduling so the data volume binds where
the instance Pod can run.
Alerts on Init:CrashLoopBackOff must move. A failing bootstrap no longer
shows as Init:CrashLoopBackOff on the instance Pod. Alert on the
BootstrapFailed condition or the BootstrapJobFailed event instead (see
Troubleshooting).
Never downgrade below 0.8.0 while a bootstrap Job is active. The old operator ignores the bootstrap Jobs and the volume annotation: it would create the instance Pod while the Job still mounts the volume, and on an RWO volume both can land on the same node, so two processes would write one data directory. Before downgrading, this must show no active Job:
kubectl get jobs -A -l mysql.cnmsql.co/bootstrap-instance
Logical backups whose source instance has not rolled yet fail during the rollout, because the backup worker no longer sends the dump account's password. Avoid taking logical backups until every instance has restarted. Scheduled ones simply run again at their next slot.
The replication account is now X.509-only: the unused replication password code path is gone. Clusters managed by the operator already replicate over mTLS, so this needs no action.
Upgrade procedure: 0.7.x to 0.8.0​
The upgrade keeps your data: checksums, archived binlogs and point-in-time recovery from backups taken before the upgrade are unaffected. What it costs is one restart of every instance, and a few settings behave differently afterwards. Follow these steps in order.
Before upgrading​
-
Use a chart that ships the 0.8.0 CRDs. 0.8.0 adds the
LogicalRestoreCRD and new fields onBackupandScheduledBackup.helm upgradeapplies them, because the chart ships its CRDs as templates. If you manage CRDs separately (GitOps,kubectl apply), apply the 0.8.0 CRDs before you move the operator image. -
Check that every cluster is
Readyand no instance is initialising. An instance still in itsbootstrapinit container must finish first (see above):kubectl get clusters -Akubectl get pods -A -l mysql.cnmsql.co/cluster | grep -E 'Init:|PodInitializing'The second command must print nothing.
-
Check that the credential Secrets match their MySQL accounts. 0.7.x read the passwords once, when a Pod started, so a Secret that was changed without
ALTER USER(or regenerated by a GitOps or External Secrets controller) had no effect until the next restart. From 0.8.0 the instance managers use the Secret's current value: a mismatchedcontrolSecret makes every instance unready within minutes, and the-rw,-roand-rServices go empty. Run this for each cluster:NS=<namespace> CLUSTER=<cluster>POD=$(kubectl -n $NS get cluster $CLUSTER -o jsonpath='{.status.currentPrimary}')for s in root control backup; dou=$(kubectl -n $NS get secret $CLUSTER-$s -o jsonpath='{.data.username}' | base64 -d)p=$(kubectl -n $NS get secret $CLUSTER-$s -o jsonpath='{.data.password}' | base64 -d)if kubectl -n $NS exec $POD -c mysql -- mysql -u"$u" -p"$p" -Ne 'select 1' >/dev/null 2>&1; thenecho "$CLUSTER-$s: ok"elseecho "$CLUSTER-$s: MISMATCH"fidoneFix any
MISMATCHbefore upgrading, withALTER USERto the Secret's value or by restoring the Secret to the account's password. If a controller regenerates these Secrets, stop it from doing so: the operator does not runALTER USERforroot,controlorbackup. From this release a deleted credential Secret is no longer regenerated either — on a cluster with bootstrapped instances the Cluster reportsDegradedwith aCredentialSecretMissingevent until the Secret is restored with its previous password. -
Plan for the restarts. Multi-instance clusters restart their replicas, switch over, then restart the old primary; writes fail for about a second during the switchover, which the Cluster reports as the
Switchoverphase. Single-instance clusters are down for as long as their instance takes to restart. You don't need to turn offinPlaceInstanceManagerUpdates: the operator never streams an in-place update to a Pod it is about to recreate, and every Pod is recreated by this roll.
Upgrading​
helm upgrade cnmsql cnmsql/cnmsql -n <operator-namespace> --version 0.8.0
Follow the roll with kubectl get clusters -A: each cluster goes through
Upgrading and returns to Ready. While it runs:
- Don't take logical backups; they fail on instances that have not restarted yet.
- Don't scale clusters or create new ones until the roll is over.
After upgrading​
-
Check that the roll finished everywhere: every cluster
Ready, and no instance Pod carries a password variable any more:kubectl get pods -A -l mysql.cnmsql.co/cluster \-o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[0].env[*].name}{"\n"}{end}' \| grep PASSWORDThis must print nothing.
-
Move alerts from
Init:CrashLoopBackOffto theBootstrapFailedcondition (see above). -
Each cluster now has a
<cluster>-dumpSecret and acnmsql_dumpaccount, which logical backups use.
Downgrading to 0.7.x​
A downgrade restarts every instance once more, back to password environment
variables. Data is not affected. Before you run helm rollback:
-
Check that no bootstrap Job is active (see above).
-
Suspend or delete every
ScheduledBackupwithmethod: logical. 0.7.x rejects that method, so its scheduler fails on every slot and retries in a loop:kubectl patch scheduledbackup <name> --type merge -p '{"spec":{"suspend":true}}' -
Expect logical Backups and
LogicalRestoreobjects to stay as they are. 0.7.x does not process them, and the chart keeps theLogicalRestoreCRD on rollback, so they are still there when you upgrade again.
After upgrading to 0.8.0 again, delete any Backup whose Progressing
condition has reason UnsupportedMethod: the 0.7.x operator created it during
the upgrade, and it never runs.
Troubleshooting​
The rollout is stuck in WaitingForUser. The primary is stale and
spec.primaryUpdateStrategy is supervised. Trigger a planned switchover to a
healthy replica (see Planned
switchover), or set the strategy to
unsupervised if you prefer automatic handling.
An in-place upgrade fails with a hash mismatch. The binary streamed from the
operator did not match the expected hash declared in the X-CNMSQL-Manager-Hash
header. The stale binary on the instance is not replaced. This is a 400 error.
Check that the operator and instance images are compatible: the operator binary
must be the same architecture and build as the one in the bootstrap-controller
init container. If you are running a multi-architecture cluster, the operator
schedules its Deployment on a node whose architecture matches the instance
images.
A replicated instance is stuck unready after a rolling upgrade. The
recreated Pod may be unable to join replication. Check its logs with
kubectl cnmsql logs and verify the bootstrap-controller init container
completed. If the instance diverged during the upgrade, re-initialise it (see
Re-initialise an
instance).
The rollout skips a stale instance. The instance may be fenced. Unfence it first (see Fence an instance).
Verification coverage​
The upgrade controller has unit coverage for candidate ordering (replicas before primary, fenced instances excluded), the single-instance and multi-instance rollout paths, supervised and unsupervised primary strategies, switchover fallback when no healthy replica exists, in-place binary streaming and hash validation, FIFO log re-adoption across re-exec, and the restart-inplace command.
E2E tests validate end-to-end in-place upgrades (operator image update, hash propagation, serialized rollout, mysqld uptime continuity) and in-place operator upgrades (operator Deployment update, streaming, re-exec, status verification).