This commit is contained in:
Gabriele Fedi 2026-04-27 09:47:56 +02:00 committed by GitHub
commit 166d553bb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 384 additions and 47 deletions

View File

@ -31,7 +31,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1" 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" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
) )
@ -62,9 +61,7 @@ func EnsureRole(
return err return err
} }
return patchRole(ctx, c, roleKey, newRole.Rules, map[string]string{ return patchRole(ctx, c, roleKey, newRole.Rules, specs.GetRequiredLabels(cluster))
metadata.ClusterLabelName: cluster.Name,
})
} }
// EnsureRoleRules updates the rules of an existing Role to match // EnsureRoleRules updates the rules of an existing Role to match
@ -90,6 +87,53 @@ func EnsureRoleRules(
return err 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 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)
}
return err
}
if !roleBindingNeedsUpdate(roleBinding, desiredRoleBinding) {
return nil
}
contextLogger.Info("Patching role binding",
"name", roleBinding.Name, "namespace", roleBinding.Namespace)
oldRoleBinding := roleBinding.DeepCopy()
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))
}
// ensureRoleExists creates the Role if it does not exist. Returns // ensureRoleExists creates the Role if it does not exist. Returns
// nil on success and nil on AlreadyExists (another writer created // nil on success and nil on AlreadyExists (another writer created
// it concurrently). The caller always follows up with patchRole. // it concurrently). The caller always follows up with patchRole.
@ -179,3 +223,17 @@ func labelsNeedUpdate(existing, desired map[string]string) bool {
} }
return false return false
} }
// roleBindingNeedsUpdate returns true if the existing RoleBinding's
// 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
}
if labelsNeedUpdate(existing.Labels, desired.Labels) {
return true
}
return false
}

View File

@ -25,6 +25,7 @@ import (
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" 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" machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -42,6 +43,15 @@ import (
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac" "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 { func newScheme() *runtime.Scheme {
s := runtime.NewScheme() s := runtime.NewScheme()
utilruntime.Must(rbacv1.AddToScheme(s)) 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].Name).To(Equal("test-cluster"))
Expect(role.OwnerReferences[0].Kind).To(Equal("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", Name: "test-cluster-barman-cloud",
Namespace: "default", Namespace: "default",
Labels: map[string]string{ 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", Name: "test-cluster-barman-cloud",
}, &role)).To(Succeed()) }, &role)).To(Succeed())
Expect(role.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "helm")) Expect(role.Labels).To(HaveKeyWithValue("custom-label", "custom-value"))
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster")) expectRequiredLabels(role.Labels, "test-cluster")
}) })
}) })
@ -257,12 +267,202 @@ var _ = Describe("EnsureRole", func() {
Name: "test-cluster-barman-cloud", Name: "test-cluster-barman-cloud",
}, &role)).To(Succeed()) }, &role)).To(Succeed())
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster")) expectRequiredLabels(role.Labels, "test-cluster")
Expect(role.Rules).To(HaveLen(3)) 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 _ = Describe("EnsureRoleRules", func() {
var ( var (
ctx context.Context ctx context.Context

View File

@ -27,14 +27,12 @@ import (
"github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/object" "github.com/cloudnative-pg/cnpg-i-machinery/pkg/pluginhelper/object"
"github.com/cloudnative-pg/cnpg-i/pkg/reconciler" "github.com/cloudnative-pg/cnpg-i/pkg/reconciler"
"github.com/cloudnative-pg/machinery/pkg/log" "github.com/cloudnative-pg/machinery/pkg/log"
rbacv1 "k8s.io/api/rbac/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors" apierrs "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1" 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/config"
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac" "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 // ReconcilerImplementation implements the Reconciler capability
@ -117,7 +115,7 @@ func (r ReconcilerImplementation) Pre(
return nil, err return nil, err
} }
if err := r.ensureRoleBinding(ctx, &cluster); err != nil { if err := rbac.EnsureRoleBinding(ctx, r.Client, &cluster); err != nil {
return nil, err return nil, err
} }
@ -136,34 +134,3 @@ func (r ReconcilerImplementation) Post(
Behavior: reconciler.ReconcilerHooksResult_BEHAVIOR_CONTINUE, Behavior: reconciler.ReconcilerHooksResult_BEHAVIOR_CONTINUE,
}, nil }, 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)
}

View File

@ -0,0 +1,47 @@
/*
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
}

View File

@ -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"))
})
})

View File

@ -29,7 +29,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/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 // BuildRole builds the Role object for this cluster
@ -41,9 +40,7 @@ func BuildRole(
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Namespace: cluster.Namespace, Namespace: cluster.Namespace,
Name: GetRBACName(cluster.Name), Name: GetRBACName(cluster.Name),
Labels: map[string]string{ Labels: GetRequiredLabels(cluster),
metadata.ClusterLabelName: cluster.Name,
},
}, },
Rules: BuildRoleRules(barmanObjects), Rules: BuildRoleRules(barmanObjects),
} }
@ -131,6 +128,7 @@ func BuildRoleBinding(
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Namespace: cluster.Namespace, Namespace: cluster.Namespace,
Name: GetRBACName(cluster.Name), Name: GetRBACName(cluster.Name),
Labels: GetRequiredLabels(cluster),
}, },
Subjects: []rbacv1.Subject{ Subjects: []rbacv1.Subject{
{ {