From a31315c0e9216fb028dac50118f72a4340a4767d Mon Sep 17 00:00:00 2001 From: carlory Date: Thu, 30 Jun 2022 02:23:17 +0800 Subject: [PATCH] use kubectl exec to implement karmadactl exec Signed-off-by: carlory --- go.mod | 2 +- pkg/karmadactl/describe.go | 2 +- pkg/karmadactl/exec.go | 330 ++----------- pkg/karmadactl/get.go | 8 +- pkg/karmadactl/logs.go | 2 +- pkg/karmadactl/promote.go | 2 +- .../pkg/cmd/apiresources/apiresources.go | 277 +++++++++++ .../pkg/cmd/apiresources/apiversions.go | 98 ++++ vendor/k8s.io/kubectl/pkg/cmd/exec/exec.go | 376 +++++++++++++++ .../kubectl/pkg/util/completion/completion.go | 453 ++++++++++++++++++ vendor/modules.txt | 3 + 11 files changed, 1251 insertions(+), 302 deletions(-) create mode 100644 vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go create mode 100644 vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiversions.go create mode 100644 vendor/k8s.io/kubectl/pkg/cmd/exec/exec.go create mode 100644 vendor/k8s.io/kubectl/pkg/util/completion/completion.go diff --git a/go.mod b/go.mod index 985db093f..e6135b0f8 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/google/uuid v1.1.2 github.com/kr/pretty v0.3.0 - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/olekukonko/tablewriter v0.0.4 github.com/onsi/ginkgo/v2 v2.1.3 github.com/onsi/gomega v1.18.1 @@ -102,6 +101,7 @@ require ( github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect diff --git a/pkg/karmadactl/describe.go b/pkg/karmadactl/describe.go index 3c7c40427..3434ef1f8 100644 --- a/pkg/karmadactl/describe.go +++ b/pkg/karmadactl/describe.go @@ -125,7 +125,7 @@ func (o *CommandDescribeOptions) Complete(karmadaConfig KarmadaConfig, args []st return err } - f := getFactory(o.Cluster, clusterInfo) + f := getFactory(o.Cluster, clusterInfo, "") o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { diff --git a/pkg/karmadactl/exec.go b/pkg/karmadactl/exec.go index bf34c3b6c..cb90dbe4b 100644 --- a/pkg/karmadactl/exec.go +++ b/pkg/karmadactl/exec.go @@ -1,27 +1,13 @@ package karmadactl import ( - "context" "fmt" - "io" - "net/url" "time" - dockerterm "github.com/moby/term" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" - coreclient "k8s.io/client-go/kubernetes/typed/core/v1" - restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/remotecommand" + kubectlexec "k8s.io/kubectl/pkg/cmd/exec" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/cmd/util/podcmd" - "k8s.io/kubectl/pkg/polymorphichelpers" - "k8s.io/kubectl/pkg/scheme" - "k8s.io/kubectl/pkg/util/interrupt" - "k8s.io/kubectl/pkg/util/term" "github.com/karmada-io/karmada/pkg/karmadactl/options" ) @@ -32,14 +18,16 @@ const ( // NewCmdExec new exec command. func NewCmdExec(karmadaConfig KarmadaConfig, parentCommand string) *cobra.Command { - ioStreams := genericclioptions.IOStreams{In: getIn, Out: getOut, ErrOut: getErr} + streams := genericclioptions.IOStreams{In: getIn, Out: getOut, ErrOut: getErr} o := &ExecOptions{ - streamOptions: streamOptions{ - IOStreams: ioStreams, + KubectlExecOptions: &kubectlexec.ExecOptions{ + StreamOptions: kubectlexec.StreamOptions{ + IOStreams: streams, + }, + Executor: &kubectlexec.DefaultRemoteExecutor{}, }, - - Executor: &DefaultRemoteExecutor{}, } + cmd := &cobra.Command{ Use: "exec (POD | TYPE/NAME) [-c CONTAINER] [flags] (-C CLUSTER) -- COMMAND [args...]", DisableFlagsInUseLine: true, @@ -48,10 +36,10 @@ func NewCmdExec(karmadaConfig KarmadaConfig, parentCommand string) *cobra.Comman Example: execExample(parentCommand), RunE: func(cmd *cobra.Command, args []string) error { argsLenAtDash := cmd.ArgsLenAtDash() - if err := o.Complete(karmadaConfig, args, argsLenAtDash); err != nil { + if err := o.Complete(karmadaConfig, cmd, args, argsLenAtDash); err != nil { return err } - if err := o.Validate(cmd); err != nil { + if err := o.Validate(); err != nil { return err } if err := o.Run(); err != nil { @@ -64,12 +52,14 @@ func NewCmdExec(karmadaConfig KarmadaConfig, parentCommand string) *cobra.Comman o.GlobalCommandOptions.AddFlags(cmd.Flags()) cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodExecTimeout) - cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.FilenameOptions.Filenames, "to use to exec into the resource") + cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.KubectlExecOptions.FilenameOptions.Filenames, "to use to exec into the resource") + cmdutil.AddContainerVarFlags(cmd, &o.KubectlExecOptions.ContainerName, o.KubectlExecOptions.ContainerName) + + cmd.Flags().BoolVarP(&o.KubectlExecOptions.Stdin, "stdin", "i", o.KubectlExecOptions.Stdin, "Pass stdin to the container") + cmd.Flags().BoolVarP(&o.KubectlExecOptions.TTY, "tty", "t", o.KubectlExecOptions.TTY, "Stdin is a TTY") + cmd.Flags().BoolVarP(&o.KubectlExecOptions.Quiet, "quiet", "q", o.KubectlExecOptions.Quiet, "Only print output from the remote session") + cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", o.Namespace, "If present, the namespace scope for this CLI request") cmd.Flags().StringVarP(&o.Cluster, "cluster", "C", "", "Specify a member cluster") - cmdutil.AddContainerVarFlags(cmd, &o.ContainerName, o.ContainerName) - cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container") - cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY") - cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "Only print output from the remote session") return cmd } @@ -78,6 +68,9 @@ func execExample(parentCommand string) string { # Get output from running the 'date' command from pod mypod, using the first container by default in cluster(member1)` + "\n" + fmt.Sprintf("%s exec mypod -C=member1 -- date", parentCommand) + ` +# Get output from running the 'date' command from pod mypod in namespace(foo), using the first container by default in cluster(member1)` + "\n" + + fmt.Sprintf("%s exec mypod -C=member1 --namespace foo -- date", parentCommand) + ` + # Get output from running the 'date' command in ruby-container from pod mypod in cluster(member1)` + "\n" + fmt.Sprintf("%s exec mypod -c ruby-container -C=member1 -- date", parentCommand) + ` @@ -93,292 +86,37 @@ func execExample(parentCommand string) string { return example } -// RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing -type RemoteExecutor interface { - Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error -} - -// DefaultRemoteExecutor is the standard implementation of remote command execution -type DefaultRemoteExecutor struct{} - -// Execute implement RemoteExecutor interface -func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { - exec, err := remotecommand.NewSPDYExecutor(config, method, url) - if err != nil { - return err - } - return exec.Stream(remotecommand.StreamOptions{ - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - Tty: tty, - TerminalSizeQueue: terminalSizeQueue, - }) -} - -type streamOptions struct { - Cluster string - Namespace string - PodName string - ContainerName string - Stdin bool - TTY bool - // minimize unnecessary output - Quiet bool - // InterruptParent, if set, is used to handle interrupts while attached - InterruptParent *interrupt.Handler - - genericclioptions.IOStreams - - // for testing - overrideStreams func() (io.ReadCloser, io.Writer, io.Writer) - isTerminalIn func(t term.TTY) bool -} - // ExecOptions declare the arguments accepted by the Exec command type ExecOptions struct { // global flags options.GlobalCommandOptions - - streamOptions - resource.FilenameOptions - - ResourceName string - Command []string - EnforceNamespace bool - - Builder func() *resource.Builder - ExecutablePodFn polymorphichelpers.AttachablePodForObjectFunc - restClientGetter genericclioptions.RESTClientGetter - - Pod *corev1.Pod - Executor RemoteExecutor - PodClient coreclient.PodsGetter - GetPodTimeout time.Duration - Config *restclient.Config + // flags specific to exec + KubectlExecOptions *kubectlexec.ExecOptions + Namespace string + Cluster string } // Complete verifies command line arguments and loads data from the command environment -func (p *ExecOptions) Complete(karmadaConfig KarmadaConfig, argsIn []string, argsLenAtDash int) error { - if len(argsIn) > 0 && argsLenAtDash != 0 { - p.ResourceName = argsIn[0] - } - if argsLenAtDash > -1 { - p.Command = argsIn[argsLenAtDash:] - } else if len(argsIn) > 1 { - if !p.Quiet { - fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n") - } - p.Command = argsIn[1:] - } else if len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0 { - if !p.Quiet { - fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n") - } - p.Command = argsIn[0:] - p.ResourceName = "" - } - - var err error - - if len(p.Cluster) == 0 { - return fmt.Errorf("must specify a cluster") - } - - karmadaRestConfig, err := karmadaConfig.GetRestConfig(p.KarmadaContext, p.KubeConfig) +func (o *ExecOptions) Complete(karmadaConfig KarmadaConfig, cmd *cobra.Command, argsIn []string, argsLenAtDash int) error { + karmadaRestConfig, err := karmadaConfig.GetRestConfig(o.KarmadaContext, o.KubeConfig) if err != nil { return fmt.Errorf("failed to get control plane rest config. context: %s, kube-config: %s, error: %v", - p.KarmadaContext, p.KubeConfig, err) + o.KarmadaContext, o.KubeConfig, err) } - - clusterInfo, err := getClusterInfo(karmadaRestConfig, p.Cluster, p.KubeConfig, p.KarmadaContext) + clusterInfo, err := getClusterInfo(karmadaRestConfig, o.Cluster, o.KubeConfig, o.KarmadaContext) if err != nil { return err } - - f := getFactory(p.Cluster, clusterInfo) - - p.Namespace, p.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() - if err != nil { - return err - } - - p.ExecutablePodFn = polymorphichelpers.AttachablePodForObjectFn - - p.Builder = f.NewBuilder - p.restClientGetter = f - - p.Config, err = f.ToRESTConfig() - if err != nil { - return err - } - - clientset, err := f.KubernetesClientSet() - if err != nil { - return err - } - p.PodClient = clientset.CoreV1() - - return nil + f := getFactory(o.Cluster, clusterInfo, o.Namespace) + return o.KubectlExecOptions.Complete(f, cmd, argsIn, argsLenAtDash) } // Validate checks that the provided exec options are specified. -func (p *ExecOptions) Validate(cmd *cobra.Command) error { - if len(p.PodName) == 0 && len(p.ResourceName) == 0 && len(p.FilenameOptions.Filenames) == 0 { - return fmt.Errorf("pod, type/name or --filename must be specified") - } - if len(p.Command) == 0 { - return fmt.Errorf("you must specify at least one command for the container") - } - if p.Out == nil || p.ErrOut == nil { - return fmt.Errorf("both output and error output must be provided") - } - - var err error - p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) - if err != nil { - return cmdutil.UsageErrorf(cmd, err.Error()) - } - return nil -} - -func (o *streamOptions) setupTTY() term.TTY { - t := term.TTY{ - Parent: o.InterruptParent, - Out: o.Out, - } - - if !o.Stdin { - // need to nil out o.In to make sure we don't create a stream for stdin - o.In = nil - o.TTY = false - return t - } - - t.In = o.In - if !o.TTY { - return t - } - - if o.isTerminalIn == nil { - o.isTerminalIn = func(tty term.TTY) bool { - return tty.IsTerminalIn() - } - } - if !o.isTerminalIn(t) { - o.TTY = false - - if !o.Quiet && o.ErrOut != nil { - fmt.Fprintln(o.ErrOut, "Unable to use a TTY - input is not a terminal or the right kind of file") - } - - return t - } - - // if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we - // can safely set t.Raw to true - t.Raw = true - - if o.overrideStreams == nil { - // use dockerterm.StdStreams() to get the right I/O handles on Windows - o.overrideStreams = dockerterm.StdStreams - } - stdin, stdout, _ := o.overrideStreams() - o.In = stdin - t.In = stdin - if o.Out != nil { - o.Out = stdout - t.Out = stdout - } - - return t +func (o *ExecOptions) Validate() error { + return o.KubectlExecOptions.Validate() } // Run executes a validated remote execution against a pod. -func (p *ExecOptions) Run() error { - var err error - // we still need legacy pod getter when PodName in ExecOptions struct is provided, - // since there are any other command run this function by providing Podname with PodsGetter - // and without resource builder, eg: `kubectl cp`. - if len(p.PodName) != 0 { - p.Pod, err = p.PodClient.Pods(p.Namespace).Get(context.TODO(), p.PodName, metav1.GetOptions{}) - if err != nil { - return err - } - } else { - builder := p.Builder(). - WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). - FilenameParam(p.EnforceNamespace, &p.FilenameOptions). - NamespaceParam(p.Namespace).DefaultNamespace() - if len(p.ResourceName) > 0 { - builder = builder.ResourceNames("pods", p.ResourceName) - } - - obj, err := builder.Do().Object() - if err != nil { - return err - } - - p.Pod, err = p.ExecutablePodFn(p.restClientGetter, obj, p.GetPodTimeout) - if err != nil { - return err - } - } - - pod := p.Pod - - if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { - return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase) - } - - containerName := p.ContainerName - if len(containerName) == 0 { - container, err := podcmd.FindOrDefaultContainerByName(pod, containerName, p.Quiet, p.ErrOut) - if err != nil { - return err - } - containerName = container.Name - } - - // ensure we can recover the terminal while attached - t := p.setupTTY() - - var sizeQueue remotecommand.TerminalSizeQueue - if t.Raw { - // this call spawns a goroutine to monitor/update the terminal size - sizeQueue = t.MonitorSize(t.GetSize()) - - // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is - // true - p.ErrOut = nil - } - - fn := func() error { - restClient, err := restclient.RESTClientFor(p.Config) - if err != nil { - return err - } - - // TODO: consider abstracting into a client invocation or client helper - req := restClient.Post(). - Resource("pods"). - Name(pod.Name). - Namespace(pod.Namespace). - SubResource("exec") - req.VersionedParams(&corev1.PodExecOptions{ - Container: containerName, - Command: p.Command, - Stdin: p.Stdin, - Stdout: p.Out != nil, - Stderr: p.ErrOut != nil, - TTY: t.Raw, - }, scheme.ParameterCodec) - - return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue) - } - - if err := t.Safe(fn); err != nil { - return err - } - - return nil +func (o *ExecOptions) Run() error { + return o.KubectlExecOptions.Run() } diff --git a/pkg/karmadactl/get.go b/pkg/karmadactl/get.go index 7750cc9c9..4f7a5e24d 100644 --- a/pkg/karmadactl/get.go +++ b/pkg/karmadactl/get.go @@ -264,7 +264,7 @@ func (g *CommandGetOptions) Run(karmadaConfig KarmadaConfig, cmd *cobra.Command, wg.Add(len(g.Clusters)) for idx := range g.Clusters { g.setClusterProxyInfo(karmadaRestConfig, g.Clusters[idx], clusterInfos) - f := getFactory(g.Clusters[idx], clusterInfos) + f := getFactory(g.Clusters[idx], clusterInfos, "") go g.getObjInfo(&wg, &mux, f, g.Clusters[idx], &objs, &watchObjs, &allErrs, args) } wg.Wait() @@ -858,7 +858,7 @@ func clusterInfoInit(g *CommandGetOptions, karmadaConfig KarmadaConfig, clusterI return karmadaRestConfig, nil } -func getFactory(clusterName string, clusterInfos map[string]*ClusterInfo) cmdutil.Factory { +func getFactory(clusterName string, clusterInfos map[string]*ClusterInfo, namespace string) cmdutil.Factory { kubeConfigFlags := NewConfigFlags(true).WithDeprecatedPasswordFlag() // Build member cluster kubeConfigFlags kubeConfigFlags.APIServer = stringptr(clusterInfos[clusterName].APIEndpoint) @@ -868,6 +868,10 @@ func getFactory(clusterName string, clusterInfos map[string]*ClusterInfo) cmduti kubeConfigFlags.Context = stringptr(clusterInfos[clusterName].Context) kubeConfigFlags.usePersistentConfig = true + if namespace != "" { + kubeConfigFlags.Namespace = stringptr(namespace) + } + matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) return cmdutil.NewFactory(matchVersionKubeConfigFlags) } diff --git a/pkg/karmadactl/logs.go b/pkg/karmadactl/logs.go index 889e12b5f..dd011eccf 100644 --- a/pkg/karmadactl/logs.go +++ b/pkg/karmadactl/logs.go @@ -224,7 +224,7 @@ func (o *CommandLogsOptions) Complete(karmadaConfig KarmadaConfig, cmd *cobra.Co return err } - o.f = getFactory(o.Cluster, clusterInfo) + o.f = getFactory(o.Cluster, clusterInfo, "") o.RESTClientGetter = o.f o.LogsForObject = polymorphichelpers.LogsForObjectFn diff --git a/pkg/karmadactl/promote.go b/pkg/karmadactl/promote.go index c4aa8efd4..2848ea559 100644 --- a/pkg/karmadactl/promote.go +++ b/pkg/karmadactl/promote.go @@ -212,7 +212,7 @@ func RunPromote(karmadaConfig KarmadaConfig, opts CommandPromoteOption, args []s f = cmdutil.NewFactory(kubeConfigFlags) } else { opts.setClusterProxyInfo(controlPlaneRestConfig, opts.Cluster, clusterInfos) - f = getFactory(opts.Cluster, clusterInfos) + f = getFactory(opts.Cluster, clusterInfos, "") } objInfo, err := opts.getObjInfo(f, opts.Cluster, args) diff --git a/vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go b/vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go new file mode 100644 index 000000000..6849cd637 --- /dev/null +++ b/vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go @@ -0,0 +1,277 @@ +/* +Copyright 2018 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 apiresources + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + apiresourcesExample = templates.Examples(` + # Print the supported API resources + kubectl api-resources + + # Print the supported API resources with more information + kubectl api-resources -o wide + + # Print the supported API resources sorted by a column + kubectl api-resources --sort-by=name + + # Print the supported namespaced resources + kubectl api-resources --namespaced=true + + # Print the supported non-namespaced resources + kubectl api-resources --namespaced=false + + # Print the supported API resources with a specific APIGroup + kubectl api-resources --api-group=extensions`) +) + +// APIResourceOptions is the start of the data required to perform the operation. +// As new fields are added, add them here instead of referencing the cmd.Flags() +type APIResourceOptions struct { + Output string + SortBy string + APIGroup string + Namespaced bool + Verbs []string + NoHeaders bool + Cached bool + + genericclioptions.IOStreams +} + +// groupResource contains the APIGroup and APIResource +type groupResource struct { + APIGroup string + APIGroupVersion string + APIResource metav1.APIResource +} + +// NewAPIResourceOptions creates the options for APIResource +func NewAPIResourceOptions(ioStreams genericclioptions.IOStreams) *APIResourceOptions { + return &APIResourceOptions{ + IOStreams: ioStreams, + Namespaced: true, + } +} + +// NewCmdAPIResources creates the `api-resources` command +func NewCmdAPIResources(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewAPIResourceOptions(ioStreams) + + cmd := &cobra.Command{ + Use: "api-resources", + Short: i18n.T("Print the supported API resources on the server"), + Long: i18n.T("Print the supported API resources on the server."), + Example: apiresourcesExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.RunAPIResources(cmd, f)) + }, + } + + cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).") + cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, `Output format. One of: (wide, name).`) + + cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.") + cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.") + cmd.Flags().StringSliceVar(&o.Verbs, "verbs", o.Verbs, "Limit to resources that support the specified verbs.") + cmd.Flags().StringVar(&o.SortBy, "sort-by", o.SortBy, "If non-empty, sort list of resources using specified field. The field can be either 'name' or 'kind'.") + cmd.Flags().BoolVar(&o.Cached, "cached", o.Cached, "Use the cached list of resources if available.") + return cmd +} + +// Validate checks to the APIResourceOptions to see if there is sufficient information run the command +func (o *APIResourceOptions) Validate() error { + supportedOutputTypes := sets.NewString("", "wide", "name") + if !supportedOutputTypes.Has(o.Output) { + return fmt.Errorf("--output %v is not available", o.Output) + } + supportedSortTypes := sets.NewString("", "name", "kind") + if len(o.SortBy) > 0 { + if !supportedSortTypes.Has(o.SortBy) { + return fmt.Errorf("--sort-by accepts only name or kind") + } + } + return nil +} + +// Complete adapts from the command line args and validates them +func (o *APIResourceOptions) Complete(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args) + } + return nil +} + +// RunAPIResources does the work +func (o *APIResourceOptions) RunAPIResources(cmd *cobra.Command, f cmdutil.Factory) error { + w := printers.GetNewTabWriter(o.Out) + defer w.Flush() + + discoveryclient, err := f.ToDiscoveryClient() + if err != nil { + return err + } + + if !o.Cached { + // Always request fresh data from the server + discoveryclient.Invalidate() + } + + errs := []error{} + lists, err := discoveryclient.ServerPreferredResources() + if err != nil { + errs = append(errs, err) + } + + resources := []groupResource{} + + groupChanged := cmd.Flags().Changed("api-group") + nsChanged := cmd.Flags().Changed("namespaced") + + for _, list := range lists { + if len(list.APIResources) == 0 { + continue + } + gv, err := schema.ParseGroupVersion(list.GroupVersion) + if err != nil { + continue + } + for _, resource := range list.APIResources { + if len(resource.Verbs) == 0 { + continue + } + // filter apiGroup + if groupChanged && o.APIGroup != gv.Group { + continue + } + // filter namespaced + if nsChanged && o.Namespaced != resource.Namespaced { + continue + } + // filter to resources that support the specified verbs + if len(o.Verbs) > 0 && !sets.NewString(resource.Verbs...).HasAll(o.Verbs...) { + continue + } + resources = append(resources, groupResource{ + APIGroup: gv.Group, + APIGroupVersion: gv.String(), + APIResource: resource, + }) + } + } + + if o.NoHeaders == false && o.Output != "name" { + if err = printContextHeaders(w, o.Output); err != nil { + return err + } + } + + sort.Stable(sortableResource{resources, o.SortBy}) + for _, r := range resources { + switch o.Output { + case "name": + name := r.APIResource.Name + if len(r.APIGroup) > 0 { + name += "." + r.APIGroup + } + if _, err := fmt.Fprintf(w, "%s\n", name); err != nil { + errs = append(errs, err) + } + case "wide": + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\n", + r.APIResource.Name, + strings.Join(r.APIResource.ShortNames, ","), + r.APIGroupVersion, + r.APIResource.Namespaced, + r.APIResource.Kind, + r.APIResource.Verbs); err != nil { + errs = append(errs, err) + } + case "": + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n", + r.APIResource.Name, + strings.Join(r.APIResource.ShortNames, ","), + r.APIGroupVersion, + r.APIResource.Namespaced, + r.APIResource.Kind); err != nil { + errs = append(errs, err) + } + } + } + + if len(errs) > 0 { + return errors.NewAggregate(errs) + } + return nil +} + +func printContextHeaders(out io.Writer, output string) error { + columnNames := []string{"NAME", "SHORTNAMES", "APIVERSION", "NAMESPACED", "KIND"} + if output == "wide" { + columnNames = append(columnNames, "VERBS") + } + _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t")) + return err +} + +type sortableResource struct { + resources []groupResource + sortBy string +} + +func (s sortableResource) Len() int { return len(s.resources) } +func (s sortableResource) Swap(i, j int) { + s.resources[i], s.resources[j] = s.resources[j], s.resources[i] +} +func (s sortableResource) Less(i, j int) bool { + ret := strings.Compare(s.compareValues(i, j)) + if ret > 0 { + return false + } else if ret == 0 { + return strings.Compare(s.resources[i].APIResource.Name, s.resources[j].APIResource.Name) < 0 + } + return true +} + +func (s sortableResource) compareValues(i, j int) (string, string) { + switch s.sortBy { + case "name": + return s.resources[i].APIResource.Name, s.resources[j].APIResource.Name + case "kind": + return s.resources[i].APIResource.Kind, s.resources[j].APIResource.Kind + } + return s.resources[i].APIGroup, s.resources[j].APIGroup +} diff --git a/vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiversions.go b/vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiversions.go new file mode 100644 index 000000000..71585d4e8 --- /dev/null +++ b/vendor/k8s.io/kubectl/pkg/cmd/apiresources/apiversions.go @@ -0,0 +1,98 @@ +/* +Copyright 2014 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 apiresources + +import ( + "fmt" + "sort" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/discovery" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + apiversionsExample = templates.Examples(i18n.T(` + # Print the supported API versions + kubectl api-versions`)) +) + +// APIVersionsOptions have the data required for API versions +type APIVersionsOptions struct { + discoveryClient discovery.CachedDiscoveryInterface + + genericclioptions.IOStreams +} + +// NewAPIVersionsOptions creates the options for APIVersions +func NewAPIVersionsOptions(ioStreams genericclioptions.IOStreams) *APIVersionsOptions { + return &APIVersionsOptions{ + IOStreams: ioStreams, + } +} + +// NewCmdAPIVersions creates the `api-versions` command +func NewCmdAPIVersions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewAPIVersionsOptions(ioStreams) + cmd := &cobra.Command{ + Use: "api-versions", + Short: i18n.T("Print the supported API versions on the server, in the form of \"group/version\""), + Long: i18n.T("Print the supported API versions on the server, in the form of \"group/version\"."), + Example: apiversionsExample, + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.RunAPIVersions()) + }, + } + return cmd +} + +// Complete adapts from the command line args and factory to the data required +func (o *APIVersionsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args) + } + var err error + o.discoveryClient, err = f.ToDiscoveryClient() + if err != nil { + return err + } + return nil +} + +// RunAPIVersions does the work +func (o *APIVersionsOptions) RunAPIVersions() error { + // Always request fresh data from the server + o.discoveryClient.Invalidate() + + groupList, err := o.discoveryClient.ServerGroups() + if err != nil { + return fmt.Errorf("couldn't get available api versions from server: %v", err) + } + apiVersions := metav1.ExtractGroupVersions(groupList) + sort.Strings(apiVersions) + for _, v := range apiVersions { + fmt.Fprintln(o.Out, v) + } + return nil +} diff --git a/vendor/k8s.io/kubectl/pkg/cmd/exec/exec.go b/vendor/k8s.io/kubectl/pkg/cmd/exec/exec.go new file mode 100644 index 000000000..f3be195db --- /dev/null +++ b/vendor/k8s.io/kubectl/pkg/cmd/exec/exec.go @@ -0,0 +1,376 @@ +/* +Copyright 2014 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 exec + +import ( + "context" + "fmt" + "io" + "net/url" + "time" + + dockerterm "github.com/moby/term" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + coreclient "k8s.io/client-go/kubernetes/typed/core/v1" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/util/podcmd" + "k8s.io/kubectl/pkg/polymorphichelpers" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util/completion" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/interrupt" + "k8s.io/kubectl/pkg/util/templates" + "k8s.io/kubectl/pkg/util/term" +) + +var ( + execExample = templates.Examples(i18n.T(` + # Get output from running the 'date' command from pod mypod, using the first container by default + kubectl exec mypod -- date + + # Get output from running the 'date' command in ruby-container from pod mypod + kubectl exec mypod -c ruby-container -- date + + # Switch to raw terminal mode; sends stdin to 'bash' in ruby-container from pod mypod + # and sends stdout/stderr from 'bash' back to the client + kubectl exec mypod -c ruby-container -i -t -- bash -il + + # List contents of /usr from the first container of pod mypod and sort by modification time + # If the command you want to execute in the pod has any flags in common (e.g. -i), + # you must use two dashes (--) to separate your command's flags/arguments + # Also note, do not surround your command and its flags/arguments with quotes + # unless that is how you would execute it normally (i.e., do ls -t /usr, not "ls -t /usr") + kubectl exec mypod -i -t -- ls -t /usr + + # Get output from running 'date' command from the first pod of the deployment mydeployment, using the first container by default + kubectl exec deploy/mydeployment -- date + + # Get output from running 'date' command from the first pod of the service myservice, using the first container by default + kubectl exec svc/myservice -- date + `)) +) + +const ( + defaultPodExecTimeout = 60 * time.Second +) + +func NewCmdExec(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + options := &ExecOptions{ + StreamOptions: StreamOptions{ + IOStreams: streams, + }, + + Executor: &DefaultRemoteExecutor{}, + } + cmd := &cobra.Command{ + Use: "exec (POD | TYPE/NAME) [-c CONTAINER] [flags] -- COMMAND [args...]", + DisableFlagsInUseLine: true, + Short: i18n.T("Execute a command in a container"), + Long: i18n.T("Execute a command in a container."), + Example: execExample, + ValidArgsFunction: completion.PodResourceNameCompletionFunc(f), + Run: func(cmd *cobra.Command, args []string) { + argsLenAtDash := cmd.ArgsLenAtDash() + cmdutil.CheckErr(options.Complete(f, cmd, args, argsLenAtDash)) + cmdutil.CheckErr(options.Validate()) + cmdutil.CheckErr(options.Run()) + }, + } + cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodExecTimeout) + cmdutil.AddJsonFilenameFlag(cmd.Flags(), &options.FilenameOptions.Filenames, "to use to exec into the resource") + // TODO support UID + cmdutil.AddContainerVarFlags(cmd, &options.ContainerName, options.ContainerName) + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc("container", completion.ContainerCompletionFunc(f))) + + cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", options.Stdin, "Pass stdin to the container") + cmd.Flags().BoolVarP(&options.TTY, "tty", "t", options.TTY, "Stdin is a TTY") + cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", options.Quiet, "Only print output from the remote session") + return cmd +} + +// RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing +type RemoteExecutor interface { + Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error +} + +// DefaultRemoteExecutor is the standard implementation of remote command execution +type DefaultRemoteExecutor struct{} + +func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { + exec, err := remotecommand.NewSPDYExecutor(config, method, url) + if err != nil { + return err + } + return exec.Stream(remotecommand.StreamOptions{ + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + Tty: tty, + TerminalSizeQueue: terminalSizeQueue, + }) +} + +type StreamOptions struct { + Namespace string + PodName string + ContainerName string + Stdin bool + TTY bool + // minimize unnecessary output + Quiet bool + // InterruptParent, if set, is used to handle interrupts while attached + InterruptParent *interrupt.Handler + + genericclioptions.IOStreams + + // for testing + overrideStreams func() (io.ReadCloser, io.Writer, io.Writer) + isTerminalIn func(t term.TTY) bool +} + +// ExecOptions declare the arguments accepted by the Exec command +type ExecOptions struct { + StreamOptions + resource.FilenameOptions + + ResourceName string + Command []string + EnforceNamespace bool + + Builder func() *resource.Builder + ExecutablePodFn polymorphichelpers.AttachablePodForObjectFunc + restClientGetter genericclioptions.RESTClientGetter + + Pod *corev1.Pod + Executor RemoteExecutor + PodClient coreclient.PodsGetter + GetPodTimeout time.Duration + Config *restclient.Config +} + +// Complete verifies command line arguments and loads data from the command environment +func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []string, argsLenAtDash int) error { + if len(argsIn) > 0 && argsLenAtDash != 0 { + p.ResourceName = argsIn[0] + } + if argsLenAtDash > -1 { + p.Command = argsIn[argsLenAtDash:] + } else if len(argsIn) > 1 { + if !p.Quiet { + fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n") + } + p.Command = argsIn[1:] + } else if len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0 { + if !p.Quiet { + fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n") + } + p.Command = argsIn[0:] + p.ResourceName = "" + } + + var err error + p.Namespace, p.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + p.ExecutablePodFn = polymorphichelpers.AttachablePodForObjectFn + + p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) + if err != nil { + return cmdutil.UsageErrorf(cmd, err.Error()) + } + + p.Builder = f.NewBuilder + p.restClientGetter = f + + p.Config, err = f.ToRESTConfig() + if err != nil { + return err + } + + clientset, err := f.KubernetesClientSet() + if err != nil { + return err + } + p.PodClient = clientset.CoreV1() + + return nil +} + +// Validate checks that the provided exec options are specified. +func (p *ExecOptions) Validate() error { + if len(p.PodName) == 0 && len(p.ResourceName) == 0 && len(p.FilenameOptions.Filenames) == 0 { + return fmt.Errorf("pod, type/name or --filename must be specified") + } + if len(p.Command) == 0 { + return fmt.Errorf("you must specify at least one command for the container") + } + if p.Out == nil || p.ErrOut == nil { + return fmt.Errorf("both output and error output must be provided") + } + return nil +} + +func (o *StreamOptions) SetupTTY() term.TTY { + t := term.TTY{ + Parent: o.InterruptParent, + Out: o.Out, + } + + if !o.Stdin { + // need to nil out o.In to make sure we don't create a stream for stdin + o.In = nil + o.TTY = false + return t + } + + t.In = o.In + if !o.TTY { + return t + } + + if o.isTerminalIn == nil { + o.isTerminalIn = func(tty term.TTY) bool { + return tty.IsTerminalIn() + } + } + if !o.isTerminalIn(t) { + o.TTY = false + + if !o.Quiet && o.ErrOut != nil { + fmt.Fprintln(o.ErrOut, "Unable to use a TTY - input is not a terminal or the right kind of file") + } + + return t + } + + // if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we + // can safely set t.Raw to true + t.Raw = true + + if o.overrideStreams == nil { + // use dockerterm.StdStreams() to get the right I/O handles on Windows + o.overrideStreams = dockerterm.StdStreams + } + stdin, stdout, _ := o.overrideStreams() + o.In = stdin + t.In = stdin + if o.Out != nil { + o.Out = stdout + t.Out = stdout + } + + return t +} + +// Run executes a validated remote execution against a pod. +func (p *ExecOptions) Run() error { + var err error + // we still need legacy pod getter when PodName in ExecOptions struct is provided, + // since there are any other command run this function by providing Podname with PodsGetter + // and without resource builder, eg: `kubectl cp`. + if len(p.PodName) != 0 { + p.Pod, err = p.PodClient.Pods(p.Namespace).Get(context.TODO(), p.PodName, metav1.GetOptions{}) + if err != nil { + return err + } + } else { + builder := p.Builder(). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + FilenameParam(p.EnforceNamespace, &p.FilenameOptions). + NamespaceParam(p.Namespace).DefaultNamespace() + if len(p.ResourceName) > 0 { + builder = builder.ResourceNames("pods", p.ResourceName) + } + + obj, err := builder.Do().Object() + if err != nil { + return err + } + + p.Pod, err = p.ExecutablePodFn(p.restClientGetter, obj, p.GetPodTimeout) + if err != nil { + return err + } + } + + pod := p.Pod + + if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { + return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase) + } + + containerName := p.ContainerName + if len(containerName) == 0 { + container, err := podcmd.FindOrDefaultContainerByName(pod, containerName, p.Quiet, p.ErrOut) + if err != nil { + return err + } + containerName = container.Name + } + + // ensure we can recover the terminal while attached + t := p.SetupTTY() + + var sizeQueue remotecommand.TerminalSizeQueue + if t.Raw { + // this call spawns a goroutine to monitor/update the terminal size + sizeQueue = t.MonitorSize(t.GetSize()) + + // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is + // true + p.ErrOut = nil + } + + fn := func() error { + restClient, err := restclient.RESTClientFor(p.Config) + if err != nil { + return err + } + + // TODO: consider abstracting into a client invocation or client helper + req := restClient.Post(). + Resource("pods"). + Name(pod.Name). + Namespace(pod.Namespace). + SubResource("exec") + req.VersionedParams(&corev1.PodExecOptions{ + Container: containerName, + Command: p.Command, + Stdin: p.Stdin, + Stdout: p.Out != nil, + Stderr: p.ErrOut != nil, + TTY: t.Raw, + }, scheme.ParameterCodec) + + return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue) + } + + if err := t.Safe(fn); err != nil { + return err + } + + return nil +} diff --git a/vendor/k8s.io/kubectl/pkg/util/completion/completion.go b/vendor/k8s.io/kubectl/pkg/util/completion/completion.go new file mode 100644 index 000000000..6110b50a0 --- /dev/null +++ b/vendor/k8s.io/kubectl/pkg/util/completion/completion.go @@ -0,0 +1,453 @@ +/* +Copyright 2021 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 completion + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/kubectl/pkg/cmd/apiresources" + "k8s.io/kubectl/pkg/cmd/get" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/polymorphichelpers" + "k8s.io/kubectl/pkg/scheme" +) + +var factory cmdutil.Factory + +// SetFactoryForCompletion Store the factory which is needed by the completion functions. +// Not all commands have access to the factory, so cannot pass it to the completion functions. +func SetFactoryForCompletion(f cmdutil.Factory) { + factory = f +} + +// ResourceTypeAndNameCompletionFunc Returns a completion function that completes resource types +// and resource names that match the toComplete prefix. It supports the / form. +func ResourceTypeAndNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return resourceTypeAndNameCompletionFunc(f, nil, true) +} + +// SpecifiedResourceTypeAndNameCompletionFunc Returns a completion function that completes resource +// types limited to the specified allowedTypes, and resource names that match the toComplete prefix. +// It allows for multiple resources. It supports the / form. +func SpecifiedResourceTypeAndNameCompletionFunc(f cmdutil.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return resourceTypeAndNameCompletionFunc(f, allowedTypes, true) +} + +// SpecifiedResourceTypeAndNameNoRepeatCompletionFunc Returns a completion function that completes resource +// types limited to the specified allowedTypes, and resource names that match the toComplete prefix. +// It only allows for one resource. It supports the / form. +func SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(f cmdutil.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return resourceTypeAndNameCompletionFunc(f, allowedTypes, false) +} + +// ResourceNameCompletionFunc Returns a completion function that completes as a first argument +// the resource names specified by the resourceType parameter, and which match the toComplete prefix. +// This function does NOT support the / form: it is meant to be used by commands +// that don't support that form. For commands that apply to pods and that support the / +// form, please use PodResourceNameCompletionFunc() +func ResourceNameCompletionFunc(f cmdutil.Factory, resourceType string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + if len(args) == 0 { + comps = CompGetResource(f, cmd, resourceType, toComplete) + } + return comps, cobra.ShellCompDirectiveNoFileComp + } +} + +// PodResourceNameCompletionFunc Returns a completion function that completes: +// 1- pod names that match the toComplete prefix +// 2- resource types containing pods which match the toComplete prefix +func PodResourceNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + if len(args) == 0 { + comps, directive = doPodResourceCompletion(f, cmd, toComplete) + } + return comps, directive + } +} + +// PodResourceNameAndContainerCompletionFunc Returns a completion function that completes, as a first argument: +// 1- pod names that match the toComplete prefix +// 2- resource types containing pods which match the toComplete prefix +// and as a second argument the containers within the specified pod. +func PodResourceNameAndContainerCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + if len(args) == 0 { + comps, directive = doPodResourceCompletion(f, cmd, toComplete) + } else if len(args) == 1 { + podName := convertResourceNameToPodName(f, args[0]) + comps = CompGetContainers(f, cmd, podName, toComplete) + } + return comps, directive + } +} + +// ContainerCompletionFunc Returns a completion function that completes the containers within the +// pod specified by the first argument. The resource containing the pod can be specified in +// the / form. +func ContainerCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + // We need the pod name to be able to complete the container names, it must be in args[0]. + // That first argument can also be of the form / so we need to convert it. + if len(args) > 0 { + podName := convertResourceNameToPodName(f, args[0]) + comps = CompGetContainers(f, cmd, podName, toComplete) + } + return comps, cobra.ShellCompDirectiveNoFileComp + } +} + +// ContextCompletionFunc is a completion function that completes as a first argument the +// context names that match the toComplete prefix +func ContextCompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + +// ClusterCompletionFunc is a completion function that completes as a first argument the +// cluster names that match the toComplete prefix +func ClusterCompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + +// UserCompletionFunc is a completion function that completes as a first argument the +// user names that match the toComplete prefix +func UserCompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + +// CompGetResource gets the list of the resource specified which begin with `toComplete`. +func CompGetResource(f cmdutil.Factory, cmd *cobra.Command, resourceName string, toComplete string) []string { + template := "{{ range .items }}{{ .metadata.name }} {{ end }}" + return CompGetFromTemplate(&template, f, "", cmd, []string{resourceName}, toComplete) +} + +// CompGetContainers gets the list of containers of the specified pod which begin with `toComplete`. +func CompGetContainers(f cmdutil.Factory, cmd *cobra.Command, podName string, toComplete string) []string { + template := "{{ range .spec.initContainers }}{{ .name }} {{end}}{{ range .spec.containers }}{{ .name }} {{ end }}" + return CompGetFromTemplate(&template, f, "", cmd, []string{"pod", podName}, toComplete) +} + +// CompGetFromTemplate executes a Get operation using the specified template and args and returns the results +// which begin with `toComplete`. +func CompGetFromTemplate(template *string, f cmdutil.Factory, namespace string, cmd *cobra.Command, args []string, toComplete string) []string { + buf := new(bytes.Buffer) + streams := genericclioptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: ioutil.Discard} + o := get.NewGetOptions("kubectl", streams) + + // Get the list of names of the specified resource + o.PrintFlags.TemplateFlags.GoTemplatePrintFlags.TemplateArgument = template + format := "go-template" + o.PrintFlags.OutputFormat = &format + + // Do the steps Complete() would have done. + // We cannot actually call Complete() or Validate() as these function check for + // the presence of flags, which, in our case won't be there + if namespace != "" { + o.Namespace = namespace + o.ExplicitNamespace = true + } else { + var err error + o.Namespace, o.ExplicitNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + } + + o.ToPrinter = func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil, err + } + return printer.PrintObj, nil + } + + o.Run(f, cmd, args) + + var comps []string + resources := strings.Split(buf.String(), " ") + for _, res := range resources { + if res != "" && strings.HasPrefix(res, toComplete) { + comps = append(comps, res) + } + } + return comps +} + +// ListContextsInConfig returns a list of context names which begin with `toComplete` +func ListContextsInConfig(toComplete string) []string { + config, err := factory.ToRawKubeConfigLoader().RawConfig() + if err != nil { + return nil + } + var ret []string + for name := range config.Contexts { + if strings.HasPrefix(name, toComplete) { + ret = append(ret, name) + } + } + return ret +} + +// ListClustersInConfig returns a list of cluster names which begin with `toComplete` +func ListClustersInConfig(toComplete string) []string { + config, err := factory.ToRawKubeConfigLoader().RawConfig() + if err != nil { + return nil + } + var ret []string + for name := range config.Clusters { + if strings.HasPrefix(name, toComplete) { + ret = append(ret, name) + } + } + return ret +} + +// ListUsersInConfig returns a list of user names which begin with `toComplete` +func ListUsersInConfig(toComplete string) []string { + config, err := factory.ToRawKubeConfigLoader().RawConfig() + if err != nil { + return nil + } + var ret []string + for name := range config.AuthInfos { + if strings.HasPrefix(name, toComplete) { + ret = append(ret, name) + } + } + return ret +} + +// compGetResourceList returns the list of api resources which begin with `toComplete`. +func compGetResourceList(f cmdutil.Factory, cmd *cobra.Command, toComplete string) []string { + buf := new(bytes.Buffer) + streams := genericclioptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: ioutil.Discard} + o := apiresources.NewAPIResourceOptions(streams) + + // Get the list of resources + o.Output = "name" + o.Cached = true + o.Verbs = []string{"get"} + // TODO:Should set --request-timeout=5s + + // Ignore errors as the output may still be valid + o.RunAPIResources(cmd, f) + + // Resources can be a comma-separated list. The last element is then + // the one we should complete. For example if toComplete=="pods,secre" + // we should return "pods,secrets" + prefix := "" + suffix := toComplete + lastIdx := strings.LastIndex(toComplete, ",") + if lastIdx != -1 { + prefix = toComplete[0 : lastIdx+1] + suffix = toComplete[lastIdx+1:] + } + var comps []string + resources := strings.Split(buf.String(), "\n") + for _, res := range resources { + if res != "" && strings.HasPrefix(res, suffix) { + comps = append(comps, fmt.Sprintf("%s%s", prefix, res)) + } + } + return comps +} + +// resourceTypeAndNameCompletionFunc Returns a completion function that completes resource types +// and resource names that match the toComplete prefix. It supports the / form. +func resourceTypeAndNameCompletionFunc(f cmdutil.Factory, allowedTypes []string, allowRepeat bool) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + + if len(args) > 0 && !strings.Contains(args[0], "/") { + // The first argument is of the form (e.g., pods) + // All following arguments should be a resource name. + if allowRepeat || len(args) == 1 { + comps = CompGetResource(f, cmd, args[0], toComplete) + + // Remove choices already on the command-line + if len(args) > 1 { + comps = cmdutil.Difference(comps, args[1:]) + } + } + } else { + slashIdx := strings.Index(toComplete, "/") + if slashIdx == -1 { + if len(args) == 0 { + // We are completing the first argument. We default to the normal + // form (not the form /). + // So we suggest resource types and let the shell add a space after + // the completion. + if len(allowedTypes) == 0 { + comps = compGetResourceList(f, cmd, toComplete) + } else { + for _, c := range allowedTypes { + if strings.HasPrefix(c, toComplete) { + comps = append(comps, c) + } + } + } + } else { + // Here we know the first argument contains a / (/). + // All other arguments must also use that form. + if allowRepeat { + // Since toComplete does not already contain a / we know we are completing a + // resource type. Disable adding a space after the completion, and add the / + directive |= cobra.ShellCompDirectiveNoSpace + + if len(allowedTypes) == 0 { + typeComps := compGetResourceList(f, cmd, toComplete) + for _, c := range typeComps { + comps = append(comps, fmt.Sprintf("%s/", c)) + } + } else { + for _, c := range allowedTypes { + if strings.HasPrefix(c, toComplete) { + comps = append(comps, fmt.Sprintf("%s/", c)) + } + } + } + } + } + } else { + // We are completing an argument of the form / + // and since the / is already present, we are completing the resource name. + if allowRepeat || len(args) == 0 { + resourceType := toComplete[:slashIdx] + toComplete = toComplete[slashIdx+1:] + nameComps := CompGetResource(f, cmd, resourceType, toComplete) + for _, c := range nameComps { + comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c)) + } + + // Remove choices already on the command-line. + if len(args) > 0 { + comps = cmdutil.Difference(comps, args[0:]) + } + } + } + } + return comps, directive + } +} + +// doPodResourceCompletion Returns completions of: +// 1- pod names that match the toComplete prefix +// 2- resource types containing pods which match the toComplete prefix +func doPodResourceCompletion(f cmdutil.Factory, cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + slashIdx := strings.Index(toComplete, "/") + if slashIdx == -1 { + // Standard case, complete pod names + comps = CompGetResource(f, cmd, "pod", toComplete) + + // Also include resource choices for the / form, + // but only for resources that contain pods + resourcesWithPods := []string{ + "daemonsets", + "deployments", + "pods", + "jobs", + "replicasets", + "replicationcontrollers", + "services", + "statefulsets"} + + if len(comps) == 0 { + // If there are no pods to complete, we will only be completing + // /. We should disable adding a space after the /. + directive |= cobra.ShellCompDirectiveNoSpace + } + + for _, resource := range resourcesWithPods { + if strings.HasPrefix(resource, toComplete) { + comps = append(comps, fmt.Sprintf("%s/", resource)) + } + } + } else { + // Dealing with the / form, use the specified resource type + resourceType := toComplete[:slashIdx] + toComplete = toComplete[slashIdx+1:] + nameComps := CompGetResource(f, cmd, resourceType, toComplete) + for _, c := range nameComps { + comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c)) + } + } + return comps, directive +} + +// convertResourceNameToPodName Converts a resource name to a pod name. +// If the resource name is of the form /, we use +// polymorphichelpers.AttachablePodForObjectFn(), if not, the resource name +// is already a pod name. +func convertResourceNameToPodName(f cmdutil.Factory, resourceName string) string { + var podName string + if !strings.Contains(resourceName, "/") { + // When we don't have the / form, the resource name is the pod name + podName = resourceName + } else { + // if the resource name is of the form /, we need to convert it to a pod name + ns, _, err := f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return "" + } + + resourceWithPod, err := f.NewBuilder(). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + ContinueOnError(). + NamespaceParam(ns).DefaultNamespace(). + ResourceNames("pods", resourceName). + Do().Object() + if err != nil { + return "" + } + + // For shell completion, use a short timeout + forwardablePod, err := polymorphichelpers.AttachablePodForObjectFn(f, resourceWithPod, 100*time.Millisecond) + if err != nil { + return "" + } + podName = forwardablePod.Name + } + return podName +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e9b3e2bd8..0c49be90b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1407,6 +1407,8 @@ k8s.io/kube-openapi/pkg/validation/spec # k8s.io/kubectl v0.24.2 ## explicit; go 1.16 k8s.io/kubectl/pkg/apps +k8s.io/kubectl/pkg/cmd/apiresources +k8s.io/kubectl/pkg/cmd/exec k8s.io/kubectl/pkg/cmd/get k8s.io/kubectl/pkg/cmd/util k8s.io/kubectl/pkg/cmd/util/podcmd @@ -1415,6 +1417,7 @@ k8s.io/kubectl/pkg/polymorphichelpers k8s.io/kubectl/pkg/rawhttp k8s.io/kubectl/pkg/scheme k8s.io/kubectl/pkg/util/certificate +k8s.io/kubectl/pkg/util/completion k8s.io/kubectl/pkg/util/deployment k8s.io/kubectl/pkg/util/event k8s.io/kubectl/pkg/util/fieldpath