github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/debug/connect.go (about)

     1  // Copyright © 2021 Alibaba Group Holding Ltd.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package debug
    16  
    17  import (
    18  	"fmt"
    19  	"net/url"
    20  
    21  	corev1 "k8s.io/api/core/v1"
    22  	"k8s.io/cli-runtime/pkg/genericclioptions"
    23  	"k8s.io/client-go/kubernetes/scheme"
    24  	restclient "k8s.io/client-go/rest"
    25  	"k8s.io/client-go/tools/remotecommand"
    26  )
    27  
    28  // Connector holds the options to connect a running container.
    29  type Connector struct {
    30  	NameSpace     string
    31  	ContainerName string
    32  	Command       []string
    33  	Stdin         bool
    34  	TTY           bool
    35  	genericclioptions.IOStreams
    36  
    37  	Motd string
    38  
    39  	Pod    *corev1.Pod
    40  	Config *restclient.Config
    41  }
    42  
    43  // Connect connects to a running container.
    44  func (connector *Connector) Connect() error {
    45  	container, err := connector.ContainerToConnect()
    46  	if err != nil {
    47  		return err
    48  	}
    49  
    50  	if connector.TTY && !container.TTY {
    51  		connector.TTY = false
    52  	}
    53  
    54  	// set the TTY
    55  	t := connector.SetTTY()
    56  
    57  	// get the terminal size queue
    58  	var sizeQueue remotecommand.TerminalSizeQueue
    59  	if t.Raw {
    60  		// this spawns a goroutine to monitor/update the terminal size
    61  		if size := t.GetSize(); size != nil {
    62  			sizePlusOne := *size
    63  			sizePlusOne.Width++
    64  			sizePlusOne.Height++
    65  
    66  			sizeQueue = t.MonitorSize(&sizePlusOne, size)
    67  		}
    68  
    69  		showMotd(connector.Out, connector.Motd)
    70  	}
    71  
    72  	if len(connector.Command) == 0 {
    73  		if err := t.Safe(connector.GetDefaultAttachFunc(container, sizeQueue)); err != nil {
    74  			return err
    75  		}
    76  	} else {
    77  		if err := t.Safe(connector.GetDefaultExecFunc(container, sizeQueue)); err != nil {
    78  			return err
    79  		}
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  // ContainerToConnect checks if there is a container to attach, and if exists returns the container object to attach.
    86  func (connector *Connector) ContainerToConnect() (*corev1.Container, error) {
    87  	pod := connector.Pod
    88  
    89  	if len(connector.ContainerName) > 0 {
    90  		for i := range pod.Spec.Containers {
    91  			if pod.Spec.Containers[i].Name == connector.ContainerName {
    92  				return &pod.Spec.Containers[i], nil
    93  			}
    94  		}
    95  
    96  		for i := range pod.Spec.InitContainers {
    97  			if pod.Spec.InitContainers[i].Name == connector.ContainerName {
    98  				return &pod.Spec.InitContainers[i], nil
    99  			}
   100  		}
   101  
   102  		for i := range pod.Spec.EphemeralContainers {
   103  			if pod.Spec.EphemeralContainers[i].Name == connector.ContainerName {
   104  				return (*corev1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon), nil
   105  			}
   106  		}
   107  
   108  		return nil, fmt.Errorf("there is no container named %s", connector.ContainerName)
   109  	}
   110  
   111  	return &pod.Spec.Containers[0], nil
   112  }
   113  
   114  // SetTTY handles the stdin and tty with following:
   115  //  1. stdin false, tty false 	--- stdout
   116  //  2. stdin false, tty true 	--- stdout
   117  //  3. stdin true, tty false 	--- stdin、stdout
   118  //  4. stdin true, tty true 	--- stdin、stdout、tty	--- t.Raw
   119  //
   120  // then returns a TTY object based on connectOpts.
   121  
   122  func (connector *Connector) SetTTY() TTY {
   123  	t := TTY{
   124  		Out: connector.Out,
   125  	}
   126  
   127  	// Stdin is false, then tty and stdin both false
   128  	if !connector.Stdin {
   129  		connector.In = nil
   130  		connector.TTY = false
   131  		return t
   132  	}
   133  
   134  	t.In = connector.In
   135  	if !connector.TTY {
   136  		return t
   137  	}
   138  
   139  	// check whether t.In is a terminal
   140  	if !t.IsTerminalIn() {
   141  		connector.TTY = false
   142  		return t
   143  	}
   144  
   145  	t.Raw = true
   146  
   147  	return t
   148  }
   149  
   150  // GetDefaultAttachFunc returns the default attach function.
   151  func (connector *Connector) GetDefaultAttachFunc(containerToAttach *corev1.Container, sizeQueue remotecommand.TerminalSizeQueue) func() error {
   152  	return func() error {
   153  		restClient, err := restclient.RESTClientFor(connector.Config)
   154  		if err != nil {
   155  			return err
   156  		}
   157  
   158  		req := restClient.Post().
   159  			Resource("pods").
   160  			Name(connector.Pod.Name).
   161  			Namespace(connector.Pod.Namespace).
   162  			SubResource("attach")
   163  		req.VersionedParams(&corev1.PodAttachOptions{
   164  			Container: containerToAttach.Name,
   165  			Stdin:     connector.Stdin,
   166  			Stdout:    connector.Out != nil,
   167  			Stderr:    connector.ErrOut != nil,
   168  			TTY:       connector.TTY,
   169  		}, scheme.ParameterCodec)
   170  
   171  		return connector.DoConnect("POST", req.URL(), sizeQueue)
   172  	}
   173  }
   174  
   175  // GetDefaultExecFunc returns the default exec function.
   176  func (connector *Connector) GetDefaultExecFunc(containerToAttach *corev1.Container, sizeQueue remotecommand.TerminalSizeQueue) func() error {
   177  	return func() error {
   178  		restClient, err := restclient.RESTClientFor(connector.Config)
   179  		if err != nil {
   180  			return err
   181  		}
   182  
   183  		req := restClient.Post().
   184  			Resource("pods").
   185  			Name(connector.Pod.Name).
   186  			Namespace(connector.Pod.Namespace).
   187  			SubResource("exec")
   188  		req.VersionedParams(&corev1.PodExecOptions{
   189  			Container: containerToAttach.Name,
   190  			Command:   connector.Command,
   191  			Stdin:     connector.Stdin,
   192  			Stdout:    connector.Out != nil,
   193  			Stderr:    connector.ErrOut != nil,
   194  			TTY:       connector.TTY,
   195  		}, scheme.ParameterCodec)
   196  
   197  		return connector.DoConnect("POST", req.URL(), sizeQueue)
   198  	}
   199  }
   200  
   201  // DoConnect executes attach to a running container with url.
   202  func (connector *Connector) DoConnect(method string, url *url.URL, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
   203  	exec, err := remotecommand.NewSPDYExecutor(connector.Config, method, url)
   204  	if err != nil {
   205  		return err
   206  	}
   207  
   208  	return exec.Stream(remotecommand.StreamOptions{
   209  		Stdin:             connector.In,
   210  		Stdout:            connector.Out,
   211  		Stderr:            connector.ErrOut,
   212  		Tty:               connector.TTY,
   213  		TerminalSizeQueue: terminalSizeQueue,
   214  	})
   215  }