Skip to content

fix(etcd): against bugs to v1alpha2 transition - #3265

Closed
Andrey Kolkov (androndo) wants to merge 2 commits into
mainfrom
fix/etcd-v1alpha2-transition
Closed

fix(etcd): against bugs to v1alpha2 transition#3265
Andrey Kolkov (androndo) wants to merge 2 commits into
mainfrom
fix/etcd-v1alpha2-transition

Conversation

@androndo

@androndo Andrey Kolkov (androndo) commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Completes the etcd v1alpha2 transition (#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-headless Service alive during adoption — packages/extra/etcd

etcd-migrate adopts legacy clusters in place: the Pods are never recreated, so they 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 (previously owned by the old operator) is pruned during the transition — so those per-pod names stop resolving (no such host), the operator's MemberList fails, and status.readyMembers never populates. The EtcdCluster then never goes Ready even though the etcd processes are 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). 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 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.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 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 (lacks control-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-operator

The controller manager's steady-state working set is ~250Mi (the VPA's own recommendation); the static limits.memory: 128Mi OOMKills 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 to 256Mi (and VPA minAllowed to 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 to maxAllowed.

Verification

  • helm unittest (etcd-operator) green; both charts render cleanly (helm template).
  • Behaviours 1 & 2 were reproduced and their fixes 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. The authoritative regression guard is the 1.5 → 1.6 e2e upgrade path.

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, and raise the operator's memory floor so it does not OOM before the VPA scales it.

Summary by CodeRabbit

  • New Features

    • Added a compatibility service for etcd pods to preserve legacy per-pod DNS access during upgrades.
    • Introduced an automated upgrade step to help recover from selector mismatches and keep upgrades moving.
  • Bug Fixes

    • Improved etcd-operator upgrade reliability by handling immutable deployment selector changes automatically.
    • Increased the controller manager’s baseline memory to reduce cold-start issues and align resource recommendations.

…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>
@dosubot dosubot Bot added area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/bug Categorizes issue or PR as related to a bug labels Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

etcd-operator upgrade compatibility

Layer / File(s) Summary
Legacy headless Service
packages/extra/etcd/templates/etcd-cluster.yaml
Adds etcd-headless Service with clusterIP: None, publishNotReadyAddresses: true, selecting etcd pods and exposing client (2379) and peer (2380) TCP ports.
Pre-upgrade hook RBAC
packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml
Adds explanatory comment, a ServiceAccount, Role (get/list/delete on Deployments), and RoleBinding for the pre-upgrade selector-fix hook.
Pre-upgrade selector-fix Job
packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml
Runs a kubectl-based Job that checks the Deployment's spec.selector.matchLabels.control-plane value and deletes the Deployment if it doesn't match controller-manager, allowing Helm to recreate it with the correct selector.
Resource/VPA and kubectlImage config
packages/system/etcd-operator/values.yaml
Raises controller-manager memory request and VPA minAllowed.memory from 128Mi to 256Mi, and adds a kubectlImage block (image, tag, pullPolicy) used by the pre-upgrade Job.

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
Loading

Suggested labels: kind/api-change, area/platform, area/kubernetes

Suggested reviewers: kvaps, lexfrei, IvanHunters

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the PR’s etcd v1alpha2 transition fixes, though the wording is awkward.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/etcd-v1alpha2-transition

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Etcd v1alpha2 Transition Bug Fix: Introduced a transitional headless Service (etcd-headless) to ensure existing etcd pods maintain DNS resolution during the v1alpha2 migration, preventing MemberList failures and ensuring cluster readiness.
  • Helm Upgrade Selector Fix: Implemented a pre-upgrade Helm hook that detects and deletes the etcd-operator-controller-manager Deployment if it uses an outdated, immutable selector. This forces Helm to recreate the Deployment with the correct selector, resolving upgrade failures from v1.5 to v1.6.
  • Etcd Operator Memory Optimization: Increased the etcd-operator's default memory limit and VPA minimum allowed memory from 128Mi to 256Mi to prevent Out-Of-Memory (OOM) issues during cold starts, aligning with its observed steady-state working set.
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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6b1e170 and 33fa5e5.

📒 Files selected for processing (3)
  • packages/extra/etcd/templates/etcd-cluster.yaml
  • packages/system/etcd-operator/templates/pre-upgrade-selector-fix.yaml
  • packages/system/etcd-operator/values.yaml

Comment on lines +50 to +53
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "delete"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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"
fi

Repository: 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:


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.

Suggested change
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.

Comment on lines +50 to +57
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

@github-actions github-actions Bot added the size/L This PR changes 100-499 lines, ignoring generated files label Jul 9, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-headless Service (packages/extra/etcd) — tests/etcd-cluster_test.yaml is the home; nothing asserts the Service renders with clusterIP: None, publishNotReadyAddresses: true, the etcd-operator.cozystack.io/cluster: etcd selector, and the 2379/2380 ports.
  • pre-upgrade-selector-fix.yaml hook (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.yaml pins exactly this kind of kubectl hook. (A manifest-contract test here would also have caught B1.)
  • OOM floor (values.yaml: limits.memory 128→256, vpa.minAllowed.memory 256) — deployment_test.yaml renders templates/deployment.yaml but 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

  1. Image is not digest-pinned, but the comment says it is. values.yaml sets tag: v1.32 with no digest, so it renders as docker.io/clastix/kubectl:v1.32 (floating tag) — yet the comment above says "Pinned by digest for reproducibility." The established pattern is packages/system/postgres-operator/values.yaml, which pins digest: sha256:b9ef7d8dbe65bcc81a46c09b8dc7543103055021c4f43287bf59e92a8f4fe05c with a # renovate: datasource=docker annotation and templates repo:tag@digest. The digest is already vendored in-repo — reuse it, or correct the comment.
  2. 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

myasnikovdaniil added a commit that referenced this pull request Jul 15, 2026
…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>
myasnikovdaniil added a commit that referenced this pull request Jul 16, 2026
…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 -->
@myasnikovdaniil

Copy link
Copy Markdown
Contributor

Completed in #3270

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants