1
16
17 package rollout
18
19 import (
20 "bytes"
21 "io"
22 "net/http"
23 "testing"
24
25 appsv1 "k8s.io/api/apps/v1"
26 "k8s.io/apimachinery/pkg/api/meta"
27 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/cli-runtime/pkg/genericclioptions"
30 "k8s.io/cli-runtime/pkg/genericiooptions"
31 "k8s.io/client-go/rest/fake"
32 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
33 "k8s.io/kubectl/pkg/polymorphichelpers"
34 "k8s.io/kubectl/pkg/scheme"
35 )
36
37 type fakeHistoryViewer struct {
38 viewHistoryFn func(namespace, name string, revision int64) (string, error)
39 getHistoryFn func(namespace, name string) (map[int64]runtime.Object, error)
40 }
41
42 func (h *fakeHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
43 return h.viewHistoryFn(namespace, name, revision)
44 }
45
46 func (h *fakeHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
47 return h.getHistoryFn(namespace, name)
48 }
49
50 func setupFakeHistoryViewer(t *testing.T) *fakeHistoryViewer {
51 fhv := &fakeHistoryViewer{
52 viewHistoryFn: func(namespace, name string, revision int64) (string, error) {
53 t.Fatalf("ViewHistory mock not implemented")
54 return "", nil
55 },
56 getHistoryFn: func(namespace, name string) (map[int64]runtime.Object, error) {
57 t.Fatalf("GetHistory mock not implemented")
58 return nil, nil
59 },
60 }
61 polymorphichelpers.HistoryViewerFn = func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (polymorphichelpers.HistoryViewer, error) {
62 return fhv, nil
63 }
64 return fhv
65 }
66
67 func TestRolloutHistory(t *testing.T) {
68 ns := scheme.Codecs.WithoutConversion()
69 tf := cmdtesting.NewTestFactory().WithNamespace("test")
70 defer tf.Cleanup()
71
72 info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
73 encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
74
75 tf.Client = &RolloutPauseRESTClient{
76 RESTClient: &fake.RESTClient{
77 GroupVersion: rolloutPauseGroupVersionEncoder,
78 NegotiatedSerializer: ns,
79 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
80 switch p, m := req.URL.Path, req.Method; {
81 case p == "/namespaces/test/deployments/foo" && m == "GET":
82 responseDeployment := &appsv1.Deployment{}
83 responseDeployment.Name = "foo"
84 body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
85 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
86 default:
87 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
88 return nil, nil
89 }
90 }),
91 },
92 }
93
94 testCases := map[string]struct {
95 flags map[string]string
96 expectedOutput string
97 expectedRevision int64
98 }{
99 "should display ViewHistory output for all revisions": {
100 expectedOutput: `deployment.apps/foo
101 Fake ViewHistory Output
102
103 `,
104 expectedRevision: int64(0),
105 },
106 "should display ViewHistory output for a single revision": {
107 flags: map[string]string{"revision": "2"},
108 expectedOutput: `deployment.apps/foo with revision #2
109 Fake ViewHistory Output
110
111 `,
112 expectedRevision: int64(2),
113 },
114 }
115
116 for name, tc := range testCases {
117 t.Run(name, func(tt *testing.T) {
118 fhv := setupFakeHistoryViewer(tt)
119 var actualNamespace, actualName *string
120 var actualRevision *int64
121 fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) {
122 actualNamespace = &namespace
123 actualName = &name
124 actualRevision = &revision
125 return "Fake ViewHistory Output\n", nil
126 }
127
128 streams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
129 cmd := NewCmdRolloutHistory(tf, streams)
130 for k, v := range tc.flags {
131 cmd.Flags().Set(k, v)
132 }
133 cmd.Run(cmd, []string{"deployment/foo"})
134
135 expectedErrorOutput := ""
136 if errBuf.String() != expectedErrorOutput {
137 tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
138 }
139
140 if buf.String() != tc.expectedOutput {
141 tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
142 }
143
144 expectedNamespace := "test"
145 if actualNamespace == nil || *actualNamespace != expectedNamespace {
146 tt.Fatalf("expected ViewHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace)
147 }
148
149 expectedName := "foo"
150 if actualName == nil || *actualName != expectedName {
151 tt.Fatalf("expected ViewHistory to have been called with name %s, but it was %v", expectedName, *actualName)
152 }
153
154 if actualRevision == nil {
155 tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was ", tc.expectedRevision)
156 } else if *actualRevision != tc.expectedRevision {
157 tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was %v", tc.expectedRevision, *actualRevision)
158 }
159 })
160 }
161 }
162
163 func TestMultipleResourceRolloutHistory(t *testing.T) {
164 ns := scheme.Codecs.WithoutConversion()
165 tf := cmdtesting.NewTestFactory().WithNamespace("test")
166 defer tf.Cleanup()
167
168 info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
169 encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
170
171 tf.Client = &RolloutPauseRESTClient{
172 RESTClient: &fake.RESTClient{
173 GroupVersion: rolloutPauseGroupVersionEncoder,
174 NegotiatedSerializer: ns,
175 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
176 switch p, m := req.URL.Path, req.Method; {
177 case p == "/namespaces/test/deployments/foo" && m == "GET":
178 responseDeployment := &appsv1.Deployment{}
179 responseDeployment.Name = "foo"
180 body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
181 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
182 case p == "/namespaces/test/deployments/bar" && m == "GET":
183 responseDeployment := &appsv1.Deployment{}
184 responseDeployment.Name = "bar"
185 body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
186 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
187 default:
188 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
189 return nil, nil
190 }
191 }),
192 },
193 }
194
195 testCases := map[string]struct {
196 flags map[string]string
197 expectedOutput string
198 }{
199 "should display ViewHistory output for all revisions": {
200 expectedOutput: `deployment.apps/foo
201 Fake ViewHistory Output
202
203 deployment.apps/bar
204 Fake ViewHistory Output
205
206 `,
207 },
208 "should display ViewHistory output for a single revision": {
209 flags: map[string]string{"revision": "2"},
210 expectedOutput: `deployment.apps/foo with revision #2
211 Fake ViewHistory Output
212
213 deployment.apps/bar with revision #2
214 Fake ViewHistory Output
215
216 `,
217 },
218 }
219
220 for name, tc := range testCases {
221 t.Run(name, func(tt *testing.T) {
222 fhv := setupFakeHistoryViewer(tt)
223 fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) {
224 return "Fake ViewHistory Output\n", nil
225 }
226
227 streams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
228 cmd := NewCmdRolloutHistory(tf, streams)
229 for k, v := range tc.flags {
230 cmd.Flags().Set(k, v)
231 }
232 cmd.Run(cmd, []string{"deployment/foo", "deployment/bar"})
233
234 expectedErrorOutput := ""
235 if errBuf.String() != expectedErrorOutput {
236 tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
237 }
238
239 if buf.String() != tc.expectedOutput {
240 tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
241 }
242 })
243 }
244 }
245
246 func TestRolloutHistoryWithOutput(t *testing.T) {
247 ns := scheme.Codecs.WithoutConversion()
248 tf := cmdtesting.NewTestFactory().WithNamespace("test")
249 defer tf.Cleanup()
250
251 info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
252 encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
253
254 tf.Client = &RolloutPauseRESTClient{
255 RESTClient: &fake.RESTClient{
256 GroupVersion: rolloutPauseGroupVersionEncoder,
257 NegotiatedSerializer: ns,
258 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
259 switch p, m := req.URL.Path, req.Method; {
260 case p == "/namespaces/test/deployments/foo" && m == "GET":
261 responseDeployment := &appsv1.Deployment{}
262 responseDeployment.Name = "foo"
263 body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
264 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
265 default:
266 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
267 return nil, nil
268 }
269 }),
270 },
271 }
272
273 testCases := map[string]struct {
274 flags map[string]string
275 expectedOutput string
276 }{
277 "json": {
278 flags: map[string]string{"revision": "2", "output": "json"},
279 expectedOutput: `{
280 "kind": "ReplicaSet",
281 "apiVersion": "apps/v1",
282 "metadata": {
283 "name": "rev2",
284 "creationTimestamp": null
285 },
286 "spec": {
287 "selector": null,
288 "template": {
289 "metadata": {
290 "creationTimestamp": null
291 },
292 "spec": {
293 "containers": null
294 }
295 }
296 },
297 "status": {
298 "replicas": 0
299 }
300 }
301 `,
302 },
303 "yaml": {
304 flags: map[string]string{"revision": "2", "output": "yaml"},
305 expectedOutput: `apiVersion: apps/v1
306 kind: ReplicaSet
307 metadata:
308 creationTimestamp: null
309 name: rev2
310 spec:
311 selector: null
312 template:
313 metadata:
314 creationTimestamp: null
315 spec:
316 containers: null
317 status:
318 replicas: 0
319 `,
320 },
321 "yaml all revisions": {
322 flags: map[string]string{"output": "yaml"},
323 expectedOutput: `apiVersion: apps/v1
324 kind: ReplicaSet
325 metadata:
326 creationTimestamp: null
327 name: rev1
328 spec:
329 selector: null
330 template:
331 metadata:
332 creationTimestamp: null
333 spec:
334 containers: null
335 status:
336 replicas: 0
337 ---
338 apiVersion: apps/v1
339 kind: ReplicaSet
340 metadata:
341 creationTimestamp: null
342 name: rev2
343 spec:
344 selector: null
345 template:
346 metadata:
347 creationTimestamp: null
348 spec:
349 containers: null
350 status:
351 replicas: 0
352 `,
353 },
354 "name": {
355 flags: map[string]string{"output": "name"},
356 expectedOutput: `replicaset.apps/rev1
357 replicaset.apps/rev2
358 `,
359 },
360 }
361
362 for name, tc := range testCases {
363 t.Run(name, func(t *testing.T) {
364 fhv := setupFakeHistoryViewer(t)
365 var actualNamespace, actualName *string
366 fhv.getHistoryFn = func(namespace, name string) (map[int64]runtime.Object, error) {
367 actualNamespace = &namespace
368 actualName = &name
369 return map[int64]runtime.Object{
370 1: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev1"}},
371 2: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev2"}},
372 }, nil
373 }
374
375 streams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
376 cmd := NewCmdRolloutHistory(tf, streams)
377 for k, v := range tc.flags {
378 cmd.Flags().Set(k, v)
379 }
380 cmd.Run(cmd, []string{"deployment/foo"})
381
382 expectedErrorOutput := ""
383 if errBuf.String() != expectedErrorOutput {
384 t.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
385 }
386
387 if buf.String() != tc.expectedOutput {
388 t.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
389 }
390
391 expectedNamespace := "test"
392 if actualNamespace == nil || *actualNamespace != expectedNamespace {
393 t.Fatalf("expected GetHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace)
394 }
395
396 expectedName := "foo"
397 if actualName == nil || *actualName != expectedName {
398 t.Fatalf("expected GetHistory to have been called with name %s, but it was %v", expectedName, *actualName)
399 }
400 })
401 }
402 }
403
404 func TestValidate(t *testing.T) {
405 opts := RolloutHistoryOptions{
406 Revision: 0,
407 Resources: []string{"deployment/foo"},
408 }
409 if err := opts.Validate(); err != nil {
410 t.Fatalf("unexpected error: %s", err)
411 }
412
413 opts.Revision = -1
414 expectedError := "revision must be a positive integer: -1"
415 if err := opts.Validate(); err == nil {
416 t.Fatalf("unexpected non error")
417 } else if err.Error() != expectedError {
418 t.Fatalf("expected error %s, but got %s", expectedError, err.Error())
419 }
420
421 opts.Revision = 0
422 opts.Resources = []string{}
423 expectedError = "required resource not specified"
424 if err := opts.Validate(); err == nil {
425 t.Fatalf("unexpected non error")
426 } else if err.Error() != expectedError {
427 t.Fatalf("expected error %s, but got %s", expectedError, err.Error())
428 }
429 }
430
View as plain text