diff --git a/pkg/scheduler/apis/config/scheme/BUILD b/pkg/scheduler/apis/config/scheme/BUILD index 810ad383699..ec34fbf0e27 100644 --- a/pkg/scheduler/apis/config/scheme/BUILD +++ b/pkg/scheduler/apis/config/scheme/BUILD @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -28,3 +28,19 @@ filegroup( tags = ["automanaged"], visibility = ["//visibility:public"], ) + +go_test( + name = "go_default_test", + srcs = ["scheme_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/scheduler/apis/config:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/kube-scheduler/config/v1alpha2:go_default_library", + "//vendor/github.com/google/go-cmp/cmp:go_default_library", + "//vendor/k8s.io/utils/pointer:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", + ], +) diff --git a/pkg/scheduler/apis/config/scheme/scheme_test.go b/pkg/scheduler/apis/config/scheme/scheme_test.go new file mode 100644 index 00000000000..e04ff744fbb --- /dev/null +++ b/pkg/scheduler/apis/config/scheme/scheme_test.go @@ -0,0 +1,450 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scheme + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-scheduler/config/v1alpha2" + "k8s.io/kubernetes/pkg/scheduler/apis/config" + "k8s.io/utils/pointer" + "sigs.k8s.io/yaml" +) + +func TestCodecsDecodePluginConfig(t *testing.T) { + testCases := []struct { + name string + data []byte + wantErr string + wantProfiles []config.KubeSchedulerProfile + }{ + { + name: "v1alpha2 all plugin args in default profile", + data: []byte(` +apiVersion: kubescheduler.config.k8s.io/v1alpha2 +kind: KubeSchedulerConfiguration +profiles: +- pluginConfig: + - name: InterPodAffinity + args: + hardPodAffinityWeight: 5 + - name: NodeLabel + args: + presentLabels: ["foo"] + - name: NodeResourcesFit + args: + ignoredResources: ["foo"] + - name: RequestedToCapacityRatio + args: + shape: + - utilization: 1 + - name: PodTopologySpread + args: + defaultConstraints: + - maxSkew: 1 + topologyKey: zone + whenUnsatisfiable: ScheduleAnyway + - name: ServiceAffinity + args: + affinityLabels: ["bar"] +`), + wantProfiles: []config.KubeSchedulerProfile{ + { + SchedulerName: "default-scheduler", + PluginConfig: []config.PluginConfig{ + { + Name: "InterPodAffinity", + Args: &config.InterPodAffinityArgs{HardPodAffinityWeight: 5}, + }, + { + Name: "NodeLabel", + Args: &config.NodeLabelArgs{PresentLabels: []string{"foo"}}, + }, + { + Name: "NodeResourcesFit", + Args: &config.NodeResourcesFitArgs{IgnoredResources: []string{"foo"}}, + }, + { + Name: "RequestedToCapacityRatio", + Args: &config.RequestedToCapacityRatioArgs{ + Shape: []config.UtilizationShapePoint{{Utilization: 1}}, + }, + }, + { + Name: "PodTopologySpread", + Args: &config.PodTopologySpreadArgs{ + DefaultConstraints: []v1.TopologySpreadConstraint{ + {MaxSkew: 1, TopologyKey: "zone", WhenUnsatisfiable: v1.ScheduleAnyway}, + }, + }, + }, + { + Name: "ServiceAffinity", + Args: &config.ServiceAffinityArgs{ + AffinityLabels: []string{"bar"}, + }, + }, + }, + }, + }, + }, + { + name: "v1alpha2 plugins can include version and kind", + data: []byte(` +apiVersion: kubescheduler.config.k8s.io/v1alpha2 +kind: KubeSchedulerConfiguration +profiles: +- pluginConfig: + - name: NodeLabel + args: + apiVersion: kubescheduler.config.k8s.io/v1alpha2 + kind: NodeLabelArgs + presentLabels: ["bars"] +`), + wantProfiles: []config.KubeSchedulerProfile{ + { + SchedulerName: "default-scheduler", + PluginConfig: []config.PluginConfig{ + { + Name: "NodeLabel", + Args: &config.NodeLabelArgs{PresentLabels: []string{"bars"}}, + }, + }, + }, + }, + }, + { + name: "plugin group and kind should match the type", + data: []byte(` +apiVersion: kubescheduler.config.k8s.io/v1alpha2 +kind: KubeSchedulerConfiguration +profiles: +- pluginConfig: + - name: NodeLabel + args: + apiVersion: kubescheduler.config.k8s.io/v1alpha2 + kind: InterPodAffinityArgs +`), + wantErr: "decoding .profiles[0].pluginConfig[0]: args for plugin NodeLabel were not of type NodeLabelArgs.kubescheduler.config.k8s.io, got InterPodAffinityArgs.kubescheduler.config.k8s.io", + }, + { + // TODO: do not replicate this case for v1beta1. + name: "v1alpha2 case insensitive RequestedToCapacityRatioArgs", + data: []byte(` +apiVersion: kubescheduler.config.k8s.io/v1alpha2 +kind: KubeSchedulerConfiguration +profiles: +- pluginConfig: + - name: RequestedToCapacityRatio + args: + shape: + - utilization: 1 + score: 2 + - Utilization: 3 + Score: 4 + resources: + - name: Upper + weight: 1 + - Name: lower + weight: 2 +`), + wantProfiles: []config.KubeSchedulerProfile{ + { + SchedulerName: "default-scheduler", + PluginConfig: []config.PluginConfig{ + { + Name: "RequestedToCapacityRatio", + Args: &config.RequestedToCapacityRatioArgs{ + Shape: []config.UtilizationShapePoint{ + {Utilization: 1, Score: 2}, + {Utilization: 3, Score: 4}, + }, + Resources: []config.ResourceSpec{ + {Name: "Upper", Weight: 1}, + {Name: "lower", Weight: 2}, + }, + }, + }, + }, + }, + }, + }, + { + name: "out-of-tree plugin args", + data: []byte(` +apiVersion: kubescheduler.config.k8s.io/v1alpha2 +kind: KubeSchedulerConfiguration +profiles: +- pluginConfig: + - name: OutOfTreePlugin + args: + foo: bar +`), + wantProfiles: []config.KubeSchedulerProfile{ + { + SchedulerName: "default-scheduler", + PluginConfig: []config.PluginConfig{ + { + Name: "OutOfTreePlugin", + Args: &runtime.Unknown{ + ContentType: "application/json", + Raw: []byte(`{"foo":"bar"}`), + }, + }, + }, + }, + }, + }, + { + name: "empty and no plugin args", + data: []byte(` +apiVersion: kubescheduler.config.k8s.io/v1alpha2 +kind: KubeSchedulerConfiguration +profiles: +- pluginConfig: + - name: InterPodAffinity + args: + - name: NodeResourcesFit + - name: OutOfTreePlugin + args: +`), + wantProfiles: []config.KubeSchedulerProfile{ + { + SchedulerName: "default-scheduler", + PluginConfig: []config.PluginConfig{ + { + Name: "InterPodAffinity", + // TODO(acondor): Set default values. + Args: &config.InterPodAffinityArgs{}, + }, + { + Name: "NodeResourcesFit", + Args: &config.NodeResourcesFitArgs{}, + }, + {Name: "OutOfTreePlugin"}, + }, + }, + }, + }, + } + decoder := Codecs.UniversalDecoder() + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + obj, gvk, err := decoder.Decode(tt.data, nil, nil) + if err != nil { + if tt.wantErr != err.Error() { + t.Fatalf("got err %w, want %w", err, tt.wantErr) + } + return + } + if len(tt.wantErr) != 0 { + t.Fatal("no error produced, wanted %w", tt.wantErr) + } + got, ok := obj.(*config.KubeSchedulerConfiguration) + if !ok { + t.Fatalf("decoded into %s, want %s", gvk, config.SchemeGroupVersion.WithKind("KubeSchedulerConfiguration")) + } + if diff := cmp.Diff(tt.wantProfiles, got.Profiles); diff != "" { + t.Errorf("unexpected configuration (-want,+got):\n%s", diff) + } + }) + } +} + +func TestCodecsEncodePluginConfig(t *testing.T) { + testCases := []struct { + name string + obj runtime.Object + version schema.GroupVersion + want string + }{ + { + name: "v1alpha2 in-tree and out-of-tree plugins", + version: v1alpha2.SchemeGroupVersion, + obj: &v1alpha2.KubeSchedulerConfiguration{ + Profiles: []v1alpha2.KubeSchedulerProfile{ + { + PluginConfig: []v1alpha2.PluginConfig{ + { + Name: "InterPodAffinity", + Args: runtime.RawExtension{ + Object: &v1alpha2.InterPodAffinityArgs{ + HardPodAffinityWeight: pointer.Int32Ptr(5), + }, + }, + }, + { + Name: "RequestedToCapacityRatio", + Args: runtime.RawExtension{ + Object: &v1alpha2.RequestedToCapacityRatioArgs{ + Shape: []v1alpha2.UtilizationShapePoint{ + {Utilization: 1, Score: 2}, + }, + Resources: []v1alpha2.ResourceSpec{ + {Name: "lower", Weight: 2}, + }, + }, + }, + }, + { + Name: "OutOfTreePlugin", + Args: runtime.RawExtension{ + Raw: []byte(`{"foo":"bar"}`), + }, + }, + }, + }, + }, + }, + want: `apiVersion: kubescheduler.config.k8s.io/v1alpha2 +clientConnection: + acceptContentTypes: "" + burst: 0 + contentType: "" + kubeconfig: "" + qps: 0 +kind: KubeSchedulerConfiguration +leaderElection: + leaderElect: null + leaseDuration: 0s + renewDeadline: 0s + resourceLock: "" + resourceName: "" + resourceNamespace: "" + retryPeriod: 0s +profiles: +- pluginConfig: + - args: + apiVersion: kubescheduler.config.k8s.io/v1alpha2 + hardPodAffinityWeight: 5 + kind: InterPodAffinityArgs + name: InterPodAffinity + - args: + apiVersion: kubescheduler.config.k8s.io/v1alpha2 + kind: RequestedToCapacityRatioArgs + resources: + - Name: lower + Weight: 2 + shape: + - Score: 2 + Utilization: 1 + name: RequestedToCapacityRatio + - args: + foo: bar + name: OutOfTreePlugin +`, + }, + { + name: "v1alpha2 in-tree and out-of-tree plugins from internal", + version: v1alpha2.SchemeGroupVersion, + obj: &config.KubeSchedulerConfiguration{ + Profiles: []config.KubeSchedulerProfile{ + { + PluginConfig: []config.PluginConfig{ + { + Name: "InterPodAffinity", + Args: &config.InterPodAffinityArgs{ + HardPodAffinityWeight: 5, + }, + }, + { + Name: "OutOfTreePlugin", + Args: &runtime.Unknown{ + Raw: []byte(`{"foo":"bar"}`), + }, + }, + }, + }, + }, + }, + want: `apiVersion: kubescheduler.config.k8s.io/v1alpha2 +bindTimeoutSeconds: 0 +clientConnection: + acceptContentTypes: "" + burst: 0 + contentType: "" + kubeconfig: "" + qps: 0 +disablePreemption: false +enableContentionProfiling: false +enableProfiling: false +healthzBindAddress: "" +kind: KubeSchedulerConfiguration +leaderElection: + leaderElect: false + leaseDuration: 0s + renewDeadline: 0s + resourceLock: "" + resourceName: "" + resourceNamespace: "" + retryPeriod: 0s +metricsBindAddress: "" +percentageOfNodesToScore: 0 +podInitialBackoffSeconds: 0 +podMaxBackoffSeconds: 0 +profiles: +- pluginConfig: + - args: + apiVersion: kubescheduler.config.k8s.io/v1alpha2 + hardPodAffinityWeight: 5 + kind: InterPodAffinityArgs + name: InterPodAffinity + - args: + foo: bar + name: OutOfTreePlugin + schedulerName: "" +`, + }, + } + yamlInfo, ok := runtime.SerializerInfoForMediaType(Codecs.SupportedMediaTypes(), runtime.ContentTypeYAML) + if !ok { + t.Fatalf("unable to locate encoder -- %q is not a supported media type", runtime.ContentTypeYAML) + } + jsonInfo, ok := runtime.SerializerInfoForMediaType(Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + if !ok { + t.Fatalf("unable to locate encoder -- %q is not a supported media type", runtime.ContentTypeJSON) + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + encoder := Codecs.EncoderForVersion(yamlInfo.Serializer, tt.version) + var buf bytes.Buffer + if err := encoder.Encode(tt.obj, &buf); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tt.want, buf.String()); diff != "" { + t.Errorf("unexpected encoded configuration:\n%s", diff) + } + encoder = Codecs.EncoderForVersion(jsonInfo.Serializer, tt.version) + buf = bytes.Buffer{} + if err := encoder.Encode(tt.obj, &buf); err != nil { + t.Fatal(err) + } + out, err := yaml.JSONToYAML(buf.Bytes()) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tt.want, string(out)); diff != "" { + t.Errorf("unexpected encoded configuration:\n%s", diff) + } + }) + } +} diff --git a/pkg/scheduler/apis/config/v1alpha2/BUILD b/pkg/scheduler/apis/config/v1alpha2/BUILD index ee1ca8a65dc..f1c0f1d2d2a 100644 --- a/pkg/scheduler/apis/config/v1alpha2/BUILD +++ b/pkg/scheduler/apis/config/v1alpha2/BUILD @@ -20,6 +20,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/component-base/config/v1alpha1:go_default_library", "//staging/src/k8s.io/kube-scheduler/config/v1:go_default_library", "//staging/src/k8s.io/kube-scheduler/config/v1alpha2:go_default_library", diff --git a/pkg/scheduler/apis/config/v1alpha2/conversion.go b/pkg/scheduler/apis/config/v1alpha2/conversion.go index 4d65e6ea672..6feff84a923 100644 --- a/pkg/scheduler/apis/config/v1alpha2/conversion.go +++ b/pkg/scheduler/apis/config/v1alpha2/conversion.go @@ -17,20 +17,93 @@ limitations under the License. package v1alpha2 import ( - conversion "k8s.io/apimachinery/pkg/conversion" - v1alpha2 "k8s.io/kube-scheduler/config/v1alpha2" + "fmt" + "sync" + + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/kube-scheduler/config/v1alpha2" "k8s.io/kubernetes/pkg/scheduler/apis/config" "k8s.io/utils/pointer" ) +var ( + // pluginArgConversionScheme is a scheme with internal and v1alpha2 registered, + // used for defaulting/converting typed PluginConfig Args. + // Access via getPluginArgConversionScheme() + pluginArgConversionScheme *runtime.Scheme + initPluginArgConversionScheme sync.Once +) + +func getPluginArgConversionScheme() *runtime.Scheme { + initPluginArgConversionScheme.Do(func() { + // set up the scheme used for plugin arg conversion + pluginArgConversionScheme = runtime.NewScheme() + utilruntime.Must(AddToScheme(pluginArgConversionScheme)) + utilruntime.Must(config.AddToScheme(pluginArgConversionScheme)) + }) + return pluginArgConversionScheme +} + func Convert_v1alpha2_KubeSchedulerConfiguration_To_config_KubeSchedulerConfiguration(in *v1alpha2.KubeSchedulerConfiguration, out *config.KubeSchedulerConfiguration, s conversion.Scope) error { if err := autoConvert_v1alpha2_KubeSchedulerConfiguration_To_config_KubeSchedulerConfiguration(in, out, s); err != nil { return err } out.AlgorithmSource.Provider = pointer.StringPtr(v1alpha2.SchedulerDefaultProviderName) + return convertToInternalPluginConfigArgs(out) +} + +// convertToInternalPluginConfigArgs converts PluginConfig#Args into internal +// types using a scheme, after applying defaults. +func convertToInternalPluginConfigArgs(out *config.KubeSchedulerConfiguration) error { + scheme := getPluginArgConversionScheme() + for i := range out.Profiles { + for j := range out.Profiles[i].PluginConfig { + args := out.Profiles[i].PluginConfig[j].Args + if args == nil { + continue + } + if _, isUnknown := args.(*runtime.Unknown); isUnknown { + continue + } + scheme.Default(args) + internalArgs, err := scheme.ConvertToVersion(args, config.SchemeGroupVersion) + if err != nil { + return fmt.Errorf("converting .Profiles[%d].PluginConfig[%d].Args into internal type: %w", i, j, err) + } + out.Profiles[i].PluginConfig[j].Args = internalArgs + } + } return nil } func Convert_config_KubeSchedulerConfiguration_To_v1alpha2_KubeSchedulerConfiguration(in *config.KubeSchedulerConfiguration, out *v1alpha2.KubeSchedulerConfiguration, s conversion.Scope) error { - return autoConvert_config_KubeSchedulerConfiguration_To_v1alpha2_KubeSchedulerConfiguration(in, out, s) + if err := autoConvert_config_KubeSchedulerConfiguration_To_v1alpha2_KubeSchedulerConfiguration(in, out, s); err != nil { + return err + } + return convertToExternalPluginConfigArgs(out) +} + +// convertToExternalPluginConfigArgs converts PluginConfig#Args into +// external (versioned) types using a scheme. +func convertToExternalPluginConfigArgs(out *v1alpha2.KubeSchedulerConfiguration) error { + scheme := getPluginArgConversionScheme() + for i := range out.Profiles { + for j := range out.Profiles[i].PluginConfig { + args := out.Profiles[i].PluginConfig[j].Args + if args.Object == nil { + continue + } + if _, isUnknown := args.Object.(*runtime.Unknown); isUnknown { + continue + } + externalArgs, err := scheme.ConvertToVersion(args.Object, SchemeGroupVersion) + if err != nil { + return err + } + out.Profiles[i].PluginConfig[j].Args.Object = externalArgs + } + } + return nil } diff --git a/staging/src/k8s.io/kube-scheduler/config/v1alpha2/BUILD b/staging/src/k8s.io/kube-scheduler/config/v1alpha2/BUILD index c8d942359f1..dcf87b027eb 100644 --- a/staging/src/k8s.io/kube-scheduler/config/v1alpha2/BUILD +++ b/staging/src/k8s.io/kube-scheduler/config/v1alpha2/BUILD @@ -19,6 +19,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/component-base/config/v1alpha1:go_default_library", "//staging/src/k8s.io/kube-scheduler/config/v1:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", ], ) diff --git a/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types.go b/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types.go index 08fa1e01ae4..b82105939a0 100644 --- a/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types.go +++ b/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types.go @@ -17,10 +17,14 @@ limitations under the License. package v1alpha2 import ( + "bytes" + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" componentbaseconfigv1alpha1 "k8s.io/component-base/config/v1alpha1" v1 "k8s.io/kube-scheduler/config/v1" + "sigs.k8s.io/yaml" ) const ( @@ -73,17 +77,17 @@ type KubeSchedulerConfiguration struct { // Duration to wait for a binding operation to complete before timing out // Value must be non-negative integer. The value zero indicates no waiting. // If this value is nil, the default value will be used. - BindTimeoutSeconds *int64 `json:"bindTimeoutSeconds"` + BindTimeoutSeconds *int64 `json:"bindTimeoutSeconds,omitempty"` // PodInitialBackoffSeconds is the initial backoff for unschedulable pods. // If specified, it must be greater than 0. If this value is null, the default value (1s) // will be used. - PodInitialBackoffSeconds *int64 `json:"podInitialBackoffSeconds"` + PodInitialBackoffSeconds *int64 `json:"podInitialBackoffSeconds,omitempty"` // PodMaxBackoffSeconds is the max backoff for unschedulable pods. // If specified, it must be greater than podInitialBackoffSeconds. If this value is null, // the default value (10s) will be used. - PodMaxBackoffSeconds *int64 `json:"podMaxBackoffSeconds"` + PodMaxBackoffSeconds *int64 `json:"podMaxBackoffSeconds,omitempty"` // Profiles are scheduling profiles that kube-scheduler supports. Pods can // choose to be scheduled under a particular profile by setting its associated @@ -91,12 +95,40 @@ type KubeSchedulerConfiguration struct { // with the "default-scheduler" profile, if present here. // +listType=map // +listMapKey=schedulerName - Profiles []KubeSchedulerProfile `json:"profiles"` + Profiles []KubeSchedulerProfile `json:"profiles,omitempty"` // Extenders are the list of scheduler extenders, each holding the values of how to communicate // with the extender. These extenders are shared by all scheduler profiles. // +listType=set - Extenders []v1.Extender `json:"extenders"` + Extenders []v1.Extender `json:"extenders,omitempty"` +} + +// DecodeNestedObjects decodes plugin args for known types. +func (c *KubeSchedulerConfiguration) DecodeNestedObjects(d runtime.Decoder) error { + for i := range c.Profiles { + prof := &c.Profiles[i] + for j := range prof.PluginConfig { + err := prof.PluginConfig[j].decodeNestedObjects(d) + if err != nil { + return fmt.Errorf("decoding .profiles[%d].pluginConfig[%d]: %w", i, j, err) + } + } + } + return nil +} + +// EncodeNestedObjects encodes plugin args. +func (c *KubeSchedulerConfiguration) EncodeNestedObjects(e runtime.Encoder) error { + for i := range c.Profiles { + prof := &c.Profiles[i] + for j := range prof.PluginConfig { + err := prof.PluginConfig[j].encodeNestedObjects(e) + if err != nil { + return fmt.Errorf("encoding .profiles[%d].pluginConfig[%d]: %w", i, j, err) + } + } + } + return nil } // KubeSchedulerProfile is a scheduling profile. @@ -202,3 +234,41 @@ type PluginConfig struct { // Args defines the arguments passed to the plugins at the time of initialization. Args can have arbitrary structure. Args runtime.RawExtension `json:"args,omitempty"` } + +func (c *PluginConfig) decodeNestedObjects(d runtime.Decoder) error { + gvk := SchemeGroupVersion.WithKind(c.Name + "Args") + // dry-run to detect and skip out-of-tree plugin args. + if _, _, err := d.Decode(nil, &gvk, nil); runtime.IsNotRegisteredError(err) { + return nil + } + + obj, parsedGvk, err := d.Decode(c.Args.Raw, &gvk, nil) + if err != nil { + return fmt.Errorf("decoding args for plugin %s: %w", c.Name, err) + } + if parsedGvk.GroupKind() != gvk.GroupKind() { + return fmt.Errorf("args for plugin %s were not of type %s, got %s", c.Name, gvk.GroupKind(), parsedGvk.GroupKind()) + } + c.Args.Object = obj + return nil +} + +func (c *PluginConfig) encodeNestedObjects(e runtime.Encoder) error { + if c.Args.Object == nil { + return nil + } + var buf bytes.Buffer + err := e.Encode(c.Args.Object, &buf) + if err != nil { + return err + } + // The encoder might be a YAML encoder, but the parent encoder expects + // JSON output, so we convert YAML back to JSON. + // This is a no-op if produces JSON. + json, err := yaml.YAMLToJSON(buf.Bytes()) + if err != nil { + return err + } + c.Args.Raw = json + return nil +} diff --git a/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types_pluginargs.go b/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types_pluginargs.go index 99b1c764ac7..87f099c2874 100644 --- a/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types_pluginargs.go +++ b/staging/src/k8s.io/kube-scheduler/config/v1alpha2/types_pluginargs.go @@ -17,6 +17,8 @@ limitations under the License. package v1alpha2 import ( + gojson "encoding/json" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -95,7 +97,7 @@ type RequestedToCapacityRatioArgs struct { Resources []ResourceSpec `json:"resources,omitempty"` } -// TODO add JSON tags and backward compatible conversion in v1beta1. +// TODO add JSON tags and remove custom unmarshalling in v1beta1. // UtilizationShapePoint and ResourceSpec fields are not annotated with JSON tags in v1alpha2 // to maintain backward compatibility with the args shipped with v1.18. // See https://github.com/kubernetes/kubernetes/pull/88585#discussion_r405021905 @@ -108,6 +110,13 @@ type UtilizationShapePoint struct { Score int32 } +// UnmarshalJSON provides case insensitive unmarshalling for the type. +// TODO remove when copying to v1beta1. +func (t *UtilizationShapePoint) UnmarshalJSON(data []byte) error { + type internal *UtilizationShapePoint + return gojson.Unmarshal(data, internal(t)) +} + // ResourceSpec represents single resource and weight for bin packing of priority RequestedToCapacityRatioArguments. type ResourceSpec struct { // Name of the resource to be managed by RequestedToCapacityRatio function. @@ -116,6 +125,13 @@ type ResourceSpec struct { Weight int64 } +// UnmarshalJSON provides case insensitive unmarshalling for the type. +// TODO remove when copying to v1beta1. +func (t *ResourceSpec) UnmarshalJSON(data []byte) error { + type internal *ResourceSpec + return gojson.Unmarshal(data, internal(t)) +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // ServiceAffinityArgs holds arguments used to configure the ServiceAffinity plugin. diff --git a/staging/src/k8s.io/kube-scheduler/go.mod b/staging/src/k8s.io/kube-scheduler/go.mod index da82d628028..d1f23fa616f 100644 --- a/staging/src/k8s.io/kube-scheduler/go.mod +++ b/staging/src/k8s.io/kube-scheduler/go.mod @@ -9,6 +9,7 @@ require ( k8s.io/api v0.0.0 k8s.io/apimachinery v0.0.0 k8s.io/component-base v0.0.0 + sigs.k8s.io/yaml v1.2.0 ) replace (