Skip to content

Commit 3001605

Browse files
author
sami-wazery
committed
Add feature gate support for CRD generation
- Add FeatureGates field to Generator struct - Implement parseFeatureGates function for CLI integration - Add FeatureGate marker type for field-level gating - Update schema generation to conditionally include fields - Add comprehensive integration tests - Support single and multi-gate syntax: featureGates="alpha=true,beta=true"
1 parent 4d70735 commit 3001605

File tree

5 files changed

+248
-30
lines changed

5 files changed

+248
-30
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
Copyright 2025.
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 crd_test
18+
19+
import (
20+
"bytes"
21+
"io"
22+
"os"
23+
"path/filepath"
24+
25+
"github.com/google/go-cmp/cmp"
26+
. "github.com/onsi/ginkgo"
27+
. "github.com/onsi/gomega"
28+
"sigs.k8s.io/controller-tools/pkg/crd"
29+
crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
30+
"sigs.k8s.io/controller-tools/pkg/genall"
31+
"sigs.k8s.io/controller-tools/pkg/loader"
32+
"sigs.k8s.io/controller-tools/pkg/markers"
33+
)
34+
35+
var _ = Describe("CRD Feature Gate Generation", func() {
36+
var (
37+
ctx *genall.GenerationContext
38+
out *featureGateOutputRule
39+
featureGateDir string
40+
originalWorkingDir string
41+
)
42+
43+
BeforeEach(func() {
44+
var err error
45+
originalWorkingDir, err = os.Getwd()
46+
Expect(err).NotTo(HaveOccurred())
47+
48+
featureGateDir = filepath.Join(originalWorkingDir, "testdata", "featuregates")
49+
50+
By("switching into featuregates testdata")
51+
err = os.Chdir(featureGateDir)
52+
Expect(err).NotTo(HaveOccurred())
53+
54+
By("loading the roots")
55+
pkgs, err := loader.LoadRoots(".")
56+
Expect(err).NotTo(HaveOccurred())
57+
Expect(pkgs).To(HaveLen(1))
58+
59+
out = &featureGateOutputRule{buf: &bytes.Buffer{}}
60+
ctx = &genall.GenerationContext{
61+
Collector: &markers.Collector{Registry: &markers.Registry{}},
62+
Roots: pkgs,
63+
Checker: &loader.TypeChecker{},
64+
OutputRule: out,
65+
}
66+
Expect(crdmarkers.Register(ctx.Collector.Registry)).To(Succeed())
67+
})
68+
69+
AfterEach(func() {
70+
By("restoring original working directory")
71+
err := os.Chdir(originalWorkingDir)
72+
Expect(err).NotTo(HaveOccurred())
73+
})
74+
75+
It("should not include feature-gated fields when no gates are enabled", func() {
76+
By("calling the generator")
77+
gen := &crd.Generator{
78+
CRDVersions: []string{"v1"},
79+
// No FeatureGates specified
80+
}
81+
Expect(gen.Generate(ctx)).NotTo(HaveOccurred())
82+
83+
By("loading the expected YAML")
84+
expectedFile, err := os.ReadFile("output_none/_featuregatetests.yaml")
85+
Expect(err).NotTo(HaveOccurred())
86+
87+
By("comparing the two")
88+
Expect(out.buf.String()).To(Equal(string(expectedFile)), cmp.Diff(out.buf.String(), string(expectedFile)))
89+
})
90+
91+
It("should include only alpha-gated fields when alpha gate is enabled", func() {
92+
By("calling the generator")
93+
gen := &crd.Generator{
94+
CRDVersions: []string{"v1"},
95+
FeatureGates: "alpha=true",
96+
}
97+
Expect(gen.Generate(ctx)).NotTo(HaveOccurred())
98+
99+
By("loading the expected YAML")
100+
expectedFile, err := os.ReadFile("output_alpha/_featuregatetests.yaml")
101+
Expect(err).NotTo(HaveOccurred())
102+
103+
By("comparing the two")
104+
Expect(out.buf.String()).To(Equal(string(expectedFile)), cmp.Diff(out.buf.String(), string(expectedFile)))
105+
})
106+
107+
It("should include only beta-gated fields when beta gate is enabled", func() {
108+
By("calling the generator")
109+
gen := &crd.Generator{
110+
CRDVersions: []string{"v1"},
111+
FeatureGates: "beta=true",
112+
}
113+
Expect(gen.Generate(ctx)).NotTo(HaveOccurred())
114+
115+
By("loading the expected YAML")
116+
expectedFile, err := os.ReadFile("output_beta/_featuregatetests.yaml")
117+
Expect(err).NotTo(HaveOccurred())
118+
119+
By("comparing the two")
120+
Expect(out.buf.String()).To(Equal(string(expectedFile)), cmp.Diff(out.buf.String(), string(expectedFile)))
121+
})
122+
123+
It("should include both feature-gated fields when both gates are enabled", func() {
124+
By("calling the generator")
125+
gen := &crd.Generator{
126+
CRDVersions: []string{"v1"},
127+
FeatureGates: "alpha=true,beta=true",
128+
}
129+
Expect(gen.Generate(ctx)).NotTo(HaveOccurred())
130+
131+
By("loading the expected YAML")
132+
expectedFile, err := os.ReadFile("output_both/_featuregatetests.yaml")
133+
Expect(err).NotTo(HaveOccurred())
134+
135+
By("comparing the two")
136+
Expect(out.buf.String()).To(Equal(string(expectedFile)), cmp.Diff(out.buf.String(), string(expectedFile)))
137+
})
138+
})
139+
140+
// Helper types for testing
141+
type featureGateOutputRule struct {
142+
buf *bytes.Buffer
143+
}
144+
145+
func (o *featureGateOutputRule) Open(_ *loader.Package, _ string) (io.WriteCloser, error) {
146+
return featureGateNopCloser{o.buf}, nil
147+
}
148+
149+
type featureGateNopCloser struct {
150+
io.Writer
151+
}
152+
153+
func (n featureGateNopCloser) Close() error {
154+
return nil
155+
}

pkg/crd/gen.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
type FeatureGateMap map[string]bool
3737

3838
// parseFeatureGates parses a feature gates string in the format "gate1=true,gate2=false"
39+
// and returns a FeatureGateMap. Supports comma-separated key=value pairs.
3940
// and returns a FeatureGateMap.
4041
func parseFeatureGates(featureGatesStr string) (FeatureGateMap, error) {
4142
gates := make(FeatureGateMap)
@@ -127,7 +128,12 @@ type Generator struct {
127128

128129
// FeatureGates specifies which feature gates are enabled for conditional field inclusion.
129130
//
130-
// Format: "gate1=true,gate2=false"
131+
// Single gate format: "gatename=true"
132+
// Multiple gates format: "gate1=true,gate2=false" (must use quoted strings for comma-separated values)
133+
//
134+
// Examples:
135+
// controller-gen crd:featureGates="alpha=true" paths=./api/...
136+
// controller-gen 'crd:featureGates="alpha=true,beta=false"' paths=./api/...
131137
FeatureGates string `marker:",optional"`
132138

133139
// DeprecatedV1beta1CompatibilityPreserveUnknownFields indicates whether

pkg/crd/markers/featuregate.go

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2025 The Kubernetes Authors.
2+
Copyright 2025.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -18,36 +18,19 @@ package markers
1818

1919
import (
2020
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
21-
"sigs.k8s.io/controller-tools/pkg/markers"
2221
)
2322

2423
// +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.
24+
25+
// FeatureGate marks a field to be conditionally included based on feature gate enablement.
26+
// Fields marked with +kubebuilder:feature-gate will only be included in generated CRDs
27+
// when the specified feature gate is enabled via --feature-gates flag.
2728
type FeatureGate string
2829

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.
30+
// ApplyToSchema does nothing for feature gates - they are processed by the generator
31+
// to conditionally include/exclude fields.
32+
func (FeatureGate) ApplyToSchema(schema *apiext.JSONSchemaProps, field string) error {
33+
// Feature gates don't modify the schema directly.
3434
// They are processed by the generator to conditionally include/exclude fields.
3535
return nil
3636
}
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/schema.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ type schemaContext struct {
7272

7373
allowDangerousTypes bool
7474
ignoreUnexportedFields bool
75-
featureGates FeatureGateMap
75+
featureGates FeatureGateMap
7676
}
7777

7878
// newSchemaContext constructs a new schemaContext for the given package and schema requester.
@@ -84,7 +84,7 @@ func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTy
8484
schemaRequester: req,
8585
allowDangerousTypes: allowDangerousTypes,
8686
ignoreUnexportedFields: ignoreUnexportedFields,
87-
featureGates: featureGates,
87+
featureGates: featureGates,
8888
}
8989
}
9090

@@ -97,7 +97,7 @@ func (c *schemaContext) ForInfo(info *markers.TypeInfo) *schemaContext {
9797
schemaRequester: c.schemaRequester,
9898
allowDangerousTypes: c.allowDangerousTypes,
9999
ignoreUnexportedFields: c.ignoreUnexportedFields,
100-
featureGates: c.featureGates,
100+
featureGates: c.featureGates,
101101
}
102102
}
103103

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2025.
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+
//go:generate ../../../../.run-controller-gen.sh crd:featureGates=alpha=true paths=. output:dir=./output_alpha
18+
//go:generate ../../../../.run-controller-gen.sh crd:featureGates=beta=true paths=. output:dir=./output_beta
19+
//go:generate ../../../../.run-controller-gen.sh crd paths=. output:dir=./output_none
20+
21+
package featuregates
22+
23+
import (
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
)
26+
27+
// FeatureGateTestSpec defines the desired state with feature-gated fields
28+
type FeatureGateTestSpec struct {
29+
// Standard field - always included
30+
Name string `json:"name"`
31+
32+
// Alpha-gated field - only included when alpha gate is enabled
33+
// +kubebuilder:feature-gate=alpha
34+
AlphaFeature *string `json:"alphaFeature,omitempty"`
35+
36+
// Beta-gated field - only included when beta gate is enabled
37+
// +kubebuilder:feature-gate=beta
38+
BetaFeature *string `json:"betaFeature,omitempty"`
39+
}
40+
41+
// FeatureGateTestStatus defines the observed state with feature-gated fields
42+
type FeatureGateTestStatus struct {
43+
// Standard status field
44+
Ready bool `json:"ready"`
45+
46+
// Alpha-gated status field
47+
// +kubebuilder:feature-gate=alpha
48+
AlphaStatus *string `json:"alphaStatus,omitempty"`
49+
50+
// Beta-gated status field
51+
// +kubebuilder:feature-gate=beta
52+
BetaStatus *string `json:"betaStatus,omitempty"`
53+
}
54+
55+
// +kubebuilder:object:root=true
56+
// +kubebuilder:subresource:status
57+
58+
// FeatureGateTest is the Schema for testing feature gates
59+
type FeatureGateTest struct {
60+
metav1.TypeMeta `json:",inline"`
61+
metav1.ObjectMeta `json:"metadata,omitempty"`
62+
63+
Spec FeatureGateTestSpec `json:"spec,omitempty"`
64+
Status FeatureGateTestStatus `json:"status,omitempty"`
65+
}
66+
67+
// +kubebuilder:object:root=true
68+
69+
// FeatureGateTestList contains a list of FeatureGateTest
70+
type FeatureGateTestList struct {
71+
metav1.TypeMeta `json:",inline"`
72+
metav1.ListMeta `json:"metadata,omitempty"`
73+
Items []FeatureGateTest `json:"items"`
74+
}

0 commit comments

Comments
 (0)