/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ 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" ) func TestGenerateDebugContainer(t *testing.T) { // Slightly less randomness for testing. 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.EphemeralContainer }{ { name: "minimum fields", opts: &DebugOptions{ Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "namespace targeting", opts: &DebugOptions{ Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, TargetContainer: "myapp", }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, TargetContainerName: "myapp", }, }, { name: "debug args as container command", opts: &DebugOptions{ Args: []string{"/bin/echo", "one", "two", "three"}, Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Command: []string{"/bin/echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "debug args as container args", opts: &DebugOptions{ ArgsOnly: true, Container: "debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "random name generation", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "random name collision", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, }, }, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-2", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "pod with init containers", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init-container-1", }, { Name: "init-container-2", }, }, Containers: []corev1.Container{ { Name: "debugger", }, }, }, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "pod with ephemeral containers", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-1", }, }, { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-2", }, }, }, }, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, } { t.Run(tc.name, func(t *testing.T) { tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() suffixCounter = 0 if tc.pod == nil { tc.pod = &corev1.Pod{} } if diff := cmp.Diff(tc.expected, tc.opts.generateDebugContainer(tc.pod)); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } 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 havePod, wantPod *corev1.Pod }{ { name: "basic", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, }, }, }, { name: "same node", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, SameNode: true, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, NodeName: "node-1", }, }, }, { name: "metadata stripping", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, havePod: &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", }, }, }, }, wantPod: &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, }, }, }, }, }, { name: "add a debug container", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &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", }}, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &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, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &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, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &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"}, PullPolicy: corev1.PullIfNotPresent, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Command: []string{"echo"}, Image: "app", Args: []string{"one", "two", "three"}, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "app", Command: []string{"sleep", "1d"}, ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "random name", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &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, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, }, }, }, wantPod: &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: "pod with init containers", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init-container-1", }, { Name: "init-container-2", }, }, Containers: []corev1.Container{ { Name: "debugger-1", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init-container-1", }, { Name: "init-container-2", }, }, Containers: []corev1.Container{ { Name: "debugger-1", }, { Name: "debugger-2", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "pod with ephemeral containers", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-1", }, }, { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-2", }, }, }, }, }, wantPod: &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, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", ImagePullPolicy: corev1.PullAlways, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, NodeName: "node-1", }, }, wantPod: &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), }, }, }, { 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", 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", 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.havePod == nil { tc.havePod = &corev1.Pod{} } 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) } }) } } func TestGenerateNodeDebugPod(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, nodeName string opts *DebugOptions expected *corev1.Pod }{ { name: "minimum options", nodeName: "node-XXX", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, }, }, }, { name: "debug args as container command", nodeName: "node-XXX", opts: &DebugOptions{ Args: []string{"/bin/echo", "one", "two", "three"}, Container: "custom-debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "custom-debugger", Command: []string{"/bin/echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, }, }, }, { name: "debug args as container args", nodeName: "node-XXX", opts: &DebugOptions{ ArgsOnly: true, Container: "custom-debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "custom-debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, }, }, }, } { t.Run(tc.name, func(t *testing.T) { tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() suffixCounter = 0 pod := tc.opts.generateNodeDebugPod(tc.nodeName) if diff := cmp.Diff(tc.expected, pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestCompleteAndValidate(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") 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: "test", PullPolicy: corev1.PullPolicy("Always"), ShareProcesses: true, TargetNames: []string{"mypod"}, }, }, { name: "Multiple targets", args: "--image=busybox mypod1 mypod2", wantOpts: &DebugOptions{ Args: []string{}, Image: "busybox", Namespace: "test", 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: "test", 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: "test", ShareProcesses: true, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Set environment variables", args: "--image=busybox --env=FOO=BAR mypod", wantOpts: &DebugOptions{ Args: []string{}, Env: []v1.EnvVar{{Name: "FOO", Value: "BAR"}}, Image: "busybox", Namespace: "test", 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: "test", 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: "test", PullPolicy: corev1.PullPolicy("Always"), ShareProcesses: true, TargetNames: []string{"mypod"}, }, }, { 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", wantError: true, }, { name: "Ephemeral container: same-node not allowed", 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", wantOpts: &DebugOptions{ Args: []string{}, Attach: true, CopyTo: "my-debugger", Image: "busybox", Interactive: true, Namespace: "test", 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: "test", ShareProcesses: true, TargetNames: []string{"mypod"}, }, }, { name: "Pod copy: explicit attach", args: "mypod --image=busybox --copy-to=my-debugger --attach -- sleep 1d", wantOpts: &DebugOptions{ Args: []string{"sleep", "1d"}, Attach: true, CopyTo: "my-debugger", Image: "busybox", Namespace: "test", ShareProcesses: true, 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: "test", 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: "test", 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: "test", 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: "test", 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: "Pod copy: specifying attach without existing or newly created container", args: "mypod --set-image=*=busybox --copy-to=my-debugger --attach", 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: "test", 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: --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", 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) } }) } }