Skip to content

Commit 2e71784

Browse files
committed
targetconfigcontroller: add special merges for admission plugins
The resource merge does not merge slice contents, it replaces them; therefore we need a special merge for the --enable-admission-plugins and --disable-admission-plugins arguments.
1 parent a62af2f commit 2e71784

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

pkg/operator/targetconfigcontroller/targetconfigcontroller.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
apierrors "k8s.io/apimachinery/pkg/api/errors"
3535
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3636
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
37+
"k8s.io/apimachinery/pkg/util/sets"
3738
"k8s.io/client-go/informers"
3839
"k8s.io/client-go/kubernetes"
3940
coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -266,7 +267,13 @@ func manageKubeAPIServerConfig(ctx context.Context, client coreclientv1.ConfigMa
266267
configMap := resourceread.ReadConfigMapV1OrDie(bindata.MustAsset("assets/kube-apiserver/cm.yaml"))
267268
defaultConfig := bindata.MustAsset("assets/config/defaultconfig.yaml")
268269
configOverrides := bindata.MustAsset("assets/config/config-overrides.yaml")
269-
specialMergeRules := map[string]resourcemerge.MergeFunc{}
270+
271+
// resourcemerge will not merge slice contents; therefore we must set up special merge rules for
272+
// enable-admission-plugins/disable-admission-plugins in order to merge the config from the various sources
273+
specialMergeRules := map[string]resourcemerge.MergeFunc{
274+
".apiServerArguments.enable-admission-plugins": mergeStringSlices,
275+
".apiServerArguments.disable-admission-plugins": mergeStringSlices,
276+
}
270277

271278
// Guarantee the authorization-mode will be present in the base config, regardless of whether the observer is running
272279
authModeOverride := map[string]interface{}{}
@@ -618,3 +625,42 @@ func manageTemplate(rawTemplate string, imagePullSpec string, operatorImagePullS
618625
}
619626
return buf.String(), nil
620627
}
628+
629+
func mergeStringSlices(dst, src any, _ string) (any, error) {
630+
if src == nil {
631+
return dst, nil
632+
}
633+
634+
if dst == nil {
635+
return src, nil
636+
}
637+
638+
dstSlice, dstIsSlice := dst.([]any)
639+
if !dstIsSlice {
640+
return nil, fmt.Errorf("destination is not a slice (type: %T)", dst)
641+
}
642+
643+
srcSlice, srcIsSlice := src.([]any)
644+
if !srcIsSlice {
645+
return nil, fmt.Errorf("source is not a slice (type: %T)", dst)
646+
}
647+
648+
values := sets.NewString()
649+
for _, elem := range dstSlice {
650+
s, ok := elem.(string)
651+
if !ok {
652+
return nil, fmt.Errorf("destination slice element is not a string (type: %T)", elem)
653+
}
654+
values.Insert(s)
655+
}
656+
657+
for _, elem := range srcSlice {
658+
s, ok := elem.(string)
659+
if !ok {
660+
return nil, fmt.Errorf("source slice element is not a string (type: %T)", elem)
661+
}
662+
values.Insert(s)
663+
}
664+
665+
return values.List(), nil
666+
}

pkg/operator/targetconfigcontroller/targetconfigcontroller_test.go

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@ import (
77
"strings"
88
"testing"
99

10-
operatorv1 "github.com/openshift/api/operator/v1"
1110
corev1 "k8s.io/api/core/v1"
11+
"k8s.io/apimachinery/pkg/api/equality"
1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313
"k8s.io/apimachinery/pkg/labels"
1414
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/util/diff"
1516
"k8s.io/client-go/kubernetes/fake"
1617
"k8s.io/client-go/kubernetes/scheme"
1718
corev1listers "k8s.io/client-go/listers/core/v1"
19+
20+
"github.com/ghodss/yaml"
21+
kubecontrolplanev1 "github.com/openshift/api/kubecontrolplane/v1"
22+
operatorv1 "github.com/openshift/api/operator/v1"
23+
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
24+
"github.com/stretchr/testify/require"
1825
)
1926

2027
var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
@@ -417,6 +424,142 @@ func TestIsRequiredConfigPresentEtcdEndpoints(t *testing.T) {
417424
}
418425
}
419426

427+
func TestSpecialMergeRules(t *testing.T) {
428+
mergeRules := map[string]resourcemerge.MergeFunc{
429+
".apiServerArguments.enable-admission-plugins": mergeStringSlices,
430+
".apiServerArguments.disable-admission-plugins": mergeStringSlices,
431+
}
432+
433+
configsToMerge := []*kubecontrolplanev1.KubeAPIServerConfig{
434+
{
435+
APIServerArguments: map[string]kubecontrolplanev1.Arguments{
436+
"audit-log-format": []string{"json"},
437+
"enable-admission-plugins": []string{"enabled0"},
438+
"disable-admission-plugins": []string{"disabled0"},
439+
},
440+
},
441+
{
442+
APIServerArguments: map[string]kubecontrolplanev1.Arguments{
443+
"audit-log-format": []string{"yaml"},
444+
"enable-admission-plugins": []string{"enabled1"},
445+
"disable-admission-plugins": []string{"disabled1"},
446+
},
447+
},
448+
{
449+
APIServerArguments: map[string]kubecontrolplanev1.Arguments{
450+
"enable-admission-plugins": []string{"enabled2"},
451+
"disable-admission-plugins": []string{"disabled2"},
452+
},
453+
},
454+
}
455+
456+
configs := make([][]byte, 0, len(configsToMerge))
457+
for _, cfg := range configsToMerge {
458+
cfgBytes, err := yaml.Marshal(cfg)
459+
require.NoError(t, err)
460+
configs = append(configs, cfgBytes)
461+
}
462+
463+
result, _, err := resourcemerge.MergePrunedConfigMap(
464+
&kubecontrolplanev1.KubeAPIServerConfig{},
465+
&corev1.ConfigMap{Data: map[string]string{"config.yaml": ""}},
466+
"config.yaml",
467+
mergeRules,
468+
configs...,
469+
)
470+
require.NoError(t, err)
471+
472+
config := &kubecontrolplanev1.KubeAPIServerConfig{}
473+
err = yaml.Unmarshal([]byte(result.Data["config.yaml"]), config)
474+
require.NoError(t, err)
475+
476+
// plugins have special merge rules, therefore slices must be merged
477+
require.ElementsMatch(t, config.APIServerArguments["enable-admission-plugins"], []string{"enabled0", "enabled1", "enabled2"})
478+
require.ElementsMatch(t, config.APIServerArguments["disable-admission-plugins"], []string{"disabled0", "disabled1", "disabled2"})
479+
480+
// audit-log-format does not have any special merge rules, therefore value gets replaced
481+
require.ElementsMatch(t, config.APIServerArguments["audit-log-format"], []string{"yaml"})
482+
}
483+
484+
func TestMergeStringSlices(t *testing.T) {
485+
for _, tt := range []struct {
486+
name string
487+
dst any
488+
src any
489+
expected any
490+
expectError bool
491+
}{
492+
{
493+
name: "dst and src empty",
494+
dst: nil,
495+
src: nil,
496+
expected: nil,
497+
expectError: false,
498+
},
499+
{
500+
name: "src empty",
501+
dst: []any{"value"},
502+
src: nil,
503+
expected: []any{"value"},
504+
expectError: false,
505+
},
506+
{
507+
name: "dst empty",
508+
dst: nil,
509+
src: []any{"value"},
510+
expected: []any{"value"},
511+
expectError: false,
512+
},
513+
{
514+
name: "dst not a slice",
515+
dst: "not-a-slice",
516+
src: []any{"new-item"},
517+
expected: nil,
518+
expectError: true,
519+
},
520+
{
521+
name: "src not a slice",
522+
dst: []any{"existing-item"},
523+
src: "not-a-slice",
524+
expected: nil,
525+
expectError: true,
526+
},
527+
{
528+
name: "dst not a string slice",
529+
dst: []any{1, 2, 3},
530+
src: []any{"new-item"},
531+
expected: nil,
532+
expectError: true,
533+
},
534+
{
535+
name: "src not a string slice",
536+
dst: []any{"existing-item"},
537+
src: []any{1, 2, 3},
538+
expected: nil,
539+
expectError: true,
540+
},
541+
{
542+
name: "dst and src merged",
543+
dst: []any{"existing-item"},
544+
src: []any{"new-item"},
545+
expected: []string{"existing-item", "new-item"},
546+
expectError: false,
547+
},
548+
} {
549+
t.Run(tt.name, func(t *testing.T) {
550+
merged, err := mergeStringSlices(tt.dst, tt.src, "")
551+
if tt.expectError != (err != nil) {
552+
t.Errorf("expected error: %v; got %v", tt.expectError, err)
553+
}
554+
555+
if !equality.Semantic.DeepEqual(tt.expected, merged) {
556+
t.Errorf("unexpected merged slice: %s", diff.ObjectReflectDiff(tt.expected, merged))
557+
}
558+
559+
})
560+
}
561+
}
562+
420563
func makeEtcdEndpointsCM(endpoints ...string) *corev1.ConfigMap {
421564
cm := &corev1.ConfigMap{}
422565
cm.Name = etcdEndpointName

0 commit comments

Comments
 (0)