Skip to content

Commit 1105b60

Browse files
committed
feat: make feature gate infrastructure optional
This change improves the UX by making feature gate infrastructure opt-in rather than always generated. Key changes: - Added --with-feature-gates flag to init and create api commands - Made feature gate scaffolding conditional based on flag or auto-detection - Updated main.go template to conditionally include feature gate code - Added auto-detection for existing feature gate infrastructure - Created FeatureGateScaffolder interface for proper type handling The infrastructure is now only generated when: 1. Explicitly requested via --with-feature-gates flag, or 2. Auto-detected when existing feature gate infrastructure is found This addresses PR feedback suggesting that feature gate infrastructure should be optional to improve user experience.
1 parent ec261d7 commit 1105b60

File tree

6 files changed

+94
-16
lines changed

6 files changed

+94
-16
lines changed

pkg/plugins/golang/v4/api.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,17 @@ type createAPISubcommand struct {
5050
resourceFlag *pflag.Flag
5151
controllerFlag *pflag.Flag
5252

53+
// featureGateFlag holds the flag for feature gates
54+
featureGateFlag *pflag.Flag
55+
5356
// force indicates that the resource should be created even if it already exists
5457
force bool
5558

5659
// runMake indicates whether to run make or not after scaffolding APIs
5760
runMake bool
61+
62+
// withFeatureGates indicates whether to include feature gate support
63+
withFeatureGates bool
5864
}
5965

6066
func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
@@ -96,6 +102,10 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
96102
fs.BoolVar(&p.force, "force", false,
97103
"attempt to create resource even if it already exists")
98104

105+
fs.BoolVar(&p.withFeatureGates, "with-feature-gates", false,
106+
"if specified, include feature gate support in scaffolded files")
107+
p.featureGateFlag = fs.Lookup("with-feature-gates")
108+
99109
p.options = &goPlugin.Options{}
100110

101111
fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")
@@ -147,6 +157,13 @@ func (p *createAPISubcommand) InjectResource(res *resource.Resource) error {
147157

148158
p.options.UpdateResource(p.resource, p.config)
149159

160+
// Auto-detect feature gates if not explicitly set and infrastructure exists
161+
if !p.featureGateFlag.Changed {
162+
if _, err := os.Stat("internal/featuregates/featuregates.go"); err == nil {
163+
p.withFeatureGates = true
164+
}
165+
}
166+
150167
if err := p.resource.Validate(); err != nil {
151168
return fmt.Errorf("error validating resource: %w", err)
152169
}
@@ -180,6 +197,11 @@ func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error {
180197
func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
181198
scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force)
182199
scaffolder.InjectFS(fs)
200+
201+
// Set feature gates flag if the scaffolder supports it
202+
if fgScaffolder, ok := scaffolder.(scaffolds.FeatureGateScaffolder); ok {
203+
fgScaffolder.SetWithFeatureGates(p.withFeatureGates)
204+
}
183205
if err := scaffolder.Scaffold(); err != nil {
184206
return fmt.Errorf("error scaffolding API: %w", err)
185207
}

pkg/plugins/golang/v4/init.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type initSubcommand struct {
5757
// flags
5858
fetchDeps bool
5959
skipGoVersionCheck bool
60+
withFeatureGates bool
6061
}
6162

6263
func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
@@ -84,6 +85,10 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
8485
// dependency args
8586
fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded")
8687

88+
// feature gate args
89+
fs.BoolVar(&p.withFeatureGates, "with-feature-gates", false,
90+
"if specified, scaffold feature gate infrastructure for experimental functionality")
91+
8792
// boilerplate args
8893
fs.StringVar(&p.license, "license", "apache2",
8994
"license to use to boilerplate, may be one of 'apache2', 'none'")
@@ -128,6 +133,10 @@ func (p *initSubcommand) PreScaffold(machinery.Filesystem) error {
128133
func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
129134
scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner, p.commandName)
130135
scaffolder.InjectFS(fs)
136+
// Set feature gates flag if the scaffolder supports it
137+
if fgScaffolder, ok := scaffolder.(scaffolds.FeatureGateScaffolder); ok {
138+
fgScaffolder.SetWithFeatureGates(p.withFeatureGates)
139+
}
131140
if err := scaffolder.Scaffold(); err != nil {
132141
return fmt.Errorf("error scaffolding init plugin: %w", err)
133142
}

pkg/plugins/golang/v4/scaffolds/api.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ type apiScaffolder struct {
4848

4949
// force indicates whether to scaffold controller files even if it exists or not
5050
force bool
51+
52+
// withFeatureGates indicates whether to include feature gate support
53+
withFeatureGates bool
5154
}
5255

5356
// NewAPIScaffolder returns a new Scaffolder for API/controller creation operations
@@ -64,6 +67,11 @@ func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
6467
s.fs = fs
6568
}
6669

70+
// SetWithFeatureGates sets whether to include feature gate support
71+
func (s *apiScaffolder) SetWithFeatureGates(withFeatureGates bool) {
72+
s.withFeatureGates = withFeatureGates
73+
}
74+
6775
// Scaffold implements cmdutil.Scaffolder
6876
func (s *apiScaffolder) Scaffold() error {
6977
slog.Info("Writing scaffold for you to edit...")
@@ -98,6 +106,14 @@ func (s *apiScaffolder) Scaffold() error {
98106
return fmt.Errorf("error updating resource: %w", err)
99107
}
100108

109+
// Check if feature gates infrastructure already exists
110+
existingFeatureGatesFile := filepath.Join("internal", "featuregates", "featuregates.go")
111+
if _, err := os.Stat(existingFeatureGatesFile); err == nil {
112+
// Feature gates infrastructure already exists, enable support
113+
s.withFeatureGates = true
114+
slog.Debug("Detected existing feature gates infrastructure, enabling support")
115+
}
116+
101117
// If using --force, discover existing feature gates before overwriting files
102118
var existingGates []string
103119
if s.force && doAPI {
@@ -108,7 +124,7 @@ func (s *apiScaffolder) Scaffold() error {
108124
if err := scaffold.Execute(
109125
&api.Types{
110126
Force: s.force,
111-
IncludeFeatureGateExample: false, // Don't include by default - keep it simple
127+
IncludeFeatureGateExample: s.withFeatureGates,
112128
},
113129
&api.Group{},
114130
); err != nil {
@@ -155,12 +171,15 @@ func (s *apiScaffolder) Scaffold() error {
155171
availableGates = newGates
156172
}
157173

158-
// Generate feature gates file
159-
featureGatesTemplate := &cmd.FeatureGates{}
160-
featureGatesTemplate.AvailableGates = availableGates
161-
featureGatesTemplate.IfExistsAction = machinery.OverwriteFile
162-
if err := scaffold.Execute(featureGatesTemplate); err != nil {
163-
return fmt.Errorf("error scaffolding feature gates: %w", err)
174+
// Only generate feature gates infrastructure if requested or if gates were discovered
175+
if s.withFeatureGates || len(availableGates) > 0 {
176+
// Generate feature gates file
177+
featureGatesTemplate := &cmd.FeatureGates{}
178+
featureGatesTemplate.AvailableGates = availableGates
179+
featureGatesTemplate.IfExistsAction = machinery.OverwriteFile
180+
if err := scaffold.Execute(featureGatesTemplate); err != nil {
181+
return fmt.Errorf("error scaffolding feature gates: %w", err)
182+
}
164183
}
165184

166185
if err := scaffold.Execute(

pkg/plugins/golang/v4/scaffolds/init.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ var _ plugins.Scaffolder = &initScaffolder{}
5353
var kustomizeVersion string
5454

5555
type initScaffolder struct {
56-
config config.Config
57-
boilerplatePath string
58-
license string
59-
owner string
60-
commandName string
56+
config config.Config
57+
boilerplatePath string
58+
license string
59+
owner string
60+
commandName string
61+
withFeatureGates bool
6162

6263
// fs is the filesystem that will be used by the scaffolder
6364
fs machinery.Filesystem
@@ -79,6 +80,11 @@ func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
7980
s.fs = fs
8081
}
8182

83+
// SetWithFeatureGates sets whether to scaffold feature gate infrastructure
84+
func (s *initScaffolder) SetWithFeatureGates(withFeatureGates bool) {
85+
s.withFeatureGates = withFeatureGates
86+
}
87+
8288
// getControllerRuntimeReleaseBranch converts the ControllerRuntime semantic versioning string to a
8389
// release branch string. Example input: "v0.17.0" -> Output: "release-0.17"
8490
func getControllerRuntimeReleaseBranch() string {
@@ -155,11 +161,12 @@ func (s *initScaffolder) Scaffold() error {
155161
}
156162
}
157163

158-
err := scaffold.Execute(
164+
// Build the list of templates to scaffold
165+
scaffoldFiles := []machinery.Builder{
159166
&cmd.Main{
160167
ControllerRuntimeVersion: ControllerRuntimeVersion,
168+
WithFeatureGates: s.withFeatureGates,
161169
},
162-
&cmd.FeatureGates{AvailableGates: []string{}},
163170
&templates.GoMod{
164171
ControllerRuntimeVersion: ControllerRuntimeVersion,
165172
},
@@ -178,7 +185,6 @@ func (s *initScaffolder) Scaffold() error {
178185
&templates.Readme{CommandName: s.commandName},
179186
&templates.Golangci{},
180187
&e2e.Test{},
181-
&e2e.WebhookTestUpdater{WireWebhook: false},
182188
&e2e.SuiteTest{},
183189
&github.E2eTestCi{},
184190
&github.TestCi{},
@@ -188,7 +194,14 @@ func (s *initScaffolder) Scaffold() error {
188194
&utils.Utils{},
189195
&templates.DevContainer{},
190196
&templates.DevContainerPostInstallScript{},
191-
)
197+
}
198+
199+
// Only add feature gates template if requested
200+
if s.withFeatureGates {
201+
scaffoldFiles = append(scaffoldFiles, &cmd.FeatureGates{AvailableGates: []string{}})
202+
}
203+
204+
err := scaffold.Execute(scaffoldFiles...)
192205
if err != nil {
193206
return fmt.Errorf("failed to execute init scaffold: %w", err)
194207
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package scaffolds
2+
3+
// FeatureGateScaffolder interface for scaffolders that support feature gates
4+
type FeatureGateScaffolder interface {
5+
SetWithFeatureGates(bool)
6+
}

pkg/plugins/golang/v4/scaffolds/internal/templates/cmd/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Main struct {
3535
machinery.RepositoryMixin
3636

3737
ControllerRuntimeVersion string
38+
WithFeatureGates bool
3839
}
3940

4041
// SetTemplateDefaults implements machinery.Template
@@ -248,8 +249,10 @@ import (
248249
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
249250
"sigs.k8s.io/controller-runtime/pkg/webhook"
250251
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
252+
{{- if .WithFeatureGates }}
251253
252254
featuregates "{{ .Repo }}/internal/featuregates"
255+
{{- end }}
253256
%s
254257
)
255258
@@ -273,7 +276,9 @@ func main() {
273276
var probeAddr string
274277
var secureMetrics bool
275278
var enableHTTP2 bool
279+
{{- if .WithFeatureGates }}
276280
var featureGates string
281+
{{- end }}
277282
var tlsOpts []func(*tls.Config)
278283
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. " +
279284
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -292,15 +297,18 @@ func main() {
292297
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
293298
flag.BoolVar(&enableHTTP2, "enable-http2", false,
294299
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
300+
{{- if .WithFeatureGates }}
295301
flag.StringVar(&featureGates, "feature-gates", "", "A set of key=value pairs that describe feature gates for alpha/experimental features. " +
296302
"Example: --feature-gates \"gate1=true,gate2=false\". " +
297303
"Options are: "+featuregates.GetFeatureGatesHelpText())
304+
{{- end }}
298305
opts := zap.Options{
299306
Development: true,
300307
}
301308
opts.BindFlags(flag.CommandLine)
302309
flag.Parse()
303310
311+
{{- if .WithFeatureGates }}
304312
// Parse feature gates
305313
parsedFeatureGates, err := featuregates.ParseFeatureGates(featureGates)
306314
if err != nil {
@@ -313,6 +321,7 @@ func main() {
313321
setupLog.Error(err, "invalid feature gates")
314322
os.Exit(1)
315323
}
324+
{{- end }}
316325
317326
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
318327

0 commit comments

Comments
 (0)