github.com/Mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/tools/kubeclient.go (about)

     1  /*
     2  Copyright 2018 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tools
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/url"
    26  	"os"
    27  	"os/signal"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  
    32  	v1 "k8s.io/api/core/v1"
    33  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/client-go/kubernetes"
    35  	"k8s.io/client-go/kubernetes/scheme"
    36  	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // GKE support
    37  	"k8s.io/client-go/rest"
    38  	"k8s.io/client-go/tools/clientcmd"
    39  	"k8s.io/client-go/tools/portforward"
    40  	"k8s.io/client-go/tools/remotecommand"
    41  	"k8s.io/client-go/transport/spdy"
    42  	"k8s.io/client-go/util/exec"
    43  )
    44  
    45  const (
    46  	runtimeAnnotation = "kubernetes.io/target-runtime"
    47  )
    48  
    49  // VMPodInfo describes a VM pod in a way that's necessary for virtletctl to
    50  // handle it
    51  type VMPodInfo struct {
    52  	// NodeName is the name of the node where the VM pod runs
    53  	NodeName string
    54  	// VirtletPodName is the name of the virtlet pod that manages this VM pod
    55  	VirtletPodName string
    56  	// ContainerID is the id of the container in the VM pod
    57  	ContainerID string
    58  	// ContainerName is the name of the container in the VM pod
    59  	ContainerName string
    60  }
    61  
    62  // LibvirtDomainName returns the name of the libvirt domain for the VMPodInfo.
    63  func (podInfo VMPodInfo) LibvirtDomainName() string {
    64  	containerID := podInfo.ContainerID
    65  	if p := strings.Index(containerID, "__"); p >= 0 {
    66  		containerID = containerID[p+2:]
    67  	}
    68  	if len(containerID) > 13 {
    69  		containerID = containerID[:13]
    70  	}
    71  	return fmt.Sprintf("virtlet-%s-%s", containerID, podInfo.ContainerName)
    72  }
    73  
    74  // ForwardedPort specifies an entry for the PortForward request
    75  type ForwardedPort struct {
    76  	// LocalPort specifies the local port to use. 0 means selecting
    77  	// a random local port.
    78  	LocalPort uint16
    79  	// RemotePort specifies the remote (pod-side) port to use.
    80  	RemotePort uint16
    81  }
    82  
    83  func (fp ForwardedPort) String() string {
    84  	if fp.LocalPort == 0 {
    85  		return fmt.Sprintf(":%d", fp.RemotePort)
    86  	}
    87  	return fmt.Sprintf("%d:%d", fp.LocalPort, fp.RemotePort)
    88  }
    89  
    90  // NOTE: this regexp ignores ipv6 port forward lines
    91  var portForwardRx = regexp.MustCompile(`Forwarding from [^[]*:(\d+) -> \d+`)
    92  
    93  // ParsePortForwardOutput extracts from returned by api "out" data information
    94  // about local ports in each of ForwardedPort
    95  func ParsePortForwardOutput(out string, ports []*ForwardedPort) error {
    96  	var localPorts []uint16
    97  	for _, l := range strings.Split(out, "\n") {
    98  		m := portForwardRx.FindStringSubmatch(l)
    99  		if m == nil {
   100  			continue
   101  		}
   102  		port, err := strconv.ParseUint(m[1], 10, 16)
   103  		if err != nil {
   104  			return fmt.Errorf("bad port forward line (can't parse the local port): %q", l)
   105  		}
   106  		localPorts = append(localPorts, uint16(port))
   107  	}
   108  	if len(localPorts) != len(ports) {
   109  		return fmt.Errorf("bad port forward output (expected %d ports, got %d). Full output from the forwarder:\n%s", len(ports), len(localPorts), out)
   110  	}
   111  	for n, lp := range localPorts {
   112  		switch {
   113  		case ports[n].LocalPort == 0:
   114  			ports[n].LocalPort = lp
   115  			continue
   116  		case ports[n].LocalPort != lp:
   117  			return fmt.Errorf("port mismatch: %d instead of %d for the remote port %d. Full output from the forwarder:\n%s", lp, ports[n].LocalPort, ports[n].RemotePort, out)
   118  		}
   119  	}
   120  	return nil
   121  }
   122  
   123  // KubeClient contains methods for interfacing with Kubernetes clusters.
   124  type KubeClient interface {
   125  	// GetNamesOfNodesMarkedForVirtlet returns a list of node names for nodes labeled
   126  	// with virtlet as an extra runtime.
   127  	GetNamesOfNodesMarkedForVirtlet() (nodeNames []string, err error)
   128  	// GetVirtletPodAndNodeNames returns a list of names of the
   129  	// virtlet pods present in the cluster and a list of
   130  	// corresponding node names that contain these pods.
   131  	GetVirtletPodAndNodeNames() (podNames []string, nodeNames []string, err error)
   132  	// GetVirtletPodNameForNode returns a name of the virtlet pod on
   133  	// the specified k8s node.
   134  	GetVirtletPodNameForNode(nodeName string) (string, error)
   135  	// GetVMPodInfo returns then name of the virtlet pod and the vm container name for
   136  	// the specified VM pod.
   137  	GetVMPodInfo(podName string) (*VMPodInfo, error)
   138  	// CreatePod creates a pod.
   139  	CreatePod(pod *v1.Pod) (*v1.Pod, error)
   140  	// GetPod retrieves a pod definition from the apiserver.
   141  	GetPod(name, namespace string) (*v1.Pod, error)
   142  	// DeletePod removes the specified pod from the specified namespace.
   143  	DeletePod(pod, namespace string) error
   144  	// ExecInContainer given a pod, a container, a namespace and a command
   145  	// executes that command inside the pod's container returning stdout and stderr output
   146  	// as strings and an error if it has occurred.
   147  	// The specified stdin, stdout and stderr are used as the
   148  	// standard input / output / error streams of the remote command.
   149  	// No TTY is allocated by this function stdin.
   150  	ExecInContainer(podName, containerName, namespace string, stdin io.Reader, stdout, stderr io.Writer, command []string) (int, error)
   151  	// ForwardPorts starts forwarding the specified ports to the specified pod in background.
   152  	// If a port entry has LocalPort = 0, it's updated with the real port number that was
   153  	// selected by the forwarder.
   154  	// The function returns when the ports are ready for use or if/when an error occurs.
   155  	// Close stopCh to stop the port forwarder.
   156  	ForwardPorts(podName, namespace string, ports []*ForwardedPort) (stopCh chan struct{}, err error)
   157  	// Retrieves the logs for the specified pod. If tailLines is
   158  	// non-zero, it limits the numer of lines to be retrieved.
   159  	PodLogs(podName, containerName, namespace string, tailLines int64) ([]byte, error)
   160  }
   161  
   162  type remoteExecutor interface {
   163  	stream(config *rest.Config, method string, url *url.URL, options remotecommand.StreamOptions) error
   164  }
   165  
   166  type defaultExecutor struct{}
   167  
   168  var _ remoteExecutor = defaultExecutor{}
   169  
   170  func (e defaultExecutor) stream(config *rest.Config, method string, url *url.URL, options remotecommand.StreamOptions) error {
   171  	executor, err := remotecommand.NewSPDYExecutor(config, method, url)
   172  	if err != nil {
   173  		return err
   174  	}
   175  	return executor.Stream(options)
   176  }
   177  
   178  type portForwarder interface {
   179  	forwardPorts(config *rest.Config, method string, url *url.URL, ports []string, stopChannel, readyChannel chan struct{}, out io.Writer) error
   180  }
   181  
   182  type defaultPortForwarder struct{}
   183  
   184  var _ portForwarder = defaultPortForwarder{}
   185  
   186  func (pf defaultPortForwarder) forwardPorts(config *rest.Config, method string, url *url.URL, ports []string, stopChannel, readyChannel chan struct{}, out io.Writer) error {
   187  	transport, upgrader, err := spdy.RoundTripperFor(config)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	fw, err := portforward.New(dialer, ports, stopChannel, readyChannel, out, os.Stderr)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	return fw.ForwardPorts()
   200  }
   201  
   202  // RealKubeClient is used to access a Kubernetes cluster.
   203  type RealKubeClient struct {
   204  	client        kubernetes.Interface
   205  	clientCfg     clientcmd.ClientConfig
   206  	restClient    rest.Interface
   207  	config        *rest.Config
   208  	namespace     string
   209  	executor      remoteExecutor
   210  	portForwarder portForwarder
   211  }
   212  
   213  var _ KubeClient = &RealKubeClient{}
   214  
   215  // NewRealKubeClient creates a RealKubeClient for the specified ClientConfig.
   216  func NewRealKubeClient(clientCfg clientcmd.ClientConfig) *RealKubeClient {
   217  	return &RealKubeClient{
   218  		clientCfg:     clientCfg,
   219  		executor:      defaultExecutor{},
   220  		portForwarder: defaultPortForwarder{},
   221  	}
   222  }
   223  
   224  func (c *RealKubeClient) setup() error {
   225  	if c.client != nil {
   226  		return nil
   227  	}
   228  
   229  	config, err := c.clientCfg.ClientConfig()
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	client, err := kubernetes.NewForConfig(config)
   235  	if err != nil {
   236  		return fmt.Errorf("can't create kubernetes api client: %v", err)
   237  	}
   238  
   239  	ns, _, err := c.clientCfg.Namespace()
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	c.client = client
   245  	c.config = config
   246  	c.namespace = ns
   247  	c.restClient = client.CoreV1().RESTClient()
   248  	return nil
   249  }
   250  
   251  // GetNamesOfNodesMarkedForVirtlet implements GetNamesOfNodesMarkedForVirtlet methor of KubeClient interface.
   252  func (c *RealKubeClient) GetNamesOfNodesMarkedForVirtlet() ([]string, error) {
   253  	if err := c.setup(); err != nil {
   254  		return nil, err
   255  	}
   256  	opts := meta_v1.ListOptions{
   257  		LabelSelector: "extraRuntime=virtlet",
   258  	}
   259  	nodes, err := c.client.CoreV1().Nodes().List(opts)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  
   264  	var nodeNames []string
   265  	for _, item := range nodes.Items {
   266  		nodeNames = append(nodeNames, item.Name)
   267  	}
   268  	return nodeNames, nil
   269  }
   270  
   271  func (c *RealKubeClient) getVirtletPodAndNodeNames(nodeName string) (podNames []string, nodeNames []string, err error) {
   272  	if err := c.setup(); err != nil {
   273  		return nil, nil, err
   274  	}
   275  	opts := meta_v1.ListOptions{
   276  		LabelSelector: "runtime=virtlet",
   277  	}
   278  	if nodeName != "" {
   279  		opts.FieldSelector = "spec.nodeName=" + nodeName
   280  	}
   281  	pods, err := c.client.CoreV1().Pods("kube-system").List(opts)
   282  	if err != nil {
   283  		return nil, nil, err
   284  	}
   285  
   286  	for _, item := range pods.Items {
   287  		podNames = append(podNames, item.Name)
   288  		nodeNames = append(nodeNames, item.Spec.NodeName)
   289  	}
   290  	return podNames, nodeNames, nil
   291  }
   292  
   293  func (c *RealKubeClient) getVMPod(podName string) (*v1.Pod, error) {
   294  	if err := c.setup(); err != nil {
   295  		return nil, err
   296  	}
   297  	pod, err := c.client.CoreV1().Pods(c.namespace).Get(podName, meta_v1.GetOptions{})
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	if pod.Annotations == nil || pod.Annotations[runtimeAnnotation] != virtletRuntime {
   302  		return nil, fmt.Errorf("%q is not a VM pod (missing annotation: %q=%q)", podName, runtimeAnnotation, virtletRuntime)
   303  	}
   304  	return pod, nil
   305  }
   306  
   307  // GetVirtletPodAndNodeNames implements GetVirtletPodAndNodeNames method of KubeClient interface.
   308  func (c *RealKubeClient) GetVirtletPodAndNodeNames() (podNames []string, nodeNames []string, err error) {
   309  	return c.getVirtletPodAndNodeNames("")
   310  }
   311  
   312  // GetVirtletPodNameForNode implements GetVirtletPodNameForNode method of KubeClient interface.
   313  func (c *RealKubeClient) GetVirtletPodNameForNode(nodeName string) (string, error) {
   314  	virtletPodNames, _, err := c.getVirtletPodAndNodeNames(nodeName)
   315  	if err != nil {
   316  		return "", err
   317  	}
   318  
   319  	if len(virtletPodNames) == 0 {
   320  		return "", fmt.Errorf("no Virtlet pods found on the node %q", nodeName)
   321  	}
   322  
   323  	if len(virtletPodNames) > 1 {
   324  		return "", fmt.Errorf("more than one Virtlet pod found on the node %q", nodeName)
   325  	}
   326  
   327  	return virtletPodNames[0], nil
   328  }
   329  
   330  // GetVMPodInfo implements GetVMPodInfo method of KubeClient interface.
   331  func (c *RealKubeClient) GetVMPodInfo(podName string) (*VMPodInfo, error) {
   332  	pod, err := c.getVMPod(podName)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	if pod.Spec.NodeName == "" {
   337  		return nil, fmt.Errorf("pod %q doesn't have a node associated with it", podName)
   338  	}
   339  	if len(pod.Spec.Containers) != 1 {
   340  		return nil, fmt.Errorf("vm pod %q is expected to have just one container but it has %d containers instead", podName, len(pod.Spec.Containers))
   341  	}
   342  
   343  	if len(pod.Status.ContainerStatuses) != 1 {
   344  		return nil, fmt.Errorf("vm pod %q is expected to have just one container status but it has %d container statuses instead", podName, len(pod.Status.ContainerStatuses))
   345  	}
   346  
   347  	virtletPodName, err := c.GetVirtletPodNameForNode(pod.Spec.NodeName)
   348  	if err != nil {
   349  		return nil, err
   350  	}
   351  
   352  	return &VMPodInfo{
   353  		NodeName:       pod.Spec.NodeName,
   354  		VirtletPodName: virtletPodName,
   355  		ContainerID:    pod.Status.ContainerStatuses[0].ContainerID,
   356  		ContainerName:  pod.Spec.Containers[0].Name,
   357  	}, nil
   358  }
   359  
   360  // CreatePod implements CreatePod method of KubeClient interface.
   361  func (c *RealKubeClient) CreatePod(pod *v1.Pod) (*v1.Pod, error) {
   362  	if err := c.setup(); err != nil {
   363  		return nil, err
   364  	}
   365  	return c.client.CoreV1().Pods(pod.Namespace).Create(pod)
   366  }
   367  
   368  // GetPod implements GetPod method of KubeClient interface.
   369  func (c *RealKubeClient) GetPod(name, namespace string) (*v1.Pod, error) {
   370  	return c.client.CoreV1().Pods(namespace).Get(name, meta_v1.GetOptions{})
   371  }
   372  
   373  // DeletePod implements DeletePod method of KubeClient interface.
   374  func (c *RealKubeClient) DeletePod(name, namespace string) error {
   375  	return c.client.CoreV1().Pods(namespace).Delete(name, &meta_v1.DeleteOptions{})
   376  }
   377  
   378  // ExecInContainer implements ExecInContainer method of KubeClient interface.
   379  func (c *RealKubeClient) ExecInContainer(podName, containerName, namespace string, stdin io.Reader, stdout, stderr io.Writer, command []string) (int, error) {
   380  	if err := c.setup(); err != nil {
   381  		return 0, err
   382  	}
   383  	if namespace == "" {
   384  		namespace = c.namespace
   385  	}
   386  	req := c.restClient.Post().
   387  		Resource("pods").
   388  		Name(podName).
   389  		Namespace(namespace).
   390  		SubResource("exec").
   391  		VersionedParams(&v1.PodExecOptions{
   392  			Container: containerName,
   393  			Command:   command,
   394  			Stdin:     stdin != nil,
   395  			Stdout:    stdout != nil,
   396  			Stderr:    stderr != nil,
   397  			TTY:       false,
   398  		}, scheme.ParameterCodec)
   399  
   400  	exitCode := 0
   401  	if err := c.executor.stream(c.config, "POST", req.URL(), remotecommand.StreamOptions{
   402  		Stdin:  stdin,
   403  		Stdout: stdout,
   404  		Stderr: stderr,
   405  	}); err != nil {
   406  		if c, ok := err.(exec.CodeExitError); ok {
   407  			exitCode = c.Code
   408  		} else {
   409  			return 0, err
   410  		}
   411  	}
   412  
   413  	return exitCode, nil
   414  }
   415  
   416  // ForwardPorts implements ForwardPorts method of KubeClient interface.
   417  func (c *RealKubeClient) ForwardPorts(podName, namespace string, ports []*ForwardedPort) (stopCh chan struct{}, err error) {
   418  	if len(ports) == 0 {
   419  		return nil, errors.New("no ports specified")
   420  	}
   421  
   422  	if err := c.setup(); err != nil {
   423  		return nil, err
   424  	}
   425  
   426  	if namespace == "" {
   427  		namespace = c.namespace
   428  	}
   429  
   430  	pod, err := c.client.CoreV1().Pods(namespace).Get(podName, meta_v1.GetOptions{})
   431  	if err != nil {
   432  		return nil, err
   433  	}
   434  
   435  	if pod.Status.Phase != v1.PodRunning {
   436  		return nil, fmt.Errorf("unable to forward port because pod is not running (current status is %v)", pod.Status.Phase)
   437  	}
   438  
   439  	signals := make(chan os.Signal, 1)
   440  	signal.Notify(signals, os.Interrupt)
   441  	defer signal.Stop(signals)
   442  
   443  	stopCh = make(chan struct{})
   444  	go func() {
   445  		<-signals
   446  		if stopCh != nil {
   447  			close(stopCh)
   448  		}
   449  	}()
   450  
   451  	req := c.restClient.Post().
   452  		Resource("pods").
   453  		Namespace(namespace).
   454  		Name(pod.Name).
   455  		SubResource("portforward")
   456  	var buf bytes.Buffer
   457  	var portStrs []string
   458  	for _, p := range ports {
   459  		portStrs = append(portStrs, p.String())
   460  	}
   461  	errCh := make(chan error, 1)
   462  	readyCh := make(chan struct{})
   463  	go func() {
   464  		errCh <- c.portForwarder.forwardPorts(c.config, "POST", req.URL(), portStrs, stopCh, readyCh, &buf)
   465  	}()
   466  
   467  	select {
   468  	case err := <-errCh:
   469  		return nil, err
   470  	case <-readyCh:
   471  		// FIXME: there appears to be no better way to get back the local ports as of now
   472  		if err := ParsePortForwardOutput(buf.String(), ports); err != nil {
   473  			return nil, err
   474  		}
   475  	}
   476  	return stopCh, nil
   477  }
   478  
   479  // PodLogs retrieves the logs of the specified container in the pod.
   480  // limitBytes of zero specifies no size limit for the logs.
   481  // limitSeconds of zero specifies no time limit for the logs.
   482  func (c *RealKubeClient) PodLogs(podName, containerName, namespace string, tailLines int64) ([]byte, error) {
   483  	// FIXME: that's hard to test properly using the fake
   484  	// clientset.
   485  	opts := &v1.PodLogOptions{
   486  		Container: containerName,
   487  	}
   488  	if tailLines != 0 {
   489  		opts.TailLines = &tailLines
   490  	}
   491  	return c.client.CoreV1().Pods(namespace).GetLogs(podName, opts).Do().Raw()
   492  }