2019-01-12 04:58:27 +00:00
/ *
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 (
2020-03-26 21:07:15 +00:00
"context"
2019-01-12 04:58:27 +00:00
"fmt"
"io"
"net/url"
2019-08-30 18:33:25 +00:00
"time"
2019-01-12 04:58:27 +00:00
dockerterm "github.com/docker/docker/pkg/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"
2019-08-30 18:33:25 +00:00
"k8s.io/cli-runtime/pkg/resource"
2019-01-12 04:58:27 +00:00
coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
2019-08-30 18:33:25 +00:00
2019-09-27 21:51:53 +00:00
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/kubectl/pkg/util/term"
2019-01-12 04:58:27 +00:00
)
var (
2019-04-07 17:07:55 +00:00
execExample = templates . Examples ( i18n . T ( `
2019-08-30 18:33:25 +00:00
# Get output from running ' date ' command from pod mypod , using the first container by default
2020-03-26 21:07:15 +00:00
kubectl exec mypod -- date
2019-01-12 04:58:27 +00:00
2019-08-30 18:33:25 +00:00
# Get output from running ' date ' command in ruby - container from pod mypod
2020-03-26 21:07:15 +00:00
kubectl exec mypod - c ruby - container -- date
2019-01-12 04:58:27 +00:00
2019-08-30 18:33:25 +00:00
# Switch to raw terminal mode , sends stdin to ' bash ' in ruby - container from pod mypod
2019-01-12 04:58:27 +00:00
# and sends stdout / stderr from ' bash ' back to the client
2019-08-30 18:33:25 +00:00
kubectl exec mypod - c ruby - container - i - t -- bash - il
2019-01-12 04:58:27 +00:00
2019-08-30 18:33:25 +00:00
# List contents of / usr from the first container of pod mypod and sort by modification time .
2019-01-12 04:58:27 +00:00
# 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" ) .
2019-08-30 18:33:25 +00:00
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
2020-03-26 21:07:15 +00:00
kubectl exec deploy / mydeployment -- date
2019-08-30 18:33:25 +00:00
# Get output from running ' date ' command from the first pod of the service myservice , using the first container by default
2020-03-26 21:07:15 +00:00
kubectl exec svc / myservice -- date
2019-01-12 04:58:27 +00:00
` ) )
)
const (
2019-08-30 18:33:25 +00:00
defaultPodExecTimeout = 60 * time . Second
2019-01-12 04:58:27 +00:00
)
func NewCmdExec ( f cmdutil . Factory , streams genericclioptions . IOStreams ) * cobra . Command {
options := & ExecOptions {
StreamOptions : StreamOptions {
IOStreams : streams ,
} ,
Executor : & DefaultRemoteExecutor { } ,
}
cmd := & cobra . Command {
2019-08-30 18:33:25 +00:00
Use : "exec (POD | TYPE/NAME) [-c CONTAINER] [flags] -- COMMAND [args...]" ,
2019-01-12 04:58:27 +00:00
DisableFlagsInUseLine : true ,
Short : i18n . T ( "Execute a command in a container" ) ,
Long : "Execute a command in a container." ,
2019-04-07 17:07:55 +00:00
Example : execExample ,
2019-01-12 04:58:27 +00:00
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 ( ) )
} ,
}
2019-08-30 18:33:25 +00:00
cmdutil . AddPodRunningTimeoutFlag ( cmd , defaultPodExecTimeout )
2020-03-26 21:07:15 +00:00
cmdutil . AddJsonFilenameFlag ( cmd . Flags ( ) , & options . FilenameOptions . Filenames , "to use to exec into the resource" )
2019-01-12 04:58:27 +00:00
// TODO support UID
cmd . Flags ( ) . StringVarP ( & options . ContainerName , "container" , "c" , options . ContainerName , "Container name. If omitted, the first container in the pod will be chosen" )
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" )
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
2020-03-26 21:07:15 +00:00
resource . FilenameOptions
2019-01-12 04:58:27 +00:00
2020-03-26 21:07:15 +00:00
ResourceName string
Command [ ] string
EnforceNamespace bool
2019-01-12 04:58:27 +00:00
2019-08-30 18:33:25 +00:00
ParentCommandName string
EnableSuggestedCmdUsage bool
2019-01-12 04:58:27 +00:00
2019-08-30 18:33:25 +00:00
Builder func ( ) * resource . Builder
ExecutablePodFn polymorphichelpers . AttachablePodForObjectFunc
restClientGetter genericclioptions . RESTClientGetter
Pod * corev1 . Pod
Executor RemoteExecutor
PodClient coreclient . PodsGetter
GetPodTimeout time . Duration
Config * restclient . Config
2019-01-12 04:58:27 +00:00
}
// 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 {
2020-03-26 21:07:15 +00:00
if len ( argsIn ) > 0 && argsLenAtDash != 0 {
p . ResourceName = argsIn [ 0 ]
}
if argsLenAtDash > - 1 {
p . Command = argsIn [ argsLenAtDash : ]
} else if len ( argsIn ) > 1 {
fmt . Fprint ( p . ErrOut , "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl kubectl exec [POD] -- [COMMAND] instead.\n" )
p . Command = argsIn [ 1 : ]
} else if len ( argsIn ) > 0 && len ( p . FilenameOptions . Filenames ) != 0 {
fmt . Fprint ( p . ErrOut , "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl kubectl exec [POD] -- [COMMAND] instead.\n" )
p . Command = argsIn [ 0 : ]
p . ResourceName = ""
2019-01-12 04:58:27 +00:00
}
2019-08-30 18:33:25 +00:00
var err error
2020-03-26 21:07:15 +00:00
p . Namespace , p . EnforceNamespace , err = f . ToRawKubeConfigLoader ( ) . Namespace ( )
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
2019-08-30 18:33:25 +00:00
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
2019-01-12 04:58:27 +00:00
cmdParent := cmd . Parent ( )
if cmdParent != nil {
2019-08-30 18:33:25 +00:00
p . ParentCommandName = cmdParent . CommandPath ( )
2019-01-12 04:58:27 +00:00
}
2019-08-30 18:33:25 +00:00
if len ( p . ParentCommandName ) > 0 && cmdutil . IsSiblingCommandExists ( cmd , "describe" ) {
p . EnableSuggestedCmdUsage = true
2019-01-12 04:58:27 +00:00
}
2019-08-30 18:33:25 +00:00
p . Config , err = f . ToRESTConfig ( )
2019-01-12 04:58:27 +00:00
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 {
2020-03-26 21:07:15 +00:00
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" )
2019-01-12 04:58:27 +00:00
}
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 . 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 {
2019-08-30 18:33:25 +00:00
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 {
2020-03-26 21:07:15 +00:00
p . Pod , err = p . PodClient . Pods ( p . Namespace ) . Get ( context . TODO ( ) , p . PodName , metav1 . GetOptions { } )
2019-08-30 18:33:25 +00:00
if err != nil {
return err
}
} else {
builder := p . Builder ( ) .
WithScheme ( scheme . Scheme , scheme . Scheme . PrioritizedVersionsAllGroups ( ) ... ) .
2020-03-26 21:07:15 +00:00
FilenameParam ( p . EnforceNamespace , & p . FilenameOptions ) .
NamespaceParam ( p . Namespace ) . DefaultNamespace ( )
if len ( p . ResourceName ) > 0 {
builder = builder . ResourceNames ( "pods" , p . ResourceName )
}
2019-08-30 18:33:25 +00:00
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
}
2019-01-12 04:58:27 +00:00
}
2019-08-30 18:33:25 +00:00
pod := p . Pod
2019-01-12 04:58:27 +00:00
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 {
if len ( pod . Spec . Containers ) > 1 {
2019-08-30 18:33:25 +00:00
fmt . Fprintf ( p . ErrOut , "Defaulting container name to %s.\n" , pod . Spec . Containers [ 0 ] . Name )
if p . EnableSuggestedCmdUsage {
fmt . Fprintf ( p . ErrOut , "Use '%s describe pod/%s -n %s' to see all of the containers in this pod.\n" , p . ParentCommandName , pod . Name , p . Namespace )
2019-01-12 04:58:27 +00:00
}
}
containerName = pod . Spec . Containers [ 0 ] . 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 ) .
2019-04-07 17:07:55 +00:00
SubResource ( "exec" )
2019-01-12 04:58:27 +00:00
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
}