Add kubectl debug alpha command

This first version of `kubectl alpha debug` is an import of the existing
kubectl-debug plugin, which supports adding ephemeral containers to
running pods. This attempts to follow patterns used by other kubectl
commands such as run, exec and scale.

Kubernetes-commit: e5b655938e55e0404545ff73e9239593a27a3272
This commit is contained in:
Lee Verberne 2020-01-31 15:29:31 +01:00 committed by Kubernetes Publisher
parent ec95730cf7
commit c3d84a9e1c
5 changed files with 637 additions and 24 deletions

30
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450 // indirect
github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995 // indirect
github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e // indirect
github.com/google/go-cmp v0.3.0
github.com/googleapis/gnostic v0.1.0
github.com/jonboulle/clockwork v0.1.0
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
@ -36,15 +37,15 @@ require (
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7
gopkg.in/yaml.v2 v2.2.8
gotest.tools v2.2.0+incompatible // indirect
k8s.io/api v0.0.0-20200304122248-037303d07dad
k8s.io/apimachinery v0.0.0-20200303201514-6584f51ae935
k8s.io/cli-runtime v0.0.0-20200304164305-f26b0857ea34
k8s.io/client-go v0.0.0-20200304162518-f12177f0a965
k8s.io/component-base v0.0.0-20200304083046-fa4920973679
k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/cli-runtime v0.0.0
k8s.io/client-go v0.0.0
k8s.io/component-base v0.0.0
k8s.io/klog v1.0.0
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c
k8s.io/metrics v0.0.0-20200304084343-4cf3f455bf52
k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab
k8s.io/metrics v0.0.0
k8s.io/utils v0.0.0-20200117235808-5f6fbceb4c31
sigs.k8s.io/kustomize v2.0.3+incompatible
sigs.k8s.io/yaml v1.2.0
vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc
@ -53,11 +54,12 @@ require (
replace (
golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // pinned to release-branch.go1.13
golang.org/x/tools => golang.org/x/tools v0.0.0-20190821162956-65e3620a7ae7 // pinned to release-branch.go1.13
k8s.io/api => k8s.io/api v0.0.0-20200304122248-037303d07dad
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20200303201514-6584f51ae935
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20200304164305-f26b0857ea34
k8s.io/client-go => k8s.io/client-go v0.0.0-20200304162518-f12177f0a965
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20200214080538-dc8f3adce97c
k8s.io/component-base => k8s.io/component-base v0.0.0-20200304083046-fa4920973679
k8s.io/metrics => k8s.io/metrics v0.0.0-20200304084343-4cf3f455bf52
k8s.io/api => ../api
k8s.io/apimachinery => ../apimachinery
k8s.io/cli-runtime => ../cli-runtime
k8s.io/client-go => ../client-go
k8s.io/code-generator => ../code-generator
k8s.io/component-base => ../component-base
k8s.io/kubectl => ../kubectl
k8s.io/metrics => ../metrics
)

11
go.sum
View File

@ -306,12 +306,6 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20200304122248-037303d07dad/go.mod h1:EFuendCidCp9DUXAn3QXS0nWIaAgQYL8VaCqs8KTZBA=
k8s.io/apimachinery v0.0.0-20200303201514-6584f51ae935/go.mod h1:5X8oEhnd931nEg6/Nkumo00nT6ZsCLp2h7Xwd7Ym6P4=
k8s.io/cli-runtime v0.0.0-20200304164305-f26b0857ea34/go.mod h1:1K5QYF8MW308AOVMg+33nPuGTXZadAOYUrG0HCZQoHM=
k8s.io/client-go v0.0.0-20200304162518-f12177f0a965/go.mod h1:RazCmNV1L0cgQUsnGA9mex0sJU0CohwSXcmn9+Qq0zw=
k8s.io/code-generator v0.0.0-20200214080538-dc8f3adce97c/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc=
k8s.io/component-base v0.0.0-20200304083046-fa4920973679/go.mod h1:lq0xL4uVP+juMbkkY6YH5AquFGmqvBg6FprvvwZcJak=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
@ -320,9 +314,8 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/metrics v0.0.0-20200304084343-4cf3f455bf52/go.mod h1:iMdzDlkMmv+0GUq8Fu+nMuIYf7N5WhZ9F+WZ1XEbm4M=
k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab h1:I3f2hcBrepGRXI1z4sukzAb8w1R4eqbsHrAsx06LGYM=
k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20200117235808-5f6fbceb4c31 h1:KCcLuc/HD1RogJgEbZi9ObRuLv1bgiRCfAbidLKrUpg=
k8s.io/utils v0.0.0-20200117235808-5f6fbceb4c31/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=

View File

@ -20,6 +20,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/cmd/debug"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
@ -35,7 +36,7 @@ func NewCmdAlpha(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.
// Alpha commands should be added here. As features graduate from alpha they should move
// from here to the CommandGroups defined by NewKubeletCommand() in cmd.go.
//cmd.AddCommand(NewCmdDebug(f, in, out, err))
cmd.AddCommand(debug.NewCmdDebug(f, streams))
// NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding
// the help function ensures a reasonable message if someone types the hidden command anyway.

451
pkg/cmd/debug/debug.go Normal file
View File

@ -0,0 +1,451 @@
/*
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 (
"context"
"fmt"
"time"
"github.com/docker/distribution/reference"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
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"
"k8s.io/kubectl/pkg/cmd/attach"
"k8s.io/kubectl/pkg/cmd/exec"
"k8s.io/kubectl/pkg/cmd/logs"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/templates"
)
var (
debugLong = templates.LongDesc(i18n.T(`Tools for debugging Kubernetes resources`))
debugExample = templates.Examples(i18n.T(`
# Create an interactive debugging session in pod mypod and immediately attach to it.
# (requires the EphemeralContainers feature to be enabled in the cluster)
kubectl alpha debug mypod -i --image=busybox
# 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`))
)
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
PodNames []string
PullPolicy corev1.PullPolicy
Quiet bool
Target string
TTY bool
builder *resource.Builder
podClient corev1client.PodsGetter
genericclioptions.IOStreams
}
// NewDebugOptions returns a DebugOptions initialized with default values.
func NewDebugOptions(streams genericclioptions.IOStreams) *DebugOptions {
return &DebugOptions{
Args: []string{},
IOStreams: streams,
PodNames: []string{},
}
}
// NewCmdDebug returns a cobra command that runs kubectl debug.
func NewCmdDebug(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
o := NewDebugOptions(streams)
cmd := &cobra.Command{
Use: "debug NAME --image=image [ -- COMMAND [args...] ]",
DisableFlagsInUseLine: true,
Short: i18n.T("Attach a debug container to a running pod"),
Long: debugLong,
Example: debugExample,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(o.Validate(cmd))
cmdutil.CheckErr(o.Run(f, cmd))
},
}
addDebugFlags(cmd, o)
return cmd
}
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().StringVar(&opt.Container, "container", opt.Container, i18n.T("Container name to use for debug container."))
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().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."))
}
// Complete finishes run-time initialization of debug.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
argsLen := cmd.ArgsLenAtDash()
o.PodNames = args
// If there is a dash and there are args after the dash, extract the args.
if argsLen >= 0 && len(args) > argsLen {
o.PodNames, o.Args = args[:argsLen], args[argsLen:]
}
// Attach
attachFlag := cmd.Flags().Lookup("attach")
if !attachFlag.Changed && o.Interactive {
o.Attach = true
}
// Environment
envStrings, err := cmd.Flags().GetStringToString("env")
if err != nil {
return fmt.Errorf("internal error getting env flag: %v", err)
}
for k, v := range envStrings {
o.Env = append(o.Env, corev1.EnvVar{Name: k, Value: v})
}
// Namespace
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}
// Clientset
clientset, err := f.KubernetesClientSet()
if err != nil {
return fmt.Errorf("internal error getting clientset: %v", err)
}
o.podClient = clientset.CoreV1()
return nil
}
// 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")
}
if !reference.ReferenceRegexp.MatchString(o.Image) {
return fmt.Errorf("Invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat)
}
// Name
if len(o.PodNames) == 0 {
return fmt.Errorf("NAME is required for debug")
}
// Pull Policy
switch o.PullPolicy {
case corev1.PullAlways, corev1.PullIfNotPresent, corev1.PullNever, "":
// continue
default:
return fmt.Errorf("invalid image pull policy: %s", o.PullPolicy)
}
// TTY
if o.TTY && !o.Interactive {
return fmt.Errorf("-i/--stdin is required for containers with -t/--tty=true")
}
return nil
}
// Run executes a kubectl debug.
func (o *DebugOptions) Run(f cmdutil.Factory, cmd *cobra.Command) error {
r := o.builder.
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().ResourceNames("pods", o.PodNames...).
Do()
if err := r.Err(); err != nil {
return err
}
ctx := context.Background()
err := r.Visit(func(info *resource.Info, err error) error {
if err != nil {
// TODO(verb): configurable early return
return err
}
pods := o.podClient.Pods(info.Namespace)
ec, err := pods.GetEphemeralContainers(ctx, info.Name, metav1.GetOptions{})
if err != nil {
// The pod has already been fetched at this point, so a NotFound error indicates the ephemeralcontainers subresource wasn't found.
if serr, ok := err.(*errors.StatusError); ok && serr.Status().Reason == metav1.StatusReasonNotFound {
return fmt.Errorf("ephemeral containers are disabled for this cluster (error from server: %q).", err)
}
return err
}
klog.V(2).Infof("existing ephemeral containers: %v", ec.EphemeralContainers)
debugContainer := o.generateDebugContainer(info.Object.(*corev1.Pod))
klog.V(2).Infof("new ephemeral container: %#v", debugContainer)
ec.EphemeralContainers = append(ec.EphemeralContainers, *debugContainer)
_, err = pods.UpdateEphemeralContainers(ctx, info.Name, ec, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("error updating ephemeral containers: %v", err)
}
if o.Attach {
opts := &attach.AttachOptions{
StreamOptions: exec.StreamOptions{
IOStreams: o.IOStreams,
Stdin: o.Interactive,
TTY: o.TTY,
Quiet: o.Quiet,
},
CommandName: cmd.Parent().CommandPath() + " attach",
Attach: &attach.DefaultRemoteAttach{},
}
config, err := f.ToRESTConfig()
if err != nil {
return err
}
opts.Config = config
opts.AttachFunc = attach.DefaultAttachFunc
attachablePod, err := polymorphichelpers.AttachablePodForObjectFn(f, info.Object, opts.GetPodTimeout)
if err != nil {
return err
}
err = handleAttachPod(ctx, f, o.podClient, attachablePod.Namespace, attachablePod.Name, debugContainer.Name, opts)
if err != nil {
return err
}
}
return nil
})
return err
}
func containerNames(pod *corev1.Pod) map[string]bool {
names := map[string]bool{}
for _, c := range pod.Spec.Containers {
names[c.Name] = true
}
for _, c := range pod.Spec.InitContainers {
names[c.Name] = true
}
for _, c := range pod.Spec.EphemeralContainers {
names[c.Name] = true
}
return names
}
// 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
}
ec := &corev1.EphemeralContainer{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: name,
Env: o.Env,
Image: o.Image,
ImagePullPolicy: o.PullPolicy,
Stdin: o.Interactive,
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
TTY: o.TTY,
},
TargetContainerName: o.Target,
}
if o.ArgsOnly {
ec.Args = o.Args
} else {
ec.Command = o.Args
}
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) {
// TODO: expose the timeout
ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, 0*time.Second)
defer cancel()
preconditionFunc := func(store cache.Store) (bool, error) {
_, exists, err := store.Get(&metav1.ObjectMeta{Namespace: ns, Name: podName})
if err != nil {
return true, err
}
if !exists {
// We need to make sure we see the object in the cache before we start waiting for events
// or we would be waiting for the timeout if such object didn't exist.
// (e.g. it was deleted before we started informers so they wouldn't even see the delete event)
return true, errors.NewNotFound(corev1.Resource("pods"), podName)
}
return false, nil
}
fieldSelector := fields.OneTermEqualSelector("metadata.name", podName).String()
lw := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
options.FieldSelector = fieldSelector
return podClient.Pods(ns).List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = fieldSelector
return podClient.Pods(ns).Watch(ctx, options)
},
}
intr := interrupt.New(nil, cancel)
var result *corev1.Pod
err := intr.Run(func() error {
ev, err := watchtools.UntilWithSync(ctx, lw, &corev1.Pod{}, preconditionFunc, func(ev watch.Event) (bool, error) {
switch ev.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "")
}
p, ok := ev.Object.(*corev1.Pod)
if !ok {
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
}
}
return false, nil
})
if ev != nil {
result = ev.Object.(*corev1.Pod)
}
return err
})
return result, err
}
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)
if err != nil {
return err
}
opts.Namespace = ns
opts.Pod = pod
opts.PodName = podName
opts.ContainerName = ephemeralContainerName
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]
}
}
if status.State.Terminated != nil {
klog.V(1).Info("Ephemeral container terminated, falling back to logs")
return logOpts(f, pod, opts)
}
if err := opts.Run(); err != nil {
fmt.Fprintf(opts.ErrOut, "Error attaching, falling back to logs: %v\n", err)
return logOpts(f, pod, opts)
}
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)
if err != nil {
return err
}
requests, err := polymorphichelpers.LogsForObjectFn(restClientGetter, pod, &corev1.PodLogOptions{Container: ctrName}, opts.GetPodTimeout, false)
if err != nil {
return err
}
for _, request := range requests {
if err := logs.DefaultConsumeRequest(request, opts.Out); err != nil {
return err
}
}
return nil
}

166
pkg/cmd/debug/debug_test.go Normal file
View File

@ -0,0 +1,166 @@
/*
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"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
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,
Target: "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",
},
},
},
} {
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)
}
})
}
}