1/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package patch
18
19import (
20	"net/http"
21	"strings"
22	"testing"
23
24	"k8s.io/cli-runtime/pkg/genericclioptions"
25	"k8s.io/cli-runtime/pkg/resource"
26	"k8s.io/client-go/rest/fake"
27	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
28	"k8s.io/kubectl/pkg/scheme"
29)
30
31func TestPatchObject(t *testing.T) {
32	_, svc, _ := cmdtesting.TestData()
33
34	tf := cmdtesting.NewTestFactory().WithNamespace("test")
35	defer tf.Cleanup()
36
37	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
38
39	tf.UnstructuredClient = &fake.RESTClient{
40		NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
41		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
42			switch p, m := req.URL.Path, req.Method; {
43			case p == "/namespaces/test/services/frontend" && (m == "PATCH" || m == "GET"):
44				obj := svc.Items[0]
45
46				// ensure patched object reflects successful
47				// patch edits from the client
48				if m == "PATCH" {
49					obj.Spec.Type = "NodePort"
50				}
51				return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &obj)}, nil
52			default:
53				t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
54				return nil, nil
55			}
56		}),
57	}
58	stream, _, buf, _ := genericclioptions.NewTestIOStreams()
59
60	cmd := NewCmdPatch(tf, stream)
61	cmd.Flags().Set("namespace", "test")
62	cmd.Flags().Set("patch", `{"spec":{"type":"NodePort"}}`)
63	cmd.Flags().Set("output", "name")
64	cmd.Run(cmd, []string{"services/frontend"})
65
66	// uses the name from the response
67	if buf.String() != "service/baz\n" {
68		t.Errorf("unexpected output: %s", buf.String())
69	}
70}
71
72func TestPatchObjectFromFile(t *testing.T) {
73	_, svc, _ := cmdtesting.TestData()
74
75	tf := cmdtesting.NewTestFactory().WithNamespace("test")
76	defer tf.Cleanup()
77
78	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
79
80	tf.UnstructuredClient = &fake.RESTClient{
81		NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
82		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
83			switch p, m := req.URL.Path, req.Method; {
84			case p == "/namespaces/test/services/frontend" && (m == "PATCH" || m == "GET"):
85				return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
86			default:
87				t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
88				return nil, nil
89			}
90		}),
91	}
92	stream, _, buf, _ := genericclioptions.NewTestIOStreams()
93
94	cmd := NewCmdPatch(tf, stream)
95	cmd.Flags().Set("namespace", "test")
96	cmd.Flags().Set("patch", `{"spec":{"type":"NodePort"}}`)
97	cmd.Flags().Set("output", "name")
98	cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml")
99	cmd.Run(cmd, []string{})
100
101	// uses the name from the response
102	if buf.String() != "service/baz\n" {
103		t.Errorf("unexpected output: %s", buf.String())
104	}
105}
106
107func TestPatchNoop(t *testing.T) {
108	_, svc, _ := cmdtesting.TestData()
109	getObject := &svc.Items[0]
110	patchObject := &svc.Items[0]
111
112	tf := cmdtesting.NewTestFactory().WithNamespace("test")
113	defer tf.Cleanup()
114
115	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
116
117	tf.UnstructuredClient = &fake.RESTClient{
118		NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
119		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
120			switch p, m := req.URL.Path, req.Method; {
121			case p == "/namespaces/test/services/frontend" && m == "PATCH":
122				return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, patchObject)}, nil
123			case p == "/namespaces/test/services/frontend" && m == "GET":
124				return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getObject)}, nil
125			default:
126				t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
127				return nil, nil
128			}
129		}),
130	}
131
132	// Patched
133	{
134		patchObject = patchObject.DeepCopy()
135		if patchObject.Annotations == nil {
136			patchObject.Annotations = map[string]string{}
137		}
138		patchObject.Annotations["foo"] = "bar"
139		stream, _, buf, _ := genericclioptions.NewTestIOStreams()
140		cmd := NewCmdPatch(tf, stream)
141		cmd.Flags().Set("namespace", "test")
142		cmd.Flags().Set("patch", `{"metadata":{"annotations":{"foo":"bar"}}}`)
143		cmd.Run(cmd, []string{"services", "frontend"})
144		if buf.String() != "service/baz patched\n" {
145			t.Errorf("unexpected output: %s", buf.String())
146		}
147	}
148}
149
150func TestPatchObjectFromFileOutput(t *testing.T) {
151	_, svc, _ := cmdtesting.TestData()
152
153	svcCopy := svc.Items[0].DeepCopy()
154	if svcCopy.Labels == nil {
155		svcCopy.Labels = map[string]string{}
156	}
157	svcCopy.Labels["post-patch"] = "post-patch-value"
158
159	tf := cmdtesting.NewTestFactory().WithNamespace("test")
160	defer tf.Cleanup()
161
162	codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
163
164	tf.UnstructuredClient = &fake.RESTClient{
165		NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
166		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
167			switch p, m := req.URL.Path, req.Method; {
168			case p == "/namespaces/test/services/frontend" && m == "GET":
169				return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil
170			case p == "/namespaces/test/services/frontend" && m == "PATCH":
171				return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svcCopy)}, nil
172			default:
173				t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
174				return nil, nil
175			}
176		}),
177	}
178	stream, _, buf, _ := genericclioptions.NewTestIOStreams()
179
180	cmd := NewCmdPatch(tf, stream)
181	cmd.Flags().Set("namespace", "test")
182	cmd.Flags().Set("patch", `{"spec":{"type":"NodePort"}}`)
183	cmd.Flags().Set("output", "yaml")
184	cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml")
185	cmd.Run(cmd, []string{})
186
187	t.Log(buf.String())
188	// make sure the value returned by the server is used
189	if !strings.Contains(buf.String(), "post-patch: post-patch-value") {
190		t.Errorf("unexpected output: %s", buf.String())
191	}
192}
193