From 02a78b382305db40cecf0248a3394acb75a69bb5 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 13 Aug 2026 13:59:54 +0200 Subject: [PATCH 01/12] remove version from pvc listener labels, correct operator name --- .../src/controller/build/mod.rs | 2 +- .../controller/build/resource/statefulset.rs | 6 ++- rust/operator-binary/src/crd/mod.rs | 2 +- tests/templates/kuttl/smoke/30-assert.yaml.j2 | 48 +++++++++---------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index aa735900..ad76660b 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -245,7 +245,7 @@ mod tests { ("app.kubernetes.io/instance", "my-hbase".to_string()), ( "app.kubernetes.io/managed-by", - "hbase.stackable.com_hbasecluster".to_string(), + "hbase.stackable.tech_hbasecluster".to_string(), ), ("app.kubernetes.io/name", "hbase".to_string()), ("app.kubernetes.io/role-group", "none".to_string()), diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index ce189e7a..16068353 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -299,8 +299,12 @@ pub fn build_rolegroup_statefulset( )); } + // Listener PVC labels should stay stable across upgrades and so should not + // include the version field (see HDFS for a similar pattern). + let unversioned_labels = cluster.role_group_selector(hbase_role, role_group_name); + let listener_pvc = - super::listener::build_listener_pvc(hbase_role, merged_config, &recommended_labels); + super::listener::build_listener_pvc(hbase_role, merged_config, &unversioned_labels); if let Some(listener_volume) = super::listener::build_listener_volume(hbase_role, merged_config, &recommended_labels) diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index b398c860..0535b077 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -44,7 +44,7 @@ pub mod security; pub const APP_NAME: &str = "hbase"; pub const FIELD_MANAGER: &str = "hbase-operator"; -pub const OPERATOR_NAME: &str = "hbase.stackable.com"; +pub const OPERATOR_NAME: &str = "hbase.stackable.tech"; // This constant is hard coded in hbase-entrypoint.sh // You need to change it there too. diff --git a/tests/templates/kuttl/smoke/30-assert.yaml.j2 b/tests/templates/kuttl/smoke/30-assert.yaml.j2 index 067fc721..120e46db 100644 --- a/tests/templates/kuttl/smoke/30-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-assert.yaml.j2 @@ -11,7 +11,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -46,7 +46,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -73,7 +73,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -108,7 +108,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -135,7 +135,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -170,7 +170,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -199,7 +199,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -224,7 +224,7 @@ spec: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -406,7 +406,7 @@ spec: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -430,7 +430,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -450,7 +450,7 @@ spec: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -641,7 +641,7 @@ spec: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -665,7 +665,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -690,7 +690,7 @@ spec: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -871,7 +871,7 @@ spec: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -893,7 +893,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: discovery stackable.tech/vendor: Stackable @@ -910,7 +910,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -927,7 +927,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -944,7 +944,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -960,7 +960,7 @@ kind: ServiceAccount metadata: labels: app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase name: test-hbase-serviceaccount ownerReferences: @@ -974,7 +974,7 @@ kind: RoleBinding metadata: labels: app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase name: test-hbase-rolebinding ownerReferences: @@ -1023,7 +1023,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -1056,7 +1056,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -1089,7 +1089,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.com_hbasecluster + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable From de3a191bef74449e501755f8003b98f7fbb7aad0 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 13 Aug 2026 14:13:13 +0200 Subject: [PATCH 02/12] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f772b97a..f29153fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,17 @@ - Fix a longstanding problem of including empty `categories`, `shortNames` and `additionalPrinterColumns` in the CRDs, which could cause problems with GitOps tools (e.g. ArgoCD) reporting a diff in the custom resources. See [our internal issue](https://github.com/stackabletech/hdfs-operator/issues/626) and [the fix](https://github.com/kube-rs/kube/pull/2042) for details ([#792]). +- BREAKING: The app.kubernetes.io/managed-by label value changed from hbase.stackable.com_hbasecluster to + hbase.stackable.tech_hbasecluster, aligning with all other operators. +- BREAKING: The rest-server listener PVC template now carries only the unversioned selector labels. + Existing rest-server StatefulSets must be deleted once before the new operator can reconcile them ([#795]). [#776]: https://github.com/stackabletech/hbase-operator/pull/776 [#782]: https://github.com/stackabletech/hbase-operator/pull/782 [#786]: https://github.com/stackabletech/hbase-operator/pull/786 [#787]: https://github.com/stackabletech/hbase-operator/pull/787 [#792]: https://github.com/stackabletech/hbase-operator/pull/792 +[#795]: https://github.com/stackabletech/hbase-operator/pull/795 ## [26.7.0] - 2026-07-21 From 6a5165d8cff020b891fcc986f8b34c72fea50979 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 13 Aug 2026 15:12:48 +0200 Subject: [PATCH 03/12] add vendor label manually, adjust test assert --- .../controller/build/resource/statefulset.rs | 5 +++- tests/templates/kuttl/smoke/30-assert.yaml.j2 | 24 ------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 16068353..86bef023 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -19,6 +19,7 @@ use stackable_operator::{ }, apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, }, + kvp::Label, product_logging, v2::{ builder::pod::container::{EnvVarName, EnvVarSet, new_container_builder}, @@ -301,7 +302,9 @@ pub fn build_rolegroup_statefulset( // Listener PVC labels should stay stable across upgrades and so should not // include the version field (see HDFS for a similar pattern). - let unversioned_labels = cluster.role_group_selector(hbase_role, role_group_name); + let mut unversioned_labels = cluster.role_group_selector(hbase_role, role_group_name); + // Vendor is not included in role_group_selector labels, so add it. + unversioned_labels.insert(Label::stackable_vendor()); let listener_pvc = super::listener::build_listener_pvc(hbase_role, merged_config, &unversioned_labels); diff --git a/tests/templates/kuttl/smoke/30-assert.yaml.j2 b/tests/templates/kuttl/smoke/30-assert.yaml.j2 index 120e46db..b2243107 100644 --- a/tests/templates/kuttl/smoke/30-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-assert.yaml.j2 @@ -11,7 +11,6 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -46,7 +45,6 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -73,7 +71,6 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -108,7 +105,6 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -135,7 +131,6 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -170,7 +165,6 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -199,7 +193,6 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -224,7 +217,6 @@ spec: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -406,7 +398,6 @@ spec: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -430,7 +421,6 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -450,7 +440,6 @@ spec: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -641,7 +630,6 @@ spec: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -665,7 +653,6 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -690,7 +677,6 @@ spec: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -871,7 +857,6 @@ spec: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -893,7 +878,6 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: discovery stackable.tech/vendor: Stackable @@ -910,7 +894,6 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -927,7 +910,6 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -944,7 +926,6 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -960,7 +941,6 @@ kind: ServiceAccount metadata: labels: app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase name: test-hbase-serviceaccount ownerReferences: @@ -974,7 +954,6 @@ kind: RoleBinding metadata: labels: app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase name: test-hbase-rolebinding ownerReferences: @@ -1023,7 +1002,6 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -1056,7 +1034,6 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -1089,7 +1066,6 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase - app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable From c1b4366869390eb0b238335378634d33f0f1bdbb Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 13 Aug 2026 15:25:45 +0200 Subject: [PATCH 04/12] added in wrongly removed labels --- .../controller/build/resource/statefulset.rs | 3 ++- tests/templates/kuttl/smoke/30-assert.yaml.j2 | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 86bef023..db926cd6 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -303,7 +303,8 @@ pub fn build_rolegroup_statefulset( // Listener PVC labels should stay stable across upgrades and so should not // include the version field (see HDFS for a similar pattern). let mut unversioned_labels = cluster.role_group_selector(hbase_role, role_group_name); - // Vendor is not included in role_group_selector labels, so add it. + // Neither vendor nor managed-by are included in role_group_selector + // labels: vendor is a required and so add it back. unversioned_labels.insert(Label::stackable_vendor()); let listener_pvc = diff --git a/tests/templates/kuttl/smoke/30-assert.yaml.j2 b/tests/templates/kuttl/smoke/30-assert.yaml.j2 index b2243107..7dbf9d9b 100644 --- a/tests/templates/kuttl/smoke/30-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-assert.yaml.j2 @@ -11,6 +11,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -45,6 +46,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -71,6 +73,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -105,6 +108,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -131,6 +135,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -165,6 +170,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default prometheus.io/scrape: "true" @@ -193,6 +199,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -217,6 +224,7 @@ spec: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -398,6 +406,7 @@ spec: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -421,6 +430,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -440,6 +450,7 @@ spec: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -630,6 +641,7 @@ spec: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -653,6 +665,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default restarter.stackable.tech/enabled: "true" @@ -677,6 +690,7 @@ spec: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -878,6 +892,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: discovery stackable.tech/vendor: Stackable @@ -894,6 +909,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -910,6 +926,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -926,6 +943,7 @@ metadata: labels: app.kubernetes.io/component: restserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -941,6 +959,7 @@ kind: ServiceAccount metadata: labels: app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase name: test-hbase-serviceaccount ownerReferences: @@ -954,6 +973,7 @@ kind: RoleBinding metadata: labels: app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase name: test-hbase-rolebinding ownerReferences: @@ -1002,6 +1022,7 @@ metadata: labels: app.kubernetes.io/component: master app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable @@ -1034,6 +1055,7 @@ metadata: labels: app.kubernetes.io/component: regionserver app.kubernetes.io/instance: test-hbase + app.kubernetes.io/managed-by: hbase.stackable.tech_hbasecluster app.kubernetes.io/name: hbase app.kubernetes.io/role-group: default stackable.tech/vendor: Stackable From 83ca24e43d4ddf8a588cfa6bf6a45a1e81ac00c9 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 14 Aug 2026 14:38:23 +0200 Subject: [PATCH 05/12] added missing PR number to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f29153fc..051a8e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ which could cause problems with GitOps tools (e.g. ArgoCD) reporting a diff in the custom resources. See [our internal issue](https://github.com/stackabletech/hdfs-operator/issues/626) and [the fix](https://github.com/kube-rs/kube/pull/2042) for details ([#792]). - BREAKING: The app.kubernetes.io/managed-by label value changed from hbase.stackable.com_hbasecluster to - hbase.stackable.tech_hbasecluster, aligning with all other operators. + hbase.stackable.tech_hbasecluster, aligning with all other operators ([#795]). - BREAKING: The rest-server listener PVC template now carries only the unversioned selector labels. Existing rest-server StatefulSets must be deleted once before the new operator can reconcile them ([#795]). From 1b85652c7a3c9aeab29802bb0bd800781e67ba1c Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 14 Aug 2026 19:05:55 +0200 Subject: [PATCH 06/12] add early exit tests --- Cargo.lock | 1 + Cargo.nix | 4 + Cargo.toml | 1 + .../templates/clusterrole-operator.yaml | 10 +- rust/operator-binary/Cargo.toml | 1 + rust/operator-binary/src/hbase_controller.rs | 132 ++++++++++++++ rust/operator-binary/src/main.rs | 20 ++- .../kuttl/cluster-operation/40-assert.yaml | 170 ++++++++++++++++++ .../40-delete-owned-resources.yaml | 57 ++++++ tests/templates/kuttl/smoke/30-assert.yaml.j2 | 6 +- .../kuttl/smoke/30-install-hbase.yaml.j2 | 5 + 11 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 tests/templates/kuttl/cluster-operation/40-assert.yaml create mode 100644 tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml diff --git a/Cargo.lock b/Cargo.lock index 36c7760f..b8c3c7df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3192,6 +3192,7 @@ dependencies = [ "const_format", "fnv", "futures 0.3.33", + "http", "indoc", "rstest", "serde", diff --git a/Cargo.nix b/Cargo.nix index 23cd76f0..281719e0 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10521,6 +10521,10 @@ rec { } ]; devDependencies = [ + { + name = "http"; + packageId = "http"; + } { name = "rstest"; packageId = "rstest"; diff --git a/Cargo.toml b/Cargo.toml index 091afb99..0aa6b605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ clap = "4.6" const_format = "0.2" fnv = "1.0" futures = { version = "0.3", features = ["compat"] } +http = "1.3" indoc = "2.0" rstest = "0.26" serde = { version = "1.0", features = ["derive"] } diff --git a/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml b/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml index 6b0fc340..af1b9062 100644 --- a/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml +++ b/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml @@ -31,7 +31,7 @@ rules: - patch - watch # ServiceAccount created per HbaseCluster for workload pod identity. - # Applied via SSA and tracked for orphan cleanup. + # Applied via SSA, tracked for orphan cleanup and watched by the controller. - apiGroups: - "" resources: @@ -42,8 +42,9 @@ rules: - get - list - patch + - watch # RoleBinding created per HbaseCluster to bind the product ClusterRole to the workload - # ServiceAccount. Applied via SSA and tracked for orphan cleanup. + # ServiceAccount. Applied via SSA, tracked for orphan cleanup and watched by the controller. - apiGroups: - rbac.authorization.k8s.io resources: @@ -54,6 +55,7 @@ rules: - get - list - patch + - watch # Required to bind the product ClusterRole to the per-cluster ServiceAccount. - apiGroups: - rbac.authorization.k8s.io @@ -75,7 +77,8 @@ rules: - list - patch - watch - # PodDisruptionBudget created per role. Applied via SSA and tracked for orphan cleanup. + # PodDisruptionBudget created per role. Applied via SSA, tracked for orphan cleanup and + # watched by the controller. - apiGroups: - policy resources: @@ -86,6 +89,7 @@ rules: - get - list - patch + - watch # Required for maintaining the CRDs within the operator (including the conversion webhook info). # Also for the startup condition check before the controller can run. - apiGroups: diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 902fedb0..472acb10 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -29,5 +29,6 @@ tracing.workspace = true built.workspace = true [dev-dependencies] +http.workspace = true rstest.workspace = true serde_yaml.workspace = true diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 6b6751d2..9ae1551d 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -12,6 +12,7 @@ use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::ClusterResourceApplyStrategy, kube::{ + Resource, core::{DeserializeGuard, error_boundary}, runtime::controller::Action, }, @@ -76,6 +77,10 @@ pub async fn reconcile_hbase( ) -> Result { tracing::info!("Starting reconcile"); + if hbase.meta().deletion_timestamp.is_some() { + return Ok(Action::await_change()); + } + let hbase = hbase .0 .as_ref() @@ -126,3 +131,130 @@ pub fn error_policy( _ => Action::requeue(*Duration::from_secs(5)), } } + +#[cfg(test)] +mod tests { + use stackable_operator::{ + client::Client, + kube::{Client as KubeClient, Config}, + }; + + use super::*; + use crate::test_utils; + + /// A [`Ctx`] whose client points at a closed port. Any API call made through it fails the + /// reconciliation, so an `Ok` result proves the reconciler returned before touching the + /// Kubernetes API. + fn unreachable_ctx() -> Arc { + let config = Config::new( + "http://127.0.0.1:1" + .parse::() + .expect("valid static URI"), + ); + let kube_client = KubeClient::try_from(config).expect("client from static config"); + + Arc::new(Ctx { + client: Client::new( + kube_client, + None, + "default".to_owned(), + test_utils::cluster_info(), + ), + operator_environment: OperatorEnvironmentOptions { + operator_namespace: "stackable-operators".to_owned(), + operator_service_name: "hbase-operator".to_owned(), + image_repository: "oci.stackable.tech/sdp".to_owned(), + }, + }) + } + + /// Drives the async reconciler from the synchronous tests used in this repo. + /// The [`Ctx`] is built inside `block_on` because the kube client needs a running reactor + /// already at construction time. + fn reconcile(hbase: DeserializeGuard) -> Result { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("current-thread tokio runtime") + .block_on(async { reconcile_hbase(Arc::new(hbase), unreachable_ctx()).await }) + } + + #[test] + fn reconcile_exits_early_for_deleted_cluster() { + let hbase = serde_yaml::from_str( + r#" +apiVersion: hbase.stackable.tech/v1alpha1 +kind: HbaseCluster +metadata: + name: hbase + namespace: default + deletionTimestamp: "2026-08-14T12:00:00Z" +spec: + image: + productVersion: 2.6.3 + clusterConfig: + hdfsConfigMapName: simple-hdfs + zookeeperConfigMapName: simple-znode +"#, + ) + .expect("valid HbaseCluster YAML"); + + let action = reconcile(hbase).expect("a deleted cluster reconciles without any API call"); + + assert_eq!(action, Action::await_change()); + } + + #[test] + fn reconcile_exits_early_for_deleted_cluster_with_invalid_spec() { + // The spec is missing all required fields, so the DeserializeGuard captures a + // deserialization error. During deletion the spec is irrelevant and the reconciler must + // still exit quietly instead of erroring through the whole teardown. + let hbase = serde_yaml::from_str( + r#" +apiVersion: hbase.stackable.tech/v1alpha1 +kind: HbaseCluster +metadata: + name: hbase + namespace: default + deletionTimestamp: "2026-08-14T12:00:00Z" +spec: {} +"#, + ) + .expect("YAML parses; the invalid spec is captured inside the DeserializeGuard"); + + let action = + reconcile(hbase).expect("a deleted cluster reconciles even when its spec is invalid"); + + assert_eq!(action, Action::await_change()); + } + + #[test] + fn reconcile_proceeds_for_live_cluster() { + // Without a deletion timestamp the reconciler must not exit early: it proceeds to + // dereference, which fails against the unreachable test API server. + let hbase = serde_yaml::from_str( + r#" +apiVersion: hbase.stackable.tech/v1alpha1 +kind: HbaseCluster +metadata: + name: hbase + namespace: default +spec: + image: + productVersion: 2.6.3 + clusterConfig: + hdfsConfigMapName: simple-hdfs + zookeeperConfigMapName: simple-znode +"#, + ) + .expect("valid HbaseCluster YAML"); + + let result = reconcile(hbase); + + assert!( + matches!(result, Err(Error::Dereference { .. })), + "a live cluster must reach the API (and fail dereferencing against the unreachable \ + test server), not exit early: {result:?}" + ); + } +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index dec10057..dd487edd 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -14,7 +14,9 @@ use stackable_operator::{ eos::EndOfSupportChecker, k8s_openapi::api::{ apps::v1::StatefulSet, - core::v1::{ConfigMap, Service}, + core::v1::{ConfigMap, Service, ServiceAccount}, + policy::v1::PodDisruptionBudget, + rbac::v1::RoleBinding, }, kube::{ CustomResourceExt, ResourceExt, @@ -134,6 +136,22 @@ async fn main() -> anyhow::Result<()> { watch_namespace.get_api::(&client), watcher::Config::default(), ) + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) .watches( watch_namespace.get_api::>(&client), watcher::Config::default(), diff --git a/tests/templates/kuttl/cluster-operation/40-assert.yaml b/tests/templates/kuttl/cluster-operation/40-assert.yaml new file mode 100644 index 00000000..4101abe2 --- /dev/null +++ b/tests/templates/kuttl/cluster-operation/40-assert.yaml @@ -0,0 +1,170 @@ +--- +# The recreated StatefulSets must bring the cluster back to ready, and the recreated +# objects must carry an owner reference back to the HbaseCluster so that garbage +# collection still works for them. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: recreate-owned-resources +timeout: 300 +commands: + - script: kubectl -n $NAMESPACE wait --for=condition=available hbaseclusters.hbase.stackable.tech/test-hbase --timeout 301s +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-hbase-master-default + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-hbase-regionserver-default + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test-hbase-restserver-default + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-hbase-serviceaccount + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: test-hbase-rolebinding + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-hbase-master + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-hbase-regionserver + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test-hbase-restserver + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-hbase + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: Service +metadata: + name: test-hbase-master-default-headless + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: Service +metadata: + name: test-hbase-master-default-metrics + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: Service +metadata: + name: test-hbase-regionserver-default-headless + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: Service +metadata: + name: test-hbase-regionserver-default-metrics + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: Service +metadata: + name: test-hbase-restserver-default-headless + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase +--- +apiVersion: v1 +kind: Service +metadata: + name: test-hbase-restserver-default-metrics + ownerReferences: + - apiVersion: hbase.stackable.tech/v1alpha1 + controller: true + kind: HbaseCluster + name: test-hbase diff --git a/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml b/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml new file mode 100644 index 00000000..7fc2b5b3 --- /dev/null +++ b/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml @@ -0,0 +1,57 @@ +--- +# The operator owns (`.owns()` in main.rs) every resource it applies. Deleting one must +# trigger a reconcile that recreates it — this is the behavioural check for the watch +# registrations and the `watch` RBAC verbs in the operator ClusterRole. It lives in this +# test because deleting the StatefulSets forces a full pod-restart cycle, and this test +# cycles the pods anyway. +# +# Recreation is proven by UID change: waiting for mere existence could pass without any +# deletion having happened. TestStep commands run exactly once (no kuttl retry loop), so +# the polling below stays quiet in the log. +# +# Owned resources deliberately NOT tested here: the role-group ConfigMaps +# (test-hbase-{master,regionserver,restserver}-default). The same `.owns(ConfigMap)` +# registration is already exercised by the discovery ConfigMap below, and they are +# mounted into running pods, which a deletion window would disturb. +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: delete-owned-resources +timeout: 300 +commands: + - script: | + set -eu + + delete_and_await_recreation() { + kind=$1 + name=$2 + old_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}') + kubectl delete -n "$NAMESPACE" "$kind" "$name" --wait=false + # Recreation is a single reconcile away, so this normally succeeds on the + # first iteration; 30s is a generous upper bound well below the step timeout. + for _ in $(seq 1 30); do + new_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}' 2>/dev/null || true) + if [ -n "$new_uid" ] && [ "$new_uid" != "$old_uid" ]; then + return 0 + fi + sleep 1 + done + echo "$kind/$name was not recreated (old uid: $old_uid, current: '${new_uid:-}')" >&2 + return 1 + } + + delete_and_await_recreation serviceaccount test-hbase-serviceaccount + delete_and_await_recreation rolebinding test-hbase-rolebinding + delete_and_await_recreation poddisruptionbudget test-hbase-master + delete_and_await_recreation poddisruptionbudget test-hbase-regionserver + delete_and_await_recreation poddisruptionbudget test-hbase-restserver + delete_and_await_recreation configmap test-hbase + delete_and_await_recreation service test-hbase-master-default-headless + delete_and_await_recreation service test-hbase-master-default-metrics + delete_and_await_recreation service test-hbase-regionserver-default-headless + delete_and_await_recreation service test-hbase-regionserver-default-metrics + delete_and_await_recreation service test-hbase-restserver-default-headless + delete_and_await_recreation service test-hbase-restserver-default-metrics + delete_and_await_recreation statefulset test-hbase-master-default + delete_and_await_recreation statefulset test-hbase-regionserver-default + delete_and_await_recreation statefulset test-hbase-restserver-default diff --git a/tests/templates/kuttl/smoke/30-assert.yaml.j2 b/tests/templates/kuttl/smoke/30-assert.yaml.j2 index 7dbf9d9b..482138a0 100644 --- a/tests/templates/kuttl/smoke/30-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-assert.yaml.j2 @@ -384,7 +384,7 @@ spec: securityContext: fsGroup: 1000 serviceAccountName: test-hbase-serviceaccount - terminationGracePeriodSeconds: 1200 + terminationGracePeriodSeconds: 60 volumes: - configMap: name: test-hbase-master-default @@ -619,7 +619,7 @@ spec: securityContext: fsGroup: 1000 serviceAccountName: test-hbase-serviceaccount - terminationGracePeriodSeconds: 3600 + terminationGracePeriodSeconds: 60 volumes: - configMap: name: test-hbase-regionserver-default @@ -850,7 +850,7 @@ spec: securityContext: fsGroup: 1000 serviceAccountName: test-hbase-serviceaccount - terminationGracePeriodSeconds: 300 + terminationGracePeriodSeconds: 60 volumes: - configMap: name: test-hbase-restserver-default diff --git a/tests/templates/kuttl/smoke/30-install-hbase.yaml.j2 b/tests/templates/kuttl/smoke/30-install-hbase.yaml.j2 index 7535a3e8..8c28c42f 100644 --- a/tests/templates/kuttl/smoke/30-install-hbase.yaml.j2 +++ b/tests/templates/kuttl/smoke/30-install-hbase.yaml.j2 @@ -20,6 +20,9 @@ spec: {% endif %} masters: config: + # Test-only: the production default (20m) becomes the pods' terminationGracePeriodSeconds, + # which the namespace controller uses to schedule its deletion retries. + gracefulShutdownTimeout: 1m logging: enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} listenerClass: {{ test_scenario['values']['listener-class'] }} @@ -32,6 +35,7 @@ spec: replicas: 2 regionServers: config: + gracefulShutdownTimeout: 1m logging: enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} listenerClass: {{ test_scenario['values']['listener-class'] }} @@ -44,6 +48,7 @@ spec: replicas: 2 restServers: config: + gracefulShutdownTimeout: 1m logging: enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} listenerClass: {{ test_scenario['values']['listener-class'] }} From 73c75fcb83fbef8204e9f39158a32c5880f52ebf Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 20 Aug 2026 19:51:34 +0200 Subject: [PATCH 07/12] changelog fix --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2632f931..9b078edf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,9 @@ See [our internal issue](https://github.com/stackabletech/hdfs-operator/issues/626) and [the fix](https://github.com/kube-rs/kube/pull/2042) for details ([#792]). - BREAKING: The app.kubernetes.io/managed-by label value changed from hbase.stackable.com_hbasecluster to hbase.stackable.tech_hbasecluster, aligning with all other operators ([#795]). -- BREAKING: The rest-server listener PVC template now carries only the unversioned selector labels. - Existing rest-server StatefulSets must be deleted once before the new operator can reconcile them ([#795]). +- BREAKING: The rest-server listener PVC template now carries the recommended labels without the + version label, so that the labels stay stable across upgrades. + Existing rest-server StatefulSets must be deleted once before the new operator can reconcile them ([#799]). [#776]: https://github.com/stackabletech/hbase-operator/pull/776 [#782]: https://github.com/stackabletech/hbase-operator/pull/782 From ca9af52e0da9cedc25783eb0e1eeffb0ae08d21f Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 20 Aug 2026 20:15:41 +0200 Subject: [PATCH 08/12] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b078edf..60e24fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ See [our internal issue](https://github.com/stackabletech/hdfs-operator/issues/626) and [the fix](https://github.com/kube-rs/kube/pull/2042) for details ([#792]). - BREAKING: The app.kubernetes.io/managed-by label value changed from hbase.stackable.com_hbasecluster to hbase.stackable.tech_hbasecluster, aligning with all other operators ([#795]). +- The operator now watches all resources that it creates and early-exits the reconcile action when the + cluster is marked for deletion ([#797]). - BREAKING: The rest-server listener PVC template now carries the recommended labels without the version label, so that the labels stay stable across upgrades. Existing rest-server StatefulSets must be deleted once before the new operator can reconcile them ([#799]). @@ -37,6 +39,7 @@ [#787]: https://github.com/stackabletech/hbase-operator/pull/787 [#792]: https://github.com/stackabletech/hbase-operator/pull/792 [#795]: https://github.com/stackabletech/hbase-operator/pull/795 +[#797]: https://github.com/stackabletech/hbase-operator/pull/797 [#799]: https://github.com/stackabletech/hbase-operator/pull/799 ## [26.7.0] - 2026-07-21 From 043c3c0df5c4801512496bbff84e2f898328646d Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 21 Aug 2026 11:08:21 +0200 Subject: [PATCH 09/12] improved comment --- .../40-delete-owned-resources.yaml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml b/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml index 7fc2b5b3..6ea2940d 100644 --- a/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml +++ b/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml @@ -1,18 +1,18 @@ --- -# The operator owns (`.owns()` in main.rs) every resource it applies. Deleting one must -# trigger a reconcile that recreates it — this is the behavioural check for the watch -# registrations and the `watch` RBAC verbs in the operator ClusterRole. It lives in this -# test because deleting the StatefulSets forces a full pod-restart cycle, and this test -# cycles the pods anyway. +# Every resource the operator applies carries an ownerReference and a `.owns()` watch +# (main.rs); deleting it must trigger a reconcile of the HbaseCluster, which re-applies +# it. This step checks that chain — the ClusterRole's `watch` verbs plus the `.owns()` +# routing — and lives here because this test already cycles the pods. The `.watches()` +# registration (referenced-but-unowned ConfigMaps) can't be tested by deletion: the +# operator never recreates what it didn't apply. # -# Recreation is proven by UID change: waiting for mere existence could pass without any -# deletion having happened. TestStep commands run exactly once (no kuttl retry loop), so -# the polling below stays quiet in the log. +# Recreation is proven by UID change: mere existence could pass without any deletion. +# TestStep commands run exactly once (no kuttl retry loop), so the polling stays quiet. # -# Owned resources deliberately NOT tested here: the role-group ConfigMaps -# (test-hbase-{master,regionserver,restserver}-default). The same `.owns(ConfigMap)` -# registration is already exercised by the discovery ConfigMap below, and they are -# mounted into running pods, which a deletion window would disturb. +# Deliberately skipped: the role-group ConfigMaps +# (test-hbase-{master,regionserver,restserver}-default) — `.owns(ConfigMap)` is already +# exercised via the discovery ConfigMap, and deleting ConfigMaps mounted into running +# pods would disturb them. apiVersion: kuttl.dev/v1beta1 kind: TestStep metadata: From f1071f7523f6a599fd2537b862b529c3da68a5d2 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 21 Aug 2026 12:31:28 +0200 Subject: [PATCH 10/12] order owns by name --- rust/operator-binary/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index f39804b8..8e7eabb8 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -129,27 +129,27 @@ async fn main() -> anyhow::Result<()> { let config_map_store = hbase_controller.store(); let hbase_controller = hbase_controller .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .owns( - watch_namespace.get_api::(&client), + watch_namespace.get_api::(&client), watcher::Config::default(), ) .watches( From 820911dbcafdff5873a3fd5cc9605f26b1077985 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Fri, 21 Aug 2026 14:55:36 +0200 Subject: [PATCH 11/12] reworked/deleted comments --- rust/operator-binary/src/hbase_controller.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 9ae1551d..2cde4c96 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -206,9 +206,6 @@ spec: #[test] fn reconcile_exits_early_for_deleted_cluster_with_invalid_spec() { - // The spec is missing all required fields, so the DeserializeGuard captures a - // deserialization error. During deletion the spec is irrelevant and the reconciler must - // still exit quietly instead of erroring through the whole teardown. let hbase = serde_yaml::from_str( r#" apiVersion: hbase.stackable.tech/v1alpha1 @@ -230,8 +227,6 @@ spec: {} #[test] fn reconcile_proceeds_for_live_cluster() { - // Without a deletion timestamp the reconciler must not exit early: it proceeds to - // dereference, which fails against the unreachable test API server. let hbase = serde_yaml::from_str( r#" apiVersion: hbase.stackable.tech/v1alpha1 @@ -253,8 +248,7 @@ spec: assert!( matches!(result, Err(Error::Dereference { .. })), - "a live cluster must reach the API (and fail dereferencing against the unreachable \ - test server), not exit early: {result:?}" + "a live cluster must reach the API but when dereferencing against the unreachable test server: {result:?}" ); } } From b51fa9bf8e50a02ab86cd949e8e5b6e096bcd5ad Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 24 Aug 2026 16:00:50 +0200 Subject: [PATCH 12/12] implement review feedback --- Cargo.lock | 1 - Cargo.nix | 4 - Cargo.toml | 1 - .../templates/clusterrole-operator.yaml | 16 +-- rust/operator-binary/Cargo.toml | 1 - rust/operator-binary/src/hbase_controller.rs | 122 ++++-------------- .../kuttl/cluster-operation/40-assert.yaml | 8 +- .../40-delete-owned-resources.yaml | 70 ++++++---- 8 files changed, 80 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e65ee62a..800d6bc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3193,7 +3193,6 @@ dependencies = [ "const_format", "fnv", "futures 0.3.34", - "http", "indoc", "rstest", "serde", diff --git a/Cargo.nix b/Cargo.nix index aa1bd155..fbbac512 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -10527,10 +10527,6 @@ rec { } ]; devDependencies = [ - { - name = "http"; - packageId = "http"; - } { name = "rstest"; packageId = "rstest"; diff --git a/Cargo.toml b/Cargo.toml index 37db4f21..8fb02868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ clap = "4.6" const_format = "0.2" fnv = "1.0" futures = { version = "0.3", features = ["compat"] } -http = "1.3" indoc = "2.0" rstest = "0.26" serde = { version = "1.0", features = ["derive"] } diff --git a/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml b/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml index af1b9062..030741be 100644 --- a/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml +++ b/deploy/helm/hbase-operator/templates/clusterrole-operator.yaml @@ -18,24 +18,14 @@ rules: # orphan cleanup (list + delete). # - configmaps: role group configuration and discovery configmap # - services: role group headless and metrics services + # - serviceaccounts created per HbaseCluster for workload pod identity. + # Applied via SSA, tracked for orphan cleanup and watched by the controller. - apiGroups: - "" resources: - configmaps - - services - verbs: - - create - - delete - - get - - list - - patch - - watch - # ServiceAccount created per HbaseCluster for workload pod identity. - # Applied via SSA, tracked for orphan cleanup and watched by the controller. - - apiGroups: - - "" - resources: - serviceaccounts + - services verbs: - create - delete diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 472acb10..902fedb0 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -29,6 +29,5 @@ tracing.workspace = true built.workspace = true [dev-dependencies] -http.workspace = true rstest.workspace = true serde_yaml.workspace = true diff --git a/rust/operator-binary/src/hbase_controller.rs b/rust/operator-binary/src/hbase_controller.rs index 2cde4c96..0c9ff348 100644 --- a/rust/operator-binary/src/hbase_controller.rs +++ b/rust/operator-binary/src/hbase_controller.rs @@ -134,6 +134,7 @@ pub fn error_policy( #[cfg(test)] mod tests { + use indoc::indoc; use stackable_operator::{ client::Client, kube::{Client as KubeClient, Config}, @@ -142,20 +143,29 @@ mod tests { use super::*; use crate::test_utils; - /// A [`Ctx`] whose client points at a closed port. Any API call made through it fails the - /// reconciliation, so an `Ok` result proves the reconciler returned before touching the - /// Kubernetes API. - fn unreachable_ctx() -> Arc { - let config = Config::new( - "http://127.0.0.1:1" - .parse::() - .expect("valid static URI"), - ); - let kube_client = KubeClient::try_from(config).expect("client from static config"); - - Arc::new(Ctx { + /// The client points at a closed port, so any API call would fail the reconciliation: an `Ok` + /// proves that a cluster being deleted returns before the reconciler touches the Kubernetes + /// API, and because the spec is invalid, before the [`DeserializeGuard`] is unwrapped. + #[tokio::test] + async fn reconcile_exits_early_for_deleted_cluster() { + let hbase = serde_yaml::from_str(indoc! {r#" + --- + apiVersion: hbase.stackable.tech/v1alpha1 + kind: HbaseCluster + metadata: + name: hbase + namespace: default + deletionTimestamp: "2026-08-14T12:00:00Z" + spec: {} + "#}) + .expect("YAML parses; the invalid spec is captured inside the DeserializeGuard"); + + let ctx = Arc::new(Ctx { client: Client::new( - kube_client, + KubeClient::try_from(Config::new( + "http://127.0.0.1:1".parse().expect("valid static URI"), + )) + .expect("client from static config"), None, "default".to_owned(), test_utils::cluster_info(), @@ -165,90 +175,12 @@ mod tests { operator_service_name: "hbase-operator".to_owned(), image_repository: "oci.stackable.tech/sdp".to_owned(), }, - }) - } + }); - /// Drives the async reconciler from the synchronous tests used in this repo. - /// The [`Ctx`] is built inside `block_on` because the kube client needs a running reactor - /// already at construction time. - fn reconcile(hbase: DeserializeGuard) -> Result { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("current-thread tokio runtime") - .block_on(async { reconcile_hbase(Arc::new(hbase), unreachable_ctx()).await }) - } - - #[test] - fn reconcile_exits_early_for_deleted_cluster() { - let hbase = serde_yaml::from_str( - r#" -apiVersion: hbase.stackable.tech/v1alpha1 -kind: HbaseCluster -metadata: - name: hbase - namespace: default - deletionTimestamp: "2026-08-14T12:00:00Z" -spec: - image: - productVersion: 2.6.3 - clusterConfig: - hdfsConfigMapName: simple-hdfs - zookeeperConfigMapName: simple-znode -"#, - ) - .expect("valid HbaseCluster YAML"); - - let action = reconcile(hbase).expect("a deleted cluster reconciles without any API call"); + let action = reconcile_hbase(Arc::new(hbase), ctx) + .await + .expect("a deleted cluster reconciles without any API call"); assert_eq!(action, Action::await_change()); } - - #[test] - fn reconcile_exits_early_for_deleted_cluster_with_invalid_spec() { - let hbase = serde_yaml::from_str( - r#" -apiVersion: hbase.stackable.tech/v1alpha1 -kind: HbaseCluster -metadata: - name: hbase - namespace: default - deletionTimestamp: "2026-08-14T12:00:00Z" -spec: {} -"#, - ) - .expect("YAML parses; the invalid spec is captured inside the DeserializeGuard"); - - let action = - reconcile(hbase).expect("a deleted cluster reconciles even when its spec is invalid"); - - assert_eq!(action, Action::await_change()); - } - - #[test] - fn reconcile_proceeds_for_live_cluster() { - let hbase = serde_yaml::from_str( - r#" -apiVersion: hbase.stackable.tech/v1alpha1 -kind: HbaseCluster -metadata: - name: hbase - namespace: default -spec: - image: - productVersion: 2.6.3 - clusterConfig: - hdfsConfigMapName: simple-hdfs - zookeeperConfigMapName: simple-znode -"#, - ) - .expect("valid HbaseCluster YAML"); - - let result = reconcile(hbase); - - assert!( - matches!(result, Err(Error::Dereference { .. })), - "a live cluster must reach the API but when dereferencing against the unreachable test server: {result:?}" - ); - } } diff --git a/tests/templates/kuttl/cluster-operation/40-assert.yaml b/tests/templates/kuttl/cluster-operation/40-assert.yaml index 4101abe2..383419ec 100644 --- a/tests/templates/kuttl/cluster-operation/40-assert.yaml +++ b/tests/templates/kuttl/cluster-operation/40-assert.yaml @@ -2,13 +2,17 @@ # The recreated StatefulSets must bring the cluster back to ready, and the recreated # objects must carry an owner reference back to the HbaseCluster so that garbage # collection still works for them. +# +# The budget is generous on purpose: recreating the role-group ConfigMaps makes the +# commons-operator restarter roll every pod once more on top of the StatefulSet +# recreation, and on a node without a cached image a single pull can take minutes. apiVersion: kuttl.dev/v1beta1 kind: TestAssert metadata: name: recreate-owned-resources -timeout: 300 +timeout: 600 commands: - - script: kubectl -n $NAMESPACE wait --for=condition=available hbaseclusters.hbase.stackable.tech/test-hbase --timeout 301s + - script: kubectl -n $NAMESPACE wait --for=condition=available hbaseclusters.hbase.stackable.tech/test-hbase --timeout 601s --- apiVersion: apps/v1 kind: StatefulSet diff --git a/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml b/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml index 6ea2940d..f23afd5b 100644 --- a/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml +++ b/tests/templates/kuttl/cluster-operation/40-delete-owned-resources.yaml @@ -6,13 +6,25 @@ # registration (referenced-but-unowned ConfigMaps) can't be tested by deletion: the # operator never recreates what it didn't apply. # +# The resources are discovered by label rather than listed by name: everything the +# operator applies is guaranteed to carry the `instance` and `managed-by` labels +# (ClusterResources::add rejects resources without them), so a newly applied resource +# or kind is swept automatically — including one whose `.owns()` watch or ClusterRole +# `watch` verb was forgotten, which then fails the recreation check below. +# +# The labels alone over-match: derived objects inherit them (the listener-operator's +# per-pod Listeners, for example, carry this operator's `managed-by`), so each match +# is additionally gated on its controller ownerReference pointing at the HbaseCluster — +# the authoritative marker for "applied by this operator", and the same relationship +# the orphan cleanup in operator-rs filters on. Cheap bulk kinds that could never pass +# that gate are excluded up front: Pods and ControllerRevisions (StatefulSet-derived), +# Endpoints/EndpointSlices (Service-derived), PersistentVolumeClaims (bound claims +# wedge in Terminating under a running pod), and Events. +# # Recreation is proven by UID change: mere existence could pass without any deletion. # TestStep commands run exactly once (no kuttl retry loop), so the polling stays quiet. -# -# Deliberately skipped: the role-group ConfigMaps -# (test-hbase-{master,regionserver,restserver}-default) — `.owns(ConfigMap)` is already -# exercised via the discovery ConfigMap, and deleting ConfigMaps mounted into running -# pods would disturb them. +# Deleting the role-group ConfigMaps briefly leaves their mounts without a source, but +# recreation is a single reconcile away and already-mounted volumes are unaffected. apiVersion: kuttl.dev/v1beta1 kind: TestStep metadata: @@ -23,35 +35,41 @@ commands: set -eu delete_and_await_recreation() { - kind=$1 - name=$2 - old_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}') - kubectl delete -n "$NAMESPACE" "$kind" "$name" --wait=false + resource=$1 + old_uid=$(kubectl get -n "$NAMESPACE" "$resource" -o jsonpath='{.metadata.uid}') + kubectl delete -n "$NAMESPACE" "$resource" --wait=false # Recreation is a single reconcile away, so this normally succeeds on the # first iteration; 30s is a generous upper bound well below the step timeout. for _ in $(seq 1 30); do - new_uid=$(kubectl get -n "$NAMESPACE" "$kind" "$name" -o jsonpath='{.metadata.uid}' 2>/dev/null || true) + new_uid=$(kubectl get -n "$NAMESPACE" "$resource" -o jsonpath='{.metadata.uid}' 2>/dev/null || true) if [ -n "$new_uid" ] && [ "$new_uid" != "$old_uid" ]; then return 0 fi sleep 1 done - echo "$kind/$name was not recreated (old uid: $old_uid, current: '${new_uid:-}')" >&2 + echo "$resource was not recreated (old uid: $old_uid, current: '${new_uid:-}')" >&2 return 1 } - delete_and_await_recreation serviceaccount test-hbase-serviceaccount - delete_and_await_recreation rolebinding test-hbase-rolebinding - delete_and_await_recreation poddisruptionbudget test-hbase-master - delete_and_await_recreation poddisruptionbudget test-hbase-regionserver - delete_and_await_recreation poddisruptionbudget test-hbase-restserver - delete_and_await_recreation configmap test-hbase - delete_and_await_recreation service test-hbase-master-default-headless - delete_and_await_recreation service test-hbase-master-default-metrics - delete_and_await_recreation service test-hbase-regionserver-default-headless - delete_and_await_recreation service test-hbase-regionserver-default-metrics - delete_and_await_recreation service test-hbase-restserver-default-headless - delete_and_await_recreation service test-hbase-restserver-default-metrics - delete_and_await_recreation statefulset test-hbase-master-default - delete_and_await_recreation statefulset test-hbase-regionserver-default - delete_and_await_recreation statefulset test-hbase-restserver-default + selector="app.kubernetes.io/instance=test-hbase,app.kubernetes.io/managed-by=hbase.stackable.tech_hbasecluster" + excluded="^(pods|persistentvolumeclaims|endpoints|events)$|^endpointslices\.|^controllerrevisions\.|^events\." + + deleted=0 + for kind in $(kubectl api-resources --verbs=list --namespaced -o name | grep -Ev "$excluded" | sort); do + for resource in $(kubectl get -n "$NAMESPACE" "$kind" -l "$selector" -o name 2>/dev/null); do + owner=$(kubectl get -n "$NAMESPACE" "$resource" -o jsonpath='{.metadata.ownerReferences[?(@.controller==true)].kind}/{.metadata.ownerReferences[?(@.controller==true)].name}' 2>/dev/null || true) + if [ "$owner" != "HbaseCluster/test-hbase" ]; then + echo "skipping $resource: controller owner is '${owner:-none}', not the HbaseCluster" + continue + fi + delete_and_await_recreation "$resource" + deleted=$((deleted + 1)) + done + done + + # Guard against the sweep silently matching nothing (wrong selector, renamed + # labels): the fixture is known to produce well over this many owned resources. + if [ "$deleted" -lt 10 ]; then + echo "only $deleted labelled resources were swept - the label selector is broken" >&2 + exit 1 + fi