Skip to content

Commit 809dc79

Browse files
committed
wip
1 parent 1f489a8 commit 809dc79

File tree

13 files changed

+592
-156
lines changed

13 files changed

+592
-156
lines changed

cmd/controller-gen/main.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ var (
6565
// - output:<generator>:<form> (per-generator output)
6666
// - output:<form> (default output)
6767
allOutputRules = map[string]genall.OutputRule{
68-
"dir": genall.OutputToDirectory(""),
69-
"none": genall.OutputToNothing,
70-
"stdout": genall.OutputToStdout,
71-
"artifacts": genall.OutputArtifacts{},
68+
"dir": genall.OutputToDirectory(""),
69+
"none": genall.OutputToNothing,
70+
"stdout": genall.OutputToStdout,
71+
"artifacts": genall.OutputArtifacts{},
72+
"featuregate-dir": genall.OutputToFeatureGateDirectories{},
7273
}
7374

7475
// optionsRegistry contains all the marker definitions used to process command line options
@@ -209,6 +210,12 @@ func main() {
209210
return helpForLevels(c.OutOrStdout(), c.OutOrStderr(), helpLevel, optionsRegistry, help.SortByOption)
210211
})
211212

213+
// Add workflow command for advanced multi-gate, multi-output generation patterns
214+
if len(os.Args) > 1 && os.Args[1] == "workflow" {
215+
workflowCmd := NewWorkflowCommand()
216+
cmd.AddCommand(workflowCmd)
217+
}
218+
212219
if err := cmd.Execute(); err != nil {
213220
var errNoUsage noUsageError
214221
if !errors.As(err, &errNoUsage) {

cmd/controller-gen/workflow.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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 main
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/spf13/cobra"
26+
"sigs.k8s.io/controller-tools/pkg/featuregate"
27+
"sigs.k8s.io/controller-tools/pkg/genall"
28+
)
29+
30+
// NewWorkflowCommand provides advanced CLI workflows for multi-gate, multi-output generation
31+
func NewWorkflowCommand() *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: "workflow",
34+
Short: "Advanced workflows for multi-gate, multi-output generation",
35+
Long: `Advanced workflows for generating multiple CRD variants with different feature gate combinations.
36+
37+
This enables developers to:
38+
- Generate CRDs for multiple feature gate combinations in a single command
39+
- Create progressive rollout scenarios
40+
- Test feature gate matrix combinations
41+
42+
Examples:
43+
# Generate CRDs for all feature gate combinations
44+
controller-gen workflow matrix --gates=alpha,beta --base-path=./output
45+
46+
# Generate CRDs for progressive rollout
47+
controller-gen workflow progressive --gates=alpha,beta,gamma --base-path=./output`,
48+
}
49+
50+
cmd.AddCommand(NewMatrixCommand())
51+
cmd.AddCommand(NewProgressiveCommand())
52+
53+
return cmd
54+
}
55+
56+
// createWorkflowCommand creates a workflow command with common flags and validation
57+
func createWorkflowCommand(use, short, long string, runFunc func([]string, string, []string) error) *cobra.Command {
58+
var gates []string
59+
var basePath string
60+
var paths []string
61+
62+
cmd := &cobra.Command{
63+
Use: use,
64+
Short: short,
65+
Long: long,
66+
RunE: func(cmd *cobra.Command, args []string) error {
67+
if len(gates) == 0 {
68+
return fmt.Errorf("at least one feature gate must be specified")
69+
}
70+
if basePath == "" {
71+
return fmt.Errorf("base path must be specified")
72+
}
73+
if len(paths) == 0 {
74+
paths = []string{"./..."}
75+
}
76+
77+
return runFunc(gates, basePath, paths)
78+
},
79+
}
80+
81+
cmd.Flags().StringSliceVar(&gates, "gates", nil, "Feature gates to generate combinations for")
82+
cmd.Flags().StringVar(&basePath, "base-path", "", "Base output directory")
83+
cmd.Flags().StringSliceVar(&paths, "paths", []string{"./..."}, "Go package paths to process")
84+
85+
_ = cmd.MarkFlagRequired("gates")
86+
_ = cmd.MarkFlagRequired("base-path")
87+
88+
return cmd
89+
}
90+
91+
// NewMatrixCommand generates CRDs for all possible feature gate combinations
92+
func NewMatrixCommand() *cobra.Command {
93+
return createWorkflowCommand(
94+
"matrix",
95+
"Generate CRDs for all possible feature gate combinations",
96+
`Generate CRDs for all possible combinations of the specified feature gates.
97+
98+
This will create directories for each combination:
99+
- no_gates/ (all gates disabled)
100+
- alpha/ (only alpha enabled)
101+
- beta/ (only beta enabled)
102+
- alpha_beta/ (both alpha and beta enabled)
103+
- etc.
104+
105+
This is useful for testing and packaging different feature variants.`,
106+
generateMatrix,
107+
)
108+
}
109+
110+
// NewProgressiveCommand generates CRDs for progressive feature rollout
111+
func NewProgressiveCommand() *cobra.Command {
112+
return createWorkflowCommand(
113+
"progressive",
114+
"Generate CRDs for progressive feature rollout",
115+
`Generate CRDs for progressive feature rollout scenarios:
116+
117+
- stage_0/ (stable features only)
118+
- stage_1/ (stable + first gate)
119+
- stage_2/ (stable + first two gates)
120+
- stage_N/ (stable + all gates)
121+
122+
This enables gradual feature introduction in production environments.`,
123+
generateProgressive,
124+
)
125+
}
126+
127+
// generateMatrix generates all possible feature gate combinations
128+
func generateMatrix(gates []string, basePath string, paths []string) error {
129+
// Generate all 2^n combinations
130+
n := len(gates)
131+
totalCombinations := 1 << n
132+
133+
fmt.Printf("Generating %d feature gate combinations for gates: %v\n", totalCombinations, gates)
134+
135+
for i := 0; i < totalCombinations; i++ {
136+
var enabledGates []string
137+
var gateSettings []string
138+
139+
for j, gate := range gates {
140+
if (i>>j)&1 == 1 {
141+
enabledGates = append(enabledGates, gate)
142+
gateSettings = append(gateSettings, fmt.Sprintf("%s=true", gate))
143+
} else {
144+
gateSettings = append(gateSettings, fmt.Sprintf("%s=false", gate))
145+
}
146+
}
147+
148+
combination := strings.Join(gateSettings, ",")
149+
outputDir := getOutputDirectory(enabledGates)
150+
151+
err := runGenerationForCombination(combination, filepath.Join(basePath, outputDir), paths)
152+
if err != nil {
153+
return fmt.Errorf("failed to generate for combination %s: %w", combination, err)
154+
}
155+
}
156+
157+
fmt.Printf("Successfully generated all %d combinations in %s\n", totalCombinations, basePath)
158+
return nil
159+
}
160+
161+
// generateProgressive generates progressive rollout configurations
162+
func generateProgressive(gates []string, basePath string, paths []string) error {
163+
stages := len(gates) + 1
164+
165+
fmt.Printf("Generating %d progressive stages for gates: %v\n", stages, gates)
166+
167+
// Stage 0: all gates disabled
168+
stage0Settings := make([]string, len(gates))
169+
for i, gate := range gates {
170+
stage0Settings[i] = fmt.Sprintf("%s=false", gate)
171+
}
172+
173+
err := runGenerationForCombination(
174+
strings.Join(stage0Settings, ","),
175+
filepath.Join(basePath, "stage_0"),
176+
paths,
177+
)
178+
if err != nil {
179+
return fmt.Errorf("failed to generate stage 0: %w", err)
180+
}
181+
182+
// Progressive stages: enable one more gate at each stage
183+
for i := 1; i <= len(gates); i++ {
184+
var stageSettings []string
185+
for j, gate := range gates {
186+
if j < i {
187+
stageSettings = append(stageSettings, fmt.Sprintf("%s=true", gate))
188+
} else {
189+
stageSettings = append(stageSettings, fmt.Sprintf("%s=false", gate))
190+
}
191+
}
192+
193+
err := runGenerationForCombination(
194+
strings.Join(stageSettings, ","),
195+
filepath.Join(basePath, fmt.Sprintf("stage_%d", i)),
196+
paths,
197+
)
198+
if err != nil {
199+
return fmt.Errorf("failed to generate stage %d: %w", i, err)
200+
}
201+
}
202+
203+
fmt.Printf("Successfully generated all %d progressive stages in %s\n", stages, basePath)
204+
return nil
205+
}
206+
207+
// getOutputDirectory determines the output directory name based on enabled gates
208+
func getOutputDirectory(enabledGates []string) string {
209+
if len(enabledGates) == 0 {
210+
return "no_gates"
211+
}
212+
213+
// Sort gates for consistent naming
214+
for i := 0; i < len(enabledGates)-1; i++ {
215+
for j := 0; j < len(enabledGates)-i-1; j++ {
216+
if enabledGates[j] > enabledGates[j+1] {
217+
enabledGates[j], enabledGates[j+1] = enabledGates[j+1], enabledGates[j]
218+
}
219+
}
220+
}
221+
222+
return strings.Join(enabledGates, "_")
223+
}
224+
225+
// runGenerationForCombination runs controller-gen for a specific feature gate combination
226+
func runGenerationForCombination(featureGates string, outputPath string, paths []string) error {
227+
// Create output directory
228+
if err := os.MkdirAll(outputPath, 0755); err != nil {
229+
return fmt.Errorf("failed to create output directory %s: %w", outputPath, err)
230+
}
231+
232+
fmt.Printf(" Generating: %s -> %s\n", featureGates, outputPath)
233+
234+
// Parse feature gates to validate them
235+
_, err := featuregate.ParseFeatureGates(featureGates, false)
236+
if err != nil {
237+
return fmt.Errorf("invalid feature gates %s: %w", featureGates, err)
238+
}
239+
240+
// Create a runtime configuration for this combination
241+
args := []string{
242+
"crd",
243+
fmt.Sprintf("crd:featureGates=%s", featureGates),
244+
fmt.Sprintf("output:crd:dir=%s", outputPath),
245+
}
246+
247+
// Add paths
248+
for _, path := range paths {
249+
args = append(args, fmt.Sprintf("paths=%s", path))
250+
}
251+
252+
// Use the existing optionsRegistry and run generation
253+
rt, err := genall.FromOptions(optionsRegistry, args)
254+
if err != nil {
255+
return fmt.Errorf("failed to create runtime for combination %s: %w", featureGates, err)
256+
}
257+
258+
if len(rt.Generators) == 0 {
259+
return fmt.Errorf("no generators specified for combination %s", featureGates)
260+
}
261+
262+
if hadErrs := rt.Run(); hadErrs {
263+
return fmt.Errorf("generation failed for combination %s", featureGates)
264+
}
265+
266+
return nil
267+
}

pkg/crd/markers/validation.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ var ValidationIshMarkers = []*definitionWithHelp{
141141

142142
must(markers.MakeDefinition("kubebuilder:featuregate", markers.DescribesField, FeatureGate(""))).
143143
WithHelp(FeatureGate("").Help()),
144+
145+
must(markers.MakeDefinition("kubebuilder:validation:featureGate", markers.DescribesField, FeatureGateValidation{})).
146+
WithHelp(markers.SimpleHelp("CRD validation feature gates", "applies validation rules conditionally based on feature gate enablement.")),
144147
}
145148

146149
func init() {
@@ -751,3 +754,46 @@ func fieldsToOneOfCelRuleStr(fields []string) string {
751754
list.WriteString("].filter(x,x==true).size()")
752755
return list.String()
753756
}
757+
758+
// +controllertools:marker:generateHelp:category="CRD validation feature gates"
759+
760+
// FeatureGateValidation marks a validation constraint to be conditionally applied based on feature gate enablement.
761+
// This allows validation rules to be enabled/disabled based on feature gates.
762+
// The validation parameter accepts the same values as standard kubebuilder:validation markers.
763+
//
764+
// Examples:
765+
// - +kubebuilder:validation:featureGate=alpha,rule="Maximum=100"
766+
// - +kubebuilder:validation:featureGate=alpha|beta,rule="MinLength=5"
767+
// - +kubebuilder:validation:featureGate=(alpha&beta)|gamma,rule="Pattern=^[a-z]+$"
768+
type FeatureGateValidation struct {
769+
// FeatureGate specifies the feature gate expression that must be satisfied
770+
// for this validation rule to be applied. Supports complex expressions with
771+
// AND (&), OR (|) operators and parentheses for precedence.
772+
FeatureGate string `marker:"featureGate"`
773+
774+
// Rule specifies the validation rule to apply when the feature gate is enabled.
775+
// This should be a valid validation marker rule (e.g., "Maximum=100", "MinLength=5").
776+
Rule string `marker:"rule"`
777+
}
778+
779+
// ApplyToSchema applies the validation rule to the schema if the feature gate is enabled.
780+
func (f FeatureGateValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
781+
// This will be called by the schema generation with access to the context
782+
// The actual feature gate evaluation will be handled by the caller
783+
return fmt.Errorf("FeatureGateValidation cannot be applied directly - use feature gate context")
784+
}
785+
786+
// SupportsFeatureGate indicates this marker supports feature gates
787+
func (f FeatureGateValidation) SupportsFeatureGate() bool {
788+
return true
789+
}
790+
791+
// GetFeatureGate returns the feature gate expression
792+
func (f FeatureGateValidation) GetFeatureGate() string {
793+
return f.FeatureGate
794+
}
795+
796+
// GetValidationRule returns the validation rule to apply
797+
func (f FeatureGateValidation) GetValidationRule() string {
798+
return f.Rule
799+
}

pkg/crd/markers/zz_generated.markerhelp.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/crd/schema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSON
436436
}
437437

438438
// Check feature gate markers - skip field if feature gate is not enabled
439-
if featureGateMarker := field.Markers.Get("kubebuilder:feature-gate"); featureGateMarker != nil {
439+
if featureGateMarker := field.Markers.Get("kubebuilder:featuregate"); featureGateMarker != nil {
440440
if featureGate, ok := featureGateMarker.(crdmarkers.FeatureGate); ok {
441441
gateName := string(featureGate)
442442
// Create evaluator to handle complex expressions (OR/AND logic)

0 commit comments

Comments
 (0)