From fdd6fda024cc559a0281db0e95fa5e86018a225c Mon Sep 17 00:00:00 2001 From: Lee Verberne Date: Fri, 30 Oct 2020 18:23:34 +0100 Subject: [PATCH] kubectl debug: Allow mutating image names Kubernetes-commit: ee9f11b95f01b32dade5d8dc7329625c40ac0e63 --- pkg/cmd/debug/debug.go | 139 +++++++++---- pkg/cmd/debug/debug_test.go | 405 ++++++++++++++++++++++++++++++------ 2 files changed, 439 insertions(+), 105 deletions(-) diff --git a/pkg/cmd/debug/debug.go b/pkg/cmd/debug/debug.go index da9ee392..d6009c90 100644 --- a/pkg/cmd/debug/debug.go +++ b/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/pkg/cmd/debug/debug_test.go b/pkg/cmd/debug/debug_test.go index eb05a295..d2024c74 100644 --- a/pkg/cmd/debug/debug_test.go +++ b/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",