diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose.go b/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose.go index 8e3a2951c21..7ba07fe2cf2 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "k8s.io/klog/v2" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" @@ -82,6 +83,8 @@ var ( ) type ExposeServiceOptions struct { + cmdutil.OverrideOptions + FilenameOptions resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags @@ -155,11 +158,11 @@ func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams) cmd.Flags().MarkDeprecated("container-port", "--container-port will be removed in the future, please use --target-port instead") cmd.Flags().String("target-port", "", i18n.T("Name or number for the port on the container that the service should direct traffic to. Optional.")) cmd.Flags().String("external-ip", "", i18n.T("Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP.")) - cmd.Flags().String("overrides", "", i18n.T("An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.")) cmd.Flags().String("name", "", i18n.T("The name for the newly created object.")) cmd.Flags().String("session-affinity", "", i18n.T("If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'")) cmd.Flags().String("cluster-ip", "", i18n.T("ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service.")) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-expose") + o.AddOverrideFlags(cmd) usage := "identifying the resource to expose a service" cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) @@ -318,12 +321,9 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro return err } - if inline := cmdutil.GetFlagString(cmd, "overrides"); len(inline) > 0 { - codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) - object, err = cmdutil.Merge(codec, object, inline) - if err != nil { - return err - } + object, err = o.NewOverrider(&corev1.Service{}).Apply(object) + if err != nil { + return err } if err := o.Recorder.Record(object); err != nil { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose_test.go index 0cb40502012..18dc26c90e0 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose_test.go @@ -637,7 +637,7 @@ func TestRunExposeService(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace(test.ns) defer tf.Cleanup() - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ @@ -676,3 +676,183 @@ func TestRunExposeService(t *testing.T) { }) } } + +func TestExposeOverride(t *testing.T) { + tests := []struct { + name string + overrides string + overrideType string + expected string + }{ + { + name: "expose with merge override type should replace the entire spec", + overrides: `{"spec": {"ports": [{"protocol": "TCP", "port": 1111, "targetPort": 2222}]}, "selector": {"app": "go"}}`, + overrideType: "merge", + expected: `apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + svc: test + name: foo + namespace: test +spec: + ports: + - port: 1111 + protocol: TCP + targetPort: 2222 + selector: + app: go +status: + loadBalancer: {} +`, + }, + { + name: "expose with strategic override type should add port before existing port", + overrides: `{"spec": {"ports": [{"protocol": "TCP", "port": 1111, "targetPort": 2222}]}}`, + overrideType: "strategic", + expected: `apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + svc: test + name: foo + namespace: test +spec: + ports: + - port: 1111 + protocol: TCP + targetPort: 2222 + - port: 14 + protocol: UDP + targetPort: 14 + selector: + app: go +status: + loadBalancer: {} +`, + }, + { + name: "expose with json override type should add port before existing port", + overrides: `[ + {"op": "add", "path": "/spec/ports/0", "value": {"port": 1111, "protocol": "TCP", "targetPort": 2222}} + ]`, + overrideType: "json", + expected: `apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + svc: test + name: foo + namespace: test +spec: + ports: + - port: 1111 + protocol: TCP + targetPort: 2222 + - port: 14 + protocol: UDP + targetPort: 14 + selector: + app: go +status: + loadBalancer: {} +`, + }, + { + name: "expose with json override type should add port after existing port", + overrides: `[ + {"op": "add", "path": "/spec/ports/1", "value": {"port": 1111, "protocol": "TCP", "targetPort": 2222}} + ]`, + overrideType: "json", + expected: `apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + svc: test + name: foo + namespace: test +spec: + ports: + - port: 14 + protocol: UDP + targetPort: 14 + - port: 1111 + protocol: TCP + targetPort: 2222 + selector: + app: go +status: + loadBalancer: {} +`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + + codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) + ns := scheme.Codecs.WithoutConversion() + + tf.Client = &fake.RESTClient{ + GroupVersion: schema.GroupVersion{Version: "v1"}, + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/namespaces/test/services/baz" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{"app": "go"}, + }, + })}, nil + case p == "/namespaces/test/services" && m == "POST": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: corev1.ProtocolUDP, + Port: 14, + TargetPort: intstr.FromInt(14), + }, + }, + Selector: map[string]string{"app": "go"}, + }, + })}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + + ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdExposeService(tf, ioStreams) + cmd.SetOut(buf) + cmd.Flags().Set("protocol", "UDP") + cmd.Flags().Set("port", "14") + cmd.Flags().Set("name", "foo") + cmd.Flags().Set("labels", "svc=test") + cmd.Flags().Set("dry-run", "client") + cmd.Flags().Set("overrides", test.overrides) + cmd.Flags().Set("override-type", test.overrideType) + cmd.Flags().Set("output", "yaml") + cmd.Run(cmd, []string{"service", "baz"}) + + out := buf.String() + + if test.expected == "" { + t.Errorf("%s: Invalid test case. Specify expected result.\n", test.name) + } + + if !strings.Contains(out, test.expected) { + t.Errorf("%s: Unexpected output! Expected\n%s\ngot\n%s", test.name, test.expected, out) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go b/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go index 75001bed330..627b8ef1071 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go @@ -100,6 +100,8 @@ type RunObject struct { } type RunOptions struct { + cmdutil.OverrideOptions + PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags @@ -122,7 +124,6 @@ type RunOptions struct { Privileged bool Quiet bool TTY bool - Overrides string fieldManager string Namespace string @@ -175,7 +176,6 @@ func addRunFlags(cmd *cobra.Command, opt *RunOptions) { cmd.MarkFlagRequired("image") cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server.")) cmd.Flags().Bool("rm", false, "If true, delete the pod after it exits. Only valid when attaching to the container, e.g. with '--attach' or with '-i/--stdin'.") - cmd.Flags().StringVar(&opt.Overrides, "overrides", "", i18n.T("An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.")) cmd.Flags().StringArray("env", []string{}, "Environment variables to set in the container.") cmd.Flags().String("serviceaccount", "", "Service account to set in the pod spec.") cmd.Flags().MarkDeprecated("serviceaccount", "has no effect and will be removed in 1.24.") @@ -197,6 +197,7 @@ func addRunFlags(cmd *cobra.Command, opt *RunOptions) { cmd.Flags().BoolVarP(&opt.Quiet, "quiet", "q", opt.Quiet, "If true, suppress prompt messages.") cmd.Flags().BoolVar(&opt.Privileged, "privileged", opt.Privileged, i18n.T("If true, run the container in privileged mode.")) cmdutil.AddFieldManagerFlagVar(cmd, &opt.fieldManager, "kubectl-run") + opt.AddOverrideFlags(cmd) } func (o *RunOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { @@ -321,7 +322,7 @@ func (o *RunOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e delete(params, "limits") var createdObjects = []*RunObject{} - runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, o.Overrides) + runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, o.NewOverrider(&corev1.Pod{})) if err != nil { return err } @@ -586,7 +587,7 @@ func (o *RunOptions) generateService(f cmdutil.Factory, cmd *cobra.Command, para params["default-name"] = name } - runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, "") + runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, nil) if err != nil { return nil, err } @@ -602,7 +603,7 @@ func (o *RunOptions) generateService(f cmdutil.Factory, cmd *cobra.Command, para return runObject, nil } -func (o *RunOptions) createGeneratedObject(f cmdutil.Factory, cmd *cobra.Command, generator generate.Generator, names []generate.GeneratorParam, params map[string]interface{}, overrides string) (*RunObject, error) { +func (o *RunOptions) createGeneratedObject(f cmdutil.Factory, cmd *cobra.Command, generator generate.Generator, names []generate.GeneratorParam, params map[string]interface{}, overrider *cmdutil.Overrider) (*RunObject, error) { err := generate.ValidateParams(names, params) if err != nil { return nil, err @@ -628,9 +629,8 @@ func (o *RunOptions) createGeneratedObject(f cmdutil.Factory, cmd *cobra.Command return nil, err } - if len(overrides) > 0 { - codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) - obj, err = cmdutil.Merge(codec, obj, overrides) + if overrider != nil { + obj, err = overrider.Apply(obj) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/run/run_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/run/run_test.go index 39c8c319f03..0d3491a37bb 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/run/run_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/run/run_test.go @@ -495,6 +495,16 @@ func TestRunValidations(t *testing.T) { }, expectedErr: "stdin is required for containers with -t/--tty", }, + { + name: "test invalid override type error", + args: []string{"test"}, + flags: map[string]string{ + "image": "busybox", + "overrides": "{}", + "override-type": "foo", + }, + expectedErr: "invalid override type: foo", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -629,3 +639,132 @@ func TestExpose(t *testing.T) { } } + +func TestRunOverride(t *testing.T) { + tests := []struct { + name string + overrides string + overrideType string + expectedOutput string + }{ + { + name: "run with merge override type should replace spec", + overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`, + overrideType: "merge", + expectedOutput: `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + labels: + run: test + name: test + namespace: ns +spec: + containers: + - name: test + resources: + limits: + cpu: 200m + dnsPolicy: ClusterFirst + restartPolicy: Always +status: {} +`, + }, + { + name: "run with no override type specified, should perform an RFC7396 JSON Merge Patch", + overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`, + overrideType: "", + expectedOutput: `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + labels: + run: test + name: test + namespace: ns +spec: + containers: + - name: test + resources: + limits: + cpu: 200m + dnsPolicy: ClusterFirst + restartPolicy: Always +status: {} +`, + }, + { + name: "run with strategic override type should merge spec, preserving container image", + overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`, + overrideType: "strategic", + expectedOutput: `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + labels: + run: test + name: test + namespace: ns +spec: + containers: + - image: busybox + name: test + resources: + limits: + cpu: 200m + dnsPolicy: ClusterFirst + restartPolicy: Always +status: {} +`, + }, + { + name: "run with json override type should perform add, replace, and remove operations", + overrides: `[ + {"op": "add", "path": "/metadata/labels/foo", "value": "bar"}, + {"op": "replace", "path": "/spec/containers/0/resources", "value": {"limits": {"cpu": "200m"}}}, + {"op": "remove", "path": "/spec/dnsPolicy"} + ]`, + overrideType: "json", + expectedOutput: `apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + labels: + foo: bar + run: test + name: test + namespace: ns +spec: + containers: + - image: busybox + name: test + resources: + limits: + cpu: 200m + restartPolicy: Always +status: {} +`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory().WithNamespace("ns") + defer tf.Cleanup() + + streams, _, bufOut, _ := genericclioptions.NewTestIOStreams() + + cmd := NewCmdRun(tf, streams) + cmd.Flags().Set("dry-run", "client") + cmd.Flags().Set("output", "yaml") + cmd.Flags().Set("image", "busybox") + cmd.Flags().Set("overrides", test.overrides) + cmd.Flags().Set("override-type", test.overrideType) + cmd.Run(cmd, []string{"test"}) + + actualOutput := bufOut.String() + if actualOutput != test.expectedOutput { + t.Errorf("unexpected output.\n\nExpected:\n%v\nActual:\n%v", test.expectedOutput, actualOutput) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go index 7b03deb29c0..14a0f126eb6 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go @@ -29,12 +29,14 @@ import ( jsonpatch "github.com/evanphx/json-patch" "github.com/spf13/cobra" "github.com/spf13/pflag" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" @@ -464,7 +466,8 @@ type ValidateOptions struct { EnableValidation bool } -// Merge requires JSON serialization +// Merge converts the passed in object to JSON, merges the fragment into it using an RFC7396 JSON Merge Patch, +// and returns the resulting object // TODO: merge assumes JSON serialization, and does not properly abstract API retrieval func Merge(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Object, error) { // encode dst into versioned json and apply fragment directly too it @@ -483,6 +486,46 @@ func Merge(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Ob return out, nil } +// StrategicMerge converts the passed in object to JSON, merges the fragment into it using a Strategic Merge Patch, +// and returns the resulting object +func StrategicMerge(codec runtime.Codec, dst runtime.Object, fragment string, dataStruct runtime.Object) (runtime.Object, error) { + target, err := runtime.Encode(codec, dst) + if err != nil { + return nil, err + } + patched, err := strategicpatch.StrategicMergePatch(target, []byte(fragment), dataStruct) + if err != nil { + return nil, err + } + out, err := runtime.Decode(codec, patched) + if err != nil { + return nil, err + } + return out, nil +} + +// JSONPatch converts the passed in object to JSON, performs an RFC6902 JSON Patch using operations specified in the +// fragment, and returns the resulting object +func JSONPatch(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Object, error) { + target, err := runtime.Encode(codec, dst) + if err != nil { + return nil, err + } + patch, err := jsonpatch.DecodePatch([]byte(fragment)) + if err != nil { + return nil, err + } + patched, err := patch.Apply(target) + if err != nil { + return nil, err + } + out, err := runtime.Decode(codec, patched) + if err != nil { + return nil, err + } + return out, nil +} + // DumpReaderToFile writes all data from the given io.Reader to the specified file // (usually for temporary use). func DumpReaderToFile(reader io.Reader, filename string) error { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go index 55785335e6d..e644b49de76 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go @@ -63,45 +63,6 @@ func TestMerge(t *testing.T) { Spec: corev1.PodSpec{}, }, }, - /* TODO: uncomment this test once Merge is updated to use - strategic-merge-patch. See #8449. - { - obj: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - corev1.Container{ - Name: "c1", - Image: "red-image", - }, - corev1.Container{ - Name: "c2", - Image: "blue-image", - }, - }, - }, - }, - fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "containers": [ { "name": "c1", "image": "green-image" } ] } }`, schema.GroupVersion{Group:"", Version: "v1"}.String()), - expected: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - corev1.Container{ - Name: "c1", - Image: "green-image", - }, - corev1.Container{ - Name: "c2", - Image: "blue-image", - }, - }, - }, - }, - }, */ { obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -199,6 +160,151 @@ func TestMerge(t *testing.T) { } } +func TestStrategicMerge(t *testing.T) { + tests := []struct { + obj runtime.Object + dataStruct runtime.Object + fragment string + expected runtime.Object + expectErr bool + }{ + { + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "c1", + Image: "red-image", + }, + { + Name: "c2", + Image: "blue-image", + }, + }, + }, + }, + dataStruct: &corev1.Pod{}, + fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "containers": [ { "name": "c1", "image": "green-image" } ] } }`, + schema.GroupVersion{Group: "", Version: "v1"}.String()), + expected: &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "c1", + Image: "green-image", + }, + { + Name: "c2", + Image: "blue-image", + }, + }, + }, + }, + }, + { + obj: &corev1.Pod{}, + dataStruct: &corev1.Pod{}, + fragment: "invalid json", + expected: &corev1.Pod{}, + expectErr: true, + }, + { + obj: &corev1.Service{}, + dataStruct: &corev1.Pod{}, + fragment: `{ "apiVersion": "badVersion" }`, + expectErr: true, + }, + } + + codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), + scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) + for i, test := range tests { + out, err := StrategicMerge(codec, test.obj, test.fragment, test.dataStruct) + if !test.expectErr { + if err != nil { + t.Errorf("testcase[%d], unexpected error: %v", i, err) + } else if !apiequality.Semantic.DeepEqual(test.expected, out) { + t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, diff.ObjectReflectDiff(test.expected, out)) + } + } + if test.expectErr && err == nil { + t.Errorf("testcase[%d], unexpected non-error", i) + } + } +} + +func TestJSONPatch(t *testing.T) { + tests := []struct { + obj runtime.Object + fragment string + expected runtime.Object + expectErr bool + }{ + { + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "run": "test", + }, + }, + }, + fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`, + expected: &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "run": "test", + "foo": "bar", + }, + }, + Spec: corev1.PodSpec{}, + }, + }, + { + obj: &corev1.Pod{}, + fragment: "invalid json", + expected: &corev1.Pod{}, + expectErr: true, + }, + { + obj: &corev1.Pod{}, + fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`, + expectErr: true, + }, + } + + codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), + scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) + for i, test := range tests { + out, err := JSONPatch(codec, test.obj, test.fragment) + if !test.expectErr { + if err != nil { + t.Errorf("testcase[%d], unexpected error: %v", i, err) + } else if !apiequality.Semantic.DeepEqual(test.expected, out) { + t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, diff.ObjectReflectDiff(test.expected, out)) + } + } + if test.expectErr && err == nil { + t.Errorf("testcase[%d], unexpected non-error", i) + } + } +} + type checkErrTestCase struct { err error expectedErr string diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go new file mode 100644 index 00000000000..1e63bc789b4 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go @@ -0,0 +1,90 @@ +/* +Copyright 2021 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 util + +import ( + "fmt" + + "github.com/spf13/cobra" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util/i18n" +) + +type OverrideType string + +const ( + // OverrideTypeJSON will use an RFC6902 JSON Patch to alter the generated output + OverrideTypeJSON OverrideType = "json" + + // OverrideTypeMerge will use an RFC7396 JSON Merge Patch to alter the generated output + OverrideTypeMerge OverrideType = "merge" + + // OverrideTypeStrategic will use a Strategic Merge Patch to alter the generated output + OverrideTypeStrategic OverrideType = "strategic" +) + +const DefaultOverrideType = OverrideTypeMerge + +type OverrideOptions struct { + Overrides string + OverrideType OverrideType +} + +func (o *OverrideOptions) AddOverrideFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Overrides, "overrides", "", i18n.T("An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.")) + cmd.Flags().StringVar((*string)(&o.OverrideType), "override-type", string(DefaultOverrideType), fmt.Sprintf("The method used to override the generated object: %s, %s, or %s.", OverrideTypeJSON, OverrideTypeMerge, OverrideTypeStrategic)) +} + +func (o *OverrideOptions) NewOverrider(dataStruct runtime.Object) *Overrider { + return &Overrider{ + Options: o, + DataStruct: dataStruct, + } +} + +type Overrider struct { + Options *OverrideOptions + DataStruct runtime.Object +} + +func (o *Overrider) Apply(obj runtime.Object) (runtime.Object, error) { + if len(o.Options.Overrides) == 0 { + return obj, nil + } + + codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) + + var overrideType OverrideType + if len(o.Options.OverrideType) == 0 { + overrideType = DefaultOverrideType + } else { + overrideType = o.Options.OverrideType + } + + switch overrideType { + case OverrideTypeJSON: + return JSONPatch(codec, obj, o.Options.Overrides) + case OverrideTypeMerge: + return Merge(codec, obj, o.Options.Overrides) + case OverrideTypeStrategic: + return StrategicMerge(codec, obj, o.Options.Overrides, o.DataStruct) + default: + return nil, fmt.Errorf("invalid override type: %v", overrideType) + } +}