1
16
17 package run
18
19 import (
20 "bytes"
21 "fmt"
22 "io"
23 "net/http"
24 "os"
25 "reflect"
26 "strconv"
27 "strings"
28 "testing"
29
30 "github.com/spf13/cobra"
31
32 corev1 "k8s.io/api/core/v1"
33 apiequality "k8s.io/apimachinery/pkg/api/equality"
34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35 "k8s.io/apimachinery/pkg/runtime"
36 "k8s.io/apimachinery/pkg/util/intstr"
37 "k8s.io/cli-runtime/pkg/genericclioptions"
38 "k8s.io/cli-runtime/pkg/genericiooptions"
39 restclient "k8s.io/client-go/rest"
40 "k8s.io/client-go/rest/fake"
41 "k8s.io/kubectl/pkg/cmd/delete"
42 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
43 cmdutil "k8s.io/kubectl/pkg/cmd/util"
44 "k8s.io/kubectl/pkg/scheme"
45 "k8s.io/kubectl/pkg/util/i18n"
46 )
47
48 func TestGetRestartPolicy(t *testing.T) {
49 tests := []struct {
50 input string
51 interactive bool
52 expected corev1.RestartPolicy
53 expectErr bool
54 }{
55 {
56 input: "",
57 expected: corev1.RestartPolicyAlways,
58 },
59 {
60 input: "",
61 interactive: true,
62 expected: corev1.RestartPolicyOnFailure,
63 },
64 {
65 input: string(corev1.RestartPolicyAlways),
66 interactive: true,
67 expected: corev1.RestartPolicyAlways,
68 },
69 {
70 input: string(corev1.RestartPolicyNever),
71 interactive: true,
72 expected: corev1.RestartPolicyNever,
73 },
74 {
75 input: string(corev1.RestartPolicyAlways),
76 expected: corev1.RestartPolicyAlways,
77 },
78 {
79 input: string(corev1.RestartPolicyNever),
80 expected: corev1.RestartPolicyNever,
81 },
82 {
83 input: "foo",
84 expectErr: true,
85 },
86 }
87 for _, test := range tests {
88 cmd := &cobra.Command{}
89 cmd.Flags().String("restart", "", i18n.T("dummy restart flag)"))
90 cmd.Flags().Lookup("restart").Value.Set(test.input)
91 policy, err := getRestartPolicy(cmd, test.interactive)
92 if test.expectErr && err == nil {
93 t.Error("unexpected non-error")
94 }
95 if !test.expectErr && err != nil {
96 t.Errorf("unexpected error: %v", err)
97 }
98 if !test.expectErr && policy != test.expected {
99 t.Errorf("expected: %s, saw: %s (%s:%v)", test.expected, policy, test.input, test.interactive)
100 }
101 }
102 }
103
104 func TestGetEnv(t *testing.T) {
105 test := struct {
106 input []string
107 expected []string
108 }{
109 input: []string{"a=b", "c=d"},
110 expected: []string{"a=b", "c=d"},
111 }
112 cmd := &cobra.Command{}
113 cmd.Flags().StringSlice("env", test.input, "")
114
115 envStrings := cmdutil.GetFlagStringSlice(cmd, "env")
116 if len(envStrings) != 2 || !reflect.DeepEqual(envStrings, test.expected) {
117 t.Errorf("expected: %s, saw: %s", test.expected, envStrings)
118 }
119 }
120
121 func TestRunArgsFollowDashRules(t *testing.T) {
122 one := int32(1)
123 rc := &corev1.ReplicationController{
124 ObjectMeta: metav1.ObjectMeta{Name: "rc1", Namespace: "test", ResourceVersion: "18"},
125 Spec: corev1.ReplicationControllerSpec{
126 Replicas: &one,
127 },
128 }
129
130 tests := []struct {
131 args []string
132 argsLenAtDash int
133 expectError bool
134 name string
135 }{
136 {
137 args: []string{},
138 argsLenAtDash: -1,
139 expectError: true,
140 name: "empty",
141 },
142 {
143 args: []string{"foo"},
144 argsLenAtDash: -1,
145 expectError: false,
146 name: "no cmd",
147 },
148 {
149 args: []string{"foo", "sleep"},
150 argsLenAtDash: -1,
151 expectError: false,
152 name: "cmd no dash",
153 },
154 {
155 args: []string{"foo", "sleep"},
156 argsLenAtDash: 1,
157 expectError: false,
158 name: "cmd has dash",
159 },
160 {
161 args: []string{"foo", "sleep"},
162 argsLenAtDash: 0,
163 expectError: true,
164 name: "no name",
165 },
166 }
167 for _, test := range tests {
168 t.Run(test.name, func(t *testing.T) {
169 tf := cmdtesting.NewTestFactory().WithNamespace("test")
170 defer tf.Cleanup()
171
172 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
173 ns := scheme.Codecs.WithoutConversion()
174
175 tf.Client = &fake.RESTClient{
176 GroupVersion: corev1.SchemeGroupVersion,
177 NegotiatedSerializer: ns,
178 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
179 if req.URL.Path == "/namespaces/test/pods" {
180 return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, rc)}, nil
181 }
182 return &http.Response{
183 StatusCode: http.StatusOK,
184 Body: io.NopCloser(bytes.NewBuffer([]byte("{}"))),
185 }, nil
186 }),
187 }
188
189 tf.ClientConfigVal = &restclient.Config{}
190
191 cmd := NewCmdRun(tf, genericiooptions.NewTestIOStreamsDiscard())
192 cmd.Flags().Set("image", "nginx")
193
194 printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme)
195 printer, err := printFlags.ToPrinter()
196 if err != nil {
197 t.Errorf("unexpected error: %v", err)
198 return
199 }
200
201 deleteFlags := delete.NewDeleteFlags("to use to replace the resource.")
202 deleteOptions, err := deleteFlags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
203 if err != nil {
204 t.Errorf("unexpected error: %v", err)
205 return
206 }
207 opts := &RunOptions{
208 PrintFlags: printFlags,
209 DeleteOptions: deleteOptions,
210
211 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
212
213 Image: "nginx",
214
215 PrintObj: func(obj runtime.Object) error {
216 return printer.PrintObj(obj, os.Stdout)
217 },
218 Recorder: genericclioptions.NoopRecorder{},
219
220 ArgsLenAtDash: test.argsLenAtDash,
221 }
222
223 err = opts.Run(tf, cmd, test.args)
224 if test.expectError && err == nil {
225 t.Errorf("unexpected non-error (%s)", test.name)
226 }
227 if !test.expectError && err != nil {
228 t.Errorf("unexpected error: %v (%s)", err, test.name)
229 }
230 })
231 }
232 }
233
234 func TestGenerateService(t *testing.T) {
235 tests := []struct {
236 name string
237 port string
238 args []string
239 params map[string]interface{}
240 expectErr bool
241 service corev1.Service
242 expectPOST bool
243 }{
244 {
245 name: "basic",
246 port: "80",
247 args: []string{"foo"},
248 params: map[string]interface{}{
249 "name": "foo",
250 },
251 expectErr: false,
252 service: corev1.Service{
253 TypeMeta: metav1.TypeMeta{
254 Kind: "Service",
255 APIVersion: "v1",
256 },
257 ObjectMeta: metav1.ObjectMeta{
258 Name: "foo",
259 },
260 Spec: corev1.ServiceSpec{
261 Ports: []corev1.ServicePort{
262 {
263 Port: 80,
264 Protocol: "TCP",
265 TargetPort: intstr.FromInt32(80),
266 },
267 },
268 Selector: map[string]string{
269 "run": "foo",
270 },
271 },
272 },
273 expectPOST: true,
274 },
275 {
276 name: "custom labels",
277 port: "80",
278 args: []string{"foo"},
279 params: map[string]interface{}{
280 "name": "foo",
281 "labels": "app=bar",
282 },
283 expectErr: false,
284 service: corev1.Service{
285 TypeMeta: metav1.TypeMeta{
286 Kind: "Service",
287 APIVersion: "v1",
288 },
289 ObjectMeta: metav1.ObjectMeta{
290 Name: "foo",
291 Labels: map[string]string{"app": "bar"},
292 },
293 Spec: corev1.ServiceSpec{
294 Ports: []corev1.ServicePort{
295 {
296 Port: 80,
297 Protocol: "TCP",
298 TargetPort: intstr.FromInt32(80),
299 },
300 },
301 Selector: map[string]string{
302 "app": "bar",
303 },
304 },
305 },
306 expectPOST: true,
307 },
308 {
309 expectErr: true,
310 name: "missing port",
311 expectPOST: false,
312 },
313 {
314 name: "dry-run",
315 port: "80",
316 args: []string{"foo"},
317 params: map[string]interface{}{
318 "name": "foo",
319 },
320 expectErr: false,
321 expectPOST: false,
322 },
323 }
324 for _, test := range tests {
325 t.Run(test.name, func(t *testing.T) {
326 sawPOST := false
327 tf := cmdtesting.NewTestFactory()
328 defer tf.Cleanup()
329
330 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
331 ns := scheme.Codecs.WithoutConversion()
332
333 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
334 tf.Client = &fake.RESTClient{
335 GroupVersion: corev1.SchemeGroupVersion,
336 NegotiatedSerializer: ns,
337 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
338 switch p, m := req.URL.Path, req.Method; {
339 case test.expectPOST && m == "POST" && p == "/namespaces/test/services":
340 sawPOST = true
341 body := cmdtesting.ObjBody(codec, &test.service)
342 data, err := io.ReadAll(req.Body)
343 if err != nil {
344 t.Fatalf("unexpected error: %v", err)
345 }
346 defer req.Body.Close()
347 svc := &corev1.Service{}
348 if err := runtime.DecodeInto(codec, data, svc); err != nil {
349 t.Fatalf("unexpected error: %v", err)
350 }
351
352 test.service.Annotations = svc.Annotations
353
354 if !apiequality.Semantic.DeepEqual(&test.service, svc) {
355 t.Errorf("expected:\n%v\nsaw:\n%v\n", &test.service, svc)
356 }
357 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
358 default:
359 t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req)
360 return nil, fmt.Errorf("unexpected request")
361 }
362 }),
363 }
364
365 printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme)
366 printer, err := printFlags.ToPrinter()
367 if err != nil {
368 t.Errorf("unexpected error: %v", err)
369 return
370 }
371
372 ioStreams, _, buff, _ := genericiooptions.NewTestIOStreams()
373 deleteFlags := delete.NewDeleteFlags("to use to replace the resource.")
374 deleteOptions, err := deleteFlags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
375 if err != nil {
376 t.Errorf("unexpected error: %v", err)
377 return
378 }
379 opts := &RunOptions{
380 PrintFlags: printFlags,
381 DeleteOptions: deleteOptions,
382
383 IOStreams: ioStreams,
384
385 Port: test.port,
386 Recorder: genericclioptions.NoopRecorder{},
387
388 PrintObj: func(obj runtime.Object) error {
389 return printer.PrintObj(obj, buff)
390 },
391
392 Namespace: "test",
393 }
394
395 cmd := &cobra.Command{}
396 cmd.Flags().Bool(cmdutil.ApplyAnnotationsFlag, false, "")
397 cmd.Flags().Bool("record", false, "Record current kubectl command in the resource annotation. If set to false, do not record the command. If set to true, record the command. If not set, default to updating the existing annotation value only if one already exists.")
398 addRunFlags(cmd, opts)
399
400 if !test.expectPOST {
401 opts.DryRunStrategy = cmdutil.DryRunClient
402 }
403
404 if len(test.port) > 0 {
405 cmd.Flags().Set("port", test.port)
406 test.params["port"] = test.port
407 }
408
409 _, err = opts.generateService(tf, cmd, test.params)
410 if test.expectErr {
411 if err == nil {
412 t.Error("unexpected non-error")
413 }
414 return
415 }
416 if err != nil {
417 t.Errorf("unexpected error: %v", err)
418 }
419 if test.expectPOST != sawPOST {
420 t.Errorf("expectPost: %v, sawPost: %v", test.expectPOST, sawPOST)
421 }
422 })
423 }
424 }
425
426 func TestRunValidations(t *testing.T) {
427 tests := []struct {
428 name string
429 args []string
430 flags map[string]string
431 expectedErr string
432 }{
433 {
434 name: "test missing name error",
435 expectedErr: "NAME is required",
436 },
437 {
438 name: "test missing --image error",
439 args: []string{"test"},
440 expectedErr: "--image is required",
441 },
442 {
443 name: "test invalid image name error",
444 args: []string{"test"},
445 flags: map[string]string{
446 "image": "#",
447 },
448 expectedErr: "Invalid image name",
449 },
450 {
451 name: "test rm errors when used on non-attached containers",
452 args: []string{"test"},
453 flags: map[string]string{
454 "image": "busybox",
455 "rm": "true",
456 },
457 expectedErr: "rm should only be used for attached containers",
458 },
459 {
460 name: "test error on attached containers options",
461 args: []string{"test"},
462 flags: map[string]string{
463 "image": "busybox",
464 "attach": "true",
465 "dry-run": "client",
466 },
467 expectedErr: "can't be used with attached containers options",
468 },
469 {
470 name: "test error on attached containers options, with value from stdin",
471 args: []string{"test"},
472 flags: map[string]string{
473 "image": "busybox",
474 "stdin": "true",
475 "dry-run": "client",
476 },
477 expectedErr: "can't be used with attached containers options",
478 },
479 {
480 name: "test error on attached containers options, with value from stdin and tty",
481 args: []string{"test"},
482 flags: map[string]string{
483 "image": "busybox",
484 "tty": "true",
485 "stdin": "true",
486 "dry-run": "client",
487 },
488 expectedErr: "can't be used with attached containers options",
489 },
490 {
491 name: "test error when tty=true and no stdin provided",
492 args: []string{"test"},
493 flags: map[string]string{
494 "image": "busybox",
495 "tty": "true",
496 },
497 expectedErr: "stdin is required for containers with -t/--tty",
498 },
499 {
500 name: "test invalid override type error",
501 args: []string{"test"},
502 flags: map[string]string{
503 "image": "busybox",
504 "overrides": "{}",
505 "override-type": "foo",
506 },
507 expectedErr: "invalid override type: foo",
508 },
509 }
510 for _, test := range tests {
511 t.Run(test.name, func(t *testing.T) {
512 tf := cmdtesting.NewTestFactory().WithNamespace("test")
513 defer tf.Cleanup()
514
515 _, _, codec := cmdtesting.NewExternalScheme()
516 ns := scheme.Codecs.WithoutConversion()
517 tf.Client = &fake.RESTClient{
518 NegotiatedSerializer: ns,
519 Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, cmdtesting.NewInternalType("", "", ""))},
520 }
521 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
522
523 streams, _, _, bufErr := genericiooptions.NewTestIOStreams()
524 cmdutil.BehaviorOnFatal(func(str string, code int) {
525 bufErr.Write([]byte(str))
526 })
527
528 cmd := NewCmdRun(tf, streams)
529 for flagName, flagValue := range test.flags {
530 cmd.Flags().Set(flagName, flagValue)
531 }
532 cmd.Run(cmd, test.args)
533
534 var err error
535 if bufErr.Len() > 0 {
536 err = fmt.Errorf("%v", bufErr.String())
537 }
538 if err != nil && len(test.expectedErr) > 0 {
539 if !strings.Contains(err.Error(), test.expectedErr) {
540 t.Errorf("unexpected error: %v", err)
541 }
542 }
543 })
544 }
545
546 }
547
548 func TestExpose(t *testing.T) {
549 tests := []struct {
550 name string
551 podName string
552 imageName string
553 podLabels map[string]string
554 port int
555 }{
556 {
557 name: "test simple expose",
558 podName: "test-pod",
559 imageName: "test-image",
560 podLabels: map[string]string{"color": "red", "shape": "square"},
561 port: 1234,
562 },
563 }
564
565 for _, test := range tests {
566 t.Run(test.name, func(t *testing.T) {
567
568 tf := cmdtesting.NewTestFactory().WithNamespace("test")
569 defer tf.Cleanup()
570
571 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
572 ns := scheme.Codecs.WithoutConversion()
573 tf.Client = &fake.RESTClient{
574 NegotiatedSerializer: ns,
575 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
576 t.Logf("path: %v, method: %v", req.URL.Path, req.Method)
577 switch p, m := req.URL.Path, req.Method; {
578 case m == "POST" && p == "/namespaces/test/pods":
579 pod := &corev1.Pod{}
580 body := cmdtesting.ObjBody(codec, pod)
581 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
582 case m == "POST" && p == "/namespaces/test/services":
583 data, err := io.ReadAll(req.Body)
584 if err != nil {
585 t.Fatalf("unexpected error: %v", err)
586 }
587
588 service := &corev1.Service{}
589 if err := runtime.DecodeInto(codec, data, service); err != nil {
590 t.Fatalf("unexpected error: %v", err)
591 }
592
593 if service.ObjectMeta.Name != test.podName {
594 t.Errorf("Invalid name on service. Expected:%v, Actual:%v", test.podName, service.ObjectMeta.Name)
595 }
596
597 if !reflect.DeepEqual(service.Spec.Selector, test.podLabels) {
598 t.Errorf("Invalid selector on service. Expected:%v, Actual:%v", test.podLabels, service.Spec.Selector)
599 }
600
601 if len(service.Spec.Ports) != 1 && service.Spec.Ports[0].Port != int32(test.port) {
602 t.Errorf("Invalid port on service: %v", service.Spec.Ports)
603 }
604
605 body := cmdtesting.ObjBody(codec, service)
606
607 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
608 default:
609 t.Errorf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
610 return nil, fmt.Errorf("unexpected request")
611 }
612 }),
613 }
614
615 streams, _, _, bufErr := genericiooptions.NewTestIOStreams()
616 cmdutil.BehaviorOnFatal(func(str string, code int) {
617 bufErr.Write([]byte(str))
618 })
619
620 cmd := NewCmdRun(tf, streams)
621 cmd.Flags().Set("image", test.imageName)
622 cmd.Flags().Set("expose", "true")
623 cmd.Flags().Set("port", strconv.Itoa(test.port))
624
625 labels := []string{}
626 for k, v := range test.podLabels {
627 labels = append(labels, fmt.Sprintf("%s=%s", k, v))
628 }
629 cmd.Flags().Set("labels", strings.Join(labels, ","))
630
631 cmd.Run(cmd, []string{test.podName})
632
633 if bufErr.Len() > 0 {
634 err := fmt.Errorf("%v", bufErr.String())
635 if err != nil {
636 t.Errorf("unexpected error: %v", err)
637 }
638 }
639 })
640
641 }
642 }
643
644 func TestRunOverride(t *testing.T) {
645 tests := []struct {
646 name string
647 overrides string
648 overrideType string
649 expectedOutput string
650 }{
651 {
652 name: "run with merge override type should replace spec",
653 overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`,
654 overrideType: "merge",
655 expectedOutput: `apiVersion: v1
656 kind: Pod
657 metadata:
658 creationTimestamp: null
659 labels:
660 run: test
661 name: test
662 namespace: ns
663 spec:
664 containers:
665 - name: test
666 resources:
667 limits:
668 cpu: 200m
669 dnsPolicy: ClusterFirst
670 restartPolicy: Always
671 status: {}
672 `,
673 },
674 {
675 name: "run with no override type specified, should perform an RFC7396 JSON Merge Patch",
676 overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`,
677 overrideType: "",
678 expectedOutput: `apiVersion: v1
679 kind: Pod
680 metadata:
681 creationTimestamp: null
682 labels:
683 run: test
684 name: test
685 namespace: ns
686 spec:
687 containers:
688 - name: test
689 resources:
690 limits:
691 cpu: 200m
692 dnsPolicy: ClusterFirst
693 restartPolicy: Always
694 status: {}
695 `,
696 },
697 {
698 name: "run with strategic override type should merge spec, preserving container image",
699 overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`,
700 overrideType: "strategic",
701 expectedOutput: `apiVersion: v1
702 kind: Pod
703 metadata:
704 creationTimestamp: null
705 labels:
706 run: test
707 name: test
708 namespace: ns
709 spec:
710 containers:
711 - image: busybox
712 name: test
713 resources:
714 limits:
715 cpu: 200m
716 dnsPolicy: ClusterFirst
717 restartPolicy: Always
718 status: {}
719 `,
720 },
721 {
722 name: "run with json override type should perform add, replace, and remove operations",
723 overrides: `[
724 {"op": "add", "path": "/metadata/labels/foo", "value": "bar"},
725 {"op": "replace", "path": "/spec/containers/0/resources", "value": {"limits": {"cpu": "200m"}}},
726 {"op": "remove", "path": "/spec/dnsPolicy"}
727 ]`,
728 overrideType: "json",
729 expectedOutput: `apiVersion: v1
730 kind: Pod
731 metadata:
732 creationTimestamp: null
733 labels:
734 foo: bar
735 run: test
736 name: test
737 namespace: ns
738 spec:
739 containers:
740 - image: busybox
741 name: test
742 resources:
743 limits:
744 cpu: 200m
745 restartPolicy: Always
746 status: {}
747 `,
748 },
749 }
750 for _, test := range tests {
751 t.Run(test.name, func(t *testing.T) {
752 tf := cmdtesting.NewTestFactory().WithNamespace("ns")
753 defer tf.Cleanup()
754
755 streams, _, bufOut, _ := genericiooptions.NewTestIOStreams()
756
757 cmd := NewCmdRun(tf, streams)
758 cmd.Flags().Set("dry-run", "client")
759 cmd.Flags().Set("output", "yaml")
760 cmd.Flags().Set("image", "busybox")
761 cmd.Flags().Set("overrides", test.overrides)
762 cmd.Flags().Set("override-type", test.overrideType)
763 cmd.Run(cmd, []string{"test"})
764
765 actualOutput := bufOut.String()
766 if actualOutput != test.expectedOutput {
767 t.Errorf("unexpected output.\n\nExpected:\n%v\nActual:\n%v", test.expectedOutput, actualOutput)
768 }
769 })
770 }
771 }
772
View as plain text