Merge pull request #2092 from carlory/karmadactl-exec
use kubectl exec to implement karmadactl exec and introduce the `--namespace` flag
This commit is contained in:
commit
d2bd9d9823
2
go.mod
2
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 <type>/<name> 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 <type>/<name> 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 <type>/<name> 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 <type>/<name> 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 <type>/<name>
|
||||
// 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 <type>/<name> 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 <type>/<name> 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 <type>/<name> 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 <type> (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
|
||||
// <type> form (not the form <type>/<name>).
|
||||
// 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 / (<type>/<name>).
|
||||
// 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 <type>/<name>
|
||||
// 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 <type>/<name> 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
|
||||
// <type>/. 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 <type>/<name> 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 <type>/<name>, 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 <type>/<name> form, the resource name is the pod name
|
||||
podName = resourceName
|
||||
} else {
|
||||
// if the resource name is of the form <type>/<name>, 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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue