Skip to content

feat(chart): deny EtcdMember deletion at admission - #348

Open
Andrei Kvapil (kvaps) wants to merge 2 commits into
mainfrom
feat/protect-members-at-admission
Open

feat(chart): deny EtcdMember deletion at admission#348
Andrei Kvapil (kvaps) wants to merge 2 commits into
mainfrom
feat/protect-members-at-admission

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Jul 30, 2026

Copy link
Copy Markdown
Member

Implements the direction proposed in the review on #347: stop the event at the API boundary instead of trying to survive it in the controllers.

Why

docs/concepts.md has said since its first version that EtcdMember objects are created and deleted by the cluster controller and users should not touch them. Nothing enforced it, and the accident it invites is unrecoverable:

  • the member's PVC is controller-owned by it, so deleting the CR takes the data volume with it — irreversibly on a Delete-reclaim StorageClass;
  • the member's finalizer removes the member from etcd on the way out, so a cluster whose members are deleted one by one dismembers itself.

One kubectl delete, a GitOps prune of an object the tool did not expect to be managed, or a cleanup script sweeping CRs by label is enough. This is not hypothetical — it is what happened on a dev cluster: the EtcdMember CRs of seven namespaces were removed, the volumes went with them, and the running pods kept serving from already-deleted volumes until a node reboot ended them twelve days later.

What

A ValidatingAdmissionPolicy in the chart denying DELETE on etcdmembers, with failurePolicy: Fail and validationActions: ["Deny"]. Allowed through:

requester why
the operator's ServiceAccount scale-down and crash-loop replacement delete members deliberately
system:serviceaccount:kube-system:generic-garbage-collector a deleted EtcdCluster must still cascade to its members
system:serviceaccount:kube-system:namespace-controller deleting a namespace must not hang
memberDeletionProtection.additionalAllowedUsers platforms whose own controllers legitimately reap these objects

Break-glass without uninstalling the policy — annotate, then delete:

kubectl annotate etcdmember.etcd-operator.cozystack.io <member> -n <ns> \
  etcd-operator.cozystack.io/allow-deletion=true

The denial message names the member, says the data is not recoverable from there, and points at scaling the EtcdCluster instead.

Notes on two decisions

Not gated on .Capabilities. ValidatingAdmissionPolicy is GA since 1.30, and my first draft failed the render when the API was absent. But .Capabilities.APIVersions is empty for a plain helm template, so that check silently dropped the policy from build-dist-manifests output — exactly the manifest where its absence is least visible. Instead the policy always renders when enabled; on an older apiserver the install fails loudly with no matches for kind ValidatingAdmissionPolicy, and memberDeletionProtection.enabled=false is the documented way out.

CRD teardown ordering. Removing the CRD makes the apiserver delete every EtcdMember, and the policy denies those requests — leaving the CRD stuck in Terminating. helm uninstall removes the policy first, so the normal path is fine; the teardown docs now spell it out for manual teardowns that skip Helm.

Docs

  • concepts.md: the API-model sentence now says delete and states the consequence — this is the one-line fix asked for twice in review. Plus a new section documenting the guard, the allowed requesters, break-glass, the 1.30 requirement and the uninstall ordering.
  • operations.md: the broken-member recovery recipe is kubectl delete etcdmember — which this policy blocks. Added the annotation step, and a note that typing it out is what separates a deliberate data-discarding recovery from the same command issued by accident.
  • installation.md: teardown removes the policy before the CRDs.

Tests

charts/etcd-operator/tests/member_deletion_policy_test.yaml — 8 cases: installed by default, binding names its policy, matches only DELETE on etcdmembers, denies rather than warns and fails closed, all three system requesters still allowed, break-glass annotation honoured, extra allowed users accepted, and enabled=false renders nothing (for pre-1.30 clusters). helm unittest 8/8; go test ./... unaffected and green.

Relationship to the other two PRs

Summary by CodeRabbit

  • New Features

    • Added optional admission protection that blocks EtcdMember deletion.
    • Allows routine operator/controller deletion paths, supports configurable additional allowed identities, and includes a “break-glass” annotation to override protection.
  • Documentation

    • Expanded guidance warning against manual EtcdMember edits/deletes, explained recovery using the “break-glass” annotation, and clarified Kubernetes version requirements.
    • Updated uninstall instructions to remove the admission policy objects left behind by non-Helm teardowns.
  • Tests

    • Added coverage to verify the deletion guard behavior and override behavior.

The docs have said since their first version that EtcdMember objects are
created and deleted by the cluster controller and users should not touch
them. Nothing enforced it, and the accident is unrecoverable: a member's PVC
is controller-owned by it, so deleting the CR takes the data volume with it
while the finalizer removes the member from etcd on the way out. Delete every
member of a cluster and it dismembers itself with nothing left to restore
from. One kubectl delete, a GitOps prune of an unexpected object, or a
cleanup script sweeping CRs by label is enough.

Install a ValidatingAdmissionPolicy denying DELETE on etcdmembers, except
for the operator's ServiceAccount (scale-down and crash-loop replacement
delete members deliberately), the garbage collector (an EtcdCluster deletion
must still cascade), the namespace controller (namespace deletion must not
hang), and anything in memberDeletionProtection.additionalAllowedUsers.
Break-glass without uninstalling: annotate the member with
etcd-operator.cozystack.io/allow-deletion=true.

Requires Kubernetes 1.30+; memberDeletionProtection.enabled=false installs
without the guard. Deliberately not gated on .Capabilities — that is empty
for a plain helm template, so gating there would silently drop the policy
from the rendered release manifests.

Docs: state in the API model that deleting a member destroys its data (the
sentence previously said only "create or edit"), document the guard and the
break-glass path, add the annotation step to the broken-member recovery
recipe it would otherwise block, and remove the policy before the CRDs
during a manual teardown — deleting a CRD makes the apiserver delete every
member, which the policy would deny.

Assisted-By: Claude
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added documentation Improvements or additions to documentation feature New feature or request labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a6bff2c-f09d-424d-85fb-fae07e7c2a9c

📥 Commits

Reviewing files that changed from the base of the PR and between 1be49d3 and 49525bc.

📒 Files selected for processing (1)
  • test/e2e/kamaji_datastore_test.go

📝 Walkthrough

Walkthrough

Adds a configurable Kubernetes ValidatingAdmissionPolicy that denies unauthorized EtcdMember deletion, supports allowlisted identities and a break-glass annotation, and documents recovery and uninstall procedures.

Changes

EtcdMember deletion protection

Layer / File(s) Summary
Policy configuration
charts/etcd-operator/values.yaml, charts/etcd-operator/templates/member-deletion-policy.yaml
Adds the memberDeletionProtection settings and documents the rendered deletion guard contract.
Admission guard and binding
charts/etcd-operator/templates/member-deletion-policy.yaml
Renders the conditional policy and binding, allowing configured service accounts or the allow-deletion=true annotation while denying other matching deletes.
Policy validation and operational procedures
charts/etcd-operator/tests/member_deletion_policy_test.yaml, docs/concepts.md, docs/installation.md, docs/operations.md, test/e2e/kamaji_datastore_test.go
Tests targeting, enforcement, allowlists, break-glass behavior, disablement, and ServiceAccount naming; documents recovery and teardown steps; updates end-to-end member deletion to annotate members before deletion.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant KubernetesAPIServer
  participant ValidatingAdmissionPolicy
  participant EtcdMember
  Client->>KubernetesAPIServer: DELETE EtcdMember
  KubernetesAPIServer->>ValidatingAdmissionPolicy: Evaluate deletion request
  ValidatingAdmissionPolicy->>EtcdMember: Read allow-deletion annotation
  ValidatingAdmissionPolicy-->>KubernetesAPIServer: Allow or Deny
  KubernetesAPIServer-->>Client: Deletion response
Loading

Suggested reviewers: lllamnyp, androndo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding chart-level admission protection against EtcdMember deletion.
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.
✨ 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 feat/protect-members-at-admission

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

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@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 `@charts/etcd-operator/templates/member-deletion-policy.yaml`:
- Around line 71-73: Update the EtcdMember deletion policy binding around
policyName and validationActions so each release is scoped to its managed
namespace using matchResources/namespaceSelector, or explicitly prevent multiple
releases from installing the chart. Ensure legitimate EtcdMember deletions
remain allowed when multiple releases exist and avoid cluster-wide binding
collisions.
- Around line 32-37: Add "system:kube-controller-manager" to the allowed users
assembled by the member-deletion policy template alongside the existing
controller identities, and update the member-deletion policy tests to verify
this fallback identity is permitted.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d39716f-91cc-4694-bd72-f78cb90175ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5d896 and 1be49d3.

📒 Files selected for processing (6)
  • charts/etcd-operator/templates/member-deletion-policy.yaml
  • charts/etcd-operator/tests/member_deletion_policy_test.yaml
  • charts/etcd-operator/values.yaml
  • docs/concepts.md
  • docs/installation.md
  • docs/operations.md

Comment on lines +32 to +37
{{- $allowed := concat
(list
(printf "system:serviceaccount:%s:%s" .Release.Namespace (include "etcd-operator.serviceAccountName" .))
"system:serviceaccount:kube-system:generic-garbage-collector"
"system:serviceaccount:kube-system:namespace-controller")
.Values.memberDeletionProtection.additionalAllowedUsers }}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)member-deletion-policy\.yaml$|etcd-operator|memberDeletionProtection' || true

echo
echo "member-deletion-policy.yaml candidates:"
fd -a 'member-deletion-policy\.yaml$' . || true

echo
echo "Search relevant symbols/config/vals:"
rg -n "memberDeletionProtection|member-deletion-policy|memberDeletion|additionalAllowedUsers|controller-manager|service-account|Service Account" -S . | head -n 200

Repository: cozystack/etcd-operator

Length of output: 4803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "member-deletion-policy.yaml:"
cat -n charts/etcd-operator/templates/member-deletion-policy.yaml

echo
echo "values memberDeletionProtection section:"
sed -n '26,40p' charts/etcd-operator/values.yaml | cat -n

echo
echo "docs concepts section around allowed users:"
sed -n '1,70p' docs/concepts.md | cat -n

echo
echo "chart tests section around member-deletion-policy assertions:"
sed -n '1,150p' charts/etcd-operator/tests/member_deletion_policy_test.yaml | cat -n

Repository: cozystack/etcd-operator

Length of output: 17388


🌐 Web query:

Kubernetes kube-controller-manager use-service-account-credentials service account username system:kube-controller-manager authorizationrbac

💡 Result:

The --use-service-account-credentials flag for kube-controller-manager controls how individual control loops (controllers) authenticate with the Kubernetes API server [1][2]. When --use-service-account-credentials is set to true (default is false), the kube-controller-manager creates a dedicated service account for each built-in controller [3][2]. These controllers then use these individual service account credentials to perform their tasks [3][2]. The roles required for each controller are predefined in Kubernetes and are prefixed with system:controller: (e.g., system:controller:deployment-controller) [2]. In this mode, the kube-controller-manager's primary credential only requires the system:kube-controller-manager ClusterRole [2][4]. If --use-service-account-credentials is set to false (monolithic mode), the kube-controller-manager runs all its internal control loops using its own single credential [2][4]. In this configuration, that credential must be granted the system:kube-controller-manager ClusterRole as well as all the permissions required by every individual controller, which is not handled automatically by the cluster [2][4]. The system:kube-controller-manager user identity is a built-in system identity (User: system:kube-controller-manager) [5]. By default, this user is bound to the system:kube-controller-manager ClusterRole via a ClusterRoleBinding [5]. This role provides the base access required for the component to function, such as accessing essential resources like namespaces, endpoints, and secrets [2][6]. If the controller manager requires additional permissions (e.g., accessing ConfigMaps), administrators may need to update this ClusterRole or bind additional roles to the system:kube-controller-manager user [6].

Citations:


Allow the kube-controller-manager fallback identity.

Clusters running kube-controller-manager without --use-service-account-credentials authenticate deletion requests as system:kube-controller-manager, so cascade deletes from a deleted EtcdCluster and namespace cleanup can be rejected and leave objects stuck. Add system:kube-controller-manager to the allowed delete users and cover it in the member-deletion policy tests.

Proposed fix
         (printf "system:serviceaccount:%s:%s" .Release.Namespace (include "etcd-operator.serviceAccountName" .))
         "system:serviceaccount:kube-system:generic-garbage-collector"
         "system:serviceaccount:kube-system:namespace-controller"
+        "system:kube-controller-manager")
📝 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
{{- $allowed := concat
(list
(printf "system:serviceaccount:%s:%s" .Release.Namespace (include "etcd-operator.serviceAccountName" .))
"system:serviceaccount:kube-system:generic-garbage-collector"
"system:serviceaccount:kube-system:namespace-controller")
.Values.memberDeletionProtection.additionalAllowedUsers }}
{{- $allowed := concat
(list
(printf "system:serviceaccount:%s:%s" .Release.Namespace (include "etcd-operator.serviceAccountName" .))
"system:serviceaccount:kube-system:generic-garbage-collector"
"system:serviceaccount:kube-system:namespace-controller"
"system:kube-controller-manager")
.Values.memberDeletionProtection.additionalAllowedUsers }}
🤖 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 `@charts/etcd-operator/templates/member-deletion-policy.yaml` around lines 32 -
37, Add "system:kube-controller-manager" to the allowed users assembled by the
member-deletion policy template alongside the existing controller identities,
and update the member-deletion policy tests to verify this fallback identity is
permitted.

Comment on lines +71 to +73
spec:
policyName: {{ include "etcd-operator.fullname" . }}-protect-members
validationActions: ["Deny"]

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)member-deletion-policy.yaml$|charts/|templates/'

echo
echo "Target file outline/stat:"
wc -l charts/etcd-operator/templates/member-deletion-policy.yaml
ast-grep outline charts/etcd-operator/templates/member-deletion-policy.yaml || true

echo
echo "Target file relevant contents:"
cat -n charts/etcd-operator/templates/member-deletion-policy.yaml | sed -n '1,140p'

echo
echo "Search for related templates/values/CRDs:"
find charts/etcd-operator -maxdepth 4 -type f -print | sort | xargs -r rg -n "allowlist|validationActions|policyName|etcd-operator\.fullname|validation|binding|Deny|EtcdMember|ServiceAccount|namespace|admission|restrict|member-deletion|protection" || true

Repository: cozystack/etcd-operator

Length of output: 47357


🌐 Web query:

Kubernetes ValidatingAdmissionPolicyBinding spec matchResources namespaceSelector no namespaceSelector applies namespace-scoped resources allNamespaces

💡 Result:

In Kubernetes, the namespaceSelector field within ValidatingAdmissionPolicyBinding (part of the matchResources configuration) utilizes a LabelSelector that defaults to an empty selector when unset [1][2][3]. An empty LabelSelector matches everything [1][4][3]. Therefore, if namespaceSelector is omitted in the ValidatingAdmissionPolicyBinding spec, it effectively does not filter by namespace, meaning the policy will apply to resources in all namespaces, provided they are otherwise matched by the policy's configuration [5][1][2]. Key points regarding this behavior: - Scope: The namespaceSelector decides whether to run the admission control policy based on whether the object's namespace matches the selector [1][2][3]. - Default Behavior: Because it defaults to an empty LabelSelector, omitting the field means no namespace-based filtering is applied, resulting in evaluation for all namespaces [1][4][3]. - Cluster-scoped Resources: For cluster-scoped resources, the policy is never skipped due to namespaceSelector [1][4][2]. - Interaction with Policy: The binding's matchResources is intersected with the ValidatingAdmissionPolicy's own matchConstraints [5]. Only requests matched by the underlying policy can be selected by the binding [5]. Do not confuse this with NetworkPolicy or other Kubernetes objects where the presence or absence of namespaceSelector may have different implicit behaviors (such as defaulting to the local namespace) [6]. In the context of ValidatingAdmissionPolicyBinding, an omitted namespaceSelector behaves as "match all" [1][3].

Citations:


Scope the EtcdMember deletion policy binding per release namespace or enforce singleton installation.

The binding has no matchResources/namespaceSelector, so one release’s policy is applied cluster-wide to every EtcdMember. A second release installs another same-name policy whose allowed list excludes the first release’s ServiceAccount, which can block legitimate scale-down/failure-replacement deletes from that operator. Share one cluster-wide allowlist across releases, scope bindings to each managed namespace, or explicitly enforce singleton installation.

🤖 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 `@charts/etcd-operator/templates/member-deletion-policy.yaml` around lines 71 -
73, Update the EtcdMember deletion policy binding around policyName and
validationActions so each release is scoped to its managed namespace using
matchResources/namespaceSelector, or explicitly prevent multiple releases from
installing the chart. Ensure legitimate EtcdMember deletions remain allowed when
multiple releases exist and avoid cluster-wide binding collisions.

The Kamaji datastore test deletes EtcdMembers on purpose to exercise
MemberRemove plus the GenerateName replacement. With member deletion denied
at admission that request is rejected — correctly: the guard exists to stop
exactly this command when it is issued by accident.

Annotate the member with etcd-operator.cozystack.io/allow-deletion=true
first, which is the documented path for a deliberate deletion. Done in the
test rather than by exempting its identity in the policy, so the e2e stays
honest about what a human has to do.

Assisted-By: Claude
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes requested — the direction is exactly right; four things need fixing before this ships, one of them empirical.

This is the guard I asked for on #347, and the hard parts are done well: Deny + failurePolicy: Fail, the break-glass annotation instead of identity exemptions for tests, the e2e replaying the human procedure rather than sidestepping it, the BYO-SA/fullname unittest, and concepts.md finally saying delete with the consequence spelled out. What follows is about the exemption list, the teardown story, and two pieces of framing.

1. The exemption list is incomplete for non-SA-credential control planes

CodeRabbit's first inline comment is correct and I want to second it explicitly: on clusters where kube-controller-manager runs without --use-service-account-credentials, the GC and namespace-cleanup deletes authenticate as the user system:kube-controller-manager, not as the per-controller ServiceAccounts. On such a cluster this policy wedges both EtcdCluster cascade deletion and namespace deletion — the two flows the allowlist exists to keep working. Add system:kube-controller-manager to the allowlist (harmless where per-controller SAs are in use) and cover it in the unittest alongside the other three.

2. Two releases of this chart deny each other's operators

CodeRabbit's second comment has the wrong details (policy names are fullname-prefixed, so there's no name collision) but the right defect: the binding carries no matchResources/namespaceSelector, so every release's policy applies cluster-wide to all etcdmembers, while each allowlist contains only its own release's SA. Admission is deny-wins across matching policies — install the chart twice and each operator's legitimate scale-down and crash-loop replacement is blocked by the other release's policy. That's not hypothetical: --watch-namespace (#341) exists precisely so multiple scoped operators can coexist. Either scope the binding (spec.matchResources.namespaceSelector) when the release is namespace-scoped, or document a hard singleton constraint — but the current combination of cluster-wide match and per-release allowlist can't stay.

3. The CRD-teardown story needs an empirical check — the mechanism as written is probably wrong

The template comment, values.yaml, and installation.md all state that deleting the CRD makes the apiserver delete every EtcdMember, the policy denies those requests, and the CRD hangs in Terminating. I don't believe the middle step. CRD-instance cleanup (customresourcecleanup.apiextensions.k8s.io) is performed by the apiextensions-apiserver in-process, through the storage layer rather than the authenticated request path — the same long-standing gap that keeps admission webhooks from firing for CRs deleted during CRD deletion. If that holds for VAP too (and I expect it does), then:

  • the documented stuck-CRD scenario doesn't occur via the policy — if a CRD wedges during manual teardown, the cause is member finalizers with no operator left to process them, which is a pre-existing, separately documented mode; and
  • more importantly, CRD deletion/replacement remains a member-destruction path the guard cannot see — and that scenario was on the original threat list this guard responds to.

Please verify once on kind: policy active, delete the CRD, observe whether the deletes are denied and what identity the audit log records. Then either keep the teardown step with a corrected rationale, or replace it with an honest known-limitation note ("the guard does not cover CRD-deletion-driven member removal"). Both outcomes are fine; an asserted mechanism nobody has observed is not.

4. Rewrite the no-.Capabilities rationale — the stated one doesn't hold

The comment justifies always-rendering with "Capabilities is empty for a plain helm template, so gating would silently drop the policy from the rendered release manifests." But helm template has --api-versions, and build-dist-manifests is our own CI — one flag in the Makefile keeps a gated policy in dist. As stated, the rationale is refutable, and someone will refute it and then gate the template.

The durable rationale is different, and worth stating instead: capability detection makes a security guard's absence silent. A live helm install against a pre-1.30 apiserver would quietly omit the policy; Argo-style GitOps renders, which pass the destination cluster's API versions, would do the same. For a control whose whole purpose is preventing an unrecoverable accident, absence must be loud and explicitly chosen — which is exactly what always-render plus memberDeletionProtection.enabled=false delivers. Right decision; please make the comment defend it with the argument that survives.

5. Drop "a GitOps prune" from the threat framing

It appears in the template comment and again in concepts.md. There is no legitimate scenario in which a GitOps tool manages EtcdMember objects — the only sanctioned non-operator writer is cmd/etcd-migrate, which creates and never deletes. Listing "a GitOps prune" alongside plausible accidents reads as if these objects might reasonably live in a desired-state repo. State the contract positively instead: nothing manages EtcdMembers declaratively, and a GitOps tool attempting this delete is a tracking misconfiguration the policy correctly stops — not a workflow to accommodate.

Minor

  • installation.md's manual-teardown commands hardcode etcd-operator-protect-members, but the objects are named {{ fullname }}-protect-members. Correct for the default release name, wrong for any other — either note that, or show a label-based delete.
  • The e2e annotates via Get + Update, which can 409 against a concurrent member-controller write and fail the test spuriously; a MergeFrom patch of the annotation is conflict-proof.

Bottom line

Fix the two allowlist/scoping defects (1, 2), run the kind experiment and make the teardown docs say what actually happens (3), and reframe the two comments (4, 5). With those, this closes the hole #347 argued about at the right layer, and I'm happy to approve. On #343: my position stands that it's worth keeping in draft as defense-in-depth for exactly the populations this chart flag can exempt (enabled=false, pre-1.30) — but that decision can wait until this lands.

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

Labels

documentation Improvements or additions to documentation feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants