Skip to content

Commit f09d1d9

Browse files
wazerysami-wazery
authored andcommitted
Add feature gate marker support for conditional CRD field inclusion
Implements +kubebuilder:feature-gate=<gate-name> marker that allows fields to be conditionally included in generated CRDs based on enabled feature gates. - Add FeatureGate marker type in pkg/crd/markers/featuregate.go - Register feature gate markers in marker registry - Add CLI parameter crd:featureGates=gate1=true,gate2=false - Implement conditional field inclusion in schema generation - Add feature gate context to Parser struct Addresses #1238
1 parent 1ad88b0 commit f09d1d9

File tree

6 files changed

+131
-3
lines changed

6 files changed

+131
-3
lines changed

pkg/crd/gen.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,46 @@ import (
3232
"sigs.k8s.io/controller-tools/pkg/version"
3333
)
3434

35+
// FeatureGateMap represents a map of feature gate names to their enabled status.
36+
type FeatureGateMap map[string]bool
37+
38+
// parseFeatureGates parses a feature gates string in the format "gate1=true,gate2=false"
39+
// and returns a FeatureGateMap.
40+
func parseFeatureGates(featureGatesStr string) (FeatureGateMap, error) {
41+
gates := make(FeatureGateMap)
42+
if featureGatesStr == "" {
43+
return gates, nil
44+
}
45+
46+
pairs := strings.Split(featureGatesStr, ",")
47+
for _, pair := range pairs {
48+
parts := strings.Split(strings.TrimSpace(pair), "=")
49+
if len(parts) != 2 {
50+
return nil, fmt.Errorf("invalid feature gate format: %s (expected format: gate1=true,gate2=false)", pair)
51+
}
52+
53+
name := strings.TrimSpace(parts[0])
54+
valueStr := strings.TrimSpace(parts[1])
55+
56+
switch valueStr {
57+
case "true":
58+
gates[name] = true
59+
case "false":
60+
gates[name] = false
61+
default:
62+
return nil, fmt.Errorf("invalid feature gate value for %s: %s (must be 'true' or 'false')", name, valueStr)
63+
}
64+
}
65+
66+
return gates, nil
67+
}
68+
69+
// isFeatureGateEnabled checks if a feature gate is enabled.
70+
func (fg FeatureGateMap) isEnabled(gateName string) bool {
71+
enabled, exists := fg[gateName]
72+
return exists && enabled
73+
}
74+
3575
// The identifier for v1 CustomResourceDefinitions.
3676
const v1 = "v1"
3777

@@ -85,6 +125,11 @@ type Generator struct {
85125
// Year specifies the year to substitute for " YEAR" in the header file.
86126
Year string `marker:",optional"`
87127

128+
// FeatureGates specifies which feature gates are enabled for conditional field inclusion.
129+
//
130+
// Format: "gate1=true,gate2=false"
131+
FeatureGates string `marker:",optional"`
132+
88133
// DeprecatedV1beta1CompatibilityPreserveUnknownFields indicates whether
89134
// or not we should turn off field pruning for this resource.
90135
//
@@ -124,6 +169,11 @@ func transformPreserveUnknownFields(value bool) func(map[string]interface{}) err
124169
}
125170

126171
func (g Generator) Generate(ctx *genall.GenerationContext) error {
172+
featureGates, err := parseFeatureGates(g.FeatureGates)
173+
if err != nil {
174+
return fmt.Errorf("invalid feature gates: %w", err)
175+
}
176+
127177
parser := &Parser{
128178
Collector: ctx.Collector,
129179
Checker: ctx.Checker,
@@ -132,6 +182,7 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error {
132182
AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes,
133183
// Indicates the parser on whether to register the ObjectMeta type or not
134184
GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta,
185+
FeatureGates: featureGates,
135186
}
136187

137188
AddKnownTypes(parser)

pkg/crd/markers/featuregate.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package markers
18+
19+
import (
20+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
21+
"sigs.k8s.io/controller-tools/pkg/markers"
22+
)
23+
24+
// +controllertools:marker:generateHelp:category="CRD feature gates"
25+
// FeatureGate marks a field or type to be conditionally included based on feature gate enablement.
26+
// The field will only be included in generated CRDs when the specified feature gate is enabled.
27+
type FeatureGate string
28+
29+
// ApplyToSchema implements SchemaMarker interface.
30+
// This marker doesn't directly modify the schema - it's used by the generator
31+
// to conditionally include/exclude fields during CRD generation.
32+
func (m FeatureGate) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
33+
// Feature gate markers don't modify the schema directly.
34+
// They are processed by the generator to conditionally include/exclude fields.
35+
return nil
36+
}
37+
38+
// Help returns the help information for this marker.
39+
func (FeatureGate) Help() *markers.DefinitionHelp {
40+
return &markers.DefinitionHelp{
41+
Category: "CRD feature gates",
42+
DetailedHelp: markers.DetailedHelp{
43+
Summary: "marks a field to be conditionally included based on feature gate enablement",
44+
Details: "Fields marked with +kubebuilder:feature-gate will only be included in generated CRDs when the specified feature gate is enabled via --feature-gates flag.",
45+
},
46+
FieldHelp: map[string]markers.DetailedHelp{
47+
"": {
48+
Summary: "the name of the feature gate that controls this field",
49+
Details: "The feature gate name should match gates passed via --feature-gates=gate1=true,gate2=false",
50+
},
51+
},
52+
}
53+
}

pkg/crd/markers/validation.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ var ValidationIshMarkers = []*definitionWithHelp{
132132
func init() {
133133
AllDefinitions = append(AllDefinitions, ValidationMarkers...)
134134

135+
// Add FeatureGate markers
136+
featureGateMarkers := []*definitionWithHelp{
137+
must(markers.MakeDefinition("kubebuilder:feature-gate", markers.DescribesField, FeatureGate(""))).WithHelp(FeatureGate("").Help()),
138+
must(markers.MakeDefinition("kubebuilder:feature-gate", markers.DescribesType, FeatureGate(""))).WithHelp(FeatureGate("").Help()),
139+
}
140+
AllDefinitions = append(AllDefinitions, featureGateMarkers...)
141+
135142
for _, def := range ValidationMarkers {
136143
typDef := def.clone()
137144
typDef.Target = markers.DescribesType

pkg/crd/parser.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ type Parser struct {
9292

9393
// GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta should be generated
9494
GenerateEmbeddedObjectMeta bool
95+
96+
// FeatureGates specifies which feature gates are enabled for conditional field inclusion
97+
FeatureGates FeatureGateMap
9598
}
9699

97100
func (p *Parser) init() {
@@ -172,7 +175,7 @@ func (p *Parser) NeedSchemaFor(typ TypeIdent) {
172175
// avoid tripping recursive schemata, like ManagedFields, by adding an empty WIP schema
173176
p.Schemata[typ] = apiext.JSONSchemaProps{}
174177

175-
schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes, p.IgnoreUnexportedFields)
178+
schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes, p.IgnoreUnexportedFields, p.FeatureGates)
176179
ctxForInfo := schemaCtx.ForInfo(info)
177180

178181
pkgMarkers, err := markers.PackageMarkers(p.Collector, typ.Package)

pkg/crd/schema.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,19 @@ type schemaContext struct {
7272

7373
allowDangerousTypes bool
7474
ignoreUnexportedFields bool
75+
featureGates FeatureGateMap
7576
}
7677

7778
// newSchemaContext constructs a new schemaContext for the given package and schema requester.
7879
// It must have type info added before use via ForInfo.
79-
func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTypes, ignoreUnexportedFields bool) *schemaContext {
80+
func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTypes, ignoreUnexportedFields bool, featureGates FeatureGateMap) *schemaContext {
8081
pkg.NeedTypesInfo()
8182
return &schemaContext{
8283
pkg: pkg,
8384
schemaRequester: req,
8485
allowDangerousTypes: allowDangerousTypes,
8586
ignoreUnexportedFields: ignoreUnexportedFields,
87+
featureGates: featureGates,
8688
}
8789
}
8890

@@ -95,6 +97,7 @@ func (c *schemaContext) ForInfo(info *markers.TypeInfo) *schemaContext {
9597
schemaRequester: c.schemaRequester,
9698
allowDangerousTypes: c.allowDangerousTypes,
9799
ignoreUnexportedFields: c.ignoreUnexportedFields,
100+
featureGates: c.featureGates,
98101
}
99102
}
100103

@@ -428,6 +431,17 @@ func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSON
428431
continue
429432
}
430433

434+
// Check feature gate markers - skip field if feature gate is not enabled
435+
if featureGateMarker := field.Markers.Get("kubebuilder:feature-gate"); featureGateMarker != nil {
436+
if featureGate, ok := featureGateMarker.(crdmarkers.FeatureGate); ok {
437+
gateName := string(featureGate)
438+
if !ctx.featureGates.isEnabled(gateName) {
439+
// Skip this field as its feature gate is not enabled
440+
continue
441+
}
442+
}
443+
}
444+
431445
jsonTag, hasTag := field.Tag.Lookup("json")
432446
if !hasTag {
433447
// if the field doesn't have a JSON tag, it doesn't belong in output (and shouldn't exist in a serialized type)

pkg/crd/schema_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func transform(t *testing.T, expr string) *apiext.JSONSchemaProps {
6464
pkg.NeedTypesInfo()
6565
failIfErrors(t, pkg.Errors)
6666

67-
schemaContext := newSchemaContext(pkg, nil, true, false).ForInfo(&markers.TypeInfo{})
67+
schemaContext := newSchemaContext(pkg, nil, true, false, FeatureGateMap{}).ForInfo(&markers.TypeInfo{})
6868
// yick: grab the only type definition
6969
definedType := pkg.Syntax[0].Decls[0].(*ast.GenDecl).Specs[0].(*ast.TypeSpec).Type
7070
result := typeToSchema(schemaContext, definedType)

0 commit comments

Comments
 (0)