diff --git a/.changes/unreleased/operator-Fixed-20260326-000000.yaml b/.changes/unreleased/operator-Fixed-20260326-000000.yaml new file mode 100644 index 000000000..b93ac343b --- /dev/null +++ b/.changes/unreleased/operator-Fixed-20260326-000000.yaml @@ -0,0 +1,4 @@ +project: operator +kind: Fixed +body: Orphaned CRs with finalizers could block namespace deletion when the operator was uninstalled while CRs still existed. An opt-in post-delete helm hook (`finalizerRemoval.enabled=true`) now strips operator finalizers from all managed CRs after uninstall, ensuring the operator is no longer running and cannot re-add finalizers. +time: 2026-03-26T00:00:00.000000-05:00 diff --git a/operator/chart/README.md b/operator/chart/README.md index f48c44479..a10f67fe8 100644 --- a/operator/chart/README.md +++ b/operator/chart/README.md @@ -160,6 +160,18 @@ Secret name and key where the license key is stored. **Default:** `{"key":""}` +### [finalizerRemoval](https://artifacthub.io/packages/helm/redpanda-data/operator?modal=values&path=finalizerRemoval) + +Opt-in post-delete job that removes finalizers from all operator-managed CRs after uninstall, preventing orphaned resources from blocking namespace deletion. + +**Default:** `{"enabled":false}` + +### [finalizerRemoval.enabled](https://artifacthub.io/packages/helm/redpanda-data/operator?modal=values&path=finalizerRemoval.enabled) + +Enables the post-delete finalizer removal job. + +**Default:** `false` + ### [fullnameOverride](https://artifacthub.io/packages/helm/redpanda-data/operator?modal=values&path=fullnameOverride) Overrides the `redpanda-operator.fullname` template. diff --git a/operator/chart/chart.go b/operator/chart/chart.go index 0aff44291..01884de16 100644 --- a/operator/chart/chart.go +++ b/operator/chart/chart.go @@ -63,6 +63,8 @@ func render(dot *helmette.Dot) []kube.Object { CRDJobServiceAccount(dot), PostUpgradeMigrationJob(dot), MigrationJobServiceAccount(dot), + PostDeleteFinalizerRemovalJob(dot), + PostDeleteFinalizerRemovalJobServiceAccount(dot), } for _, svc := range StretchClusterService(dot) { diff --git a/operator/chart/post_delete_finalizer_removal_job.go b/operator/chart/post_delete_finalizer_removal_job.go new file mode 100644 index 000000000..2728e4f5c --- /dev/null +++ b/operator/chart/post_delete_finalizer_removal_job.go @@ -0,0 +1,87 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +// +gotohelm:filename=_post-delete-finalizer-removal-job.go.tpl +package operator + +import ( + "fmt" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/redpanda-data/redpanda-operator/gotohelm/helmette" +) + +// PostDeleteFinalizerRemovalJob is a post-delete hook job that removes +// finalizers from all operator-managed CRs after uninstall. Running +// post-delete ensures the operator is already gone before finalizers are +// stripped, eliminating any race where the controller could re-add them. +func PostDeleteFinalizerRemovalJob(dot *helmette.Dot) *batchv1.Job { + values := helmette.Unwrap[Values](dot.Values) + + if !values.FinalizerRemoval.Enabled { + return nil + } + + return &batchv1.Job{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "batch/v1", + Kind: "Job", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-finalizer-removal", Fullname(dot)), + Namespace: dot.Release.Namespace, + Labels: helmette.Merge(Labels(dot)), + Annotations: map[string]string{ + "helm.sh/hook": "post-delete", + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded,hook-failed", + "helm.sh/hook-weight": "-5", + }, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: values.PodAnnotations, + Labels: helmette.Merge(SelectorLabels(dot), values.PodLabels), + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + AutomountServiceAccountToken: ptr.To(false), + TerminationGracePeriodSeconds: ptr.To(int64(10)), + ImagePullSecrets: values.ImagePullSecrets, + ServiceAccountName: PostDeleteFinalizerRemovalJobServiceAccountName(dot), + NodeSelector: values.NodeSelector, + Tolerations: values.Tolerations, + Volumes: []corev1.Volume{serviceAccountTokenVolume()}, + Containers: finalizerRemovalJobContainers(dot), + }, + }, + }, + } +} + +func finalizerRemovalJobContainers(dot *helmette.Dot) []corev1.Container { + values := helmette.Unwrap[Values](dot.Values) + + return []corev1.Container{ + { + Name: "finalizer-removal", + Image: containerImage(dot), + ImagePullPolicy: values.Image.PullPolicy, + Command: []string{"/redpanda-operator"}, + Args: []string{"finalizer-removal"}, + SecurityContext: &corev1.SecurityContext{AllowPrivilegeEscalation: ptr.To(false)}, + VolumeMounts: []corev1.VolumeMount{serviceAccountTokenVolumeMount()}, + Resources: values.Resources, + }, + } +} diff --git a/operator/chart/rbac.go b/operator/chart/rbac.go index 5031b11bf..2892e173c 100644 --- a/operator/chart/rbac.go +++ b/operator/chart/rbac.go @@ -92,6 +92,20 @@ func rbacBundles(dot *helmette.Dot) []RBACBundle { RuleFiles: bundles[0].RuleFiles, }) + // the finalizer removal job needs the same general RBAC policy as the operator itself + // (v2-manager.ClusterRole.yaml already covers list+patch on all CR finalizers) + bundles = append(bundles, RBACBundle{ + Name: PostDeleteFinalizerRemovalJobServiceAccountName(dot), + Enabled: values.FinalizerRemoval.Enabled, + Subject: PostDeleteFinalizerRemovalJobServiceAccountName(dot), + Annotations: map[string]string{ + "helm.sh/hook": "post-delete", + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded,hook-failed", + "helm.sh/hook-weight": "-10", + }, + RuleFiles: bundles[0].RuleFiles, + }) + return bundles } diff --git a/operator/chart/serviceaccount.go b/operator/chart/serviceaccount.go index ced813521..3a9240ad3 100644 --- a/operator/chart/serviceaccount.go +++ b/operator/chart/serviceaccount.go @@ -32,6 +32,10 @@ func MigrationJobServiceAccountName(dot *helmette.Dot) string { return ServiceAccountName(dot) + "-migration-job" } +func PostDeleteFinalizerRemovalJobServiceAccountName(dot *helmette.Dot) string { + return ServiceAccountName(dot) + "-finalizer-removal-job" +} + func ServiceAccount(dot *helmette.Dot) *corev1.ServiceAccount { values := helmette.Unwrap[Values](dot.Values) @@ -88,6 +92,40 @@ func CRDJobServiceAccount(dot *helmette.Dot) *corev1.ServiceAccount { } } +// PostDeleteFinalizerRemovalJobServiceAccount returns a ServiceAccount used by +// [PostDeleteFinalizerRemovalJob]. Helm will delete it after the job completes. +func PostDeleteFinalizerRemovalJobServiceAccount(dot *helmette.Dot) *corev1.ServiceAccount { + values := helmette.Unwrap[Values](dot.Values) + + if !values.FinalizerRemoval.Enabled { + return nil + } + + return &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: PostDeleteFinalizerRemovalJobServiceAccountName(dot), + Labels: Labels(dot), + Namespace: dot.Release.Namespace, + Annotations: helmette.Merge( + helmette.Default( + map[string]string{}, + values.ServiceAccount.Annotations, + ), + map[string]string{ + "helm.sh/hook": "post-delete", + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded,hook-failed", + "helm.sh/hook-weight": "-10", + }, + ), + }, + AutomountServiceAccountToken: ptr.To(false), + } +} + // MigrationJobServiceAccount returns a ServiceAccount that's used by // [PostUpgradeMigrationJob]. Helm will delete it after the job succeeds. func MigrationJobServiceAccount(dot *helmette.Dot) *corev1.ServiceAccount { diff --git a/operator/chart/templates/_chart.go.tpl b/operator/chart/templates/_chart.go.tpl index c247802ff..364220bf2 100644 --- a/operator/chart/templates/_chart.go.tpl +++ b/operator/chart/templates/_chart.go.tpl @@ -5,7 +5,7 @@ {{- $dot := (index .a 0) -}} {{- range $_ := (list 1) -}} {{- $_is_returning := false -}} -{{- $manifests := (list (get (fromJson (include "operator.Issuer" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.Certificate" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ConfigMap" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.MetricsService" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.WebhookService" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.MutatingWebhookConfiguration" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ValidatingWebhookConfiguration" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ServiceAccount" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ServiceMonitor" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.Deployment" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.PreInstallCRDJob" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.CRDJobServiceAccount" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.PostUpgradeMigrationJob" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.MigrationJobServiceAccount" (dict "a" (list $dot)))) "r")) -}} +{{- $manifests := (list (get (fromJson (include "operator.Issuer" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.Certificate" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ConfigMap" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.MetricsService" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.WebhookService" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.MutatingWebhookConfiguration" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ValidatingWebhookConfiguration" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ServiceAccount" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.ServiceMonitor" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.Deployment" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.PreInstallCRDJob" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.CRDJobServiceAccount" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.PostUpgradeMigrationJob" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.MigrationJobServiceAccount" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.PostDeleteFinalizerRemovalJob" (dict "a" (list $dot)))) "r") (get (fromJson (include "operator.PostDeleteFinalizerRemovalJobServiceAccount" (dict "a" (list $dot)))) "r")) -}} {{- range $_, $svc := (get (fromJson (include "operator.StretchClusterService" (dict "a" (list $dot)))) "r") -}} {{- $manifests = (concat (default (list) $manifests) (list $svc)) -}} {{- end -}} diff --git a/operator/chart/templates/_post-delete-finalizer-removal-job.go.tpl b/operator/chart/templates/_post-delete-finalizer-removal-job.go.tpl new file mode 100644 index 000000000..eab65b6b4 --- /dev/null +++ b/operator/chart/templates/_post-delete-finalizer-removal-job.go.tpl @@ -0,0 +1,30 @@ +{{- /* GENERATED FILE DO NOT EDIT */ -}} +{{- /* Transpiled by gotohelm from "github.com/redpanda-data/redpanda-operator/operator/chart/post_delete_finalizer_removal_job.go" */ -}} + +{{- define "operator.PostDeleteFinalizerRemovalJob" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $values := $dot.Values.AsMap -}} +{{- if (not $values.finalizerRemoval.enabled) -}} +{{- $_is_returning = true -}} +{{- (dict "r" (coalesce nil)) | toJson -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" (mustMergeOverwrite (dict "metadata" (dict) "spec" (dict "template" (dict "metadata" (dict) "spec" (dict "containers" (coalesce nil)))) "status" (dict)) (mustMergeOverwrite (dict) (dict "apiVersion" "batch/v1" "kind" "Job")) (dict "metadata" (mustMergeOverwrite (dict) (dict "name" (printf "%s-finalizer-removal" (get (fromJson (include "operator.Fullname" (dict "a" (list $dot)))) "r")) "namespace" $dot.Release.Namespace "labels" (merge (dict) (get (fromJson (include "operator.Labels" (dict "a" (list $dot)))) "r")) "annotations" (dict "helm.sh/hook" "post-delete" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded,hook-failed" "helm.sh/hook-weight" "-5"))) "spec" (mustMergeOverwrite (dict "template" (dict "metadata" (dict) "spec" (dict "containers" (coalesce nil)))) (dict "template" (mustMergeOverwrite (dict "metadata" (dict) "spec" (dict "containers" (coalesce nil))) (dict "metadata" (mustMergeOverwrite (dict) (dict "annotations" $values.podAnnotations "labels" (merge (dict) (get (fromJson (include "operator.SelectorLabels" (dict "a" (list $dot)))) "r") $values.podLabels))) "spec" (mustMergeOverwrite (dict "containers" (coalesce nil)) (dict "restartPolicy" "OnFailure" "automountServiceAccountToken" false "terminationGracePeriodSeconds" ((10 | int64) | int64) "imagePullSecrets" $values.imagePullSecrets "serviceAccountName" (get (fromJson (include "operator.PostDeleteFinalizerRemovalJobServiceAccountName" (dict "a" (list $dot)))) "r") "nodeSelector" $values.nodeSelector "tolerations" $values.tolerations "volumes" (list (get (fromJson (include "operator.serviceAccountTokenVolume" (dict "a" (list)))) "r")) "containers" (get (fromJson (include "operator.finalizerRemovalJobContainers" (dict "a" (list $dot)))) "r")))))))))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "operator.finalizerRemovalJobContainers" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $values := $dot.Values.AsMap -}} +{{- $_is_returning = true -}} +{{- (dict "r" (list (mustMergeOverwrite (dict "name" "" "resources" (dict)) (dict "name" "finalizer-removal" "image" (get (fromJson (include "operator.containerImage" (dict "a" (list $dot)))) "r") "imagePullPolicy" $values.image.pullPolicy "command" (list "/redpanda-operator") "args" (list "finalizer-removal") "securityContext" (mustMergeOverwrite (dict) (dict "allowPrivilegeEscalation" false)) "volumeMounts" (list (get (fromJson (include "operator.serviceAccountTokenVolumeMount" (dict "a" (list)))) "r")) "resources" $values.resources)))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/operator/chart/templates/_rbac.go.tpl b/operator/chart/templates/_rbac.go.tpl index 9cde9d0e6..5dc161002 100644 --- a/operator/chart/templates/_rbac.go.tpl +++ b/operator/chart/templates/_rbac.go.tpl @@ -8,6 +8,7 @@ {{- $values := $dot.Values.AsMap -}} {{- $bundles := (list (mustMergeOverwrite (dict "Enabled" false "Name" "" "Subject" "" "RuleFiles" (coalesce nil) "Annotations" (coalesce nil)) (dict "Name" (get (fromJson (include "operator.Fullname" (dict "a" (list $dot)))) "r") "Enabled" true "Subject" (get (fromJson (include "operator.ServiceAccountName" (dict "a" (list $dot)))) "r") "RuleFiles" (dict "files/rbac/console.ClusterRole.yaml" true "files/rbac/leader-election.ClusterRole.yaml" true "files/rbac/leader-election.Role.yaml" true "files/rbac/pvcunbinder.ClusterRole.yaml" true "files/rbac/pvcunbinder.Role.yaml" true "files/rbac/rack-awareness.ClusterRole.yaml" true "files/rbac/rpk-debug-bundle.Role.yaml" true "files/rbac/sidecar.Role.yaml" true "files/rbac/v1-manager.ClusterRole.yaml" $values.vectorizedControllers.enabled "files/rbac/v1-manager.Role.yaml" $values.vectorizedControllers.enabled "files/rbac/v2-manager.ClusterRole.yaml" true "files/rbac/multicluster-manager.ClusterRole.yaml" $values.multicluster.enabled))) (mustMergeOverwrite (dict "Enabled" false "Name" "" "Subject" "" "RuleFiles" (coalesce nil) "Annotations" (coalesce nil)) (dict "Name" (get (fromJson (include "operator.cleanForK8sWithSuffix" (dict "a" (list (get (fromJson (include "operator.Fullname" (dict "a" (list $dot)))) "r") "additional-controllers")))) "r") "Enabled" $values.rbac.createAdditionalControllerCRs "Subject" (get (fromJson (include "operator.ServiceAccountName" (dict "a" (list $dot)))) "r") "RuleFiles" (dict "files/rbac/decommission.ClusterRole.yaml" true "files/rbac/decommission.Role.yaml" true "files/rbac/node-watcher.ClusterRole.yaml" true "files/rbac/node-watcher.Role.yaml" true "files/rbac/old-decommission.ClusterRole.yaml" true "files/rbac/old-decommission.Role.yaml" true "files/rbac/pvcunbinder.ClusterRole.yaml" true "files/rbac/pvcunbinder.Role.yaml" true))) (mustMergeOverwrite (dict "Enabled" false "Name" "" "Subject" "" "RuleFiles" (coalesce nil) "Annotations" (coalesce nil)) (dict "Name" (get (fromJson (include "operator.CRDJobServiceAccountName" (dict "a" (list $dot)))) "r") "Enabled" (or $values.crds.enabled $values.crds.experimental) "Subject" (get (fromJson (include "operator.CRDJobServiceAccountName" (dict "a" (list $dot)))) "r") "Annotations" (dict "helm.sh/hook" "pre-install,pre-upgrade" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded,hook-failed" "helm.sh/hook-weight" "-10") "RuleFiles" (dict "files/rbac/crd-installation.ClusterRole.yaml" true)))) -}} {{- $bundles = (concat (default (list) $bundles) (list (mustMergeOverwrite (dict "Enabled" false "Name" "" "Subject" "" "RuleFiles" (coalesce nil) "Annotations" (coalesce nil)) (dict "Name" (get (fromJson (include "operator.MigrationJobServiceAccountName" (dict "a" (list $dot)))) "r") "Enabled" true "Subject" (get (fromJson (include "operator.MigrationJobServiceAccountName" (dict "a" (list $dot)))) "r") "Annotations" (dict "helm.sh/hook" "post-upgrade" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded,hook-failed" "helm.sh/hook-weight" "-10") "RuleFiles" (index $bundles (0 | int)).RuleFiles)))) -}} +{{- $bundles = (concat (default (list) $bundles) (list (mustMergeOverwrite (dict "Enabled" false "Name" "" "Subject" "" "RuleFiles" (coalesce nil) "Annotations" (coalesce nil)) (dict "Name" (get (fromJson (include "operator.PostDeleteFinalizerRemovalJobServiceAccountName" (dict "a" (list $dot)))) "r") "Enabled" $values.finalizerRemoval.enabled "Subject" (get (fromJson (include "operator.PostDeleteFinalizerRemovalJobServiceAccountName" (dict "a" (list $dot)))) "r") "Annotations" (dict "helm.sh/hook" "post-delete" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded,hook-failed" "helm.sh/hook-weight" "-10") "RuleFiles" (index $bundles (0 | int)).RuleFiles)))) -}} {{- $_is_returning = true -}} {{- (dict "r" $bundles) | toJson -}} {{- break -}} diff --git a/operator/chart/templates/_serviceaccount.go.tpl b/operator/chart/templates/_serviceaccount.go.tpl index caebddf39..4e61910dc 100644 --- a/operator/chart/templates/_serviceaccount.go.tpl +++ b/operator/chart/templates/_serviceaccount.go.tpl @@ -32,6 +32,16 @@ {{- end -}} {{- end -}} +{{- define "operator.PostDeleteFinalizerRemovalJobServiceAccountName" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $_is_returning = true -}} +{{- (dict "r" (printf "%s%s" (get (fromJson (include "operator.ServiceAccountName" (dict "a" (list $dot)))) "r") "-finalizer-removal-job")) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + {{- define "operator.ServiceAccount" -}} {{- $dot := (index .a 0) -}} {{- range $_ := (list 1) -}} @@ -64,6 +74,22 @@ {{- end -}} {{- end -}} +{{- define "operator.PostDeleteFinalizerRemovalJobServiceAccount" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $_is_returning := false -}} +{{- $values := $dot.Values.AsMap -}} +{{- if (not $values.finalizerRemoval.enabled) -}} +{{- $_is_returning = true -}} +{{- (dict "r" (coalesce nil)) | toJson -}} +{{- break -}} +{{- end -}} +{{- $_is_returning = true -}} +{{- (dict "r" (mustMergeOverwrite (dict "metadata" (dict)) (mustMergeOverwrite (dict) (dict "kind" "ServiceAccount" "apiVersion" "v1")) (dict "metadata" (mustMergeOverwrite (dict) (dict "name" (get (fromJson (include "operator.PostDeleteFinalizerRemovalJobServiceAccountName" (dict "a" (list $dot)))) "r") "labels" (get (fromJson (include "operator.Labels" (dict "a" (list $dot)))) "r") "namespace" $dot.Release.Namespace "annotations" (merge (dict) (default (dict) $values.serviceAccount.annotations) (dict "helm.sh/hook" "post-delete" "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded,hook-failed" "helm.sh/hook-weight" "-10")))) "automountServiceAccountToken" false))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + {{- define "operator.MigrationJobServiceAccount" -}} {{- $dot := (index .a 0) -}} {{- range $_ := (list 1) -}} diff --git a/operator/chart/testdata/template-cases.golden.txtar b/operator/chart/testdata/template-cases.golden.txtar index 2e6d6f1d9..29069e1a4 100644 --- a/operator/chart/testdata/template-cases.golden.txtar +++ b/operator/chart/testdata/template-cases.golden.txtar @@ -81482,6 +81482,1933 @@ spec: apiVersion: v1 fieldPath: metadata.namespace path: namespace +-- testdata/finalizer-removal-enabled.yaml.golden -- +--- +# Source: operator/templates/entry-point.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: null + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: v1 +data: + controller_manager_config.yaml: |- + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + health: + healthProbeBindAddress: :8081 + kind: ControllerManagerConfig + leaderElection: + leaderElect: true + resourceName: aa9fc693.vectorized.io + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 +kind: ConfigMap +metadata: + annotations: null + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-config + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: null + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-default-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-default +rules: +- apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles/status + verbs: + - get + - patch + - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - pods + verbs: + - delete + - get + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - events + - limitranges + - persistentvolumeclaims + - pods + - pods/log + - replicationcontrollers + - resourcequotas + - serviceaccounts + - services + verbs: + - get + - list +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - pods + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - controllerrevisions + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cert-manager.io + resources: + - certificates + - issuers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles + - nodepools + - redpandas + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - groups + - redpandaroles + - schemas + - shadowlinks + - stretchclusters + - topics + - users + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - groups/finalizers + - nodepools/finalizers + - redpandaroles/finalizers + - redpandas/finalizers + - schemas/finalizers + - shadowlinks/finalizers + - stretchclusters/finalizers + - topics/finalizers + - users/finalizers + verbs: + - update +- apiGroups: + - cluster.redpanda.com + resources: + - groups/status + - nodepools/status + - redpandaroles/status + - redpandas/status + - schemas/status + - shadowlinks/status + - stretchclusters/status + - topics/status + - users/status + verbs: + - get + - patch + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - monitoring.coreos.com + resources: + - podmonitors + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - multicluster.x-k8s.io + resources: + - serviceexports + - serviceimports + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-additional-controllers-default +rules: +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - delete + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - configmaps + - nodes + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - delete + - get + - list + - patch + - update +- apiGroups: + - cluster.redpanda.com + resources: + - redpandas + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - delete + - get + - list + - patch + - update +- apiGroups: + - "" + resources: + - configmaps + - pods + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - statefulsets/status + verbs: + - patch + - update +- apiGroups: + - cluster.redpanda.com + resources: + - redpandas + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - pods + verbs: + - delete + - get + - list + - watch +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-default +subjects: +- kind: ServiceAccount + name: operator + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-additional-controllers-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-additional-controllers-default +subjects: +- kind: ServiceAccount + name: operator + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: v1 +kind: Service +metadata: + annotations: null + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-metrics-service + namespace: default +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + app.kubernetes.io/instance: operator + app.kubernetes.io/name: operator +--- +# Source: operator/templates/entry-point.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: null + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: operator + app.kubernetes.io/name: operator + strategy: + type: RollingUpdate + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/name: operator + spec: + automountServiceAccountToken: false + containers: + - args: + - --configurator-base-image=docker.redpanda.com/redpandadata/redpanda-operator + - --configurator-tag=v26.1.1 + - --enable-console=true + - --enable-vectorized-controllers=false + - --health-probe-bind-address=:8081 + - --leader-elect + - --log-level=info + - --metrics-bind-address=:8443 + - --webhook-enabled=false + command: + - /manager + env: [] + image: docker.redpanda.com/redpandadata/redpanda-operator:v26.1.1 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz/ + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + - containerPort: 8443 + name: https + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {} + securityContext: + allowPrivilegeEscalation: false + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access + readOnly: true + ephemeralContainers: null + imagePullSecrets: [] + initContainers: [] + nodeSelector: {} + securityContext: + runAsUser: 65532 + serviceAccountName: operator + terminationGracePeriodSeconds: 10 + tolerations: [] + volumes: + - name: kube-api-access + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace +--- +# Source: operator/templates/entry-point.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: + helm.sh/hook: post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-10" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-migration-job + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: + helm.sh/hook: post-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-10" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-finalizer-removal-job + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + helm.sh/hook: post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-10" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-migration-job-default +rules: +- apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles/status + verbs: + - get + - patch + - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - pods + verbs: + - delete + - get + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - events + - limitranges + - persistentvolumeclaims + - pods + - pods/log + - replicationcontrollers + - resourcequotas + - serviceaccounts + - services + verbs: + - get + - list +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - pods + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - controllerrevisions + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cert-manager.io + resources: + - certificates + - issuers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles + - nodepools + - redpandas + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - groups + - redpandaroles + - schemas + - shadowlinks + - stretchclusters + - topics + - users + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - groups/finalizers + - nodepools/finalizers + - redpandaroles/finalizers + - redpandas/finalizers + - schemas/finalizers + - shadowlinks/finalizers + - stretchclusters/finalizers + - topics/finalizers + - users/finalizers + verbs: + - update +- apiGroups: + - cluster.redpanda.com + resources: + - groups/status + - nodepools/status + - redpandaroles/status + - redpandas/status + - schemas/status + - shadowlinks/status + - stretchclusters/status + - topics/status + - users/status + verbs: + - get + - patch + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - monitoring.coreos.com + resources: + - podmonitors + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - multicluster.x-k8s.io + resources: + - serviceexports + - serviceimports + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + helm.sh/hook: post-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-10" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-finalizer-removal-job-default +rules: +- apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles/status + verbs: + - get + - patch + - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - pods + verbs: + - delete + - get + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - events + - limitranges + - persistentvolumeclaims + - pods + - pods/log + - replicationcontrollers + - resourcequotas + - serviceaccounts + - services + verbs: + - get + - list +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - pods + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - controllerrevisions + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cert-manager.io + resources: + - certificates + - issuers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - consoles + - nodepools + - redpandas + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - groups + - redpandaroles + - schemas + - shadowlinks + - stretchclusters + - topics + - users + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - cluster.redpanda.com + resources: + - groups/finalizers + - nodepools/finalizers + - redpandaroles/finalizers + - redpandas/finalizers + - schemas/finalizers + - shadowlinks/finalizers + - stretchclusters/finalizers + - topics/finalizers + - users/finalizers + verbs: + - update +- apiGroups: + - cluster.redpanda.com + resources: + - groups/status + - nodepools/status + - redpandaroles/status + - redpandas/status + - schemas/status + - shadowlinks/status + - stretchclusters/status + - topics/status + - users/status + verbs: + - get + - patch + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - monitoring.coreos.com + resources: + - podmonitors + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - multicluster.x-k8s.io + resources: + - serviceexports + - serviceimports + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + helm.sh/hook: post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-10" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-migration-job-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-migration-job-default +subjects: +- kind: ServiceAccount + name: operator-migration-job + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + helm.sh/hook: post-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-10" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-finalizer-removal-job-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-finalizer-removal-job-default +subjects: +- kind: ServiceAccount + name: operator-finalizer-removal-job + namespace: default +--- +# Source: operator/templates/entry-point.yaml +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + helm.sh/hook: post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-4" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-migration + namespace: default +spec: + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/name: operator + spec: + automountServiceAccountToken: false + containers: + - args: + - migration + command: + - /redpanda-operator + image: docker.redpanda.com/redpandadata/redpanda-operator:v26.1.1 + imagePullPolicy: IfNotPresent + name: migration + resources: {} + securityContext: + allowPrivilegeEscalation: false + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access + readOnly: true + imagePullSecrets: [] + nodeSelector: {} + restartPolicy: OnFailure + serviceAccountName: operator-migration-job + terminationGracePeriodSeconds: 10 + tolerations: [] + volumes: + - name: kube-api-access + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace +--- +# Source: operator/templates/entry-point.yaml +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + helm.sh/hook: post-delete + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded,hook-failed + helm.sh/hook-weight: "-5" + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: operator + app.kubernetes.io/version: v26.1.1 + helm.sh/chart: operator-26.1.1 + name: operator-finalizer-removal + namespace: default +spec: + template: + metadata: + annotations: {} + labels: + app.kubernetes.io/instance: operator + app.kubernetes.io/name: operator + spec: + automountServiceAccountToken: false + containers: + - args: + - finalizer-removal + command: + - /redpanda-operator + image: docker.redpanda.com/redpandadata/redpanda-operator:v26.1.1 + imagePullPolicy: IfNotPresent + name: finalizer-removal + resources: {} + securityContext: + allowPrivilegeEscalation: false + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access + readOnly: true + imagePullSecrets: [] + nodeSelector: {} + restartPolicy: OnFailure + serviceAccountName: operator-finalizer-removal-job + terminationGracePeriodSeconds: 10 + tolerations: [] + volumes: + - name: kube-api-access + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace -- testdata/license-with-key.yaml.golden -- --- # Source: operator/templates/entry-point.yaml diff --git a/operator/chart/testdata/template-cases.txtar b/operator/chart/testdata/template-cases.txtar index 599602f33..424143a0d 100644 --- a/operator/chart/testdata/template-cases.txtar +++ b/operator/chart/testdata/template-cases.txtar @@ -136,3 +136,7 @@ multicluster: enterprise: licenseSecretRef: name: my-secret + +-- finalizer-removal-enabled -- +finalizerRemoval: + enabled: true diff --git a/operator/chart/values.go b/operator/chart/values.go index 9f5b8832c..07a57bacd 100644 --- a/operator/chart/values.go +++ b/operator/chart/values.go @@ -75,6 +75,7 @@ type Values struct { CRDs CRDs `json:"crds"` VectorizedControllers VectorizedControllers `json:"vectorizedControllers"` Multicluster Multicluster `json:"multicluster"` + FinalizerRemoval FinalizerRemoval `json:"finalizerRemoval"` } type VectorizedControllers struct { @@ -86,6 +87,10 @@ type CRDs struct { Experimental bool `json:"experimental"` } +type FinalizerRemoval struct { + Enabled bool `json:"enabled"` +} + type PodTemplateSpec struct { Metadata Metadata `json:"metadata,omitempty"` Spec corev1.PodSpec `json:"spec,omitempty" jsonschema:"required"` diff --git a/operator/chart/values.schema.json b/operator/chart/values.schema.json index 02062f8b1..75392ffc3 100644 --- a/operator/chart/values.schema.json +++ b/operator/chart/values.schema.json @@ -701,6 +701,15 @@ }, "type": "object" }, + "finalizerRemoval": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, "fullnameOverride": { "type": "string" }, diff --git a/operator/chart/values.yaml b/operator/chart/values.yaml index 3e92bb877..1e4306555 100644 --- a/operator/chart/values.yaml +++ b/operator/chart/values.yaml @@ -203,3 +203,9 @@ multicluster: # address: ip/dns of operator host peers: [] servicePerOperatorDeployment: false + +# -- Opt-in post-delete job that removes finalizers from all operator-managed +# CRs after uninstall, preventing orphaned resources from blocking namespace deletion. +finalizerRemoval: + # -- Enables the post-delete finalizer removal job. + enabled: false diff --git a/operator/chart/values_partial.gen.go b/operator/chart/values_partial.gen.go index fa10c0110..3110d1474 100644 --- a/operator/chart/values_partial.gen.go +++ b/operator/chart/values_partial.gen.go @@ -49,6 +49,7 @@ type PartialValues struct { CRDs *PartialCRDs "json:\"crds,omitempty\"" VectorizedControllers *PartialVectorizedControllers "json:\"vectorizedControllers,omitempty\"" Multicluster *PartialMulticluster "json:\"multicluster,omitempty\"" + FinalizerRemoval *PartialFinalizerRemoval "json:\"finalizerRemoval,omitempty\"" } type PartialImage struct { @@ -103,6 +104,10 @@ type PartialMulticluster struct { Peers []PartialPeer "json:\"peers,omitempty\"" } +type PartialFinalizerRemoval struct { + Enabled *bool "json:\"enabled,omitempty\"" +} + type PartialEnterprise struct { LicenseSecretRef *corev1.SecretKeySelector "json:\"licenseSecretRef,omitempty\"" } diff --git a/operator/cmd/finalizerremoval/finalizer_removal.go b/operator/cmd/finalizerremoval/finalizer_removal.go new file mode 100644 index 000000000..46b3ef512 --- /dev/null +++ b/operator/cmd/finalizerremoval/finalizer_removal.go @@ -0,0 +1,107 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +// Package finalizerremoval contains a post-delete job that removes finalizers +// from all operator-managed CRs to allow clean uninstall of the operator. +package finalizerremoval + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + crds "github.com/redpanda-data/redpanda-operator/operator/config/crd/bases" + "github.com/redpanda-data/redpanda-operator/operator/internal/controller" +) + +const finalizerKey = "operator.redpanda.com/finalizer" + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "finalizer-removal", + Short: "Remove finalizers from operator-managed CRs to allow clean uninstall", + Run: func(cmd *cobra.Command, args []string) { + run(cmd.Context()) + }, + } +} + +func run(ctx context.Context) { + log.Printf("Removing finalizers from operator-managed CRs") + + k8sClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: controller.UnifiedScheme}) + if err != nil { + log.Fatalf("%s", fmt.Errorf("unable to create client: %w", err)) + } + + // Derive GVKs from the embedded CRD definitions. This automatically picks + // up any new types added in future without requiring changes to this command + // and avoids iterating over primitive Kubernetes types in the scheme. + var gvks []schema.GroupVersionKind + for _, crd := range crds.All() { + for _, v := range crd.Spec.Versions { + gvks = append(gvks, schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: v.Name, + Kind: crd.Spec.Names.Kind, + }) + } + } + + var errs []error + for _, gvk := range gvks { + errs = append(errs, removeFinalizersForGVK(ctx, k8sClient, gvk)) + } + + if err := errors.Join(errs...); err != nil { + log.Fatalf("%s", fmt.Errorf("errors while removing finalizers: %w", err)) + } + + log.Printf("Finalizer removal complete") +} + +func removeFinalizersForGVK(ctx context.Context, k8sClient client.Client, gvk schema.GroupVersionKind) error { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind + "List", + }) + + if err := k8sClient.List(ctx, list); err != nil { + // If the CRD isn't installed, the API server returns a "no kind is registered" + // or "resource not found" error. Log it and continue rather than failing. + log.Printf("skipping %v: %v", gvk, err) + return nil + } + + var errs []error + for i := range list.Items { + obj := &list.Items[i] + if !controllerutil.ContainsFinalizer(obj, finalizerKey) { + continue + } + patch := client.MergeFrom(obj.DeepCopy()) + controllerutil.RemoveFinalizer(obj, finalizerKey) + if err := k8sClient.Patch(ctx, obj, patch); err != nil { + errs = append(errs, fmt.Errorf("patch %v %s/%s: %w", gvk, obj.GetNamespace(), obj.GetName(), err)) + } else { + log.Printf("removed finalizer from %v %s/%s", gvk, obj.GetNamespace(), obj.GetName()) + } + } + return errors.Join(errs...) +} diff --git a/operator/cmd/finalizerremoval/finalizer_removal_test.go b/operator/cmd/finalizerremoval/finalizer_removal_test.go new file mode 100644 index 000000000..ff99560a5 --- /dev/null +++ b/operator/cmd/finalizerremoval/finalizer_removal_test.go @@ -0,0 +1,157 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package finalizerremoval + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + crds "github.com/redpanda-data/redpanda-operator/operator/config/crd/bases" + "github.com/redpanda-data/redpanda-operator/operator/internal/testutils" +) + +// TestRemoveFinalizersForGVK exercises the post-delete finalizer removal logic +// that runs as a helm hook after the operator is uninstalled. The primary +// scenario is orphaned CRs: the operator has been removed but CRs still carry +// the operator.redpanda.com/finalizer, which blocks Kubernetes from garbage +// collecting them and prevents namespace deletion from completing. +// +// Covered scenarios: +// - Orphaned CRs have their operator finalizer stripped so they no longer +// block garbage collection. +// - Multiple CR types (Topic, User, etc.) are handled independently, +// reflecting real deployments where several resource kinds coexist. +// - CRs that never had the operator finalizer are left untouched. +// - GVKs whose CRDs are not installed on the cluster are skipped gracefully, +// which happens when the operator manages types the cluster never used. +// - After finalizer removal, CRs can be deleted and the namespace teardown +// is no longer blocked. +func TestRemoveFinalizersForGVK(t *testing.T) { + ctx := context.Background() + testEnv := testutils.RedpandaTestEnv{} + cfg, err := testEnv.StartRedpandaTestEnv(false) + require.NoError(t, err) + + k8sClient, err := client.New(cfg, client.Options{}) + require.NoError(t, err) + + topicCRD := crds.Topic() + gvk := schema.GroupVersionKind{ + Group: topicCRD.Spec.Group, + Version: topicCRD.Spec.Versions[0].Name, + Kind: topicCRD.Spec.Names.Kind, + } + + userCRD := crds.User() + userGVK := schema.GroupVersionKind{ + Group: userCRD.Spec.Group, + Version: userCRD.Spec.Versions[0].Name, + Kind: userCRD.Spec.Names.Kind, + } + + // Helper to create an unstructured CR with the operator finalizer. + createCR := func(t *testing.T, gvk schema.GroupVersionKind, name, ns string) *unstructured.Unstructured { + t.Helper() + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + obj.SetName(name) + obj.SetNamespace(ns) + // CRDs require spec.cluster.clusterRef to pass validation. + _ = unstructured.SetNestedField(obj.Object, "dummy", "spec", "cluster", "clusterRef", "name") + require.NoError(t, k8sClient.Create(ctx, obj)) + // Add finalizer via patch after creation. + patch := client.MergeFrom(obj.DeepCopy()) + controllerutil.AddFinalizer(obj, finalizerKey) + require.NoError(t, k8sClient.Patch(ctx, obj, patch)) + return obj + } + + // Orphaned CR: the operator is gone but the CR still carries the finalizer. + // The post-delete hook must strip it so the object is no longer stuck. + t.Run("removes finalizer from orphaned CRs", func(t *testing.T) { + topic := createCR(t, gvk, "orphaned-topic", "default") + + require.True(t, controllerutil.ContainsFinalizer(topic, finalizerKey)) + + require.NoError(t, removeFinalizersForGVK(ctx, k8sClient, gvk)) + + require.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(topic), topic)) + require.False(t, controllerutil.ContainsFinalizer(topic, finalizerKey)) + }) + + // Real deployments typically have several CR types (Topics, Users, Roles, + // Schemas, etc.) all with orphaned finalizers after operator uninstall. + // Each type must be handled independently. + t.Run("removes finalizers from multiple CR types", func(t *testing.T) { + topic := createCR(t, gvk, "multi-topic", "default") + user := createCR(t, userGVK, "multi-user", "default") + + require.NoError(t, removeFinalizersForGVK(ctx, k8sClient, gvk)) + require.NoError(t, removeFinalizersForGVK(ctx, k8sClient, userGVK)) + + require.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(topic), topic)) + require.False(t, controllerutil.ContainsFinalizer(topic, finalizerKey)) + + require.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(user), user)) + require.False(t, controllerutil.ContainsFinalizer(user, finalizerKey)) + }) + + // CRs that were created but never reconciled by the operator (or whose + // finalizer was already removed) must not be modified. + t.Run("skips CRs without the operator finalizer", func(t *testing.T) { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + obj.SetName("no-finalizer-topic") + obj.SetNamespace("default") + _ = unstructured.SetNestedField(obj.Object, "dummy", "spec", "cluster", "clusterRef", "name") + require.NoError(t, k8sClient.Create(ctx, obj)) + + require.NoError(t, removeFinalizersForGVK(ctx, k8sClient, gvk)) + + require.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)) + require.Empty(t, obj.GetFinalizers()) + }) + + // The operator manages many CRD types but not all may be installed on a + // given cluster. When the API server doesn't recognize a GVK the removal + // should log and continue rather than failing the entire job. + t.Run("skips unknown GVK without error", func(t *testing.T) { + unknownGVK := schema.GroupVersionKind{ + Group: "nonexistent.redpanda.com", + Version: "v1", + Kind: "Fake", + } + require.NoError(t, removeFinalizersForGVK(ctx, k8sClient, unknownGVK)) + }) + + // End-to-end: a CR with a finalizer cannot be garbage collected. After + // the finalizer is removed the CR should be deletable, which unblocks + // namespace deletion. + t.Run("orphaned CRs deletable after finalizer removal", func(t *testing.T) { + topic := createCR(t, gvk, "deletable-topic", "default") + + require.NoError(t, removeFinalizersForGVK(ctx, k8sClient, gvk)) + + require.NoError(t, k8sClient.Get(ctx, client.ObjectKeyFromObject(topic), topic)) + require.False(t, controllerutil.ContainsFinalizer(topic, finalizerKey)) + + // The object should now be deletable without the finalizer blocking. + require.NoError(t, k8sClient.Delete(ctx, topic)) + + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(topic), topic) + require.True(t, client.IgnoreNotFound(err) == nil, "object should be deleted") + }) +} diff --git a/operator/cmd/main.go b/operator/cmd/main.go index affdbf032..42d30072f 100644 --- a/operator/cmd/main.go +++ b/operator/cmd/main.go @@ -20,6 +20,7 @@ import ( "github.com/redpanda-data/redpanda-operator/operator/cmd/bootstrap" "github.com/redpanda-data/redpanda-operator/operator/cmd/configurator" "github.com/redpanda-data/redpanda-operator/operator/cmd/crd" + "github.com/redpanda-data/redpanda-operator/operator/cmd/finalizerremoval" "github.com/redpanda-data/redpanda-operator/operator/cmd/migration" "github.com/redpanda-data/redpanda-operator/operator/cmd/multicluster" "github.com/redpanda-data/redpanda-operator/operator/cmd/ready" @@ -57,6 +58,7 @@ func init() { syncclusterconfig.Command(), version.Command(), migration.Command(), + finalizerremoval.Command(), ) logOptions.BindFlags(rootCmd.PersistentFlags())