From e5b655938e55e0404545ff73e9239593a27a3272 Mon Sep 17 00:00:00 2001 From: Lee Verberne Date: Fri, 31 Jan 2020 15:29:31 +0100 Subject: [PATCH] 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. --- hack/.golint_failures | 1 + staging/src/k8s.io/kubectl/go.mod | 1 + staging/src/k8s.io/kubectl/pkg/cmd/BUILD | 2 + staging/src/k8s.io/kubectl/pkg/cmd/alpha.go | 3 +- .../src/k8s.io/kubectl/pkg/cmd/debug/BUILD | 61 +++ .../src/k8s.io/kubectl/pkg/cmd/debug/debug.go | 451 ++++++++++++++++++ .../kubectl/pkg/cmd/debug/debug_test.go | 166 +++++++ vendor/modules.txt | 1 + 8 files changed, 685 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go diff --git a/hack/.golint_failures b/hack/.golint_failures index 916eb98ee56..d4f4d7f6472 100644 --- a/hack/.golint_failures +++ b/hack/.golint_failures @@ -463,6 +463,7 @@ staging/src/k8s.io/kubectl/pkg/cmd/autoscale staging/src/k8s.io/kubectl/pkg/cmd/certificates staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo 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/diff diff --git a/staging/src/k8s.io/kubectl/go.mod b/staging/src/k8s.io/kubectl/go.mod index 4adca46bb10..4c5d7dbf176 100644 --- a/staging/src/k8s.io/kubectl/go.mod +++ b/staging/src/k8s.io/kubectl/go.mod @@ -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 diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/BUILD index 5a557905a86..dc15ca33874 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/BUILD @@ -10,6 +10,7 @@ go_library( ], deps = [ "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/debug:go_default_library", "//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library", "//staging/src/k8s.io/kubectl/pkg/util/i18n:go_default_library", "//staging/src/k8s.io/kubectl/pkg/util/templates:go_default_library", @@ -37,6 +38,7 @@ filegroup( "//staging/src/k8s.io/kubectl/pkg/cmd/completion:all-srcs", "//staging/src/k8s.io/kubectl/pkg/cmd/config:all-srcs", "//staging/src/k8s.io/kubectl/pkg/cmd/create:all-srcs", + "//staging/src/k8s.io/kubectl/pkg/cmd/debug:all-srcs", "//staging/src/k8s.io/kubectl/pkg/cmd/delete:all-srcs", "//staging/src/k8s.io/kubectl/pkg/cmd/describe:all-srcs", "//staging/src/k8s.io/kubectl/pkg/cmd/diff:all-srcs", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go b/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go index 0722a36479d..38f7fd4dd86 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go @@ -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. diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD new file mode 100644 index 00000000000..fda77d84f1f --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/BUILD @@ -0,0 +1,61 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["debug.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/kubectl/pkg/cmd/debug", + importpath = "k8s.io/kubectl/pkg/cmd/debug", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", + "//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", + "//staging/src/k8s.io/client-go/tools/cache:go_default_library", + "//staging/src/k8s.io/client-go/tools/watch:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/attach:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/exec:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/logs:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/polymorphichelpers:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/scheme:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/util/i18n:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/util/interrupt:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/util/templates:go_default_library", + "//vendor/github.com/docker/distribution/reference:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", + "//vendor/k8s.io/klog:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["debug_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/api/core/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", + ], +) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go new file mode 100644 index 00000000000..86ac6e07c44 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go @@ -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 +} 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 new file mode 100644 index 00000000000..f8c0268318d --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go @@ -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) + } + }) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 85cf480eb63..57ff9f44124 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1791,6 +1791,7 @@ k8s.io/kubectl/pkg/cmd/clusterinfo k8s.io/kubectl/pkg/cmd/completion k8s.io/kubectl/pkg/cmd/config k8s.io/kubectl/pkg/cmd/create +k8s.io/kubectl/pkg/cmd/debug k8s.io/kubectl/pkg/cmd/delete k8s.io/kubectl/pkg/cmd/describe k8s.io/kubectl/pkg/cmd/diff