Merge pull request #105140 from brianpursley/kubectl-1101

Add --override-type flag to kubectl run and kubectl expose
This commit is contained in:
Kubernetes Prow Robot 2021-11-05 05:13:52 -07:00 committed by GitHub
commit 47041cd2a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 614 additions and 56 deletions

View File

@ -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 {

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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 {

View File

@ -66,45 +66,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{
@ -202,6 +163,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

View File

@ -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)
}
}