Skip to content

Commit f643cd8

Browse files
author
sami-wazery
committed
feat(feature-gates) implement feature gates for #849
1 parent 869ca6b commit f643cd8

File tree

13 files changed

+1163
-2
lines changed

13 files changed

+1163
-2
lines changed

FEATURE_GATES.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Feature Gates for Kubebuilder
2+
3+
This document describes the feature gates functionality implemented in Kubebuilder, which allows developers to mark fields in their API types as belonging to specific feature gates.
4+
5+
## Overview
6+
7+
Feature gates provide a way to enable or disable experimental features in your CRDs, similar to how Kubernetes core types handle feature gates. This allows for:
8+
9+
- Gradual rollout of new features
10+
- A/B testing of experimental functionality
11+
- Safe deprecation of features
12+
- Better control over API stability
13+
14+
## Usage
15+
16+
### 1. Marking Fields with Feature Gates
17+
18+
In your API types, you can mark fields with feature gate annotations:
19+
20+
```go
21+
// MyResourceSpec defines the desired state of MyResource
22+
type MyResourceSpec struct {
23+
// Standard field
24+
Name string `json:"name"`
25+
26+
// Experimental field that requires the "experimental-feature" gate
27+
// +feature-gate experimental-feature
28+
ExperimentalField string `json:"experimentalField,omitempty"`
29+
30+
// Another experimental field
31+
// +feature-gate another-feature
32+
AnotherField int `json:"anotherField,omitempty"`
33+
}
34+
```
35+
36+
### 2. Running with Feature Gates
37+
38+
When you run your controller, you can enable or disable feature gates:
39+
40+
```bash
41+
# Enable all feature gates
42+
./manager --feature-gates=experimental-feature,another-feature
43+
44+
# Enable some gates and disable others
45+
./manager --feature-gates=experimental-feature=true,another-feature=false
46+
47+
# Disable a specific gate
48+
./manager --feature-gates=experimental-feature=false
49+
```
50+
51+
### 3. Feature Gate Discovery
52+
53+
Kubebuilder automatically discovers feature gates from your API types and generates validation code. The available feature gates are listed in the help text:
54+
55+
```bash
56+
./manager --help
57+
```
58+
59+
You'll see output like:
60+
```
61+
--feature-gates string A set of key=value pairs that describe feature gates for alpha/experimental features. Options are: experimental-feature, another-feature
62+
```
63+
64+
## Implementation Details
65+
66+
### Feature Gate Parsing
67+
68+
The feature gate string follows this format:
69+
- `feature1` - enables feature1 (default)
70+
- `feature1=true` - explicitly enables feature1
71+
- `feature1=false` - explicitly disables feature1
72+
- `feature1,feature2=false,feature3` - mixed enabled/disabled gates
73+
74+
### Marker Parsing
75+
76+
Kubebuilder scans your API types for markers in the format:
77+
```
78+
// +feature-gate gate-name
79+
```
80+
81+
These markers are found in:
82+
- Struct field comments
83+
- Function comments
84+
- Type comments
85+
86+
### Validation
87+
88+
The controller validates that:
89+
1. All specified feature gates are valid (exist in the codebase)
90+
2. Feature gate values are properly formatted
91+
3. No duplicate or conflicting gate specifications
92+
93+
## Examples
94+
95+
### Basic Example
96+
97+
```go
98+
// api/v1/myresource_types.go
99+
type MyResourceSpec struct {
100+
// Standard field
101+
Name string `json:"name"`
102+
103+
// Experimental field
104+
// +feature-gate alpha-feature
105+
AlphaField string `json:"alphaField,omitempty"`
106+
}
107+
```
108+
109+
### Advanced Example
110+
111+
```go
112+
// api/v1/complexresource_types.go
113+
type ComplexResourceSpec struct {
114+
// Standard fields
115+
Name string `json:"name"`
116+
Replicas int32 `json:"replicas"`
117+
118+
// Beta feature
119+
// +feature-gate beta-feature
120+
BetaField string `json:"betaField,omitempty"`
121+
122+
// Alpha feature
123+
// +feature-gate alpha-feature
124+
AlphaField string `json:"alphaField,omitempty"`
125+
126+
// Experimental feature
127+
// +feature-gate experimental-feature
128+
ExperimentalField string `json:"experimentalField,omitempty"`
129+
}
130+
```
131+
132+
Running with different configurations:
133+
134+
```bash
135+
# Enable all features
136+
./manager --feature-gates=alpha-feature,beta-feature,experimental-feature
137+
138+
# Enable only beta features
139+
./manager --feature-gates=beta-feature
140+
141+
# Disable experimental features
142+
./manager --feature-gates=alpha-feature,beta-feature,experimental-feature=false
143+
```
144+
145+
## Best Practices
146+
147+
1. **Use descriptive gate names**: Choose names that clearly indicate what the feature does
148+
2. **Document your gates**: Add comments explaining what each feature gate enables
149+
3. **Gradual rollout**: Start with alpha features, then beta, then stable
150+
4. **Consistent naming**: Use kebab-case for feature gate names
151+
5. **Validation**: Always validate feature gate inputs in your controllers
152+
153+
## Migration Guide
154+
155+
### From No Feature Gates
156+
157+
1. Add feature gate markers to your experimental fields
158+
2. Rebuild your project: `make manifests`
159+
3. Update your deployment to include the `--feature-gates` flag
160+
4. Test with different gate combinations
161+
162+
### To Stable Features
163+
164+
When a feature is ready for stable release:
165+
1. Remove the feature gate marker
166+
2. Update documentation
167+
3. Consider the field stable and always available
168+
169+
## Troubleshooting
170+
171+
### Common Issues
172+
173+
1. **Unknown feature gate**: Make sure the gate name matches exactly in your code
174+
2. **Invalid format**: Check that your feature gate string follows the correct format
175+
3. **Missing validation**: Ensure your controller validates feature gates before use
176+
177+
### Debugging
178+
179+
Enable verbose logging to see feature gate processing:
180+
181+
```bash
182+
./manager --feature-gates=your-gate --v=2
183+
```
184+
185+
## Future Enhancements
186+
187+
Planned improvements include:
188+
- CRD schema modification based on enabled gates
189+
- Automatic documentation generation
190+
- Integration with controller-runtime feature gates
191+
- Webhook validation based on feature gates

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/spf13/afero v1.14.0
1111
github.com/spf13/cobra v1.9.1
1212
github.com/spf13/pflag v1.0.7
13+
github.com/stretchr/testify v1.10.0
1314
golang.org/x/mod v0.26.0
1415
golang.org/x/text v0.27.0
1516
golang.org/x/tools v0.35.0
@@ -19,13 +20,15 @@ require (
1920

2021
require (
2122
github.com/Masterminds/semver/v3 v3.3.0 // indirect
23+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2224
github.com/go-logr/logr v1.4.2 // indirect
2325
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
2426
github.com/google/go-cmp v0.7.0 // indirect
2527
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
2628
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2729
github.com/kr/pretty v0.3.1 // indirect
2830
github.com/pkg/errors v0.9.1 // indirect
31+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2932
github.com/rogpeppe/go-internal v1.14.1 // indirect
3033
go.uber.org/automaxprocs v1.6.0 // indirect
3134
go.yaml.in/yaml/v2 v2.4.2 // indirect

pkg/machinery/featuregate.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 machinery
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
)
23+
24+
// FeatureGate represents a feature gate with its name and enabled state
25+
type FeatureGate struct {
26+
Name string
27+
Enabled bool
28+
}
29+
30+
// FeatureGates represents a collection of feature gates
31+
type FeatureGates map[string]bool
32+
33+
// ParseFeatureGates parses a comma-separated string of feature gates
34+
// Format: "feature1=true,feature2=false,feature3"
35+
// If no value is specified, the feature gate defaults to enabled
36+
func ParseFeatureGates(featureGates string) (FeatureGates, error) {
37+
if featureGates == "" {
38+
return make(FeatureGates), nil
39+
}
40+
41+
gates := make(FeatureGates)
42+
parts := strings.Split(featureGates, ",")
43+
44+
for i, part := range parts {
45+
part = strings.TrimSpace(part)
46+
if part == "" {
47+
// If this is the first part and it's empty, it's an error
48+
if i == 0 {
49+
return nil, fmt.Errorf("empty feature gate name")
50+
}
51+
continue
52+
}
53+
54+
// Parse the feature gate name and value
55+
var name string
56+
var enabled bool = true // Default to enabled
57+
58+
if strings.Contains(part, "=") {
59+
kv := strings.SplitN(part, "=", 2)
60+
if len(kv) != 2 {
61+
return nil, fmt.Errorf("invalid feature gate format: %s", part)
62+
}
63+
name = strings.TrimSpace(kv[0])
64+
value := strings.TrimSpace(kv[1])
65+
66+
switch strings.ToLower(value) {
67+
case "true", "1", "yes", "on":
68+
enabled = true
69+
case "false", "0", "no", "off":
70+
enabled = false
71+
default:
72+
return nil, fmt.Errorf("invalid feature gate value for %s: %s", name, value)
73+
}
74+
} else {
75+
name = part
76+
}
77+
78+
if name == "" {
79+
return nil, fmt.Errorf("empty feature gate name")
80+
}
81+
82+
gates[name] = enabled
83+
}
84+
85+
return gates, nil
86+
}
87+
88+
// IsEnabled checks if a specific feature gate is enabled
89+
func (fg FeatureGates) IsEnabled(name string) bool {
90+
enabled, exists := fg[name]
91+
return exists && enabled
92+
}
93+
94+
// GetEnabledGates returns a list of enabled feature gate names
95+
func (fg FeatureGates) GetEnabledGates() []string {
96+
var enabled []string
97+
for name, isEnabled := range fg {
98+
if isEnabled {
99+
enabled = append(enabled, name)
100+
}
101+
}
102+
return enabled
103+
}
104+
105+
// GetDisabledGates returns a list of disabled feature gate names
106+
func (fg FeatureGates) GetDisabledGates() []string {
107+
var disabled []string
108+
for name, enabled := range fg {
109+
if !enabled {
110+
disabled = append(disabled, name)
111+
}
112+
}
113+
return disabled
114+
}
115+
116+
// String returns a string representation of the feature gates
117+
func (fg FeatureGates) String() string {
118+
if len(fg) == 0 {
119+
return ""
120+
}
121+
122+
var parts []string
123+
for name, enabled := range fg {
124+
if enabled {
125+
parts = append(parts, name)
126+
} else {
127+
parts = append(parts, fmt.Sprintf("%s=false", name))
128+
}
129+
}
130+
return strings.Join(parts, ",")
131+
}

0 commit comments

Comments
 (0)