github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/exec/exec.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package exec
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  
    27  	corev1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/cli-runtime/pkg/genericiooptions"
    30  	"k8s.io/client-go/dynamic"
    31  	"k8s.io/client-go/kubernetes"
    32  	"k8s.io/client-go/kubernetes/scheme"
    33  	restclient "k8s.io/client-go/rest"
    34  	"k8s.io/client-go/tools/remotecommand"
    35  	cmdexec "k8s.io/kubectl/pkg/cmd/exec"
    36  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    37  	"k8s.io/kubectl/pkg/cmd/util/podcmd"
    38  )
    39  
    40  type ExecOptions struct {
    41  	cmdexec.StreamOptions
    42  
    43  	Factory  cmdutil.Factory
    44  	Executor cmdexec.RemoteExecutor
    45  	Config   *restclient.Config
    46  	Client   *kubernetes.Clientset
    47  	Dynamic  dynamic.Interface
    48  
    49  	// Pod target pod to execute command
    50  	Pod *corev1.Pod
    51  
    52  	// Command is the command to execute
    53  	Command []string
    54  }
    55  
    56  func NewExecOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *ExecOptions {
    57  	return &ExecOptions{
    58  		Factory: f,
    59  		StreamOptions: cmdexec.StreamOptions{
    60  			IOStreams: streams,
    61  			Stdin:     true,
    62  			TTY:       true,
    63  		},
    64  		Executor: &cmdexec.DefaultRemoteExecutor{},
    65  	}
    66  }
    67  
    68  // Complete receives exec parameters
    69  func (o *ExecOptions) Complete() error {
    70  	var err error
    71  	o.Config, err = o.Factory.ToRESTConfig()
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	o.Namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace()
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	o.Dynamic, err = o.Factory.DynamicClient()
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	o.Client, err = o.Factory.KubernetesClientSet()
    87  	return err
    88  }
    89  
    90  func (o *ExecOptions) validate() error {
    91  	var err error
    92  
    93  	// pod is not set, try to get it by pod name
    94  	if o.Pod == nil && len(o.PodName) > 0 {
    95  		if o.Pod, err = o.Client.CoreV1().Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{}); err != nil {
    96  			return err
    97  		}
    98  	}
    99  
   100  	if o.Pod == nil {
   101  		return fmt.Errorf("failed to get the pod to execute")
   102  	}
   103  	if len(o.Command) == 0 {
   104  		return fmt.Errorf("you must specify at least one command for the container")
   105  	}
   106  	if o.Out == nil || o.ErrOut == nil {
   107  		return fmt.Errorf("both output and error output must be provided")
   108  	}
   109  
   110  	if o.Pod.Status.Phase == corev1.PodSucceeded ||
   111  		o.Pod.Status.Phase == corev1.PodFailed {
   112  		return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", o.Pod.Status.Phase)
   113  	}
   114  
   115  	// check and get the container to execute command
   116  	if len(o.ContainerName) == 0 {
   117  		container, err := podcmd.FindOrDefaultContainerByName(o.Pod, "", o.Quiet, o.ErrOut)
   118  		if err != nil {
   119  			return err
   120  		}
   121  		o.ContainerName = container.Name
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  func (o *ExecOptions) Run() error {
   128  	return o.RunWithRedirect(o.Out, o.ErrOut)
   129  }
   130  
   131  func (o *ExecOptions) RunWithRedirect(outWriter io.Writer, errWriter io.Writer) error {
   132  	if err := o.validate(); err != nil {
   133  		return err
   134  	}
   135  
   136  	// ensure we can recover the terminal while attached
   137  	t := o.SetupTTY()
   138  
   139  	var sizeQueue remotecommand.TerminalSizeQueue
   140  	if t.Raw {
   141  		// this call spawns a goroutine to monitor/update the terminal size
   142  		sizeQueue = t.MonitorSize(t.GetSize())
   143  
   144  		// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
   145  		// true
   146  		o.ErrOut = nil
   147  	}
   148  
   149  	fn := func() error {
   150  		restClient, err := restclient.RESTClientFor(o.Config)
   151  		if err != nil {
   152  			return err
   153  		}
   154  
   155  		req := restClient.Post().
   156  			Resource("pods").
   157  			Name(o.Pod.Name).
   158  			Namespace(o.Pod.Namespace).
   159  			SubResource("exec")
   160  		req.VersionedParams(&corev1.PodExecOptions{
   161  			Container: o.ContainerName,
   162  			Command:   o.Command,
   163  			Stdin:     o.Stdin,
   164  			Stdout:    outWriter != nil,
   165  			Stderr:    errWriter != nil,
   166  			TTY:       t.Raw,
   167  		}, scheme.ParameterCodec)
   168  
   169  		return o.Executor.Execute("POST", req.URL(), o.Config, o.In, outWriter, errWriter, t.Raw, sizeQueue)
   170  	}
   171  
   172  	if err := t.Safe(fn); err != nil {
   173  		return err
   174  	}
   175  	return nil
   176  }