diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD index 5045fca3846..977ebc8a96e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD @@ -32,6 +32,7 @@ go_library( "//vendor/github.com/docker/distribution/reference:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/klog/v2:go_default_library", + "//vendor/k8s.io/utils/pointer:go_default_library", ], ) @@ -55,7 +56,9 @@ go_test( embed = [":go_default_library"], deps = [ "//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", "//vendor/github.com/google/go-cmp/cmp: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 109c1825b49..e0ce0998f3e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -23,6 +23,7 @@ import ( "github.com/docker/distribution/reference" "github.com/spf13/cobra" + "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -37,7 +38,6 @@ import ( corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" - "k8s.io/klog/v2" "k8s.io/kubectl/pkg/cmd/attach" "k8s.io/kubectl/pkg/cmd/exec" "k8s.io/kubectl/pkg/cmd/logs" @@ -47,6 +47,7 @@ import ( "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" + "k8s.io/utils/pointer" ) var ( @@ -59,26 +60,39 @@ var ( # Create a debug container named debugger using a custom automated debugging image. # (requires the EphemeralContainers feature to be enabled in the cluster) - kubectl alpha debug --image=myproj/debug-tools -c debugger mypod`)) + 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 + 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 +`)) ) 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 - Env []corev1.EnvVar - Image string - Interactive bool - Namespace string - TargetNames []string - PullPolicy corev1.PullPolicy - Quiet 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 + Target string + TTY bool + + shareProcessedChanged bool builder *resource.Builder podClient corev1client.PodsGetter @@ -89,9 +103,10 @@ type DebugOptions struct { // NewDebugOptions returns a DebugOptions initialized with default values. func NewDebugOptions(streams genericclioptions.IOStreams) *DebugOptions { return &DebugOptions{ - Args: []string{}, - IOStreams: streams, - TargetNames: []string{}, + Args: []string{}, + IOStreams: streams, + TargetNames: []string{}, + ShareProcesses: true, } } @@ -120,12 +135,16 @@ func addDebugFlags(cmd *cobra.Command, opt *DebugOptions) { cmd.Flags().BoolVar(&opt.ArgsOnly, "arguments-only", opt.ArgsOnly, i18n.T("If specified, everything after -- will be passed to the new container as Args instead of Command.")) cmd.Flags().BoolVar(&opt.Attach, "attach", opt.Attach, i18n.T("If true, wait for the Pod to start running, and then attach to the Pod 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().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", string(corev1.PullIfNotPresent), i18n.T("The image pull policy for the container.")) 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 prompt messages.")) + cmd.Flags().BoolVar(&opt.SameNode, "same-node", opt.SameNode, i18n.T("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("Target processes in this container name.")) cmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, i18n.T("Allocated a TTY for each container in the pod.")) } @@ -173,6 +192,9 @@ func (o *DebugOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st } o.podClient = clientset.CoreV1() + // Share processes + o.shareProcessedChanged = cmd.Flags().Changed("share-processes") + return nil } @@ -273,9 +295,17 @@ func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error { // visitPod handles debugging for pod targets by (depending on options): // 1. Creating an ephemeral debug container in an existing pod, OR -// 2. Making a copy of pod with certain attributes changed (NOT YET IMPLEMENTED) +// 2. Making a copy of pod with certain attributes changed // visitPod returns a pod and debug container name for subsequent attach, if applicable. func (o *DebugOptions) visitPod(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { + if len(o.CopyTo) > 0 { + return o.debugByCopy(ctx, pod) + } + return o.debugByEphemeralContainer(ctx, pod) +} + +// debugByEphemeralContainer runs an EphemeralContainer in the target Pod for use as a debug container +func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { pods := o.podClient.Pods(pod.Namespace) ec, err := pods.GetEphemeralContainers(ctx, pod.Name, metav1.GetOptions{}) if err != nil { @@ -298,34 +328,26 @@ func (o *DebugOptions) visitPod(ctx context.Context, pod *corev1.Pod) (*corev1.P return pod, debugContainer.Name, nil } -func containerNames(pod *corev1.Pod) map[string]bool { - names := map[string]bool{} - for _, c := range pod.Spec.Containers { - names[c.Name] = true +// 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{}) + if err != nil { + return nil, "", err } - for _, c := range pod.Spec.InitContainers { - names[c.Name] = true + if o.Replace { + err := o.podClient.Pods(pod.Namespace).Delete(ctx, pod.Name, *metav1.NewDeleteOptions(0)) + if err != nil { + return nil, "", err + } } - for _, c := range pod.Spec.EphemeralContainers { - names[c.Name] = true - } - return names + return copied, dc, nil } // generateDebugContainer returns an EphemeralContainer suitable for use as a debug container // in the given pod. func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) *corev1.EphemeralContainer { - name := o.Container - if len(name) == 0 { - cn, existing := "", containerNames(pod) - for len(cn) == 0 || existing[cn] { - cn = fmt.Sprintf("debugger-%s", nameSuffixFunc(5)) - } - if !o.Quiet { - fmt.Fprintf(o.ErrOut, "Defaulting debug container name to %s.\n", cn) - } - name = cn - } + name := o.computeDebugContainerName(pod) ec := &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ @@ -349,8 +371,87 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) *corev1.Ephemeral return ec } -// waitForEphemeralContainer watches the given pod until the ephemeralContainer is running -func waitForEphemeralContainer(ctx context.Context, podClient corev1client.PodsGetter, ns, podName, ephemeralContainerName string) (*corev1.Pod, error) { +// 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) { + copied := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: o.CopyTo, + Namespace: pod.Namespace, + Annotations: pod.Annotations, + }, + Spec: *pod.Spec.DeepCopy(), + } + // change ShareProcessNamespace configuration only when commanded explicitly + if o.shareProcessedChanged { + copied.Spec.ShareProcessNamespace = pointer.BoolPtr(o.ShareProcesses) + } + if !o.SameNode { + copied.Spec.NodeName = "" + } + + 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 + c.Env = o.Env + } + c.Image = o.Image + 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 +} + +func (o *DebugOptions) computeDebugContainerName(pod *corev1.Pod) string { + if len(o.Container) > 0 { + return o.Container + } + name := o.Container + if len(name) == 0 { + cn, containerByName := "", containerNameToRef(pod) + for len(cn) == 0 || (containerByName[cn] != nil) { + cn = fmt.Sprintf("debugger-%s", nameSuffixFunc(5)) + } + if !o.Quiet { + fmt.Fprintf(o.ErrOut, "Defaulting debug container name to %s.\n", cn) + } + name = cn + } + return name +} + +func containerNameToRef(pod *corev1.Pod) map[string]*corev1.Container { + names := map[string]*corev1.Container{} + for i := range pod.Spec.Containers { + ref := &pod.Spec.Containers[i] + names[ref.Name] = ref + } + for i := range pod.Spec.InitContainers { + ref := &pod.Spec.Containers[i] + names[ref.Name] = ref + } + for i := range pod.Spec.EphemeralContainers { + ref := &pod.Spec.Containers[i] + names[ref.Name] = ref + } + return names +} + +// waitForContainer watches the given pod until the container is running +func waitForContainer(ctx context.Context, podClient corev1client.PodsGetter, ns, podName, containerName string) (*corev1.Pod, error) { // TODO: expose the timeout ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, 0*time.Second) defer cancel() @@ -381,17 +482,14 @@ func waitForEphemeralContainer(ctx context.Context, podClient corev1client.PodsG return false, fmt.Errorf("watch did not return a pod: %v", ev.Object) } - for _, s := range p.Status.EphemeralContainerStatuses { - if s.Name != ephemeralContainerName { - continue - } - - klog.V(2).Infof("debug container status is %v", s) - if s.State.Running != nil || s.State.Terminated != nil { - return true, nil - } + s := getContainerStatusByName(p, containerName) + if s == nil { + return false, nil + } + klog.V(2).Infof("debug container status is %v", s) + if s.State.Running != nil || s.State.Terminated != nil { + return true, nil } - return false, nil }) if ev != nil { @@ -403,9 +501,8 @@ func waitForEphemeralContainer(ctx context.Context, podClient corev1client.PodsG return result, err } -// TODO(verb): handle other types of containers -func handleAttachPod(ctx context.Context, f cmdutil.Factory, podClient corev1client.PodsGetter, ns, podName, ephemeralContainerName string, opts *attach.AttachOptions) error { - pod, err := waitForEphemeralContainer(ctx, podClient, ns, podName, ephemeralContainerName) +func handleAttachPod(ctx context.Context, f cmdutil.Factory, podClient corev1client.PodsGetter, ns, podName, containerName string, opts *attach.AttachOptions) error { + pod, err := waitForContainer(ctx, podClient, ns, podName, containerName) if err != nil { return err } @@ -413,16 +510,15 @@ func handleAttachPod(ctx context.Context, f cmdutil.Factory, podClient corev1cli opts.Namespace = ns opts.Pod = pod opts.PodName = podName - opts.ContainerName = ephemeralContainerName + opts.ContainerName = containerName if opts.AttachFunc == nil { opts.AttachFunc = attach.DefaultAttachFunc } - var status *corev1.ContainerStatus - for i := range pod.Status.EphemeralContainerStatuses { - if pod.Status.EphemeralContainerStatuses[i].Name == ephemeralContainerName { - status = &pod.Status.EphemeralContainerStatuses[i] - } + status := getContainerStatusByName(pod, containerName) + if status == nil { + // impossible path + return fmt.Errorf("Error get container status of %s: %+v", containerName, err) } if status.State.Terminated != nil { klog.V(1).Info("Ephemeral container terminated, falling back to logs") @@ -436,6 +532,18 @@ func handleAttachPod(ctx context.Context, f cmdutil.Factory, podClient corev1cli return nil } +func getContainerStatusByName(pod *corev1.Pod, containerName string) *corev1.ContainerStatus { + allContainerStatus := [][]corev1.ContainerStatus{pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses, pod.Status.EphemeralContainerStatuses} + for _, statusSlice := range allContainerStatus { + for i := range statusSlice { + if statusSlice[i].Name == containerName { + return &statusSlice[i] + } + } + } + return nil +} + // logOpts logs output from opts to the pods log. func logOpts(restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error { ctrName, err := opts.GetContainerName(pod) 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 f8c0268318d..8409b3d1b34 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 @@ -19,10 +19,13 @@ package debug import ( "fmt" "testing" + "time" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/utils/pointer" ) func TestGenerateDebugContainer(t *testing.T) { @@ -164,3 +167,481 @@ func TestGenerateDebugContainer(t *testing.T) { }) } } + +func TestGeneratePodCopyWithDebugContainer(t *testing.T) { + defer func(old func(int) string) { nameSuffixFunc = old }(nameSuffixFunc) + var suffixCounter int + nameSuffixFunc = func(int) string { + suffixCounter++ + return fmt.Sprint(suffixCounter) + } + + for _, tc := range []struct { + name string + opts *DebugOptions + pod *corev1.Pod + expected *corev1.Pod + }{ + { + name: "basic", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + }, + }, + NodeName: "node-1", + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "same node", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + SameNode: true, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + }, + }, + NodeName: "node-1", + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + NodeName: "node-1", + }, + }, + }, + { + name: "metadata stripping", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + Labels: map[string]string{ + "app": "business", + }, + Annotations: map[string]string{ + "test": "test", + }, + ResourceVersion: "1", + CreationTimestamp: metav1.Time{time.Now()}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + Annotations: map[string]string{ + "test": "test", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "add a debug container", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "customize envs", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{{ + Name: "TEST", + Value: "test", + }}, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + Env: []corev1.EnvVar{{ + Name: "TEST", + Value: "test", + }}, + }, + }, + }, + }, + }, + { + name: "debug args as container command", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Args: []string{"/bin/echo", "one", "two", "three"}, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + { + Name: "debugger", + Image: "busybox", + Command: []string{"/bin/echo", "one", "two", "three"}, + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "debug args as container command", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Args: []string{"one", "two", "three"}, + ArgsOnly: true, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + { + Name: "debugger", + Image: "busybox", + Args: []string{"one", "two", "three"}, + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "modify existing command to debug args", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Args: []string{"sleep", "1d"}, + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Command: []string{"echo"}, + Args: []string{"one", "two", "three"}, + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + Command: []string{"sleep", "1d"}, + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "random name", + opts: &DebugOptions{ + CopyTo: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "business", + }, + { + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "random name collision", + opts: &DebugOptions{ + CopyTo: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger-1", + }, + }, + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger-1", + }, + { + Name: "debugger-2", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + }, + }, + }, + { + name: "shared process namespace", + opts: &DebugOptions{ + CopyTo: "debugger", + Container: "debugger", + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + ShareProcesses: true, + shareProcessedChanged: true, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + }, + }, + NodeName: "node-1", + }, + }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "debugger", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "debugger", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + }, + }, + ShareProcessNamespace: pointer.BoolPtr(true), + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() + suffixCounter = 0 + + if tc.pod == nil { + tc.pod = &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) + } + }) + } +}