Repository navigation
etcd v1alpha2 adoption migration hard-fails 1.5→1.6 upgrade on clusters with an existing etcd #3243
Description
Activity
- addedkind/bugCategorizes issue or PR as related to a bugCategorizes issue or PR as related to a bug
on Jul 8, 2026 Ran a full 1.5.2 → 1.6.0-rc.1 etcd-adoption on a 3-node cluster and drove migration 50 all the way to a healthy adopted v1alpha2 cluster. Some deeper detail on mode 2 (cert re-issue race) plus the additional blockers that surface after it, with fixes that got the adoption to
readyMembers=N / Available=True:Mode 2 (cert wait) — deeper root cause. It isn't only "a few seconds too early."
ensure_wildcard_sansdoeskubectl get … 2>/dev/null | tr | grep, so on any transient GET failure (API discovery refresh, apiserver blip, throttling) the suppressed error yields an empty string that is indistinguishable from "SAN absent" → false miss, and the 120s wait never recovers. The identical check passes in isolation but the in-migration wait fails ~100% under a flaky API path. Also: the legacy operator is still running during the wait and re-applies the Certificate (stripping the just-added wildcard); the migration only scales it to 0 after the cert step.
Fix that worked: treat an empty read as a transient error and retry (a real Certificate/Secret never has emptydnsNames/alt-names), and check the Certificatespec.dnsNames(source of truth) in addition to the issued-Secretalt-namesannotation.Then, in order, three more blockers before the adoption completes:
-
etcd-migratehas no kubeconfig in-cluster — aborts witherror building kubeconfig: stat /root/.kube/config: no such file. It only reads a kubeconfig file and doesn't fall back to the in-cluster SA. Neither the hook Job normigrations/50provide one. (filed separately with a fix.) -
etcd-operator OOMKilled → CrashLoopBackOff — the v0.5.2 controller Deployment ships
limits.memory: 128Mi; the operator needs ~250Mi (VPA's own recommendation). WithupdateMode: Autothe VPA is expected to rewrite the limit, but a Deployment/pod recreated out of band (e.g. via the etcd-operator upgrade fails on 1.5.x→1.6.0-rc.1: Deployment spec.selector is immutable #3242 workaround) comes up at the chart default 128Mi and OOMs (exit 137) before finishing a reconcile. Deleting the pod so the VPA webhook re-applies (→ 500Mi) fixed it. Consider a higher static floor so the operator isn't dependent on VPA timing. -
Adopted pods lose per-pod DNS → operator
MemberListfails — after adoption the pods keepsubdomain: etcd-headless(and theheadless-service-name=etcd-headlessannotation), but the legacyetcd-headlessService gets pruned during the transition (the v1alpha2 etcd chart's headless Service is namedetcd), soetcd-N.etcd-headless.<ns>.svcstops resolving (no such host). The operator'sMemberListthen times out and never populatesstatus.readyMembers, so the cluster never goes Ready even though the members are healthy. Re-creating a headlessetcd-headlessService (selector on the etcd pods,publishNotReadyAddresses: true) restored DNS and the cluster wentreadyMembers=3, Available=True. The adoption should keepetcd-headlessalive until the members have rolled onto the operator's native Service.
Net: with the cert-wait fix + a kubeconfig for
etcd-migrate+ the operator memory floor + keepingetcd-headlessalive, the adoption completes cleanly and in-place (pods never restarted, data intact). Happy to send a PR for themigrations/50bits (cert-wait robustness + kubeconfig).-
- added 3 commits that reference this issue
on Jul 15, 2026 myasnikovdaniil commented
on Jul 21, 2026 ContributorAuthorMore actionsCompleted in #3270
myasnikovdaniil commented
on Jul 21, 2026 ContributorAuthorMore actionsFixed by #3270 (
f6a03e93f, merged 2026-07-16), which consolidates the etcdv1alpha2transition fixes for in-cluster 1.5 → 1.6 upgrades. Both failure modes in the description and all three follow-up blockers from Andrey Kolkov (@androndo)'s comment are addressed onmain:Reported Fix 1. Backup-target gate — no way to set ETCD_ADOPT_SKIP_BACKUPwithout hand-editing the hook Jobmigrations.etcdAdoptSkipBackupis now a first-class platform value (core/platform/values.yaml→templates/migration-hook.yaml→ the migration's env), covered bytests/migration_hook_skip_backup_test.yaml(bool / string / null / invalid). The refusal message now prints the exact YAML to set.1b. Backup-target resolution backupstrategy-controllerderives the default S3 endpoint (and per-driver scheme / TLS) from the provisioned bucket Secret instead of requiring it hand-set, so the etcd strategy resolves by default. Migration 50 also distinguishes a BucketClaim-provisioned bucket (resolved from the projected Secret) from one described by a Strategy CR, and says which it used.2. Cert re-issue race Root-caused as Andrey Kolkov (@androndo) diagnosed: the SAN check now reads both the issued Secret's cert-manager.io/alt-namesannotation and the Certificate'sspec.dnsNames, and treats an empty read from both as a transient error to retry rather than a false "SAN absent".Follow-up 1 — etcd-migratehas no kubeconfig in-clusterThe migration synthesizes an in-cluster kubeconfig from the mounted ServiceAccount and passes it via --kubeconfig. Uses the in-cluster DNS name rather than$KUBERNETES_SERVICE_HOST:PORT, so it is IP-family-agnostic (IPv6-safe).Follow-up 2 — operator OOMKilled at limits.memory: 128MiCold-start floor raised to 256Mi, with VPAminAllowedmatched to it, so the operator never depends on VPA admission timing to avoid a crash loop.Follow-up 3 — adopted pods lose per-pod DNS, MemberListfailsextra/etcdnow ships a chart-managed transitional headlessetcd-headlessService (publishNotReadyAddresses: true, selector mirroring the operator's native Service), the DNS counterpart of the legacy*.etcd-headless.<ns>.svcSAN already kept for the adoption window. Removable together with that SAN once members roll onto the native subdomain.#3270 also carries the fix for the immutable controller-Deployment selector (#3242), which blocks the same upgrade path.
Availability
The fix is on
mainand will ship inv1.6.0-rc.3. Note thatv1.6.0-rc.2was tagged 2026-07-10, six days before the merge, so neitherv1.6.0-rc.1norv1.6.0-rc.2contains it — if you are testing the upgrade path against either of those, you will still hit the original hard-fail. Test against rc.3 or amainbuild.Known residuals (not blocking, filing separately if they resurface)
- The cert re-issue wait is still a fixed
30 × 4s; it was not made configurable, since the empty-read retry addresses the actual cause rather than the symptom. - The SAN patch runs before the legacy operator is scaled to 0, and the wait loop patches once and then polls only the Secret. If the legacy operator re-applies the Certificate mid-wait, there is no re-patch. Not observed in the live 1.5.2 → 1.6.0-rc.1 adoption, which completed to
readyMembers=3 / Available=True.
Closing — please reopen if a 1.5 → 1.6 upgrade on a build that includes #3270 still fails.
- The cert re-issue wait is still a fixed
What happened
The 1.6 platform pre-upgrade migration that adopts existing
etcd.aenix.ioEtcdClusters into the newetcd-operator.cozystack.io/v1alpha2API (#2859) gates the entire platform upgrade and hard-fails for any cluster that has an existing etcd (e.g. the Kamaji tenant DataStore). Two distinct failure modes:1. Backup-target gate
This fires even when the
cozy-backups-credsSecret and a Readycozy-backupsBucket both exist — it additionally requires a resolvablecozy-default-etcdstrategy and a running backupstrategy-controller. The only bypass isETCD_ADOPT_SKIP_BACKUP=1, which is not exposed as a HelmRelease value, so an operator cannot set it without hand-editing the generated pre-upgrade hook Job.2. Cert re-issue race (after bypassing #1)
The migration patches the etcd server Certificate to add a
*.etcd.<ns>.svcwildcard SAN, then waits for cert-manager to re-issue the secret. The wait times out a few seconds too early — cert-manager completes the re-issue right after. Flaky; a re-run gets past it.Impact
Every 1.5 → 1.6 upgrade on a cluster running managed Kubernetes (which uses an etcd DataStore). The whole platform upgrade is blocked at the pre-upgrade hook. Fresh installs are unaffected.
Suggested fixes
Found via a 1.5.0 → 1.6.0-rc.1 upgrade test on a 3-node cluster.