From 2b12aa2b18b2b865744665abdc236f117647f957 Mon Sep 17 00:00:00 2001 From: Gabriele Fedi Date: Mon, 20 Apr 2026 17:12:43 +0200 Subject: [PATCH 1/4] feat: configure k8s recommended labels on subresources Signed-off-by: Gabriele Fedi --- internal/cnpgi/operator/rbac/ensure.go | 69 +++++++++++++++++-- internal/cnpgi/operator/reconciler.go | 35 +--------- internal/cnpgi/operator/specs/metadata.go | 46 +++++++++++++ .../cnpgi/operator/specs/metadata_test.go | 67 ++++++++++++++++++ internal/cnpgi/operator/specs/role.go | 6 +- 5 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 internal/cnpgi/operator/specs/metadata.go create mode 100644 internal/cnpgi/operator/specs/metadata_test.go diff --git a/internal/cnpgi/operator/rbac/ensure.go b/internal/cnpgi/operator/rbac/ensure.go index b9d8c6a8..588d145d 100644 --- a/internal/cnpgi/operator/rbac/ensure.go +++ b/internal/cnpgi/operator/rbac/ensure.go @@ -31,7 +31,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1" - "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs" ) @@ -62,9 +61,7 @@ func EnsureRole( return err } - return patchRole(ctx, c, roleKey, newRole.Rules, map[string]string{ - metadata.ClusterLabelName: cluster.Name, - }) + return patchRole(ctx, c, roleKey, newRole.Rules, specs.GetRequiredLabels(cluster)) } // EnsureRoleRules updates the rules of an existing Role to match @@ -90,6 +87,48 @@ func EnsureRoleRules( return err } +// EnsureRoleBinding ensures the RoleBinding for the given Cluster matches +// the desired state. +// +// This function is called from the Pre hook (gRPC). It creates the RoleBinding +// if it does not exist, otherwise it patches RoleRef, Subjects, and labels to match +// the desired state. +func EnsureRoleBinding(ctx context.Context, c client.Client, cluster *cnpgv1.Cluster) error { + contextLogger := log.FromContext(ctx) + + desiredRoleBinding := specs.BuildRoleBinding(cluster) + if err := specs.SetControllerReference(cluster, desiredRoleBinding); err != nil { + return err + } + + roleBinding := &rbacv1.RoleBinding{} + + if err := c.Get(ctx, client.ObjectKey{ + Namespace: cluster.Namespace, + Name: specs.GetRBACName(cluster.Name), + }, roleBinding); err != nil { + if apierrs.IsNotFound(err) { + contextLogger.Info("Creating RoleBinding", "name", desiredRoleBinding.Name, + "namespace", desiredRoleBinding.Namespace) + return c.Create(ctx, desiredRoleBinding) + } + } + + if !roleBindingNeedsUpdate(roleBinding, desiredRoleBinding) { + return nil + } + + contextLogger.Info("Patching role binding", + "name", roleBinding.Name, "namespace", roleBinding.Namespace) + + oldRoleBinding := roleBinding.DeepCopy() + roleBinding.Labels = desiredRoleBinding.Labels + roleBinding.RoleRef = desiredRoleBinding.RoleRef + roleBinding.Subjects = desiredRoleBinding.Subjects + + return c.Patch(ctx, roleBinding, client.MergeFrom(oldRoleBinding)) +} + // ensureRoleExists creates the Role if it does not exist. Returns // nil on success and nil on AlreadyExists (another writer created // it concurrently). The caller always follows up with patchRole. @@ -179,3 +218,25 @@ func labelsNeedUpdate(existing, desired map[string]string) bool { } return false } + +// roleBindingNeedsUpdate returns true if the existing RoleBinding's +// RoleRef or Subjects differ from the desired, or if labels need update. +func roleBindingNeedsUpdate(existing, desired *rbacv1.RoleBinding) bool { + if existing == nil || desired == nil { + return existing != desired + } + + if !equality.Semantic.DeepEqual(existing.RoleRef, desired.RoleRef) { + return true + } + + if !equality.Semantic.DeepEqual(existing.Subjects, desired.Subjects) { + return true + } + + if labelsNeedUpdate(existing.Labels, desired.Labels) { + return true + } + + return false +} diff --git a/internal/cnpgi/operator/reconciler.go b/internal/cnpgi/operator/reconciler.go index 5ac61cd5..dd92dd87 100644 --- a/internal/cnpgi/operator/reconciler.go +++ b/internal/cnpgi/operator/reconciler.go @@ -27,14 +27,12 @@ import ( "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/object" "github.com/cloudnative-pg/cnpg-i/pkg/reconciler" "github.com/cloudnative-pg/machinery/pkg/log" - rbacv1 "k8s.io/api/rbac/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac" - "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs" ) // ReconcilerImplementation implements the Reconciler capability @@ -117,7 +115,7 @@ func (r ReconcilerImplementation) Pre( return nil, err } - if err := r.ensureRoleBinding(ctx, &cluster); err != nil { + if err := rbac.EnsureRoleBinding(ctx, r.Client, &cluster); err != nil { return nil, err } @@ -136,34 +134,3 @@ func (r ReconcilerImplementation) Post( Behavior: reconciler.ReconcilerHooksResult_BEHAVIOR_CONTINUE, }, nil } - -func (r ReconcilerImplementation) ensureRoleBinding( - ctx context.Context, - cluster *cnpgv1.Cluster, -) error { - var roleBinding rbacv1.RoleBinding - if err := r.Client.Get(ctx, client.ObjectKey{ - Namespace: cluster.Namespace, - Name: specs.GetRBACName(cluster.Name), - }, &roleBinding); err != nil { - if apierrs.IsNotFound(err) { - return r.createRoleBinding(ctx, cluster) - } - return err - } - - // TODO: this assumes role bindings never change. - // Is that true? Should we relax this assumption? - return nil -} - -func (r ReconcilerImplementation) createRoleBinding( - ctx context.Context, - cluster *cnpgv1.Cluster, -) error { - roleBinding := specs.BuildRoleBinding(cluster) - if err := specs.SetControllerReference(cluster, roleBinding); err != nil { - return err - } - return r.Client.Create(ctx, roleBinding) -} diff --git a/internal/cnpgi/operator/specs/metadata.go b/internal/cnpgi/operator/specs/metadata.go new file mode 100644 index 00000000..812f1810 --- /dev/null +++ b/internal/cnpgi/operator/specs/metadata.go @@ -0,0 +1,46 @@ +/* +Copyright © contributors to CloudNativePG, established as +CloudNativePG a Series of LF Projects, LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package specs + +import ( + "fmt" + + cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" + "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata" +) + +// GetRequiredLabels returns the labels that must be present on all plugin-managed objects for a given Cluster. +func GetRequiredLabels(cluster *cnpgv1.Cluster) map[string]string { + requiredLabels := map[string]string{ + metadata.ClusterLabelName: cluster.Name, + // Kubernetes recommended labels + utils.KubernetesAppLabelName: utils.AppName, + utils.KubernetesAppInstanceLabelName: cluster.Name, + utils.KubernetesAppManagedByLabelName: "plugin-barman-cloud", + utils.KubernetesAppComponentLabelName: utils.DatabaseComponentName, + } + + if version, err := cluster.GetPostgresqlMajorVersion(); err == nil && version != 0 { + requiredLabels[utils.KubernetesAppVersionLabelName] = fmt.Sprint(version) + } + + return requiredLabels +} diff --git a/internal/cnpgi/operator/specs/metadata_test.go b/internal/cnpgi/operator/specs/metadata_test.go new file mode 100644 index 00000000..bc53a250 --- /dev/null +++ b/internal/cnpgi/operator/specs/metadata_test.go @@ -0,0 +1,67 @@ +/* +Copyright © contributors to CloudNativePG, established as +CloudNativePG a Series of LF Projects, LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package specs + +import ( + cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata" +) + +var _ = Describe("GetRequiredLabels", func() { + It("should return all expected labels for a cluster without an image", func() { + cluster := &cnpgv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster", + Namespace: "default", + }, + } + labels := GetRequiredLabels(cluster) + + Expect(labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "my-cluster")) + Expect(labels).To(HaveKeyWithValue(utils.KubernetesAppLabelName, utils.AppName)) + Expect(labels).To(HaveKeyWithValue(utils.KubernetesAppInstanceLabelName, "my-cluster")) + Expect(labels).To(HaveKeyWithValue(utils.KubernetesAppManagedByLabelName, "plugin-barman-cloud")) + Expect(labels).To(HaveKeyWithValue(utils.KubernetesAppComponentLabelName, utils.DatabaseComponentName)) + Expect(labels).To(HaveLen(6)) + }) + + It("should use the major version from the image catalog ref", func() { + cluster := &cnpgv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pg16-cluster", + Namespace: "default", + }, + Spec: cnpgv1.ClusterSpec{ + ImageCatalogRef: &cnpgv1.ImageCatalogRef{ + Major: 16, + }, + }, + } + labels := GetRequiredLabels(cluster) + + Expect(labels).To(HaveKeyWithValue(utils.KubernetesAppVersionLabelName, "16")) + Expect(labels).To(HaveKeyWithValue(utils.KubernetesAppInstanceLabelName, "pg16-cluster")) + }) +}) diff --git a/internal/cnpgi/operator/specs/role.go b/internal/cnpgi/operator/specs/role.go index 0972f473..86af92d0 100644 --- a/internal/cnpgi/operator/specs/role.go +++ b/internal/cnpgi/operator/specs/role.go @@ -29,7 +29,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1" - "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata" ) // BuildRole builds the Role object for this cluster @@ -41,9 +40,7 @@ func BuildRole( ObjectMeta: metav1.ObjectMeta{ Namespace: cluster.Namespace, Name: GetRBACName(cluster.Name), - Labels: map[string]string{ - metadata.ClusterLabelName: cluster.Name, - }, + Labels: GetRequiredLabels(cluster), }, Rules: BuildRoleRules(barmanObjects), } @@ -131,6 +128,7 @@ func BuildRoleBinding( ObjectMeta: metav1.ObjectMeta{ Namespace: cluster.Namespace, Name: GetRBACName(cluster.Name), + Labels: GetRequiredLabels(cluster), }, Subjects: []rbacv1.Subject{ { From fea7cb1414518885404ee2617fda8af4d6566a65 Mon Sep 17 00:00:00 2001 From: Gabriele Fedi Date: Tue, 21 Apr 2026 10:01:25 +0200 Subject: [PATCH 2/4] fix: merge labels instead of replacing Signed-off-by: Gabriele Fedi --- internal/cnpgi/operator/rbac/ensure.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/internal/cnpgi/operator/rbac/ensure.go b/internal/cnpgi/operator/rbac/ensure.go index 588d145d..a39432f7 100644 --- a/internal/cnpgi/operator/rbac/ensure.go +++ b/internal/cnpgi/operator/rbac/ensure.go @@ -91,7 +91,7 @@ func EnsureRoleRules( // the desired state. // // This function is called from the Pre hook (gRPC). It creates the RoleBinding -// if it does not exist, otherwise it patches RoleRef, Subjects, and labels to match +// if it does not exist, otherwise it patches Subjects and labels to match // the desired state. func EnsureRoleBinding(ctx context.Context, c client.Client, cluster *cnpgv1.Cluster) error { contextLogger := log.FromContext(ctx) @@ -112,6 +112,7 @@ func EnsureRoleBinding(ctx context.Context, c client.Client, cluster *cnpgv1.Clu "namespace", desiredRoleBinding.Namespace) return c.Create(ctx, desiredRoleBinding) } + return err } if !roleBindingNeedsUpdate(roleBinding, desiredRoleBinding) { @@ -122,8 +123,12 @@ func EnsureRoleBinding(ctx context.Context, c client.Client, cluster *cnpgv1.Clu "name", roleBinding.Name, "namespace", roleBinding.Namespace) oldRoleBinding := roleBinding.DeepCopy() - roleBinding.Labels = desiredRoleBinding.Labels - roleBinding.RoleRef = desiredRoleBinding.RoleRef + if roleBinding.Labels == nil { + roleBinding.Labels = make(map[string]string, len(desiredRoleBinding.Labels)) + } + for k, v := range desiredRoleBinding.Labels { + roleBinding.Labels[k] = v + } roleBinding.Subjects = desiredRoleBinding.Subjects return c.Patch(ctx, roleBinding, client.MergeFrom(oldRoleBinding)) @@ -222,14 +227,6 @@ func labelsNeedUpdate(existing, desired map[string]string) bool { // roleBindingNeedsUpdate returns true if the existing RoleBinding's // RoleRef or Subjects differ from the desired, or if labels need update. func roleBindingNeedsUpdate(existing, desired *rbacv1.RoleBinding) bool { - if existing == nil || desired == nil { - return existing != desired - } - - if !equality.Semantic.DeepEqual(existing.RoleRef, desired.RoleRef) { - return true - } - if !equality.Semantic.DeepEqual(existing.Subjects, desired.Subjects) { return true } From a2657553e321fba19a548be3c1bbac32572003ce Mon Sep 17 00:00:00 2001 From: Gabriele Fedi Date: Tue, 21 Apr 2026 11:17:54 +0200 Subject: [PATCH 3/4] test: unit tests Signed-off-by: Gabriele Fedi --- internal/cnpgi/operator/rbac/ensure.go | 2 +- internal/cnpgi/operator/rbac/ensure_test.go | 210 +++++++++++++++++++- 2 files changed, 206 insertions(+), 6 deletions(-) diff --git a/internal/cnpgi/operator/rbac/ensure.go b/internal/cnpgi/operator/rbac/ensure.go index a39432f7..c23676c8 100644 --- a/internal/cnpgi/operator/rbac/ensure.go +++ b/internal/cnpgi/operator/rbac/ensure.go @@ -225,7 +225,7 @@ func labelsNeedUpdate(existing, desired map[string]string) bool { } // roleBindingNeedsUpdate returns true if the existing RoleBinding's -// RoleRef or Subjects differ from the desired, or if labels need update. +// Subjects differ from the desired or if labels need update. func roleBindingNeedsUpdate(existing, desired *rbacv1.RoleBinding) bool { if !equality.Semantic.DeepEqual(existing.Subjects, desired.Subjects) { return true diff --git a/internal/cnpgi/operator/rbac/ensure_test.go b/internal/cnpgi/operator/rbac/ensure_test.go index 56a78242..6214b63e 100644 --- a/internal/cnpgi/operator/rbac/ensure_test.go +++ b/internal/cnpgi/operator/rbac/ensure_test.go @@ -25,6 +25,7 @@ import ( barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" machineryapi "github.com/cloudnative-pg/machinery/pkg/api" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -42,6 +43,15 @@ import ( "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac" ) +func expectRequiredLabels(labels map[string]string, clusterName string) { + ExpectWithOffset(1, labels).To(HaveKeyWithValue(metadata.ClusterLabelName, clusterName)) + ExpectWithOffset(1, labels).To(HaveKeyWithValue(utils.KubernetesAppLabelName, utils.AppName)) + ExpectWithOffset(1, labels).To(HaveKeyWithValue(utils.KubernetesAppInstanceLabelName, clusterName)) + ExpectWithOffset(1, labels).To(HaveKeyWithValue(utils.KubernetesAppManagedByLabelName, "plugin-barman-cloud")) + ExpectWithOffset(1, labels).To(HaveKeyWithValue(utils.KubernetesAppComponentLabelName, utils.DatabaseComponentName)) + ExpectWithOffset(1, labels).To(HaveKey(utils.KubernetesAppVersionLabelName)) +} + func newScheme() *runtime.Scheme { s := runtime.NewScheme() utilruntime.Must(rbacv1.AddToScheme(s)) @@ -124,7 +134,7 @@ var _ = Describe("EnsureRole", func() { Expect(role.OwnerReferences[0].Name).To(Equal("test-cluster")) Expect(role.OwnerReferences[0].Kind).To(Equal("Cluster")) - Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster")) + expectRequiredLabels(role.Labels, "test-cluster") }) }) @@ -210,7 +220,7 @@ var _ = Describe("EnsureRole", func() { Name: "test-cluster-barman-cloud", Namespace: "default", Labels: map[string]string{ - "app.kubernetes.io/managed-by": "helm", + "custom-label": "custom-value", }, }, } @@ -227,8 +237,8 @@ var _ = Describe("EnsureRole", func() { Name: "test-cluster-barman-cloud", }, &role)).To(Succeed()) - Expect(role.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "helm")) - Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster")) + Expect(role.Labels).To(HaveKeyWithValue("custom-label", "custom-value")) + expectRequiredLabels(role.Labels, "test-cluster") }) }) @@ -257,12 +267,202 @@ var _ = Describe("EnsureRole", func() { Name: "test-cluster-barman-cloud", }, &role)).To(Succeed()) - Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster")) + expectRequiredLabels(role.Labels, "test-cluster") Expect(role.Rules).To(HaveLen(3)) }) }) }) +var _ = Describe("EnsureRoleBinding", func() { + var ( + ctx context.Context + cluster *cnpgv1.Cluster + fakeClient client.Client + ) + + BeforeEach(func() { + ctx = context.Background() + cluster = newCluster("test-cluster", "default") + }) + + Context("when the RoleBinding does not exist", func() { + BeforeEach(func() { + fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build() + }) + + It("should create the RoleBinding with owner reference, labels, and correct subjects", func() { + err := rbac.EnsureRoleBinding(ctx, fakeClient, cluster) + Expect(err).NotTo(HaveOccurred()) + + var rb rbacv1.RoleBinding + Expect(fakeClient.Get(ctx, client.ObjectKey{ + Namespace: "default", + Name: "test-cluster-barman-cloud", + }, &rb)).To(Succeed()) + + Expect(rb.OwnerReferences).To(HaveLen(1)) + Expect(rb.OwnerReferences[0].Name).To(Equal("test-cluster")) + Expect(rb.OwnerReferences[0].Kind).To(Equal("Cluster")) + + expectRequiredLabels(rb.Labels, "test-cluster") + + Expect(rb.Subjects).To(HaveLen(1)) + Expect(rb.Subjects[0].Name).To(Equal("test-cluster")) + Expect(rb.Subjects[0].Kind).To(Equal("ServiceAccount")) + + Expect(rb.RoleRef.Kind).To(Equal("Role")) + Expect(rb.RoleRef.Name).To(Equal("test-cluster-barman-cloud")) + }) + }) + + Context("when the RoleBinding exists with matching state", func() { + BeforeEach(func() { + fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build() + Expect(rbac.EnsureRoleBinding(ctx, fakeClient, cluster)).To(Succeed()) + }) + + It("should not patch the RoleBinding", func() { + var before rbacv1.RoleBinding + Expect(fakeClient.Get(ctx, client.ObjectKey{ + Namespace: "default", + Name: "test-cluster-barman-cloud", + }, &before)).To(Succeed()) + + err := rbac.EnsureRoleBinding(ctx, fakeClient, cluster) + Expect(err).NotTo(HaveOccurred()) + + var after rbacv1.RoleBinding + Expect(fakeClient.Get(ctx, client.ObjectKey{ + Namespace: "default", + Name: "test-cluster-barman-cloud", + }, &after)).To(Succeed()) + + Expect(after.ResourceVersion).To(Equal(before.ResourceVersion)) + }) + }) + + Context("when the RoleBinding exists with different subjects", func() { + BeforeEach(func() { + fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build() + existing := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-barman-cloud", + Namespace: "default", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "old-sa", + APIGroup: "", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-cluster-barman-cloud", + }, + } + Expect(fakeClient.Create(ctx, existing)).To(Succeed()) + }) + + It("should patch the subjects to match the desired state", func() { + err := rbac.EnsureRoleBinding(ctx, fakeClient, cluster) + Expect(err).NotTo(HaveOccurred()) + + var rb rbacv1.RoleBinding + Expect(fakeClient.Get(ctx, client.ObjectKey{ + Namespace: "default", + Name: "test-cluster-barman-cloud", + }, &rb)).To(Succeed()) + + Expect(rb.Subjects).To(HaveLen(1)) + Expect(rb.Subjects[0].Name).To(Equal("test-cluster")) + }) + }) + + Context("when the RoleBinding has pre-existing unrelated labels", func() { + BeforeEach(func() { + fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build() + existing := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-barman-cloud", + Namespace: "default", + Labels: map[string]string{ + "custom-label": "custom-value", + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "test-cluster", + Namespace: "default", + APIGroup: "", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-cluster-barman-cloud", + }, + } + Expect(fakeClient.Create(ctx, existing)).To(Succeed()) + }) + + It("should preserve unrelated labels while adding the required labels", func() { + err := rbac.EnsureRoleBinding(ctx, fakeClient, cluster) + Expect(err).NotTo(HaveOccurred()) + + var rb rbacv1.RoleBinding + Expect(fakeClient.Get(ctx, client.ObjectKey{ + Namespace: "default", + Name: "test-cluster-barman-cloud", + }, &rb)).To(Succeed()) + + Expect(rb.Labels).To(HaveKeyWithValue("custom-label", "custom-value")) + expectRequiredLabels(rb.Labels, "test-cluster") + }) + }) + + Context("when the RoleBinding exists without labels (upgrade scenario)", func() { + BeforeEach(func() { + fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build() + existing := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-barman-cloud", + Namespace: "default", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "test-cluster", + Namespace: "default", + APIGroup: "", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "test-cluster-barman-cloud", + }, + } + Expect(fakeClient.Create(ctx, existing)).To(Succeed()) + }) + + It("should add the required labels", func() { + err := rbac.EnsureRoleBinding(ctx, fakeClient, cluster) + Expect(err).NotTo(HaveOccurred()) + + var rb rbacv1.RoleBinding + Expect(fakeClient.Get(ctx, client.ObjectKey{ + Namespace: "default", + Name: "test-cluster-barman-cloud", + }, &rb)).To(Succeed()) + + expectRequiredLabels(rb.Labels, "test-cluster") + }) + }) +}) + var _ = Describe("EnsureRoleRules", func() { var ( ctx context.Context From 6d261ae068f76fb5ec71ca60b34ceb3f3a53d3d6 Mon Sep 17 00:00:00 2001 From: Gabriele Fedi Date: Tue, 21 Apr 2026 11:33:09 +0200 Subject: [PATCH 4/4] chore: lint fix Signed-off-by: Gabriele Fedi --- internal/cnpgi/operator/specs/metadata.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cnpgi/operator/specs/metadata.go b/internal/cnpgi/operator/specs/metadata.go index 812f1810..fc4c614c 100644 --- a/internal/cnpgi/operator/specs/metadata.go +++ b/internal/cnpgi/operator/specs/metadata.go @@ -24,6 +24,7 @@ import ( cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" + "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata" )