mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Add --override-type flag to kubectl run and kubectl expose to allow the choice of using a JSON Patch or Strategic Merge Patch to apply the override to the generated output.
This commit is contained in:
parent
17da6a2345
commit
0e697e19ac
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
90
staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go
Normal file
90
staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user