Skip to content

Commit 75e9cf1

Browse files
authored
CAP-3059 consolidate RBAC creation logic for custom resources (#2262)
1 parent c350dbf commit 75e9cf1

File tree

5 files changed

+446
-57
lines changed

5 files changed

+446
-57
lines changed

internal/controller/datadogagent/feature/kubernetesstatecore/rbac.go

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
package kubernetesstatecore
77

88
import (
9-
"maps"
10-
"slices"
119
"strings"
1210

1311
"github.com/gobuffalo/flect"
1412
rbacv1 "k8s.io/api/rbac/v1"
1513

14+
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils"
1615
"github.com/DataDog/datadog-operator/pkg/kubernetes/rbac"
1716
)
1817

@@ -136,33 +135,16 @@ func getRBACPolicyRules(collectorOpts collectorOptions) []rbacv1.PolicyRule {
136135

137136
// Add permissions for custom resources
138137
if len(collectorOpts.customResources) > 0 {
139-
// Group custom resources by API group using sets to avoid duplicates
140-
groupedResources := make(map[string]map[string]struct{})
138+
rbacBuilder := utils.NewRBACBuilder(commonVerbs...)
141139
for _, cr := range collectorOpts.customResources {
142-
apiGroup := cr.GroupVersionKind.Group
143140
// Use the resource plural if specified, otherwise derive it from the Kind
144141
resourceName := cr.ResourcePlural
145142
if resourceName == "" {
146143
resourceName = strings.ToLower(flect.Pluralize(cr.GroupVersionKind.Kind))
147144
}
148-
149-
if _, exists := groupedResources[apiGroup]; !exists {
150-
groupedResources[apiGroup] = make(map[string]struct{})
151-
}
152-
groupedResources[apiGroup][resourceName] = struct{}{}
153-
}
154-
155-
// Create RBAC rules for each API group
156-
for apiGroup, resourceSet := range groupedResources {
157-
// Convert set to sorted slice for deterministic output
158-
resources := slices.Sorted(maps.Keys(resourceSet))
159-
160-
rbacRules = append(rbacRules, rbacv1.PolicyRule{
161-
APIGroups: []string{apiGroup},
162-
Resources: resources,
163-
Verbs: commonVerbs,
164-
})
145+
rbacBuilder.AddGroupKind(cr.GroupVersionKind.Group, resourceName)
165146
}
147+
rbacRules = append(rbacRules, rbacBuilder.Build()...)
166148
}
167149

168150
return rbacRules

internal/controller/datadogagent/feature/orchestratorexplorer/rbac.go

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1515

1616
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/common"
17+
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils"
1718
"github.com/DataDog/datadog-operator/pkg/kubernetes/rbac"
1819
)
1920

@@ -125,12 +126,17 @@ func getRBACPolicyRules(logger logr.Logger, crs []string) []rbacv1.PolicyRule {
125126
},
126127
}
127128

128-
groupResources := mapAPIGroupsResources(logger, crs)
129-
for group, resources := range groupResources {
130-
rbacRules = append(rbacRules, rbacv1.PolicyRule{
131-
APIGroups: []string{group},
132-
Resources: append([]string{}, resources...),
133-
})
129+
if len(crs) > 0 {
130+
rbacBuilder := utils.NewRBACBuilder()
131+
for _, cr := range crs {
132+
crSplit := strings.Split(cr, "/")
133+
if len(crSplit) == 3 {
134+
rbacBuilder.AddGroupKind(crSplit[0], crSplit[2])
135+
} else {
136+
logger.Error(fmt.Errorf("unable to create cluster role for %s, skipping", cr), "correct format should be group/version/kind")
137+
}
138+
}
139+
rbacRules = append(rbacRules, rbacBuilder.Build()...)
134140
}
135141

136142
defaultVerbs := []string{
@@ -147,19 +153,3 @@ func getRBACPolicyRules(logger logr.Logger, crs []string) []rbacv1.PolicyRule {
147153

148154
return rbacRules
149155
}
150-
151-
func mapAPIGroupsResources(logger logr.Logger, customResources []string) map[string][]string {
152-
groupResources := make(map[string][]string, len(customResources))
153-
for _, cr := range customResources {
154-
crSplit := strings.Split(cr, "/")
155-
if len(crSplit) == 3 {
156-
if _, ok := groupResources[crSplit[0]]; !ok {
157-
groupResources[crSplit[0]] = make([]string, 0, len(customResources))
158-
}
159-
groupResources[crSplit[0]] = append(groupResources[crSplit[0]], crSplit[2])
160-
} else {
161-
logger.Error(fmt.Errorf("unable to create cluster role for %s, skipping", cr), "correct format should be group/version/kind")
162-
}
163-
}
164-
return groupResources
165-
}

internal/controller/datadogagent/feature/orchestratorexplorer/rbac_test.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,59 @@
66
package orchestratorexplorer
77

88
import (
9+
"strings"
910
"testing"
1011

11-
"github.com/go-logr/logr"
1212
"github.com/stretchr/testify/assert"
13-
)
13+
rbacv1 "k8s.io/api/rbac/v1"
1414

15-
func TestMapAPIGroupsResources(t *testing.T) {
15+
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils"
16+
)
1617

18+
func TestRBACBuilderFromCustomResourceStrings(t *testing.T) {
1719
for _, tt := range []struct {
1820
name string
1921
customResources []string
20-
expected map[string][]string
22+
expectedRules []rbacv1.PolicyRule
2123
}{
2224
{
2325
name: "empty crs",
2426
customResources: []string{},
25-
expected: map[string][]string{},
27+
expectedRules: nil,
2628
},
2729
{
2830
name: "two crs, same group",
2931
customResources: []string{"datadoghq.com/v1alpha1/datadogmetrics", "datadoghq.com/v1alpha1/watermarkpodautoscalers"},
30-
expected: map[string][]string{
31-
"datadoghq.com": {"datadogmetrics", "watermarkpodautoscalers"},
32+
expectedRules: []rbacv1.PolicyRule{
33+
{
34+
APIGroups: []string{"datadoghq.com"},
35+
Resources: []string{"datadogmetrics", "watermarkpodautoscalers"},
36+
},
3237
},
3338
},
3439
{
3540
name: "three crs, different groups",
3641
customResources: []string{"datadoghq.com/v1alpha1/datadogmetrics", "datadoghq.com/v1alpha1/watermarkpodautoscalers", "cilium.io/v1/ciliumendpoints"},
37-
expected: map[string][]string{
38-
"datadoghq.com": {"datadogmetrics", "watermarkpodautoscalers"},
39-
"cilium.io": {"ciliumendpoints"},
42+
expectedRules: []rbacv1.PolicyRule{
43+
{
44+
APIGroups: []string{"cilium.io"},
45+
Resources: []string{"ciliumendpoints"},
46+
},
47+
{
48+
APIGroups: []string{"datadoghq.com"},
49+
Resources: []string{"datadogmetrics", "watermarkpodautoscalers"},
50+
},
4051
},
4152
},
4253
} {
43-
actualGroupsResources := mapAPIGroupsResources(logr.Logger{}, tt.customResources)
44-
assert.Equal(t, tt.expected, actualGroupsResources)
54+
t.Run(tt.name, func(t *testing.T) {
55+
rbacBuilder := utils.NewRBACBuilder()
56+
for _, cr := range tt.customResources {
57+
crSplit := strings.Split(cr, "/")
58+
rbacBuilder.AddGroupKind(crSplit[0], crSplit[2])
59+
}
60+
actualRules := rbacBuilder.Build()
61+
assert.Equal(t, tt.expectedRules, actualRules)
62+
})
4563
}
46-
4764
}

internal/controller/datadogagent/feature/utils/utils.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
package utils
77

88
import (
9+
"maps"
10+
"slices"
911
"strconv"
12+
"strings"
1013

14+
rbacv1 "k8s.io/api/rbac/v1"
1115
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1216

1317
"github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1"
@@ -67,3 +71,98 @@ func HasAgentDataPlaneAnnotation(dda metav1.Object) bool {
6771
func HasFineGrainedKubeletAuthz(dda metav1.Object) bool {
6872
return hasFeatureEnableAnnotation(dda, EnableFineGrainedKubeletAuthz)
6973
}
74+
75+
// resourceSet represents a set of resources with their verbs
76+
type resourceSet map[string][]string
77+
78+
// groupedResources maps API groups to their resource sets
79+
type groupedResources map[string]resourceSet
80+
81+
// RBACBuilder provides a simple builder for creating RBAC policy rules for custom resources
82+
type RBACBuilder struct {
83+
// groupedResources stores resources grouped by API group
84+
groupedResources groupedResources
85+
// verbs stores the verbs to apply to all rules
86+
verbs []string
87+
}
88+
89+
// NewRBACBuilder creates a new RBACBuilder instance with the specified verbs
90+
func NewRBACBuilder(verbs ...string) *RBACBuilder {
91+
return &RBACBuilder{
92+
groupedResources: make(groupedResources),
93+
verbs: verbs,
94+
}
95+
}
96+
97+
// AddGroupKind adds a custom resource by group and resource name with optional verbs
98+
// If no verbs are provided, uses the default verbs from NewRBACBuilder
99+
func (rb *RBACBuilder) AddGroupKind(group, resource string, verbs ...string) *RBACBuilder {
100+
if _, exists := rb.groupedResources[group]; !exists {
101+
rb.groupedResources[group] = make(resourceSet)
102+
}
103+
104+
// Use provided verbs or fall back to default verbs
105+
resourceVerbs := verbs
106+
if len(verbs) == 0 {
107+
resourceVerbs = rb.verbs
108+
}
109+
110+
existingVerbs := rb.groupedResources[group][resource]
111+
allVerbs := append(existingVerbs, resourceVerbs...)
112+
verbSet := make(map[string]struct{})
113+
var uniqueVerbs []string
114+
for _, verb := range allVerbs {
115+
if _, exists := verbSet[verb]; !exists {
116+
verbSet[verb] = struct{}{}
117+
uniqueVerbs = append(uniqueVerbs, verb)
118+
}
119+
}
120+
121+
rb.groupedResources[group][resource] = uniqueVerbs
122+
return rb
123+
}
124+
125+
// Build creates the final RBAC policy rules
126+
func (rb *RBACBuilder) Build() []rbacv1.PolicyRule {
127+
if len(rb.groupedResources) == 0 {
128+
return nil
129+
}
130+
131+
var rbacRules []rbacv1.PolicyRule
132+
133+
// Sort API groups for deterministic output
134+
apiGroups := slices.Sorted(maps.Keys(rb.groupedResources))
135+
136+
// Create RBAC rules for each API group
137+
for _, apiGroup := range apiGroups {
138+
resourceSet := rb.groupedResources[apiGroup]
139+
140+
// Group resources by their verbs to minimize the number of rules
141+
verbToResources := make(map[string][]string)
142+
verbsKeyToActualVerbs := make(map[string][]string)
143+
144+
for resource, verbs := range resourceSet {
145+
verbsKey := strings.Join(verbs, ",")
146+
verbToResources[verbsKey] = append(verbToResources[verbsKey], resource)
147+
verbsKeyToActualVerbs[verbsKey] = verbs
148+
}
149+
150+
// Create one rule per verb combination
151+
// Sort verbsKeys for deterministic output
152+
verbsKeys := slices.Sorted(maps.Keys(verbToResources))
153+
for _, verbsKey := range verbsKeys {
154+
resources := verbToResources[verbsKey]
155+
slices.Sort(resources)
156+
157+
rule := rbacv1.PolicyRule{
158+
APIGroups: []string{apiGroup},
159+
Resources: resources,
160+
Verbs: verbsKeyToActualVerbs[verbsKey],
161+
}
162+
163+
rbacRules = append(rbacRules, rule)
164+
}
165+
}
166+
167+
return rbacRules
168+
}

0 commit comments

Comments
 (0)