From 3cfcf3a74fc24e6b4b3f58710fe454d1bc3644cc Mon Sep 17 00:00:00 2001 From: Lee Verberne Date: Fri, 30 Oct 2020 08:54:13 +0100 Subject: [PATCH 1/2] kubectl debug: add tests for Complete,Validate --- build/visible_to/BUILD | 1 + .../src/k8s.io/kubectl/pkg/cmd/debug/BUILD | 3 + .../src/k8s.io/kubectl/pkg/cmd/debug/debug.go | 72 ++--- .../kubectl/pkg/cmd/debug/debug_test.go | 271 +++++++++++++++++- 4 files changed, 311 insertions(+), 36 deletions(-) diff --git a/build/visible_to/BUILD b/build/visible_to/BUILD index 1f2b7ecc4e2..191b7731b9c 100644 --- a/build/visible_to/BUILD +++ b/build/visible_to/BUILD @@ -248,6 +248,7 @@ package_group( "//staging/src/k8s.io/kubectl/pkg/cmd/config", "//staging/src/k8s.io/kubectl/pkg/cmd/cp", "//staging/src/k8s.io/kubectl/pkg/cmd/create", + "//staging/src/k8s.io/kubectl/pkg/cmd/debug", "//staging/src/k8s.io/kubectl/pkg/cmd/delete", "//staging/src/k8s.io/kubectl/pkg/cmd/describe", "//staging/src/k8s.io/kubectl/pkg/cmd/drain", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD index 977ebc8a96e..91532a81b46 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD @@ -58,7 +58,10 @@ go_test( "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library", "//vendor/github.com/google/go-cmp/cmp:go_default_library", + "//vendor/github.com/google/go-cmp/cmp/cmpopts:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/utils/pointer:go_default_library", ], ) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go index 03288dfc995..da9ee3922db 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -94,27 +94,26 @@ var nameSuffixFunc = utilrand.String // DebugOptions holds the options for an invocation of kubectl debug. type DebugOptions struct { - Args []string - ArgsOnly bool - Attach bool - Container string - CopyTo string - Replace bool - Env []corev1.EnvVar - Image string - Interactive bool - Namespace string - TargetNames []string - PullPolicy corev1.PullPolicy - Quiet bool - SameNode bool - ShareProcesses bool - Target string - TTY bool + Args []string + ArgsOnly bool + Attach bool + Container string + CopyTo string + Replace bool + Env []corev1.EnvVar + Image string + Interactive bool + Namespace string + TargetNames []string + PullPolicy corev1.PullPolicy + Quiet bool + SameNode bool + ShareProcesses bool + TargetContainer string + TTY bool shareProcessedChanged bool - builder *resource.Builder podClient corev1client.PodsGetter genericclioptions.IOStreams @@ -165,7 +164,7 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) { cmd.Flags().BoolVar(&opt.Quiet, "quiet", opt.Quiet, i18n.T("If true, suppress informational messages.")) cmd.Flags().BoolVar(&opt.SameNode, "same-node", opt.SameNode, i18n.T("When used with '--copy-to', schedule the copy of target Pod on the same node.")) cmd.Flags().BoolVar(&opt.ShareProcesses, "share-processes", opt.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy.")) - cmd.Flags().StringVar(&opt.Target, "target", "", i18n.T("When debugging a pod, target processes in this container name.")) + cmd.Flags().StringVar(&opt.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name.")) cmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, i18n.T("Allocate a TTY for the debugging container.")) } @@ -173,7 +172,6 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) { func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error - o.builder = f.NewBuilder() o.PullPolicy = corev1.PullPolicy(cmdutil.GetFlagString(cmd, "image-pull-policy")) // Arguments @@ -205,13 +203,6 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st return err } - // Clientset - clientset, err := f.KubernetesClientSet() - if err != nil { - return fmt.Errorf("internal error getting clientset: %v", err) - } - o.podClient = clientset.CoreV1() - // Share processes o.shareProcessedChanged = cmd.Flags().Changed("share-processes") @@ -220,6 +211,17 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st // Validate checks that the provided debug options are specified. func (o *DebugOptions) Validate(cmd *cobra.Command) error { + // CopyTo + // These flags are exclusive to --copy-to + if len(o.CopyTo) == 0 { + switch { + case o.Replace: + return fmt.Errorf("--replace may only be used with --copy-to.") + case o.SameNode: + return fmt.Errorf("--same-node may only be used with --copy-to.") + } + } + // Image if len(o.Image) == 0 { return fmt.Errorf("--image is required") @@ -241,8 +243,8 @@ func (o *DebugOptions) Validate(cmd *cobra.Command) error { return fmt.Errorf("invalid image pull policy: %s", o.PullPolicy) } - // Target - if len(o.Target) > 0 && len(o.CopyTo) > 0 { + // TargetContainer + if len(o.TargetContainer) > 0 && len(o.CopyTo) > 0 { return fmt.Errorf("--target is incompatible with --copy-to. Use --share-processes instead.") } @@ -258,7 +260,13 @@ func (o *DebugOptions) Validate(cmd *cobra.Command) error { func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error { ctx := context.Background() - r := o.builder. + clientset, err := f.KubernetesClientSet() + if err != nil { + return fmt.Errorf("internal error getting clientset: %v", err) + } + o.podClient = clientset.CoreV1() + + r := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace().ResourceNames("pods", o.TargetNames...). Do() @@ -266,7 +274,7 @@ func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error { return err } - err := r.Visit(func(info *resource.Info, err error) error { + err = r.Visit(func(info *resource.Info, err error) error { if err != nil { // TODO(verb): configurable early return return err @@ -398,7 +406,7 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) *corev1.Ephemeral TerminationMessagePolicy: corev1.TerminationMessageReadFile, TTY: o.TTY, }, - TargetContainerName: o.Target, + TargetContainerName: o.TargetContainer, } if o.ArgsOnly { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go index 3e060920289..eb05a295206 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go @@ -18,13 +18,18 @@ package debug import ( "fmt" + "strings" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/utils/pointer" ) @@ -62,10 +67,10 @@ func TestGenerateDebugContainer(t *testing.T) { { name: "namespace targeting", opts: &DebugOptions{ - Container: "debugger", - Image: "busybox", - PullPolicy: corev1.PullIfNotPresent, - Target: "myapp", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + TargetContainer: "myapp", }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -976,3 +981,261 @@ func TestGenerateNodeDebugPod(t *testing.T) { }) } } + +func TestCompleteAndValidate(t *testing.T) { + tf := cmdtesting.NewTestFactory() + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() + cmpFilter := cmp.FilterPath(func(p cmp.Path) bool { + switch p.String() { + // IOStreams contains unexported fields + case "IOStreams": + return true + } + return false + }, cmp.Ignore()) + + tests := []struct { + name, args string + wantOpts *DebugOptions + wantError bool + }{ + { + name: "No targets", + args: "--image=image", + wantError: true, + }, + { + name: "Invalid environment variables", + args: "--image=busybox --env=FOO mypod", + wantError: true, + }, + { + name: "Invalid image name", + args: "--image=image:label@deadbeef mypod", + wantError: true, + }, + { + name: "Invalid pull policy", + args: "--image=image --image-pull-policy=whenever-you-feel-like-it", + wantError: true, + }, + { + name: "TTY without stdin", + args: "--image=image --tty", + wantError: true, + }, + { + name: "Set image pull policy", + args: "--image=busybox --image-pull-policy=Always mypod", + wantOpts: &DebugOptions{ + Args: []string{}, + Image: "busybox", + Namespace: "default", + PullPolicy: corev1.PullPolicy("Always"), + ShareProcesses: true, + TargetNames: []string{"mypod"}, + }, + }, + { + name: "Multiple targets", + args: "--image=busybox mypod1 mypod2", + wantOpts: &DebugOptions{ + Args: []string{}, + Image: "busybox", + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod1", "mypod2"}, + }, + }, + { + name: "Arguments with dash", + args: "--image=busybox mypod1 mypod2 -- echo 1 2", + wantOpts: &DebugOptions{ + Args: []string{"echo", "1", "2"}, + Image: "busybox", + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod1", "mypod2"}, + }, + }, + { + name: "Interactive no attach", + args: "-ti --image=busybox --attach=false mypod", + wantOpts: &DebugOptions{ + Args: []string{}, + Attach: false, + Image: "busybox", + Interactive: true, + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + }, + { + name: "Set environment variables", + args: "--image=busybox --env=FOO=BAR,BAZ=BAZ mypod", + wantOpts: &DebugOptions{ + Args: []string{}, + Env: []v1.EnvVar{ + {Name: "FOO", Value: "BAR"}, + {Name: "BAZ", Value: "BAZ"}, + }, + Image: "busybox", + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + }, + }, + { + name: "Ephemeral container: interactive session minimal args", + args: "mypod -it --image=busybox", + wantOpts: &DebugOptions{ + Args: []string{}, + Attach: true, + Image: "busybox", + Interactive: true, + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + }, + { + name: "Ephemeral container: non-interactive debugger with image and name", + args: "--image=myproj/debug-tools --image-pull-policy=Always -c debugger mypod", + wantOpts: &DebugOptions{ + Args: []string{}, + Container: "debugger", + Image: "myproj/debug-tools", + Namespace: "default", + PullPolicy: corev1.PullPolicy("Always"), + ShareProcesses: true, + TargetNames: []string{"mypod"}, + }, + }, + { + name: "Ephemeral container: image not specified", + args: "mypod", + wantError: true, + }, + { + name: "Ephemeral container: replace not allowed", + args: "--replace --image=busybox mypod", + wantError: true, + }, + { + name: "Ephemeral container: same-node not allowed", + args: "--same-node --image=busybox mypod", + wantError: true, + }, + { + name: "Pod copy: interactive debug container minimal args", + args: "mypod -it --image=busybox --copy-to=my-debugger", + wantOpts: &DebugOptions{ + Args: []string{}, + Attach: true, + CopyTo: "my-debugger", + Image: "busybox", + Interactive: true, + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + }, + { + name: "Pod copy: non-interactive with debug container, image name and command", + args: "mypod --image=busybox --container=my-container --copy-to=my-debugger -- sleep 1d", + wantOpts: &DebugOptions{ + Args: []string{"sleep", "1d"}, + Container: "my-container", + CopyTo: "my-debugger", + Image: "busybox", + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + }, + }, + { + name: "Pod copy: no image specified", + args: "mypod -it --copy-to=my-debugger", + wantError: true, + }, + { + name: "Pod copy: --target not allowed", + args: "mypod --target --image=busybox --copy-to=my-debugger", + wantError: true, + }, + { + name: "Node: interactive session minimal args", + args: "node/mynode -it --image=busybox", + wantOpts: &DebugOptions{ + Args: []string{}, + Attach: true, + Image: "busybox", + Interactive: true, + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"node/mynode"}, + TTY: true, + }, + }, + { + name: "Node: no image specified", + args: "node/mynode -it", + wantError: true, + }, + { + name: "Node: replace not allowed", + args: "--image=busybox --replace node/mynode", + wantError: true, + }, + { + name: "Node: same-node not allowed", + args: "--image=busybox --same-node node/mynode", + wantError: true, + }, + { + name: "Node: --target not allowed", + args: "node/mynode --target --image=busybox", + wantError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + opts := NewDebugOptions(ioStreams) + var gotError error + + cmd := &cobra.Command{ + Run: func(cmd *cobra.Command, args []string) { + gotError = opts.Complete(tf, cmd, args) + if gotError != nil { + return + } + gotError = opts.Validate(cmd) + }, + } + cmd.SetArgs(strings.Split(tc.args, " ")) + addDebugFlags(cmd, opts) + + cmdError := cmd.Execute() + + if tc.wantError { + if cmdError != nil || gotError != nil { + return + } + t.Fatalf("CompleteAndValidate got nil errors but wantError: %v", tc.wantError) + } else if cmdError != nil { + t.Fatalf("cmd.Execute got error '%v' executing test cobra.Command, wantError: %v", cmdError, tc.wantError) + } else if gotError != nil { + t.Fatalf("CompleteAndValidate got error: '%v', wantError: %v", gotError, tc.wantError) + } + + if diff := cmp.Diff(tc.wantOpts, opts, cmpFilter, cmpopts.IgnoreUnexported(DebugOptions{})); diff != "" { + t.Error("CompleteAndValidate unexpected diff in generated object: (-want +got):\n", diff) + } + }) + } +} From ee9f11b95f01b32dade5d8dc7329625c40ac0e63 Mon Sep 17 00:00:00 2001 From: Lee Verberne Date: Fri, 30 Oct 2020 18:23:34 +0100 Subject: [PATCH 2/2] kubectl debug: Allow mutating image names --- .../src/k8s.io/kubectl/pkg/cmd/debug/debug.go | 139 ++++-- .../kubectl/pkg/cmd/debug/debug_test.go | 405 +++++++++++++++--- 2 files changed, 439 insertions(+), 105 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go index da9ee3922db..d6009c9084a 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -55,7 +55,7 @@ var ( Debug cluster resources using interactive debugging containers. 'debug' provides automation for common debugging tasks for cluster objects identified by - resource and name. Pods will be used by default if resource is not specified. + resource and name. Pods will be used by default if no resource is specified. The action taken by 'debug' varies depending on what resource is specified. Supported actions include: @@ -66,8 +66,7 @@ var ( debugging utilities without restarting the pod. * Node: Create a new pod that runs in the node's host namespaces and can access the node's filesystem. - - Alpha disclaimer: command line flags may change`)) +`)) debugExample = templates.Examples(i18n.T(` # Create an interactive debugging session in pod mypod and immediately attach to it. @@ -78,11 +77,17 @@ var ( # (requires the EphemeralContainers feature to be enabled in the cluster) kubectl alpha debug --image=myproj/debug-tools -c debugger mypod - # Create a debug container as a copy of the original Pod and attach to it + # Create a copy of mypod adding a debug container and attach to it kubectl alpha debug mypod -it --image=busybox --copy-to=my-debugger - # Create a copy of mypod named my-debugger with my-container's image changed to busybox - kubectl alpha debug mypod --image=busybox --container=my-container --copy-to=my-debugger -- sleep 1d + # Create a copy of mypod changing the command of mycontainer + kubectl alpha debug mypod -it --copy-to=my-debugger --container=mycontainer -- sh + + # Create a copy of mypod changing all container images to busybox + kubectl alpha debug mypod --copy-to=my-debugger --set-image=*=busybox + + # Create a copy of mypod adding a debug container and changing container images + kubectl alpha debug mypod -it --copy-to=my-debugger --image=debian --set-image=app=app:debug,sidecar=sidecar:debug # Create an interactive debugging session on a node and immediately attach to it. # The container will run in the host namespaces and the host's filesystem will be mounted at /host @@ -108,6 +113,7 @@ type DebugOptions struct { PullPolicy corev1.PullPolicy Quiet bool SameNode bool + SetImages map[string]string ShareProcesses bool TargetContainer string TTY bool @@ -134,9 +140,9 @@ func NewCmdDebug(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra. o := NewDebugOptions(streams) cmd := &cobra.Command{ - Use: "debug (POD | TYPE[[.VERSION].GROUP]/NAME) --image=image [ -- COMMAND [args...] ]", + Use: "debug (POD | TYPE[[.VERSION].GROUP]/NAME) [ -- COMMAND [args...] ]", DisableFlagsInUseLine: true, - Short: i18n.T("Attach a debug container to a running pod"), + Short: i18n.T("Create debugging sessions for troubleshooting workloads and nodes"), Long: debugLong, Example: debugExample, Run: func(cmd *cobra.Command, args []string) { @@ -155,11 +161,11 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) { cmd.Flags().BoolVar(&opt.Attach, "attach", opt.Attach, i18n.T("If true, wait for the container to start running, and then attach as if 'kubectl attach ...' were called. Default false, unless '-i/--stdin' is set, in which case the default is true.")) cmd.Flags().StringVarP(&opt.Container, "container", "c", opt.Container, i18n.T("Container name to use for debug container.")) cmd.Flags().StringVar(&opt.CopyTo, "copy-to", opt.CopyTo, i18n.T("Create a copy of the target Pod with this name.")) - cmd.Flags().BoolVar(&opt.Replace, "replace", opt.Replace, i18n.T("When used with '--copy-to', delete the original Pod")) + cmd.Flags().BoolVar(&opt.Replace, "replace", opt.Replace, i18n.T("When used with '--copy-to', delete the original Pod.")) cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container.")) cmd.Flags().StringVar(&opt.Image, "image", opt.Image, i18n.T("Container image to use for debug container.")) - 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().StringToStringVar(&opt.SetImages, "set-image", opt.SetImages, i18n.T("When used with '--copy-to', a list of name=image pairs for changing container images, similar to how 'kubectl set image' works.")) + 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().BoolVarP(&opt.Interactive, "stdin", "i", opt.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached.")) cmd.Flags().BoolVar(&opt.Quiet, "quiet", opt.Quiet, i18n.T("If true, suppress informational messages.")) cmd.Flags().BoolVar(&opt.SameNode, "same-node", opt.SameNode, i18n.T("When used with '--copy-to', schedule the copy of target Pod on the same node.")) @@ -212,22 +218,30 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st // Validate checks that the provided debug options are specified. func (o *DebugOptions) Validate(cmd *cobra.Command) error { // CopyTo - // These flags are exclusive to --copy-to - if len(o.CopyTo) == 0 { + if len(o.CopyTo) > 0 { + if len(o.Image) == 0 && len(o.SetImages) == 0 && len(o.Args) == 0 { + return fmt.Errorf("you must specify --image, --set-image or command arguments.") + } + if len(o.Args) > 0 && len(o.Container) == 0 && len(o.Image) == 0 { + return fmt.Errorf("you must specify an existing container or a new image when specifying args.") + } + } else { + // These flags are exclusive to --copy-to switch { case o.Replace: return fmt.Errorf("--replace may only be used with --copy-to.") case o.SameNode: return fmt.Errorf("--same-node may only be used with --copy-to.") + case len(o.SetImages) > 0: + return fmt.Errorf("--set-image may only be used with --copy-to.") + case len(o.Image) == 0: + return fmt.Errorf("you must specify --image when not using --copy-to.") } } // Image - if len(o.Image) == 0 { - return fmt.Errorf("--image is required") - } - if !reference.ReferenceRegexp.MatchString(o.Image) { - return fmt.Errorf("Invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat) + if len(o.Image) > 0 && !reference.ReferenceRegexp.MatchString(o.Image) { + return fmt.Errorf("invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat) } // Name @@ -243,6 +257,13 @@ func (o *DebugOptions) Validate(cmd *cobra.Command) error { return fmt.Errorf("invalid image pull policy: %s", o.PullPolicy) } + // SetImages + for name, image := range o.SetImages { + if !reference.ReferenceRegexp.MatchString(image) { + return fmt.Errorf("invalid image name %q for container %q: %v", image, name, reference.ErrReferenceInvalidFormat) + } + } + // TargetContainer if len(o.TargetContainer) > 0 && len(o.CopyTo) > 0 { return fmt.Errorf("--target is incompatible with --copy-to. Use --share-processes instead.") @@ -377,8 +398,11 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev // debugByCopy runs a copy of the target Pod with a debug container added or an original container modified func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { - copied, dc := o.generatePodCopyWithDebugContainer(pod) - copied, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{}) + copied, dc, err := o.generatePodCopyWithDebugContainer(pod) + if err != nil { + return nil, "", err + } + created, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{}) if err != nil { return nil, "", err } @@ -388,7 +412,7 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev return nil, "", err } } - return copied, dc, nil + return created, dc, nil } // generateDebugContainer returns an EphemeralContainer suitable for use as a debug container @@ -483,8 +507,8 @@ func (o *DebugOptions) generateNodeDebugPod(node string) *corev1.Pod { return p } -// generatePodCopy takes a Pod and returns a copy and the debug container name of that copy -func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*corev1.Pod, string) { +// generatePodCopyWithDebugContainer takes a Pod and returns a copy and the debug container name of that copy +func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*corev1.Pod, string, error) { copied := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: o.CopyTo, @@ -503,30 +527,59 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core copied.Spec.NodeName = "" } + // Apply image mutations + for i, c := range copied.Spec.Containers { + override := o.SetImages["*"] + if img, ok := o.SetImages[c.Name]; ok { + override = img + } + if len(override) > 0 { + copied.Spec.Containers[i].Image = override + } + } + containerByName := containerNameToRef(copied) - c, containerExists := containerByName[o.Container] - // Add a new container if the specified container does not exist - if !containerExists { - name := o.computeDebugContainerName(copied) - c = &corev1.Container{Name: name} - // envs are customizable when adding new container + name := o.Container + if len(name) == 0 { + name = o.computeDebugContainerName(copied) + } + + c, ok := containerByName[name] + if !ok { + // Adding a new debug container + if len(o.Image) == 0 { + return nil, "", fmt.Errorf("you must specify image when creating new container") + } + c = &corev1.Container{ + Name: name, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + } + defer func() { + copied.Spec.Containers = append(copied.Spec.Containers, *c) + }() + } + + if len(o.Args) > 0 { + if o.ArgsOnly { + c.Args = o.Args + } else { + c.Command = o.Args + c.Args = nil + } + } + if len(o.Env) > 0 { c.Env = o.Env } - c.Image = o.Image - c.ImagePullPolicy = o.PullPolicy + if len(o.Image) > 0 { + c.Image = o.Image + } + if len(o.PullPolicy) > 0 { + c.ImagePullPolicy = o.PullPolicy + } c.Stdin = o.Interactive - c.TerminationMessagePolicy = corev1.TerminationMessageReadFile c.TTY = o.TTY - if o.ArgsOnly { - c.Args = o.Args - } else { - c.Command = o.Args - c.Args = nil - } - if !containerExists { - copied.Spec.Containers = append(copied.Spec.Containers, *c) - } - return copied, c.Name + + return copied, name, nil } func (o *DebugOptions) computeDebugContainerName(pod *corev1.Pod) string { @@ -632,7 +685,7 @@ func handleAttachPod(ctx context.Context, f cmdutil.Factory, podClient corev1cli status := getContainerStatusByName(pod, containerName) if status == nil { // impossible path - return fmt.Errorf("Error get container status of %s: %+v", containerName, err) + return fmt.Errorf("error getting container status of container name %q: %+v", containerName, err) } if status.State.Terminated != nil { klog.V(1).Info("Ephemeral container terminated, falling back to logs") diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go index eb05a295206..d2024c74c9d 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go @@ -250,10 +250,9 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { } for _, tc := range []struct { - name string - opts *DebugOptions - pod *corev1.Pod - expected *corev1.Pod + name string + opts *DebugOptions + havePod, wantPod *corev1.Pod }{ { name: "basic", @@ -263,7 +262,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -276,17 +275,16 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { NodeName: "node-1", }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "debugger", - Image: "busybox", - ImagePullPolicy: corev1.PullIfNotPresent, - TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, }, }, }, @@ -301,7 +299,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { PullPolicy: corev1.PullIfNotPresent, SameNode: true, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -314,17 +312,16 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { NodeName: "node-1", }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "debugger", - Image: "busybox", - ImagePullPolicy: corev1.PullIfNotPresent, - TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, }, }, NodeName: "node-1", @@ -339,7 +336,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", Labels: map[string]string{ @@ -359,7 +356,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", Annotations: map[string]string{ @@ -369,10 +366,9 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "debugger", - Image: "busybox", - ImagePullPolicy: corev1.PullIfNotPresent, - TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, }, }, }, @@ -386,7 +382,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -398,7 +394,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -429,7 +425,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Value: "test", }}, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -441,7 +437,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -473,7 +469,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -485,7 +481,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -515,7 +511,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -527,7 +523,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -553,24 +549,25 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { CopyTo: "debugger", Container: "debugger", Args: []string{"sleep", "1d"}, - Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "debugger", - Command: []string{"echo"}, - Args: []string{"one", "two", "three"}, + Name: "debugger", + Command: []string{"echo"}, + Image: "app", + Args: []string{"one", "two", "three"}, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -578,7 +575,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Containers: []corev1.Container{ { Name: "debugger", - Image: "busybox", + Image: "app", Command: []string{"sleep", "1d"}, ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, @@ -594,7 +591,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -606,7 +603,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -632,7 +629,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -644,7 +641,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -670,7 +667,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -690,7 +687,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -724,7 +721,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, @@ -748,7 +745,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -777,20 +774,22 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { ShareProcesses: true, shareProcessedChanged: true, }, - pod: &corev1.Pod{ + havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "debugger", + Name: "debugger", + ImagePullPolicy: corev1.PullAlways, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, NodeName: "node-1", }, }, - expected: &corev1.Pod{ + wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, @@ -807,17 +806,215 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) { }, }, }, + { + name: "Change image for a named container", + opts: &DebugOptions{ + Args: []string{}, + CopyTo: "myapp-copy", + Container: "app", + Image: "busybox", + TargetNames: []string{"myapp"}, + }, + havePod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "myapp"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + wantPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "myapp-copy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "busybox"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + }, + { + name: "Change image for a named container with set-image", + opts: &DebugOptions{ + CopyTo: "myapp-copy", + Container: "app", + SetImages: map[string]string{"app": "busybox"}, + }, + havePod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + wantPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp-copy", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "busybox"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + }, + { + name: "Change image for all containers with set-image", + opts: &DebugOptions{ + CopyTo: "myapp-copy", + Container: "app", + SetImages: map[string]string{"*": "busybox"}, + }, + havePod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + wantPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp-copy", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "busybox"}, + {Name: "sidecar", Image: "busybox"}, + }, + }, + }, + }, + { + name: "Change image for multiple containers with set-image", + opts: &DebugOptions{ + CopyTo: "myapp-copy", + Container: "app", + SetImages: map[string]string{"*": "busybox", "app": "app-debugger"}, + }, + havePod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + wantPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp-copy", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "app-debugger"}, + {Name: "sidecar", Image: "busybox"}, + }, + }, + }, + }, + { + name: "Add interactive debug container minimal args", + opts: &DebugOptions{ + Args: []string{}, + Attach: true, + CopyTo: "my-debugger", + Image: "busybox", + Interactive: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + havePod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "mypod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + wantPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "my-debugger"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + { + Name: "debugger-1", + Image: "busybox", + Stdin: true, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + TTY: true, + }, + }, + }, + }, + }, + { + name: "Pod copy: add container and also mutate images", + opts: &DebugOptions{ + Args: []string{}, + Attach: true, + CopyTo: "my-debugger", + Image: "debian", + Interactive: true, + Namespace: "default", + SetImages: map[string]string{ + "app": "app:debug", + "sidecar": "sidecar:debug", + }, + ShareProcesses: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + havePod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "mypod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "sidecar", Image: "sidecarimage"}, + }, + }, + }, + wantPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "my-debugger"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "app:debug"}, + {Name: "sidecar", Image: "sidecar:debug"}, + { + Name: "debugger-1", + Image: "debian", + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Stdin: true, + TTY: true, + }, + }, + }, + }, + }, } { t.Run(tc.name, func(t *testing.T) { tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() suffixCounter = 0 - if tc.pod == nil { - tc.pod = &corev1.Pod{} + if tc.havePod == nil { + tc.havePod = &corev1.Pod{} } - pod, _ := tc.opts.generatePodCopyWithDebugContainer(tc.pod) - if diff := cmp.Diff(tc.expected, pod); diff != "" { - t.Error("unexpected diff in generated object: (-want +got):\n", diff) + gotPod, _, _ := tc.opts.generatePodCopyWithDebugContainer(tc.havePod) + if diff := cmp.Diff(tc.wantPod, gotPod); diff != "" { + t.Error("TestGeneratePodCopyWithDebugContainer: diff in generated object: (-want +got):\n", diff) } }) } @@ -1074,13 +1271,10 @@ func TestCompleteAndValidate(t *testing.T) { }, { name: "Set environment variables", - args: "--image=busybox --env=FOO=BAR,BAZ=BAZ mypod", + args: "--image=busybox --env=FOO=BAR mypod", wantOpts: &DebugOptions{ - Args: []string{}, - Env: []v1.EnvVar{ - {Name: "FOO", Value: "BAR"}, - {Name: "BAZ", Value: "BAZ"}, - }, + Args: []string{}, + Env: []v1.EnvVar{{Name: "FOO", Value: "BAR"}}, Image: "busybox", Namespace: "default", ShareProcesses: true, @@ -1115,10 +1309,15 @@ func TestCompleteAndValidate(t *testing.T) { }, }, { - name: "Ephemeral container: image not specified", + name: "Ephemeral container: no image specified", args: "mypod", wantError: true, }, + { + name: "Ephemeral container: no image but args", + args: "mypod -- echo 1 2", + wantError: true, + }, { name: "Ephemeral container: replace not allowed", args: "--replace --image=busybox mypod", @@ -1129,6 +1328,11 @@ func TestCompleteAndValidate(t *testing.T) { args: "--same-node --image=busybox mypod", wantError: true, }, + { + name: "Ephemeral container: incompatible with --set-image", + args: "--set-image=*=busybox mypod", + wantError: true, + }, { name: "Pod copy: interactive debug container minimal args", args: "mypod -it --image=busybox --copy-to=my-debugger", @@ -1157,16 +1361,88 @@ func TestCompleteAndValidate(t *testing.T) { TargetNames: []string{"mypod"}, }, }, + { + name: "Pod copy: replace single image of existing container", + args: "mypod --image=busybox --container=my-container --copy-to=my-debugger", + wantOpts: &DebugOptions{ + Args: []string{}, + Container: "my-container", + CopyTo: "my-debugger", + Image: "busybox", + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + }, + }, + { + name: "Pod copy: mutate existing container images", + args: "mypod --set-image=*=busybox,app=app-debugger --copy-to=my-debugger", + wantOpts: &DebugOptions{ + Args: []string{}, + CopyTo: "my-debugger", + Namespace: "default", + SetImages: map[string]string{ + "*": "busybox", + "app": "app-debugger", + }, + ShareProcesses: true, + TargetNames: []string{"mypod"}, + }, + }, + { + name: "Pod copy: add container and also mutate images", + args: "mypod -it --copy-to=my-debugger --image=debian --set-image=app=app:debug,sidecar=sidecar:debug", + wantOpts: &DebugOptions{ + Args: []string{}, + Attach: true, + CopyTo: "my-debugger", + Image: "debian", + Interactive: true, + Namespace: "default", + SetImages: map[string]string{ + "app": "app:debug", + "sidecar": "sidecar:debug", + }, + ShareProcesses: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + }, + { + name: "Pod copy: change command", + args: "mypod -it --copy-to=my-debugger --container=mycontainer -- sh", + wantOpts: &DebugOptions{ + Attach: true, + Args: []string{"sh"}, + Container: "mycontainer", + CopyTo: "my-debugger", + Interactive: true, + Namespace: "default", + ShareProcesses: true, + TargetNames: []string{"mypod"}, + TTY: true, + }, + }, { name: "Pod copy: no image specified", args: "mypod -it --copy-to=my-debugger", wantError: true, }, + { + name: "Pod copy: args but no image specified", + args: "mypod --copy-to=my-debugger -- echo milo", + wantError: true, + }, { name: "Pod copy: --target not allowed", args: "mypod --target --image=busybox --copy-to=my-debugger", wantError: true, }, + { + name: "Pod copy: invalid --set-image", + args: "mypod --set-image=*=SUPERGOODIMAGE#1!!!! --copy-to=my-debugger", + wantError: true, + }, { name: "Node: interactive session minimal args", args: "node/mynode -it --image=busybox", @@ -1187,15 +1463,20 @@ func TestCompleteAndValidate(t *testing.T) { wantError: true, }, { - name: "Node: replace not allowed", + name: "Node: --replace not allowed", args: "--image=busybox --replace node/mynode", wantError: true, }, { - name: "Node: same-node not allowed", + name: "Node: --same-node not allowed", args: "--image=busybox --same-node node/mynode", wantError: true, }, + { + name: "Node: --set-image not allowed", + args: "--image=busybox --set-image=*=busybox node/mynode", + wantError: true, + }, { name: "Node: --target not allowed", args: "node/mynode --target --image=busybox",