Skip to content

Commit ad4f3d2

Browse files
committed
fix(rbac): reconcile Role when ObjectStore spec changes
When an ObjectStore's credentials change (e.g., secret rename), the RBAC Role granting the Cluster's ServiceAccount access to those secrets was not updated because nothing triggered a Cluster reconciliation. Implement the ObjectStore controller's Reconcile to detect affected Roles and update their rules directly, without needing access to Cluster objects. The controller lists Roles by a well-known label (barmancloud.cnpg.io/cluster), inspects their rules to find which ObjectStores they reference, fetches those ObjectStores, and patches the rules to match the current specs. Extract BuildRoleRules into a shared function and add ObjectStoreNamesFromRole for semantic rule inspection. Add EnsureRoleRules for the ObjectStore controller path (rules-only patch, no owner references). Refactor patchRole to use retry.RetryOnConflict (matching the existing project pattern) and support atomic label + rules patching. Replace the custom setOwnerReference helper (ownership.go) with controllerutil.SetControllerReference. Add dynamic CNPG scheme registration (internal/scheme) to the operator, instance, and restore managers. Remove clusters get/list/watch RBAC permission (the plugin no longer lists Cluster objects). Retain clusters/finalizers update permission required by OwnerReferencesPermissionEnforcement admission controller (see #465). Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
1 parent 3593c18 commit ad4f3d2

9 files changed

Lines changed: 678 additions & 273 deletions

File tree

config/rbac/role.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ rules:
4444
- postgresql.cnpg.io
4545
resources:
4646
- backups
47-
- clusters
4847
verbs:
4948
- get
5049
- list

internal/cnpgi/metadata/constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import "github.com/cloudnative-pg/cnpg-i/pkg/identity"
2626
const PluginName = "barman-cloud.cloudnative-pg.io"
2727

2828
const (
29+
// ClusterLabelName is the label applied to RBAC resources created
30+
// by this plugin. Its value is the name of the owning Cluster.
31+
ClusterLabelName = "barmancloud.cnpg.io/cluster"
32+
2933
// CheckEmptyWalArchiveFile is the name of the file in the PGDATA that,
3034
// if present, requires the WAL archiver to check that the backup object
3135
// store is empty.

internal/cnpgi/operator/rbac/ensure.go

Lines changed: 93 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -29,130 +29,149 @@ import (
2929
rbacv1 "k8s.io/api/rbac/v1"
3030
"k8s.io/apimachinery/pkg/api/equality"
3131
apierrs "k8s.io/apimachinery/pkg/api/errors"
32+
"k8s.io/client-go/util/retry"
3233
"sigs.k8s.io/controller-runtime/pkg/client"
3334
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3435

3536
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
37+
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
3638
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
3739
)
3840

3941
// EnsureRole ensures the RBAC Role for the given Cluster matches
4042
// the desired state derived from the given ObjectStores. On creation,
4143
// the Cluster is set as the owner of the Role for garbage collection.
4244
//
43-
// This function is called from both the Pre hook (gRPC) and the
44-
// ObjectStore controller. To handle concurrent modifications
45-
// gracefully, AlreadyExists on Create and Conflict on Patch are
46-
// retried once rather than returned as errors.
45+
// This function is called from the Pre hook (gRPC). It creates the
46+
// Role if it does not exist, then patches rules and labels to match
47+
// the desired state.
4748
func EnsureRole(
4849
ctx context.Context,
4950
c client.Client,
5051
cluster *cnpgv1.Cluster,
5152
barmanObjects []barmancloudv1.ObjectStore,
5253
) error {
5354
newRole := specs.BuildRole(cluster, barmanObjects)
55+
roleKey := client.ObjectKeyFromObject(newRole)
5456

55-
roleKey := client.ObjectKey{
56-
Namespace: newRole.Namespace,
57-
Name: newRole.Name,
57+
if err := ensureRoleExists(ctx, c, cluster, newRole); err != nil {
58+
return err
5859
}
5960

60-
var role rbacv1.Role
61-
err := c.Get(ctx, roleKey, &role)
62-
63-
switch {
64-
case apierrs.IsNotFound(err):
65-
role, err := createRole(ctx, c, cluster, newRole)
66-
if err != nil {
67-
return err
68-
}
69-
if role == nil {
70-
// Created successfully, nothing else to do.
71-
return nil
72-
}
73-
// AlreadyExists: fall through to patch with the re-read role.
74-
return patchRoleRules(ctx, c, newRole.Rules, role)
75-
76-
case err != nil:
77-
return err
61+
return patchRole(ctx, c, roleKey, newRole.Rules, map[string]string{
62+
metadata.ClusterLabelName: cluster.Name,
63+
})
64+
}
7865

79-
default:
80-
return patchRoleRules(ctx, c, newRole.Rules, &role)
66+
// EnsureRoleRules updates the rules of an existing Role to match
67+
// the desired state derived from the given ObjectStores. Unlike
68+
// EnsureRole, this function does not create Roles or set owner
69+
// references — it only patches rules on Roles that already exist.
70+
// It is intended for the ObjectStore controller path where no
71+
// Cluster object is available. Returns nil if the Role does not
72+
// exist (the Pre hook has not created it yet).
73+
func EnsureRoleRules(
74+
ctx context.Context,
75+
c client.Client,
76+
roleKey client.ObjectKey,
77+
barmanObjects []barmancloudv1.ObjectStore,
78+
) error {
79+
err := patchRole(ctx, c, roleKey, specs.BuildRoleRules(barmanObjects), nil)
80+
if apierrs.IsNotFound(err) {
81+
log.FromContext(ctx).Debug("Role not found, skipping rule update",
82+
"name", roleKey.Name, "namespace", roleKey.Namespace)
83+
return nil
8184
}
85+
86+
return err
8287
}
8388

84-
// createRole attempts to create the Role. If another writer created
85-
// it concurrently (AlreadyExists), it re-reads and returns the
86-
// existing Role for the caller to patch. On success it returns nil.
87-
func createRole(
89+
// ensureRoleExists creates the Role if it does not exist. Returns
90+
// nil on success and nil on AlreadyExists (another writer created
91+
// it concurrently). The caller always follows up with patchRole.
92+
func ensureRoleExists(
8893
ctx context.Context,
8994
c client.Client,
9095
cluster *cnpgv1.Cluster,
9196
newRole *rbacv1.Role,
92-
) (*rbacv1.Role, error) {
97+
) error {
9398
contextLogger := log.FromContext(ctx)
9499

100+
var existing rbacv1.Role
101+
err := c.Get(ctx, client.ObjectKeyFromObject(newRole), &existing)
102+
if err == nil {
103+
return nil
104+
}
105+
if !apierrs.IsNotFound(err) {
106+
return err
107+
}
108+
95109
if err := controllerutil.SetControllerReference(cluster, newRole, c.Scheme()); err != nil {
96-
return nil, err
110+
return err
97111
}
98112

99113
contextLogger.Info("Creating role",
100114
"name", newRole.Name, "namespace", newRole.Namespace)
101115

102116
createErr := c.Create(ctx, newRole)
103-
if createErr == nil {
104-
return nil, nil
105-
}
106-
if !apierrs.IsAlreadyExists(createErr) {
107-
return nil, createErr
108-
}
109-
110-
contextLogger.Info("Role was created concurrently, checking rules")
111-
112-
var role rbacv1.Role
113-
if err := c.Get(ctx, client.ObjectKeyFromObject(newRole), &role); err != nil {
114-
return nil, err
117+
if createErr == nil || apierrs.IsAlreadyExists(createErr) {
118+
return nil
115119
}
116120

117-
return &role, nil
121+
return createErr
118122
}
119123

120-
// patchRoleRules patches the Role's rules if they differ from the
121-
// desired state. On Conflict (concurrent modification), it retries
122-
// once with a fresh read.
123-
func patchRoleRules(
124+
// patchRole patches the Role's rules and optionally its labels to
125+
// match the desired state. When desiredLabels is nil, labels are
126+
// not modified. Uses retry.RetryOnConflict for concurrent
127+
// modification handling.
128+
func patchRole(
124129
ctx context.Context,
125130
c client.Client,
131+
roleKey client.ObjectKey,
126132
desiredRules []rbacv1.PolicyRule,
127-
role *rbacv1.Role,
133+
desiredLabels map[string]string,
128134
) error {
129-
if equality.Semantic.DeepEqual(desiredRules, role.Rules) {
130-
return nil
131-
}
135+
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
136+
var role rbacv1.Role
137+
if err := c.Get(ctx, roleKey, &role); err != nil {
138+
return err
139+
}
132140

133-
contextLogger := log.FromContext(ctx)
134-
contextLogger.Info("Patching role",
135-
"name", role.Name, "namespace", role.Namespace, "rules", desiredRules)
141+
rulesMatch := equality.Semantic.DeepEqual(desiredRules, role.Rules)
142+
labelsMatch := desiredLabels == nil || !labelsNeedUpdate(role.Labels, desiredLabels)
143+
144+
if rulesMatch && labelsMatch {
145+
return nil
146+
}
136147

137-
oldRole := role.DeepCopy()
138-
role.Rules = desiredRules
148+
contextLogger := log.FromContext(ctx)
149+
contextLogger.Info("Patching role",
150+
"name", role.Name, "namespace", role.Namespace)
139151

140-
patchErr := c.Patch(ctx, role, client.MergeFrom(oldRole))
141-
if patchErr == nil || !apierrs.IsConflict(patchErr) {
142-
return patchErr
143-
}
152+
oldRole := role.DeepCopy()
153+
role.Rules = desiredRules
144154

145-
// Conflict: re-read and retry once.
146-
contextLogger.Info("Role was modified concurrently, retrying patch")
147-
if err := c.Get(ctx, client.ObjectKeyFromObject(role), role); err != nil {
148-
return err
149-
}
150-
if equality.Semantic.DeepEqual(desiredRules, role.Rules) {
151-
return nil
152-
}
155+
if desiredLabels != nil {
156+
if role.Labels == nil {
157+
role.Labels = make(map[string]string, len(desiredLabels))
158+
}
159+
for k, v := range desiredLabels {
160+
role.Labels[k] = v
161+
}
162+
}
153163

154-
oldRole = role.DeepCopy()
155-
role.Rules = desiredRules
164+
return c.Patch(ctx, &role, client.MergeFrom(oldRole))
165+
})
166+
}
156167

157-
return c.Patch(ctx, role, client.MergeFrom(oldRole))
168+
// labelsNeedUpdate returns true if any key in desired is missing
169+
// or has a different value in existing.
170+
func labelsNeedUpdate(existing, desired map[string]string) bool {
171+
for k, v := range desired {
172+
if existing[k] != v {
173+
return true
174+
}
175+
}
176+
return false
158177
}

0 commit comments

Comments
 (0)