Skip to content

Commit 08fbf57

Browse files
committed
feat: make feature gates additive instead of replacing defaults
1 parent fca4659 commit 08fbf57

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed

internal/controller/component_customizer.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,16 @@ func customizeManagerContainer(mSpec *operatorv1.ManagerSpec, c *corev1.Containe
280280
}
281281

282282
if len(mSpec.FeatureGates) > 0 {
283-
fgValue := []string{}
283+
// Start with existing feature gates from the manifest (defaults from upstream)
284+
mergedGates := parseFeatureGates(c.Args)
284285

286+
// Merge user-specified feature gates (user values override defaults)
285287
for fg, val := range mSpec.FeatureGates {
288+
mergedGates[fg] = val
289+
}
290+
291+
fgValue := []string{}
292+
for fg, val := range mergedGates {
286293
fgValue = append(fgValue, fg+"="+bool2Str[val])
287294
}
288295

@@ -338,6 +345,31 @@ func customizeContainer(cSpec operatorv1.ContainerSpec, d *appsv1.Deployment) er
338345
return fmt.Errorf("cannot find container %q in deployment %q", cSpec.Name, d.Name)
339346
}
340347

348+
// parseFeatureGates parses existing --feature-gates argument and returns a map of feature gates.
349+
// This allows user-specified feature gates to be merged with defaults instead of replacing them entirely.
350+
func parseFeatureGates(args []string) map[string]bool {
351+
gates := make(map[string]bool)
352+
353+
for _, arg := range args {
354+
if !strings.HasPrefix(arg, "--feature-gates=") {
355+
continue
356+
}
357+
358+
value := strings.TrimPrefix(arg, "--feature-gates=")
359+
360+
for _, gate := range strings.Split(value, ",") {
361+
parts := strings.SplitN(gate, "=", 2)
362+
if len(parts) == 2 {
363+
gates[parts[0]] = parts[1] == operatorv1.TrueValue
364+
}
365+
}
366+
367+
break
368+
}
369+
370+
return gates
371+
}
372+
341373
// setArg set container arguments.
342374
func setArgs(args []string, name, value string) []string {
343375
for i, a := range args {

internal/controller/component_customizer_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,3 +780,110 @@ func TestCustomizeMultipleDeployment(t *testing.T) {
780780
})
781781
}
782782
}
783+
784+
func TestParseFeatureGates(t *testing.T) {
785+
tests := []struct {
786+
name string
787+
args []string
788+
expected map[string]bool
789+
}{
790+
{
791+
name: "no feature gates",
792+
args: []string{"--webhook-port=2345"},
793+
expected: map[string]bool{},
794+
},
795+
{
796+
name: "single feature gate",
797+
args: []string{"--feature-gates=MachinePool=true"},
798+
expected: map[string]bool{"MachinePool": true},
799+
},
800+
{
801+
name: "multiple feature gates",
802+
args: []string{"--feature-gates=MachinePool=true,ClusterTopology=false,RuntimeSDK=true"},
803+
expected: map[string]bool{"MachinePool": true, "ClusterTopology": false, "RuntimeSDK": true},
804+
},
805+
{
806+
name: "feature gates among other args",
807+
args: []string{"--webhook-port=2345", "--feature-gates=MachinePool=true,ClusterTopology=false", "--v=5"},
808+
expected: map[string]bool{"MachinePool": true, "ClusterTopology": false},
809+
},
810+
}
811+
812+
for _, tt := range tests {
813+
t.Run(tt.name, func(t *testing.T) {
814+
result := parseFeatureGates(tt.args)
815+
if !reflect.DeepEqual(result, tt.expected) {
816+
t.Errorf("parseFeatureGates() = %v, want %v", result, tt.expected)
817+
}
818+
})
819+
}
820+
}
821+
822+
func TestAdditiveFeatureGates(t *testing.T) {
823+
deplWithExistingGates := &appsv1.Deployment{
824+
ObjectMeta: metav1.ObjectMeta{
825+
Name: "manager",
826+
Namespace: metav1.NamespaceSystem,
827+
},
828+
Spec: appsv1.DeploymentSpec{
829+
Template: corev1.PodTemplateSpec{
830+
ObjectMeta: metav1.ObjectMeta{
831+
Name: "manager",
832+
},
833+
Spec: corev1.PodSpec{
834+
Containers: []corev1.Container{
835+
{
836+
Name: "manager",
837+
Image: "registry.k8s.io/a-manager:1.6.2",
838+
Args: []string{
839+
"--webhook-port=2345",
840+
"--feature-gates=MachinePool=true,MachineSetPreflightChecks=true,PriorityQueue=false",
841+
},
842+
},
843+
},
844+
},
845+
},
846+
},
847+
}
848+
849+
managerSpec := &operatorv1.ManagerSpec{
850+
FeatureGates: map[string]bool{
851+
"ClusterTopology": true,
852+
"MachineSetPreflightChecks": false,
853+
},
854+
}
855+
856+
container := findManagerContainer(&deplWithExistingGates.Spec)
857+
if container == nil {
858+
t.Fatal("expected container to be found")
859+
}
860+
861+
if err := customizeManagerContainer(managerSpec, container); err != nil {
862+
t.Fatalf("customizeManagerContainer failed: %v", err)
863+
}
864+
865+
featureGatesArg := ""
866+
867+
for _, arg := range container.Args {
868+
if len(arg) > 16 && arg[:16] == "--feature-gates=" {
869+
featureGatesArg = arg[16:]
870+
break
871+
}
872+
}
873+
874+
if featureGatesArg == "" {
875+
t.Fatal("expected --feature-gates arg to be present")
876+
}
877+
878+
actualGates := parseFeatureGates([]string{"--feature-gates=" + featureGatesArg})
879+
expectedGates := map[string]bool{
880+
"MachinePool": true,
881+
"ClusterTopology": true,
882+
"MachineSetPreflightChecks": false,
883+
"PriorityQueue": false,
884+
}
885+
886+
if !reflect.DeepEqual(actualGates, expectedGates) {
887+
t.Errorf("Feature gates not merged correctly.\nGot: %v\nWant: %v", actualGates, expectedGates)
888+
}
889+
}

0 commit comments

Comments
 (0)