fix(etcd): against bugs to v1alpha2 transition - #3265
fix(etcd): against bugs to v1alpha2 transition#3265Andrey Kolkov (androndo) wants to merge 2 commits into
Conversation
…option etcd-migrate adopts legacy clusters IN PLACE, so the Pods keep their original spec.subdomain: etcd-headless and are dialed at etcd-<i>.etcd-headless.<ns>.svc until they eventually roll onto the operator's native <member>.etcd.<ns>.svc domain. The v1alpha2 operator only creates the native `etcd` Service, and the legacy `etcd-headless` Service is pruned during the transition, so those names stop resolving (no such host), the operator's MemberList fails, and status.readyMembers never populates -- the EtcdCluster never goes Ready even though the etcd processes are healthy and in quorum. Ship a chart-managed transitional headless `etcd-headless` Service (selector mirrors the operator's native `etcd` Service via etcd-operator.cozystack.io/cluster, publishNotReadyAddresses: true). This is the DNS counterpart of the legacy *.etcd-headless.<ns>.svc wildcard already kept in the server/peer cert SANs for the same transition window -- the TLS half of the compat was done, the DNS half was missing. Safe to remove, together with that SAN, once the members have rolled onto the native `etcd` subdomain. Verified live: recreating this Service on an adopted 3-node cluster restored per-pod DNS and the cluster went readyMembers=3 / Available=True. Refs: #3243 Signed-off-by: Andrey Kolkov <androndo@gmail.com>
…OM floor) Two defects that break the etcd-operator on an in-cluster 1.5 -> 1.6 upgrade (#2859 swapped the upstream chart for the cozystack-authored one): - #3242: the controller Deployment's spec.selector.matchLabels changed ({instance,name} -> {name,control-plane}). spec.selector is immutable, so `helm upgrade` cannot patch the existing Deployment and the whole HelmRelease upgrade fails ("field is immutable") -- the operator and the v1alpha2 CRDs it serves never come up, which blocks the etcd v1alpha2 adoption. Fresh installs use the new selector directly, so fresh-install CI does not catch it. Add a pre-upgrade hook that deletes the Deployment ONLY when its live selector is the pre-1.6 one (lacks control-plane=controller-manager), so Helm recreates it cleanly. No-op when the selector already matches (rc.1 -> later) and never runs on a fresh install (pre-upgrade only). Keeping the selector stable in the chart is not an option: rc.1 already shipped the new selector, so aligning it back would merely move the immutable break to rc.1 -> next. - Raise the manager's cold-start memory limit floor 128Mi -> 256Mi (and the VPA minAllowed to match). The steady-state working set is ~250Mi (the VPA's own recommendation); at 128Mi a Pod that starts before the VPA admission webhook rewrites it (e.g. a Deployment recreated out of band, or the webhook briefly unavailable during upgrade) OOMKills into a crash loop. Defense in depth so the operator never depends on VPA timing merely to avoid crashing; the VPA still scales it further under load up to maxAllowed. Verified live on a 1.5 -> 1.6 adoption: the delete-Deployment step plus the VPA re-applying 256Mi+ let the operator come up and the adoption complete. Refs: #3242, #3243 Signed-off-by: Andrey Kolkov <androndo@gmail.com>
📝 WalkthroughWalkthroughThis PR adds a legacy-compatible headless Service to the etcd Helm chart, introduces a pre-upgrade Helm hook Job with RBAC to delete etcd-operator-controller-manager Deployments with mismatched selectors so Helm can recreate them, and updates memory/VPA settings plus a new kubectlImage config value. Changesetcd-operator upgrade compatibility
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Helm
participant PreUpgradeJob
participant KubernetesAPI
participant ControllerManagerDeployment
Helm->>PreUpgradeJob: Trigger pre-upgrade hook
PreUpgradeJob->>KubernetesAPI: Get controller-manager Deployment
KubernetesAPI-->>PreUpgradeJob: Return spec.selector.matchLabels.control-plane
alt selector already "controller-manager"
PreUpgradeJob->>PreUpgradeJob: Exit without changes
else selector mismatched (pre-1.6)
PreUpgradeJob->>KubernetesAPI: Delete Deployment (--wait, timeout)
KubernetesAPI->>ControllerManagerDeployment: Remove Deployment
end
Helm->>KubernetesAPI: Recreate Deployment with new selector
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses critical issues related to the etcd v1alpha2 transition and operator stability. It ensures seamless upgrades by resolving an immutable selector problem in the etcd-operator Deployment and maintains cluster functionality during migration by providing a necessary transitional service. Additionally, it enhances the operator's resilience against OOM errors during startup by adjusting memory allocations. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml`:
- Around line 50-53: The Deployment RBAC used by the pre-upgrade hook is missing
the watch verb, which can break the delete-wait flow. Update the rules in the
Deployment permission block for the pre-upgrade selector fix template to include
watch alongside get, list, and delete so the hook can observe the Deployment
disappearing cleanly.
In `@packages/system/etcd-operator/values.yaml`:
- Around line 50-57: The kubectlImage configuration in values.yaml claims digest
pinning but currently uses a mutable tag, so update the image reference to be
reproducible by pinning it with a digest, or adjust the surrounding comment to
accurately describe tag-based pinning. Keep the fix localized to kubectlImage
(repository/tag/pullPolicy) and ensure the comment matches the actual image
reference semantics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a27db496-5c2f-4078-820f-113154cae0e7
📒 Files selected for processing (3)
packages/extra/etcd/templates/etcd-cluster.yamlpackages/system/etcd-operator/templates/pre-upgrade-selector-fix.yamlpackages/system/etcd-operator/values.yaml
| rules: | ||
| - apiGroups: ["apps"] | ||
| resources: ["deployments"] | ||
| verbs: ["get", "list", "delete"] |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target template and any system-specific guidance.
git ls-files packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml docs/agents/overview.md
echo
echo "---- target file ----"
cat -n packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml | sed -n '1,220p'
echo
echo "---- docs/agents/overview.md ----"
if [ -f docs/agents/overview.md ]; then
cat -n docs/agents/overview.md | sed -n '1,220p'
else
echo "docs/agents/overview.md not present"
fiRepository: cozystack/cozystack
Length of output: 16000
🌐 Web query:
kubectl delete --wait watch verb RBAC list watch Kubernetes 1.25 delete --wait permission
💡 Result:
When you run kubectl delete with the default --wait behavior (or explicitly with --wait=true), kubectl performs a "wait" operation to confirm the resource has been successfully removed from the API server [1][2]. As of Kubernetes 1.25 and later, this waiting process relies on list and watch verbs [1][3]. If a user lacks list and watch permissions for the specific resource being deleted, kubectl may fail to correctly determine that the deletion is complete [1]. In some configurations, this can cause the command to hang indefinitely because it cannot confirm the resource's removal via the watch mechanism [1][3]. ### RBAC Requirements To ensure the --wait functionality works as intended, the user or service account must have the following RBAC permissions on the target resource: * list [1][4] * watch [1][4] * get (often required in conjunction with other operations) [4] ### Workarounds If you do not have or cannot obtain the necessary list and watch permissions, you can use the following workarounds: 1. Disable Wait: Run the command with --wait=false to prevent kubectl from attempting to confirm the deletion [1]. bash kubectl delete <resource-type> <resource-name> --wait=false 2. Understand the Limitation: Recognize that if your RBAC policy strictly limits you to delete or get permissions without list and watch, the command will return a "Forbidden" error or hang during the wait phase [1][3][4]. While earlier versions of Kubernetes included patches to make kubectl delete more tolerant of missing verbs, changes introduced in version 1.25 altered how the wait logic functions, making these specific permissions necessary for the default wait behavior to terminate correctly [1][5][3].
Citations:
- 1: kubectl delete doesn't account for list/watch permissions on resource being deleted since Kubernetes 1.25 kubernetes/kubectl#1411
- 2: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/delete/delete.go
- 3: kubectl delete doesn't account for list/watch permissions on resource being deleted since Kubernetes 1.25 kubernetes/community#7252
- 4: Error Forbidden: unknown while running kubectl wait kubernetes/kubernetes#78227
- 5: Automated cherry pick of #65370: delete should tolerate a failed wait because of missing verbs kubernetes/kubernetes#66164
Add watch to the Deployment RBAC
kubectl delete deployment ... --wait=true needs watch on deployments; with only get, list, and delete, the hook can fail while waiting for the object to disappear and abort the pre-upgrade job.
Proposed fix
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
- verbs: ["get", "list", "delete"]
+ verbs: ["get", "list", "watch", "delete"]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| rules: | |
| - apiGroups: ["apps"] | |
| resources: ["deployments"] | |
| verbs: ["get", "list", "delete"] | |
| rules: | |
| - apiGroups: ["apps"] | |
| resources: ["deployments"] | |
| verbs: ["get", "list", "watch", "delete"] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml` around
lines 50 - 53, The Deployment RBAC used by the pre-upgrade hook is missing the
watch verb, which can break the delete-wait flow. Update the rules in the
Deployment permission block for the pre-upgrade selector fix template to include
watch alongside get, list, and delete so the hook can observe the Deployment
disappearing cleanly.
| # Image for the pre-upgrade selector-migration hook Job (see | ||
| # templates/pre-upgrade-selector-fix.yaml). Only needs a kubectl binary. Pinned | ||
| # by digest for reproducibility; clastix/kubectl is already used elsewhere in | ||
| # cozystack (Kamaji). | ||
| kubectlImage: | ||
| repository: docker.io/clastix/kubectl | ||
| tag: v1.32 | ||
| pullPolicy: IfNotPresent |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Comment claims digest pinning but the image is pinned by a mutable tag.
The comment says "Pinned by digest for reproducibility," yet tag: v1.32 is a mutable tag, not a digest — the referenced image can drift over time, defeating the reproducibility claim. Either pin by digest, or reword the comment to match reality.
🔧 Option: pin by digest
kubectlImage:
repository: docker.io/clastix/kubectl
- tag: v1.32
+ tag: v1.32@sha256:<digest>
pullPolicy: IfNotPresent🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/system/etcd-operator/values.yaml` around lines 50 - 57, The
kubectlImage configuration in values.yaml claims digest pinning but currently
uses a mutable tag, so update the image reference to be reproducible by pinning
it with a digest, or adjust the surrounding comment to accurately describe
tag-based pinning. Keep the fix localized to kubectlImage
(repository/tag/pullPolicy) and ensure the comment matches the actual image
reference semantics.
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
myasnikovdaniil
left a comment
There was a problem hiding this comment.
NOT LGTM — two blockers: the new pre-upgrade hook pod fails admission and cannot run, and three new behaviors ship untested in two packages that have active CI test suites.
Business context: The 1.5→1.6 etcd v1alpha2 transition has upgrade-blockers beyond the migration hook: the operator Deployment's spec.selector is immutable so helm upgrade fails (#3242), the 128Mi cold-start floor OOMKills the operator before VPA mutates it, and adopted pods lose their legacy *.etcd-headless DNS mid-transition. This PR fixes all three.
The direction is right, and both existing test suites still pass on this branch (etcd-operator 10/10, extra/etcd 17/17 — no breakage). Two blockers:
Blockers
B1: the selector-fix hook pod fails admission and cannot run (runAsUser omitted)
packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml — the Job sets pod securityContext.runAsNonRoot: true but no numeric runAsUser (neither pod nor container).
Evidence: docker.io/clastix/kubectl's image config declares its default user as the non-numeric string nonroot (crane config … → User: "nonroot"). When runAsNonRoot: true is set and the image user is a name (not a numeric UID) with no runAsUser override, the kubelet cannot verify the user is non-root and refuses to start the container: container has runAsNonRoot and image has non-numeric user (nonroot), cannot verify user is non-root. The sibling hook running the identical image — packages/system/postgres-operator/templates/webhook-ready-hook.yaml — sets runAsUser: 65532 for exactly this reason.
Impact: the hook Job never starts → after activeDeadlineSeconds: 120 it is marked Failed → the pre-upgrade hook fails → the etcd-operator HelmRelease upgrade fails. The immutable-selector fix never applies, and the upgrade is now additionally blocked by the hook meant to unblock it. Deterministic on every cluster.
Fix: add runAsUser: 65532 to the pod securityContext (match postgres-operator).
B2: new behavior ships untested in two packages that have active, contract-pinning test suites
Both changed charts run helm-unittest in CI (Makefile test: targets; 10 + 17 tests). The PR adds three behaviors and touches no test file:
etcd-headlessService (packages/extra/etcd) —tests/etcd-cluster_test.yamlis the home; nothing asserts the Service renders withclusterIP: None,publishNotReadyAddresses: true, theetcd-operator.cozystack.io/cluster: etcdselector, and the 2379/2380 ports.pre-upgrade-selector-fix.yamlhook (packages/system/etcd-operator) — nothing pins the hook annotations/weights, the namespaced RBAC scope, the security context, or the image templating. Direct precedent:packages/system/postgres-operator/tests/webhook-ready-hook_test.yamlpins exactly this kind of kubectl hook. (A manifest-contract test here would also have caught B1.)- OOM floor (
values.yaml:limits.memory128→256,vpa.minAllowed.memory256) —deployment_test.yamlrenderstemplates/deployment.yamlbut asserts nothing about resources. This is precisely the "values rename that's easy to break" the suite header says it exists to catch.
Per the repo's tests convention, changed/new code in a tested area must be pinned.
Non-blocking follow-ups
- Image is not digest-pinned, but the comment says it is.
values.yamlsetstag: v1.32with nodigest, so it renders asdocker.io/clastix/kubectl:v1.32(floating tag) — yet the comment above says "Pinned by digest for reproducibility." The established pattern ispackages/system/postgres-operator/values.yaml, which pinsdigest: sha256:b9ef7d8dbe65bcc81a46c09b8dc7543103055021c4f43287bf59e92a8f4fe05cwith a# renovate: datasource=dockerannotation and templatesrepo:tag@digest. The digest is already vendored in-repo — reuse it, or correct the comment. - Empty PR body and release note for a change that unblocks every 1.5→1.6 upgrade — worth a release note and a line on how it was validated.
For reference, the hardcoded etcd-operator.cozystack.io/cluster: etcd selector on the new Service is fine — the EtcdCluster in this chart is itself named etcd, so it is internally consistent.
| serviceAccountName: {{ $name }} | ||
| restartPolicy: Never | ||
| securityContext: | ||
| runAsNonRoot: true |
There was a problem hiding this comment.
The image docker.io/clastix/kubectl runs as the non-numeric user nonroot (its image config User). With runAsNonRoot: true and no numeric runAsUser, the kubelet can't verify the user is non-root and refuses to start the container (container has runAsNonRoot and image has non-numeric user (nonroot), cannot verify user is non-root) — so this hook Job never runs, its pre-upgrade hook fails, and the etcd-operator upgrade fails: the exact upgrade this PR is meant to unblock. The identical image in postgres-operator/templates/webhook-ready-hook.yaml sets runAsUser: 65532 for this reason. Add runAsUser: 65532 here.
| # cozystack (Kamaji). | ||
| kubectlImage: | ||
| repository: docker.io/clastix/kubectl | ||
| tag: v1.32 |
There was a problem hiding this comment.
This renders docker.io/clastix/kubectl:v1.32 — a floating tag, not a digest — but the comment above says "Pinned by digest for reproducibility." postgres-operator/values.yaml pins the same image by digest (sha256:b9ef7d8dbe65bcc81a46c09b8dc7543103055021c4f43287bf59e92a8f4fe05c) with a # renovate: datasource=docker annotation and templates repo:tag@digest. Reuse that digest (and template it in), or fix the comment.
…bectl, tests Addresses review on #3265: - pre-upgrade-selector-fix hook: add runAsUser: 65532. clastix/kubectl's image user is the non-numeric name `nonroot`, which the kubelet cannot verify against runAsNonRoot: true — so the Pod fails admission and the hook never runs, silently blocking the very 1.5->1.6 upgrade it exists to unblock. Matches postgres-operator's webhook-ready hook (same image). - Digest-pin the kubectl image (the comment already claimed digest-pinning but shipped a floating v1.32 tag). Reuse the digest postgres-operator already vendors, add the renovate annotation, and template repo:tag@digest. - Tests (both charts have CI helm-unittest suites, none previously covered these): - etcd-operator/tests/selector-fix-hook_test.yaml: hook wiring, namespaced least-privilege RBAC, numeric-non-root securityContext (guards the runAsUser regression above), digest-pinned image. - etcd-operator/tests/deployment_test.yaml: assert the 256Mi cold-start memory floor so it can't silently drop back to an OOMKilling value. - extra/etcd/tests/etcd-cluster_test.yaml: assert the transitional etcd-headless Service (headless, publishNotReadyAddresses, member selector, client/peer ports). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…es (#3270) Consolidates the etcd `v1alpha2` transition fix for **in-cluster 1.5 → 1.6 upgrades** into a single PR, rebased on current `main`. Supersedes #3265 and #3261 — their commits are carried here (authorship preserved), so those PRs can be closed once this lands. Fresh installs use the new shapes directly and are unaffected, so fresh-install CI does not catch these; the 1.5 → 1.6 e2e upgrade path is the authoritative regression guard. ## What this PR does ### 1. Keep the legacy `etcd-headless` Service alive during adoption — `packages/extra/etcd` `etcd-migrate` adopts legacy clusters **in place**: the Pods keep their original `spec.subdomain: etcd-headless` and are dialed at `etcd-<i>.etcd-headless.<ns>.svc` until they roll onto the operator's native `<member>.etcd.<ns>.svc` domain. The v1alpha2 operator only creates the native `etcd` Service and the legacy `etcd-headless` Service is pruned, so those per-pod names stop resolving (`no such host`), `MemberList` fails, and `status.readyMembers` never populates — the `EtcdCluster` never goes `Ready` even though etcd is healthy and in quorum. We ship a chart-managed transitional headless `etcd-headless` Service (selector mirrors the operator's native `etcd` Service via `etcd-operator.cozystack.io/cluster`, `publishNotReadyAddresses: true`) — the DNS counterpart of the legacy `*.etcd-headless.<ns>.svc` SAN already kept for this window. Removable together with that SAN once members roll onto the native subdomain. ### 2. Survive the immutable controller-Deployment selector on upgrade — `packages/system/etcd-operator` (#3242) #2859 replaced the upstream etcd-operator chart with the cozystack-authored one, changing `Deployment.spec.selector.matchLabels`. `spec.selector` is immutable, so `helm upgrade` cannot patch the existing Deployment and the whole HelmRelease upgrade fails (`field is immutable`). A **pre-upgrade hook** (`templates/pre-upgrade-selector-fix.yaml`: ServiceAccount + Role + RoleBinding + Job) deletes the Deployment **only** when its live selector is the pre-1.6 one, so Helm recreates it cleanly. No-op when the selector already matches, never runs on fresh install. ### 3. Raise the operator's memory cold-start floor — `packages/system/etcd-operator` Steady-state working set is ~250Mi; the static `limits.memory: 128Mi` OOMKills a Pod that starts before the VPA admission webhook rewrites it. Raise the floor to `256Mi` (and VPA `minAllowed` to match) as defense in depth so the operator never depends on VPA timing to avoid crashing. ### 4. Make migration 50 (etcd adoption) robust in-cluster — `packages/core/platform/images/migrations/migrations/50` (was #3261) Exact server/peer cert-SAN match, Secret-gated wait on the adoption Secret, and in-cluster (IPv6-safe) kubeconfig handling, so the migration script drives the adoption reliably from inside the cluster. Covered by `hack/migration-50-etcd-adopt.bats`. ### 5. Hardening / review fixes (this PR's original scope) - **Hook `runAsUser: 65532`.** `pre-upgrade-selector-fix.yaml` set `runAsNonRoot: true` but no numeric `runAsUser`; `clastix/kubectl`'s image user is the non-numeric name `nonroot`, which the kubelet cannot verify against `runAsNonRoot` — so the hook Pod fails admission and silently blocks the very upgrade it exists to unblock. Adds `runAsUser: 65532`, matching the postgres-operator webhook-ready hook that runs the same image. - **Digest-pin the kubectl image.** The values comment claimed digest-pinning but shipped a floating `v1.32` tag. Reuses the digest postgres-operator vendors, adds the `renovate` annotation, and templates `repo:tag@digest`. - **Tests.** `etcd-operator/tests/selector-fix-hook_test.yaml` (hook wiring, weight ordering, namespaced least-privilege RBAC, numeric-non-root security context, digest-pinned image), `etcd-operator/tests/deployment_test.yaml` (256Mi cold-start floor), `extra/etcd/tests/etcd-cluster_test.yaml` (transitional `etcd-headless` Service). Each assertion was mutation-tested. ### 6. Derive the default S3 endpoint from the provisioned bucket — `packages/system/backupstrategy-controller` Derive the default S3 endpoint (and per-driver scheme / TLS / `secure_connection`) from the provisioned bucket Secret instead of requiring it to be hand-set, so etcd (and other) backup strategies get a working endpoint by default. Docs in `docs/operations/backup-classes.md`; covered by `tests/endpoint_form_test.yaml`. ### Verification - `helm unittest` green on current `main`: etcd-operator **18/18**, extra/etcd **18/18**, backupstrategy-controller **11/11**. - Behaviours 1 & 2 were reproduced and confirmed live on a 1.5.2 → 1.6.0-rc.1 adoption (3-node cluster) — recreating the `etcd-headless` Service took the adopted cluster to `readyMembers=3 / Available=True`. ```release-note fix(etcd): complete the v1alpha2 transition on in-cluster 1.5→1.6 upgrades — keep the legacy etcd-headless Service alive so adopted members stay resolvable, delete the pre-1.6 operator Deployment via a pre-upgrade hook to get past the immutable selector, raise the operator's memory floor so it does not OOM before the VPA scales it, and make the etcd adoption migration robust in-cluster. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved backup storage endpoint handling across supported backup drivers, including provisioned and external S3 storage. * Added compatibility support for legacy etcd pod discovery during migration. * Added an automated upgrade safeguard for etcd operator deployments. * **Bug Fixes** * Improved certificate SAN detection and etcd migration authentication. * Increased the etcd operator’s minimum startup memory to prevent early restarts. * **Documentation** * Clarified backup endpoint, TLS, and driver-specific behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
Completed in #3270 |
What this PR does
Completes the etcd
v1alpha2transition (#2859) by fixing the three remaining blockers that surface on an in-cluster 1.5 → 1.6 upgrade of a cluster that already has an etcd. Fresh installs use the new shapes directly and are unaffected, so fresh-install CI does not catch these. Scope is limited to the etcd charts; the sibling migration-script fixes (#3243 cert-SAN wait, #3255 in-cluster kubeconfig) are in #3261.1. Keep the legacy
etcd-headlessService alive during adoption —packages/extra/etcdetcd-migrateadopts legacy clusters in place: the Pods are never recreated, so they keep their originalspec.subdomain: etcd-headlessand are dialed atetcd-<i>.etcd-headless.<ns>.svcuntil they eventually roll onto the operator's native<member>.etcd.<ns>.svcdomain. The v1alpha2 operator only creates the nativeetcdService, and the legacyetcd-headlessService (previously owned by the old operator) is pruned during the transition — so those per-pod names stop resolving (no such host), the operator'sMemberListfails, andstatus.readyMembersnever populates. TheEtcdClusterthen never goesReadyeven though the etcd processes are healthy and in quorum.We ship a chart-managed transitional headless
etcd-headlessService (selector mirrors the operator's nativeetcdService viaetcd-operator.cozystack.io/cluster,publishNotReadyAddresses: true). This is the DNS counterpart of the legacy*.etcd-headless.<ns>.svcwildcard already kept in the server/peer cert SANs for the same transition window — the TLS half of the compat was done, the DNS half was missing. Safe to remove, together with that SAN, once members have rolled onto the native subdomain.2. Survive the immutable controller-Deployment selector on upgrade —
packages/system/etcd-operator(#3242)#2859 replaced the upstream etcd-operator chart with the cozystack-authored one, changing
Deployment.spec.selector.matchLabels({instance,name}→{name,control-plane}).spec.selectoris immutable, sohelm upgradecannot patch the existing Deployment and the whole HelmRelease upgrade fails (field is immutable) — the operator and the v1alpha2 CRDs it serves never come up, which blocks the etcd adoption.A pre-upgrade hook (
templates/pre-upgrade-selector-fix.yaml: ServiceAccount + Role + RoleBinding + Job) deletes the Deployment only when its live selector is the pre-1.6 one (lackscontrol-plane=controller-manager), so Helm recreates it cleanly. It is a no-op when the selector already matches (rc.1 → later upgrades) and never runs on a fresh install (pre-upgrade only). Keeping the selector stable in the chart is not viable — rc.1 already shipped the new selector, which would only move the immutable break to rc.1 → next.3. Raise the operator's memory cold-start floor —
packages/system/etcd-operatorThe controller manager's steady-state working set is ~250Mi (the VPA's own recommendation); the static
limits.memory: 128MiOOMKills a Pod that starts before the VPA admission webhook rewrites it (e.g. a Deployment recreated out of band, or the webhook briefly unavailable during an upgrade). Raise the floor to256Mi(and VPAminAllowedto match) as defense in depth so the operator never depends on VPA timing merely to avoid crashing; the VPA still scales it further under load up tomaxAllowed.Verification
helm unittest(etcd-operator) green; both charts render cleanly (helm template).etcd-headlessService took the adopted cluster toreadyMembers=3 / Available=True. The authoritative regression guard is the 1.5 → 1.6 e2e upgrade path.Release note
Summary by CodeRabbit
New Features
Bug Fixes