Merge pull request #96058 from verb/1.20-cli-debug-image-mutations

Add tests and set-image option to kubectl debug
This commit is contained in:
Kubernetes Prow Robot 2020-11-03 20:04:17 -08:00 committed by GitHub
commit 1ba27096ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 739 additions and 130 deletions

View File

@ -249,6 +249,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",

View File

@ -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",
],
)

View File

@ -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
@ -94,27 +99,27 @@ 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
SetImages map[string]string
ShareProcesses bool
TargetContainer string
TTY bool
shareProcessedChanged bool
builder *resource.Builder
podClient corev1client.PodsGetter
genericclioptions.IOStreams
@ -135,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) {
@ -156,16 +161,16 @@ 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."))
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 +178,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 +209,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,12 +217,31 @@ 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 {
// Image
if len(o.Image) == 0 {
return fmt.Errorf("--image is required")
// CopyTo
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.")
}
}
if !reference.ReferenceRegexp.MatchString(o.Image) {
return fmt.Errorf("Invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat)
// Image
if len(o.Image) > 0 && !reference.ReferenceRegexp.MatchString(o.Image) {
return fmt.Errorf("invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat)
}
// Name
@ -241,8 +257,15 @@ 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 {
// 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.")
}
@ -258,7 +281,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 +295,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
@ -369,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
}
@ -380,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
@ -398,7 +430,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 {
@ -475,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,
@ -495,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 {
@ -624,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")

View File

@ -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{
@ -245,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",
@ -258,7 +262,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -271,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,
},
},
},
@ -296,7 +299,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
PullPolicy: corev1.PullIfNotPresent,
SameNode: true,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -309,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",
@ -334,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{
@ -354,7 +356,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
Annotations: map[string]string{
@ -364,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,
},
},
},
@ -381,7 +382,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -393,7 +394,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -424,7 +425,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Value: "test",
}},
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -436,7 +437,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -468,7 +469,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -480,7 +481,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -510,7 +511,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -522,7 +523,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -548,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",
},
@ -573,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,
@ -589,7 +591,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -601,7 +603,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -627,7 +629,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -639,7 +641,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -665,7 +667,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -685,7 +687,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -719,7 +721,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
Image: "busybox",
PullPolicy: corev1.PullIfNotPresent,
},
pod: &corev1.Pod{
havePod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "target",
},
@ -743,7 +745,7 @@ func TestGeneratePodCopyWithDebugContainer(t *testing.T) {
},
},
},
expected: &corev1.Pod{
wantPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "debugger",
},
@ -772,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",
},
@ -802,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)
}
})
}
@ -976,3 +1178,345 @@ 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 mypod",
wantOpts: &DebugOptions{
Args: []string{},
Env: []v1.EnvVar{{Name: "FOO", Value: "BAR"}},
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: 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: "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: 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",
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: --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)
}
})
}
}