feat: configure k8s recommended labels on subresources

Signed-off-by: Gabriele Fedi <gabriele.fedi@enterprisedb.com>
This commit is contained in:
Gabriele Fedi 2026-04-20 17:12:43 +02:00 committed by Leonardo Cecchi
parent a81f6e3d73
commit 3b6c25c2dc
5 changed files with 181 additions and 42 deletions

View File

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

View File

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

View File

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

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"
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{
{