Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions charts/etcd-operator/templates/member-deletion-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{{- /*
Enforces at the API boundary the contract the docs have carried since day one:
EtcdMember objects are created and deleted by the operator, not by users.

Deleting one is not a recoverable mistake. Its data PVC is controller-owned by
the member, so the volume — and on a Delete-reclaim StorageClass the data
itself — goes with it, while the member's finalizer removes the member from
etcd on the way out. A cluster whose members are deleted one by one therefore
dismembers itself and leaves nothing to restore from.

Denied here rather than survived in the controllers: the accident (a stray
kubectl delete, a GitOps prune, a cleanup script sweeping CRs) is what needs to
stop, and the requester learns why at the moment they try it.

Allowed through:
- the operator's own ServiceAccount — scale-down and crash-loop replacement
delete members deliberately;
- the garbage collector — cascade from a deleted EtcdCluster must still work;
- the namespace controller — deleting a namespace must not hang.

Break-glass without uninstalling the policy: annotate the member with
etcd-operator.cozystack.io/allow-deletion=true, then delete it.

Requires Kubernetes 1.30+ (ValidatingAdmissionPolicy GA). On an older
apiserver the install fails with "no matches for kind
ValidatingAdmissionPolicy"; set memberDeletionProtection.enabled=false to
install 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.
*/ -}}
{{- if .Values.memberDeletionProtection.enabled }}
{{- $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 }}
Comment on lines +32 to +37

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.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: {{ include "etcd-operator.fullname" . }}-protect-members
labels:
{{- include "etcd-operator.labels" . | nindent 4 }}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["etcd-operator.cozystack.io"]
apiVersions: ["*"]
operations: ["DELETE"]
resources: ["etcdmembers"]
validations:
- expression: >-
request.userInfo.username in {{ $allowed | toJson }}
|| (has(oldObject.metadata.annotations)
&& "etcd-operator.cozystack.io/allow-deletion" in oldObject.metadata.annotations
&& oldObject.metadata.annotations["etcd-operator.cozystack.io/allow-deletion"] == "true")
reason: Forbidden
messageExpression: >-
"EtcdMember " + oldObject.metadata.name + " is managed exclusively by etcd-operator, and deleting it
removes the member from etcd and releases its data volume — the data is not recoverable from here.
Scale the EtcdCluster instead. If you really mean it, annotate the member with
etcd-operator.cozystack.io/allow-deletion=true first."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: {{ include "etcd-operator.fullname" . }}-protect-members
labels:
{{- include "etcd-operator.labels" . | nindent 4 }}
spec:
policyName: {{ include "etcd-operator.fullname" . }}-protect-members
validationActions: ["Deny"]
Comment on lines +71 to +73

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.

{{- end }}
128 changes: 128 additions & 0 deletions charts/etcd-operator/tests/member_deletion_policy_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
suite: EtcdMember deletion is denied at admission

# The policy is the enforcement point for a contract the docs have always
# stated: EtcdMember objects belong to the operator. What matters in these
# assertions is not the YAML shape but who keeps the ability to delete —
# getting that list wrong either wedges cluster/namespace deletion (too
# strict) or leaves the hole open (too loose).

templates:
- member-deletion-policy.yaml

tests:
- it: is installed by default
asserts:
- hasDocuments:
count: 2
- containsDocument:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
name: RELEASE-NAME-etcd-operator-protect-members
documentIndex: 0
- containsDocument:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
name: RELEASE-NAME-etcd-operator-protect-members
documentIndex: 1
# The binding must name the policy it enforces — a typo here installs a
# policy that matches nothing and silently protects nothing.
- equal:
path: spec.policyName
value: RELEASE-NAME-etcd-operator-protect-members
documentIndex: 1

- it: matches only DELETE on etcdmembers
documentSelector:
path: kind
value: ValidatingAdmissionPolicy
asserts:
- equal:
path: spec.matchConstraints.resourceRules[0].operations
value: ["DELETE"]
- equal:
path: spec.matchConstraints.resourceRules[0].resources
value: ["etcdmembers"]
- equal:
path: spec.matchConstraints.resourceRules[0].apiGroups
value: ["etcd-operator.cozystack.io"]

- it: denies rather than warns, and fails closed
asserts:
- equal:
path: spec.validationActions
value: ["Deny"]
documentSelector:
path: kind
value: ValidatingAdmissionPolicyBinding
- equal:
path: spec.failurePolicy
value: Fail
documentSelector:
path: kind
value: ValidatingAdmissionPolicy

# The operator deletes members itself on scale-down and crash-loop
# replacement; the GC has to cascade from a deleted EtcdCluster; the
# namespace controller has to finish a namespace deletion. Denying any of
# the three turns a routine operation into a wedge.
- it: still allows the operator, the garbage collector and the namespace controller
documentSelector:
path: kind
value: ValidatingAdmissionPolicy
asserts:
- matchRegex:
path: spec.validations[0].expression
pattern: system:serviceaccount:NAMESPACE:RELEASE-NAME-etcd-operator
- matchRegex:
path: spec.validations[0].expression
pattern: system:serviceaccount:kube-system:generic-garbage-collector
- matchRegex:
path: spec.validations[0].expression
pattern: system:serviceaccount:kube-system:namespace-controller

- it: honours the break-glass annotation
documentSelector:
path: kind
value: ValidatingAdmissionPolicy
asserts:
- matchRegex:
path: spec.validations[0].expression
pattern: etcd-operator\.cozystack\.io/allow-deletion

# A platform whose own controller legitimately reaps members needs a way in
# that does not mean disabling the guard outright.
- it: accepts additional allowed users
set:
memberDeletionProtection:
additionalAllowedUsers:
- system:serviceaccount:platform:reaper
documentSelector:
path: kind
value: ValidatingAdmissionPolicy
asserts:
- matchRegex:
path: spec.validations[0].expression
pattern: system:serviceaccount:platform:reaper

# Kubernetes below 1.30 has no ValidatingAdmissionPolicy; the chart must
# still install there.
- it: can be switched off for older apiservers
set:
memberDeletionProtection:
enabled: false
asserts:
- hasDocuments:
count: 0

- it: names the ServiceAccount the release actually uses
set:
serviceAccount:
create: false
name: byo-sa
documentSelector:
path: kind
value: ValidatingAdmissionPolicy
asserts:
- matchRegex:
path: spec.validations[0].expression
pattern: system:serviceaccount:NAMESPACE:byo-sa
22 changes: 22 additions & 0 deletions charts/etcd-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ crds:
# EtcdCluster and its data.
keep: true

# Deny DELETE on EtcdMember objects at the API boundary, except for the
# operator itself, the garbage collector (so a deleted EtcdCluster still
# cascades) and the namespace controller (so namespace deletion does not hang).
#
# Deleting an EtcdMember by hand is not a recoverable mistake: its data PVC is
# controller-owned by the member, so the volume goes with it, while the
# member's finalizer removes the member from etcd on the way out. The docs have
# always said these objects are the operator's; this enforces it.
#
# Break-glass without uninstalling: annotate the member with
# etcd-operator.cozystack.io/allow-deletion=true, then delete it.
#
# Requires Kubernetes 1.30+ (ValidatingAdmissionPolicy GA). Set to false on
# older apiservers.
memberDeletionProtection:
# -- Install the ValidatingAdmissionPolicy protecting EtcdMember objects.
enabled: true
# -- Extra usernames allowed to delete EtcdMembers, in apiserver form
# (e.g. "system:serviceaccount:<ns>:<sa>" or a user name). For platforms
# whose own controllers legitimately reap these objects.
additionalAllowedUsers: []

# -- Render a Namespace object. Off by default (real `helm install` uses
# --create-namespace); build-dist-manifests turns it on so the rendered
# kubectl-apply manifest is self-contained.
Expand Down
27 changes: 26 additions & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,37 @@ Two custom resources, one of them user-facing.

**`EtcdCluster`** — the user-facing object. It captures cluster-wide intent: replica count, etcd version, per-member storage size, a progress deadline. This is the only resource users normally touch.

**`EtcdMember`** — one per etcd member. Created and deleted by the cluster controller. Each `EtcdMember` owns its Pod and PVC. Users should not create or edit these directly.
**`EtcdMember`** — one per etcd member. Created and deleted by the cluster controller. Each `EtcdMember` owns its Pod and PVC. Users should not create, edit or **delete** these directly.

Deleting one by hand is not a recoverable mistake: the member's PVC is controller-owned by it, so the data volume is removed with the CR (and on a `Delete`-reclaim StorageClass the data itself), while the member's finalizer removes the member from etcd on the way out. Delete every member of a cluster and it dismembers itself, leaving nothing to restore from. Scale the `EtcdCluster` instead — see [member deletion is denied at admission](#member-deletion-is-denied-at-admission).

There is **no StatefulSet**. Each member's Pod and PVC are reconciled independently by the member controller. The motivation is protocol awareness: scale-up adds a member as a learner first and only promotes once it's caught up; scale-down runs `MemberRemove` via a finalizer before reclaiming the Pod; pod restarts reuse the existing data dir and rejoin with the same etcd-side member ID. None of these flows fit StatefulSet's "all replicas are one fungible workload" model.

The cluster controller decides *which* members exist and orchestrates the etcd-side state machine (`MemberAddAsLearner` / `MemberPromote` / `MemberRemove`). The member controller decides *how* a member becomes real — Pod, PVC, etcd flags — and reports observed facts (member ID, readiness) back up to its CR's status.

## Member deletion is denied at admission

The chart installs a `ValidatingAdmissionPolicy` that rejects `DELETE` on `etcdmembers` for everyone except:

- the operator's own 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;
- anything listed in `memberDeletionProtection.additionalAllowedUsers`, for platforms whose own controllers legitimately reap these objects.

The rejection message says what would have happened and how to proceed deliberately. Break-glass without uninstalling the policy:

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

The guard exists because the accident it prevents is unrecoverable and easy: one `kubectl delete`, a GitOps prune of an unexpected object, a cleanup script sweeping CRs by label. The controllers cannot make that survivable — a member's data volume is bound to its identity, and a replacement member gets a fresh name and UID — so the event is stopped at the boundary instead.

Requires Kubernetes 1.30+ (`ValidatingAdmissionPolicy` GA). On older apiservers, install with `memberDeletionProtection.enabled=false`; the operator behaves as before, without the guard.

**Uninstalling:** remove the policy before removing the CRDs. `helm uninstall` does this in the right order, but a manual teardown that deletes the CRDs first will find member deletion denied and stall.

## Member naming

`EtcdMember` CRs are created with `ObjectMeta.GenerateName="<cluster>-"`. Each member's name is an apiserver-assigned random suffix (e.g. `mycluster-7xq2k`). Names are not predictable, and that is deliberate — the previous design used `<cluster>-<ordinal>` and tied cluster identity to ordinal reuse across incarnations, which is exactly the trap to avoid for stateful systems. Now:
Expand Down
7 changes: 7 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ kubectl delete etcdcluster.etcd-operator.cozystack.io --all -A
# intentionally left in place.
make undeploy

# Remove the member-deletion guard before the CRDs. Removing the CRD makes the
# apiserver delete every EtcdMember, and the policy would deny those requests —
# leaving the CRD stuck in Terminating. `helm uninstall` above already removed
# it; this is for teardowns that skipped Helm:
kubectl delete validatingadmissionpolicybinding etcd-operator-protect-members --ignore-not-found
kubectl delete validatingadmissionpolicy etcd-operator-protect-members --ignore-not-found

# Remove the CRDs too (only after all EtcdClusters are gone) — deleting them
# cascade-deletes every remaining EtcdCluster:
kubectl delete crd etcdclusters.etcd-operator.cozystack.io \
Expand Down
10 changes: 8 additions & 2 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,20 @@ Manual recovery:
```sh
# 1. Identify the broken member.
kubectl get etcdmember.etcd-operator.cozystack.io -n <ns>
# 2. Delete it — the finalizer runs MemberRemove against peers, then GC takes
# 2. Deleting a member is denied by default (see concepts: member deletion is
# denied at admission) — this is the deliberate exception, so unlock it:
kubectl annotate etcdmember.etcd-operator.cozystack.io <broken-member> -n <ns> \
etcd-operator.cozystack.io/allow-deletion=true
# 3. Delete it — the finalizer runs MemberRemove against peers, then GC takes
# the Pod and PVC. Quorum holds because we remove before adding.
kubectl delete etcdmember.etcd-operator.cozystack.io <broken-member> -n <ns>
# 3. The cluster controller's next reconcile observes current < desired and
# 4. The cluster controller's next reconcile observes current < desired and
# scales up automatically — a new member is added with GenerateName and
# fresh storage.
```

The annotation step is the point of the guard: this recovery discards a data volume on purpose, and typing that out is what separates it from the same command issued by accident.

This sequence preserves quorum if you have an odd number of voters and only one is broken. If multiple voters are broken simultaneously, quorum is lost and you can't `MemberRemove` cleanly. In that case the recovery is to delete the EtcdCluster, recreate it, and restore from a snapshot — see [Restoring a cluster from a snapshot](#restoring-a-cluster-from-a-snapshot). Snapshots only exist if you have been taking `EtcdSnapshot`s, so set that up *before* you need it.

## Taking a snapshot
Expand Down
17 changes: 16 additions & 1 deletion test/e2e/kamaji_datastore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,22 @@ func TestKamajiDataStore(t *testing.T) {
t.Logf("original members: %v", original)
for _, victim := range original {
t.Logf("deleting EtcdMember %q (operator does MemberRemove + a GenerateName replacement)", victim)
m := &etcdv1alpha2.EtcdMember{ObjectMeta: metav1.ObjectMeta{Namespace: e2eNamespace, Name: victim}}
// Member deletion is denied at admission — the guard exists precisely to
// stop this command when it is issued by accident. This test issues it on
// purpose, so it takes the documented break-glass path: annotate first.
// Doing it here rather than exempting the test's identity in the policy
// keeps the e2e honest about what a human has to do.
m := &etcdv1alpha2.EtcdMember{}
if err := kube.Get(ctx, client.ObjectKey{Namespace: e2eNamespace, Name: victim}, m); err != nil {
t.Fatalf("get member %s before deletion: %v", victim, err)
}
if m.Annotations == nil {
m.Annotations = map[string]string{}
}
m.Annotations["etcd-operator.cozystack.io/allow-deletion"] = "true"
if err := kube.Update(ctx, m); err != nil {
t.Fatalf("annotate member %s for deletion: %v", victim, err)
}
if err := kube.Delete(ctx, m); err != nil && !apierrors.IsNotFound(err) {
t.Fatalf("delete member %s: %v", victim, err)
}
Expand Down
Loading