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:
parent
ec95730cf7
commit
c3d84a9e1c
30
go.mod
30
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
|
||||
|
@ -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
11
go.sum
|
@ -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=
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue