Skip to content

Commit 756a2f3

Browse files
author
sami-wazery
committed
feat: Add RBAC feature gate support
- Add featureGate parameter to +kubebuilder:rbac marker - Enable conditional RBAC rule generation based on feature gates - Add FeatureGate field to Rule struct and Generator struct - Implement feature gate parsing and filtering logic - Update GenerateRoles function to accept feature gates parameter - Add comprehensive test suite for feature gate functionality - Maintain backward compatibility with existing RBAC generation Example usage: // +kubebuilder:rbac:featureGate=alpha,groups=apps,resources=deployments,verbs=get;list controller-gen 'rbac:roleName=manager,featureGates="alpha=true,beta=false"' paths=./...
1 parent de2450b commit 756a2f3

File tree

4 files changed

+176
-3
lines changed

4 files changed

+176
-3
lines changed

pkg/rbac/feature_gates_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package rbac
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/onsi/gomega"
8+
"sigs.k8s.io/controller-tools/pkg/genall"
9+
"sigs.k8s.io/controller-tools/pkg/loader"
10+
"sigs.k8s.io/controller-tools/pkg/markers"
11+
rbacv1 "k8s.io/api/rbac/v1"
12+
)
13+
14+
func TestFeatureGates(t *testing.T) {
15+
g := gomega.NewWithT(t)
16+
17+
// Load test packages
18+
pkgs, err := loader.LoadRoots("./testdata/feature_gates")
19+
g.Expect(err).NotTo(gomega.HaveOccurred())
20+
21+
// Set up generation context
22+
reg := &markers.Registry{}
23+
g.Expect(reg.Register(RuleDefinition)).To(gomega.Succeed())
24+
25+
ctx := &genall.GenerationContext{
26+
Collector: &markers.Collector{Registry: reg},
27+
Roots: pkgs,
28+
}
29+
30+
tests := []struct {
31+
name string
32+
featureGates string
33+
expectedRules int
34+
shouldContain []string
35+
shouldNotContain []string
36+
}{
37+
{
38+
name: "no feature gates",
39+
featureGates: "",
40+
expectedRules: 2, // only always-on rules
41+
shouldContain: []string{"pods", "configmaps"},
42+
shouldNotContain: []string{"deployments", "ingresses"},
43+
},
44+
{
45+
name: "alpha enabled",
46+
featureGates: "alpha=true",
47+
expectedRules: 3, // always-on + alpha
48+
shouldContain: []string{"pods", "configmaps", "deployments"},
49+
shouldNotContain: []string{"ingresses"},
50+
},
51+
{
52+
name: "beta enabled",
53+
featureGates: "beta=true",
54+
expectedRules: 3, // always-on + beta
55+
shouldContain: []string{"pods", "configmaps", "ingresses"},
56+
shouldNotContain: []string{"deployments"},
57+
},
58+
{
59+
name: "both enabled",
60+
featureGates: "alpha=true,beta=true",
61+
expectedRules: 4, // all rules
62+
shouldContain: []string{"pods", "configmaps", "deployments", "ingresses"},
63+
shouldNotContain: []string{},
64+
},
65+
{
66+
name: "alpha enabled beta disabled",
67+
featureGates: "alpha=true,beta=false",
68+
expectedRules: 3, // always-on + alpha
69+
shouldContain: []string{"pods", "configmaps", "deployments"},
70+
shouldNotContain: []string{"ingresses"},
71+
},
72+
}
73+
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
g := gomega.NewWithT(t)
77+
78+
objs, err := GenerateRoles(ctx, "test-role", tt.featureGates)
79+
g.Expect(err).NotTo(gomega.HaveOccurred())
80+
g.Expect(objs).To(gomega.HaveLen(1))
81+
82+
role, ok := objs[0].(rbacv1.ClusterRole)
83+
g.Expect(ok).To(gomega.BeTrue())
84+
g.Expect(role.Rules).To(gomega.HaveLen(tt.expectedRules))
85+
86+
// Convert rules to string for easier checking
87+
rulesStr := ""
88+
for _, rule := range role.Rules {
89+
rulesStr += strings.Join(rule.Resources, ",") + " "
90+
}
91+
92+
for _, resource := range tt.shouldContain {
93+
g.Expect(rulesStr).To(gomega.ContainSubstring(resource),
94+
"Expected resource %s to be present", resource)
95+
}
96+
97+
for _, resource := range tt.shouldNotContain {
98+
g.Expect(rulesStr).NotTo(gomega.ContainSubstring(resource),
99+
"Expected resource %s to be absent", resource)
100+
}
101+
})
102+
}
103+
}

pkg/rbac/parser.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ type Rule struct {
6060
// If not set, the Rule belongs to the generated ClusterRole.
6161
// If set, the Rule belongs to a Role, whose namespace is specified by this field.
6262
Namespace string `marker:",optional"`
63+
// FeatureGate specifies the feature gate that controls this RBAC rule.
64+
// If not set, the rule is always included.
65+
// If set, the rule is only included when the specified feature gate is enabled.
66+
FeatureGate string `marker:"featureGate,optional"`
6367
}
6468

6569
// ruleKey represents the resources and non-resources a Rule applies.
@@ -169,6 +173,10 @@ type Generator struct {
169173

170174
// Year specifies the year to substitute for " YEAR" in the header file.
171175
Year string `marker:",optional"`
176+
177+
// FeatureGates is a comma-separated list of feature gates to enable (e.g., "alpha=true,beta=false").
178+
// Only RBAC rules with matching feature gates will be included in the generated output.
179+
FeatureGates string `marker:",optional"`
172180
}
173181

174182
func (Generator) RegisterMarkers(into *markers.Registry) error {
@@ -179,9 +187,46 @@ func (Generator) RegisterMarkers(into *markers.Registry) error {
179187
return nil
180188
}
181189

190+
// FeatureGateMap represents enabled feature gates as a map for efficient lookup
191+
type FeatureGateMap map[string]bool
192+
193+
// parseFeatureGates parses a comma-separated feature gate string into a map
194+
// Format: "gate1=true,gate2=false,gate3=true"
195+
func parseFeatureGates(featureGates string) FeatureGateMap {
196+
gates := make(FeatureGateMap)
197+
if featureGates == "" {
198+
return gates
199+
}
200+
201+
pairs := strings.Split(featureGates, ",")
202+
for _, pair := range pairs {
203+
parts := strings.Split(strings.TrimSpace(pair), "=")
204+
if len(parts) != 2 {
205+
continue
206+
}
207+
gateName := strings.TrimSpace(parts[0])
208+
gateValue := strings.TrimSpace(parts[1])
209+
gates[gateName] = gateValue == "true"
210+
}
211+
return gates
212+
}
213+
214+
// shouldIncludeRule determines if an RBAC rule should be included based on feature gates
215+
func shouldIncludeRule(rule *Rule, enabledGates FeatureGateMap) bool {
216+
if rule.FeatureGate == "" {
217+
// No feature gate specified, always include
218+
return true
219+
}
220+
221+
// Check if the feature gate is enabled
222+
enabled, exists := enabledGates[rule.FeatureGate]
223+
return exists && enabled
224+
}
225+
182226
// GenerateRoles generate a slice of objs representing either a ClusterRole or a Role object
183227
// The order of the objs in the returned slice is stable and determined by their namespaces.
184-
func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{}, error) {
228+
func GenerateRoles(ctx *genall.GenerationContext, roleName string, featureGates string) ([]interface{}, error) {
229+
enabledGates := parseFeatureGates(featureGates)
185230
rulesByNSResource := make(map[string][]*Rule)
186231
for _, root := range ctx.Roots {
187232
markerSet, err := markers.PackageMarkers(ctx.Collector, root)
@@ -192,6 +237,12 @@ func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{
192237
// group RBAC markers by namespace and separate by resource
193238
for _, markerValue := range markerSet[RuleDefinition.Name] {
194239
rule := markerValue.(Rule)
240+
241+
// Apply feature gate filtering
242+
if !shouldIncludeRule(&rule, enabledGates) {
243+
continue
244+
}
245+
195246
if len(rule.Resources) == 0 {
196247
// Add a rule without any resource if Resources is empty.
197248
r := Rule{
@@ -201,6 +252,7 @@ func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{
201252
URLs: rule.URLs,
202253
Namespace: rule.Namespace,
203254
Verbs: rule.Verbs,
255+
FeatureGate: rule.FeatureGate,
204256
}
205257
namespace := r.Namespace
206258
rulesByNSResource[namespace] = append(rulesByNSResource[namespace], &r)
@@ -214,6 +266,7 @@ func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{
214266
URLs: rule.URLs,
215267
Namespace: rule.Namespace,
216268
Verbs: rule.Verbs,
269+
FeatureGate: rule.FeatureGate,
217270
}
218271
namespace := r.Namespace
219272
rulesByNSResource[namespace] = append(rulesByNSResource[namespace], &r)
@@ -367,7 +420,7 @@ func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{
367420
}
368421

369422
func (g Generator) Generate(ctx *genall.GenerationContext) error {
370-
objs, err := GenerateRoles(ctx, g.RoleName)
423+
objs, err := GenerateRoles(ctx, g.RoleName, g.FeatureGates)
371424
if err != nil {
372425
return err
373426
}

pkg/rbac/parser_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ var _ = Describe("ClusterRole generated by the RBAC Generator", func() {
4242
}
4343

4444
By("generating a ClusterRole")
45-
objs, err := rbac.GenerateRoles(ctx, "manager-role")
45+
objs, err := rbac.GenerateRoles(ctx, "manager-role", "")
4646
Expect(err).NotTo(HaveOccurred())
4747

4848
By("loading the desired YAML")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package testdata
2+
3+
// Always included RBAC rule
4+
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
5+
6+
// Another always included rule
7+
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list
8+
9+
// Alpha feature gate RBAC rule
10+
// +kubebuilder:rbac:featureGate=alpha,groups=apps,resources=deployments,verbs=get;list;create;update;delete
11+
12+
// Beta feature gate RBAC rule
13+
// +kubebuilder:rbac:featureGate=beta,groups=extensions,resources=ingresses,verbs=get;list;create;update;delete
14+
15+
func main() {
16+
// Test file for RBAC feature gates
17+
}

0 commit comments

Comments
 (0)