github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/tools/kubeclient_test.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  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"reflect"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/davecgh/go-spew/spew"
    32  	v1 "k8s.io/api/core/v1"
    33  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/client-go/dynamic"
    37  	fakekube "k8s.io/client-go/kubernetes/fake"
    38  	"k8s.io/client-go/rest"
    39  	fakerest "k8s.io/client-go/rest/fake"
    40  	testcore "k8s.io/client-go/testing"
    41  	"k8s.io/client-go/tools/remotecommand"
    42  )
    43  
    44  const (
    45  	sampleContainerID   = "docker://virtlet.cloud__2232e3bf-d702-5824-5e3c-f12e60e616b0"
    46  	portForwardWaitTime = 1 * time.Minute
    47  )
    48  
    49  type fakeExecutor struct {
    50  	t             *testing.T
    51  	called        bool
    52  	config        *rest.Config
    53  	method        string
    54  	url           *url.URL
    55  	streamOptions *remotecommand.StreamOptions
    56  }
    57  
    58  var _ remoteExecutor = &fakeExecutor{}
    59  
    60  func (e *fakeExecutor) stream(config *rest.Config, method string, url *url.URL, options remotecommand.StreamOptions) error {
    61  	if e.called {
    62  		e.t.Errorf("Stream called twice")
    63  	}
    64  	e.called = true
    65  	e.config = config
    66  	e.method = method
    67  	e.url = url
    68  	e.streamOptions = &options
    69  	return nil
    70  }
    71  
    72  func parsePort(portStr string) (uint16, uint16, error) {
    73  	var localStr, remoteStr string
    74  	parts := strings.Split(portStr, ":")
    75  	switch {
    76  	case len(parts) == 1:
    77  		localStr = parts[0]
    78  		remoteStr = parts[0]
    79  	case len(parts) == 2:
    80  		localStr = parts[0]
    81  		if localStr == "" {
    82  			localStr = "0"
    83  		}
    84  		remoteStr = parts[1]
    85  	default:
    86  		return 0, 0, fmt.Errorf("invalid port string %q", portStr)
    87  	}
    88  
    89  	localPort, err := strconv.ParseUint(localStr, 10, 16)
    90  	if err != nil {
    91  		return 0, 0, fmt.Errorf("bad local port string %q", localStr)
    92  	}
    93  
    94  	remotePort, err := strconv.ParseUint(remoteStr, 10, 16)
    95  	if err != nil {
    96  		return 0, 0, fmt.Errorf("bad remtoe port string %q", remoteStr)
    97  	}
    98  
    99  	if remotePort == 0 {
   100  		return 0, 0, fmt.Errorf("remote port must not be zero")
   101  	}
   102  
   103  	return uint16(localPort), uint16(remotePort), nil
   104  }
   105  
   106  type fakePortForwarder struct {
   107  	t      *testing.T
   108  	called bool
   109  	config *rest.Config
   110  	method string
   111  	url    *url.URL
   112  	ports  string
   113  }
   114  
   115  var _ portForwarder = &fakePortForwarder{}
   116  
   117  func (pf *fakePortForwarder) forwardPorts(config *rest.Config, method string, url *url.URL, ports []string, stopChannel, readyChannel chan struct{}, out io.Writer) error {
   118  	if pf.called {
   119  		pf.t.Errorf("ForwardPorts called twice")
   120  	}
   121  	pf.called = true
   122  	pf.config = config
   123  	pf.method = method
   124  	pf.url = url
   125  	pf.ports = strings.Join(ports, " ")
   126  	if readyChannel != nil {
   127  		close(readyChannel)
   128  	}
   129  	for n, portStr := range ports {
   130  		localPort, remotePort, err := parsePort(portStr)
   131  		if err != nil {
   132  			return err
   133  		}
   134  		if localPort == 0 {
   135  			// "random" local port https://xkcd.com/221/
   136  			localPort = 4242 + uint16(n)
   137  		}
   138  		fmt.Fprintf(out, "Forwarding from 127.0.0.1:%d -> %d\n", localPort, remotePort)
   139  	}
   140  	if stopChannel == nil {
   141  		pf.t.Errorf("no stop channel set")
   142  	} else {
   143  		select {
   144  		case <-stopChannel:
   145  		case <-time.After(portForwardWaitTime):
   146  			pf.t.Errorf("timed out waiting for the port forwarder to stop")
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  func TestGetVirtletPodNames(t *testing.T) {
   153  	fc := &fakekube.Clientset{}
   154  	fc.AddReactor("list", "pods", func(action testcore.Action) (bool, runtime.Object, error) {
   155  		expectedNamespace := "kube-system"
   156  		if action.GetNamespace() != expectedNamespace {
   157  			t.Errorf("wrong namespace: %q instead of %q", action.GetNamespace(), expectedNamespace)
   158  		}
   159  		return true, &v1.PodList{
   160  			Items: []v1.Pod{
   161  				{
   162  					ObjectMeta: meta_v1.ObjectMeta{
   163  						Name:      "virtlet-foo42",
   164  						Namespace: "kube-system",
   165  						Labels: map[string]string{
   166  							"runtime": "virtlet",
   167  						},
   168  					},
   169  					Spec: v1.PodSpec{
   170  						NodeName: "kube-node-1",
   171  					},
   172  				},
   173  				{
   174  					ObjectMeta: meta_v1.ObjectMeta{
   175  						Name:      "virtlet-g9wtz",
   176  						Namespace: "kube-system",
   177  						Labels: map[string]string{
   178  							"runtime": "virtlet",
   179  						},
   180  					},
   181  					Spec: v1.PodSpec{
   182  						NodeName: "kube-node-2",
   183  					},
   184  				},
   185  				// this pod doesn't have proper labels and thus
   186  				// it should be ignored
   187  				{
   188  					ObjectMeta: meta_v1.ObjectMeta{
   189  						Name:      "whatever",
   190  						Namespace: "kube-system",
   191  					},
   192  				},
   193  			},
   194  		}, nil
   195  	})
   196  
   197  	c := &RealKubeClient{client: fc}
   198  	podNames, nodeNames, err := c.GetVirtletPodAndNodeNames()
   199  	if err != nil {
   200  		t.Fatalf("GetVirtletPodNames(): %v", err)
   201  	}
   202  	podNamesStr := strings.Join(podNames, ",")
   203  	expectedPodNamesStr := "virtlet-foo42,virtlet-g9wtz"
   204  	if podNamesStr != expectedPodNamesStr {
   205  		t.Errorf("Bad pod names: %q instead of %q", podNamesStr, expectedPodNamesStr)
   206  	}
   207  	nodeNamesStr := strings.Join(nodeNames, ",")
   208  	expectedNodeNamesStr := "kube-node-1,kube-node-2"
   209  	if nodeNamesStr != expectedNodeNamesStr {
   210  		t.Errorf("Bad pod names: %q instead of %q", podNamesStr, expectedPodNamesStr)
   211  	}
   212  }
   213  
   214  func TestGetVMPodInfo(t *testing.T) {
   215  	fc := &fakekube.Clientset{}
   216  	fc.AddReactor("get", "pods", func(action testcore.Action) (bool, runtime.Object, error) {
   217  		expectedNamespace := "default"
   218  		if action.GetNamespace() != expectedNamespace {
   219  			t.Errorf("Wrong namespace: %q instead of %q", action.GetNamespace(), expectedNamespace)
   220  		}
   221  		getAction := action.(testcore.GetAction)
   222  		expectedName := "cirros-vm"
   223  		if getAction.GetName() != expectedName {
   224  			t.Errorf("Bad pod name: %q instead of %q", getAction.GetName(), expectedName)
   225  		}
   226  		return true, &v1.Pod{
   227  			ObjectMeta: meta_v1.ObjectMeta{
   228  				Name:      "cirros-vm",
   229  				Namespace: "default",
   230  				Annotations: map[string]string{
   231  					"kubernetes.io/target-runtime": "virtlet.cloud",
   232  				},
   233  			},
   234  			Spec: v1.PodSpec{
   235  				NodeName: "kube-node-1",
   236  				Containers: []v1.Container{
   237  					{
   238  						Name: "foocontainer",
   239  					},
   240  				},
   241  			},
   242  			Status: v1.PodStatus{
   243  				ContainerStatuses: []v1.ContainerStatus{
   244  					{
   245  						Name:        "foocontainer",
   246  						ContainerID: sampleContainerID,
   247  					},
   248  				},
   249  			},
   250  		}, nil
   251  	})
   252  	fc.AddReactor("list", "pods", func(action testcore.Action) (bool, runtime.Object, error) {
   253  		expectedNamespace := "kube-system"
   254  		if action.GetNamespace() != expectedNamespace {
   255  			t.Errorf("wrong namespace: %q instead of %q", action.GetNamespace(), expectedNamespace)
   256  		}
   257  		// fake Clientset doesn't handle the field selector currently
   258  		listAction := action.(testcore.ListAction)
   259  		expectedFieldSelector := "spec.nodeName=kube-node-1"
   260  		fieldSelector := listAction.GetListRestrictions().Fields.String()
   261  		if fieldSelector != expectedFieldSelector {
   262  			t.Errorf("bad fieldSelector: %q instead of %q", fieldSelector, expectedFieldSelector)
   263  		}
   264  		return true, &v1.PodList{
   265  			Items: []v1.Pod{
   266  				{
   267  					ObjectMeta: meta_v1.ObjectMeta{
   268  						Name:      "virtlet-g9wtz",
   269  						Namespace: "kube-system",
   270  						Labels: map[string]string{
   271  							"runtime": "virtlet",
   272  						},
   273  					},
   274  					Spec: v1.PodSpec{
   275  						NodeName: "kube-node-1",
   276  					},
   277  				},
   278  				// this pod doesn't have proper labels and thus
   279  				// it should be ignored
   280  				{
   281  					ObjectMeta: meta_v1.ObjectMeta{
   282  						Name:      "whatever",
   283  						Namespace: "kube-system",
   284  					},
   285  					Spec: v1.PodSpec{
   286  						NodeName: "kube-node-1",
   287  					},
   288  				},
   289  			},
   290  		}, nil
   291  	})
   292  
   293  	c := &RealKubeClient{client: fc, namespace: "default"}
   294  	vmPodInfo, err := c.GetVMPodInfo("cirros-vm")
   295  	if err != nil {
   296  		t.Fatalf("GetVirtletPodNames(): %v", err)
   297  	}
   298  
   299  	expectedVMPodInfo := &VMPodInfo{
   300  		NodeName:       "kube-node-1",
   301  		VirtletPodName: "virtlet-g9wtz",
   302  		ContainerID:    sampleContainerID,
   303  		ContainerName:  "foocontainer",
   304  	}
   305  	if !reflect.DeepEqual(expectedVMPodInfo, vmPodInfo) {
   306  		t.Errorf("Bad VM PodInfo: got:\n%s\ninstead of\n%s", spew.Sdump(vmPodInfo), spew.Sdump(expectedVMPodInfo))
   307  	}
   308  
   309  	expectedDomainName := "virtlet-2232e3bf-d702-foocontainer"
   310  	if vmPodInfo.LibvirtDomainName() != expectedDomainName {
   311  		t.Errorf("Bad libvirt domain name: %q instead of %q", vmPodInfo.LibvirtDomainName(), expectedDomainName)
   312  	}
   313  }
   314  
   315  func TestCheckForVMPod(t *testing.T) {
   316  	for _, tc := range []struct {
   317  		name        string
   318  		annotations map[string]string
   319  	}{
   320  		{
   321  			name:        "no annotations",
   322  			annotations: nil,
   323  		},
   324  		{
   325  			name: "wrong annotation",
   326  			annotations: map[string]string{
   327  				"kubernetes.io/target-runtime": "foobar",
   328  			},
   329  		},
   330  	} {
   331  		t.Run(tc.name, func(t *testing.T) {
   332  			fc := &fakekube.Clientset{}
   333  			fc.AddReactor("get", "pods", func(action testcore.Action) (bool, runtime.Object, error) {
   334  				return true, &v1.Pod{
   335  					ObjectMeta: meta_v1.ObjectMeta{
   336  						Name:        "cirros-vm",
   337  						Namespace:   "default",
   338  						Annotations: tc.annotations,
   339  					},
   340  					Spec: v1.PodSpec{
   341  						NodeName: "kube-node-1",
   342  						Containers: []v1.Container{
   343  							{
   344  								Name: "foocontainer",
   345  							},
   346  						},
   347  					},
   348  					Status: v1.PodStatus{
   349  						ContainerStatuses: []v1.ContainerStatus{
   350  							{
   351  								Name:        "foocontainer",
   352  								ContainerID: sampleContainerID,
   353  							},
   354  						},
   355  					},
   356  				}, nil
   357  			})
   358  
   359  			c := &RealKubeClient{client: fc, namespace: "default"}
   360  			switch _, err := c.GetVMPodInfo("cirros-vm"); {
   361  			case err == nil:
   362  				t.Errorf("didn't get an expected error for a pod w/o Virtlet runtime annotation")
   363  			case !strings.Contains(err.Error(), "annotation"):
   364  				t.Errorf("wrong error message for a pod w/o Virtlet runtime annotation: %q", err)
   365  			}
   366  		})
   367  	}
   368  }
   369  
   370  func TestExecInContainer(t *testing.T) {
   371  	restClient := &fakerest.RESTClient{
   372  		GroupVersion: schema.GroupVersion{Version: "v1"},
   373  		NegotiatedSerializer://testapi.Default.NegotiatedSerializer(),
   374  		dynamic.ContentConfig().NegotiatedSerializer,
   375  		Client: fakerest.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   376  			// this handler is not actually invoked
   377  			return nil, nil
   378  		}),
   379  	}
   380  	config := &rest.Config{Host: "foo-host"}
   381  	fe := &fakeExecutor{t: t}
   382  	c := &RealKubeClient{
   383  		client:     &fakekube.Clientset{},
   384  		config:     config,
   385  		restClient: restClient,
   386  		executor:   fe,
   387  	}
   388  	var stdin, stdout, stderr bytes.Buffer
   389  	exitCode, err := c.ExecInContainer("virtlet-foo42", "virtlet", "kube-system", &stdin, &stdout, &stderr, []string{"echo", "foobar"})
   390  	if err != nil {
   391  		t.Errorf("ExecInContainer returned error: %v", err)
   392  	}
   393  	if exitCode != 0 {
   394  		t.Errorf("ExecInContainer returned non-zero exit code %v", exitCode)
   395  	}
   396  	if fe.config == nil {
   397  		t.Errorf("fe.config not set")
   398  	} else if fe.config.Host != "foo-host" {
   399  		t.Errorf("Bad host in rest config: %q instead of foo-host", fe.config.Host)
   400  	}
   401  	if fe.method != "POST" {
   402  		t.Errorf("Bad method %q instead of POST", fe.method)
   403  	}
   404  	expectedPath := "/namespaces/kube-system/pods/virtlet-foo42/exec"
   405  	if fe.url == nil {
   406  		t.Errorf("fe.url not set")
   407  	} else if fe.url.Path != expectedPath {
   408  		t.Errorf("Bad expectedPath: %q instead of %q", fe.url.Path, expectedPath)
   409  	}
   410  
   411  	if fe.streamOptions == nil {
   412  		t.Errorf("StreamOptions not set (perhaps Stream() not called)")
   413  	} else {
   414  		if fe.streamOptions.Stdin != &stdin {
   415  			t.Errorf("Bad stdin")
   416  		}
   417  		if fe.streamOptions.Stdout != &stdout {
   418  			t.Errorf("Bad stdout")
   419  		}
   420  		if fe.streamOptions.Stderr != &stderr {
   421  			t.Errorf("Bad stderr")
   422  		}
   423  	}
   424  
   425  	expectedValues := url.Values{
   426  		"command":   {"echo", "foobar"},
   427  		"container": {"virtlet"},
   428  		"stderr":    {"true"},
   429  		"stdin":     {"true"},
   430  		"stdout":    {"true"},
   431  	}
   432  	if !reflect.DeepEqual(expectedValues, fe.url.Query()) {
   433  		t.Errorf("Bad query: %#v", fe.url.Query())
   434  	}
   435  }
   436  
   437  func TestPortForward(t *testing.T) {
   438  	fc := &fakekube.Clientset{}
   439  	fc.AddReactor("get", "pods", func(action testcore.Action) (bool, runtime.Object, error) {
   440  		expectedNamespace := "kube-system"
   441  		if action.GetNamespace() != expectedNamespace {
   442  			t.Errorf("Wrong namespace: %q instead of %q", action.GetNamespace(), expectedNamespace)
   443  		}
   444  		getAction := action.(testcore.GetAction)
   445  		expectedName := "virtlet-foo42"
   446  		if getAction.GetName() != expectedName {
   447  			t.Errorf("Bad pod name: %q instead of %q", getAction.GetName(), expectedName)
   448  		}
   449  		return true, &v1.Pod{
   450  			ObjectMeta: meta_v1.ObjectMeta{
   451  				Name:      "virtlet-foo42",
   452  				Namespace: "kube-system",
   453  			},
   454  			Status: v1.PodStatus{
   455  				Phase: v1.PodRunning,
   456  			},
   457  		}, nil
   458  	})
   459  	restClient := &fakerest.RESTClient{
   460  		GroupVersion: schema.GroupVersion{Version: "v1"},
   461  		NegotiatedSerializer://testapi.Default.NegotiatedSerializer(),
   462  		dynamic.ContentConfig().NegotiatedSerializer,
   463  		Client: fakerest.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   464  			// this handler is not actually invoked
   465  			return nil, nil
   466  		}),
   467  	}
   468  	config := &rest.Config{Host: "foo-host"}
   469  	pf := &fakePortForwarder{t: t}
   470  	c := &RealKubeClient{
   471  		client:        fc,
   472  		config:        config,
   473  		restClient:    restClient,
   474  		portForwarder: pf,
   475  	}
   476  	portsToForward := []*ForwardedPort{
   477  		{
   478  			LocalPort:  59000,
   479  			RemotePort: 5900,
   480  		},
   481  		{
   482  			RemotePort: 5901,
   483  		},
   484  		{
   485  			RemotePort: 5902,
   486  		},
   487  	}
   488  	stopCh, err := c.ForwardPorts("virtlet-foo42", "kube-system", portsToForward)
   489  	if err != nil {
   490  		t.Fatalf("ForwardPorts(): %v", err)
   491  	}
   492  	if pf.config == nil {
   493  		t.Errorf("pf.config not set")
   494  	} else if pf.config.Host != "foo-host" {
   495  		t.Errorf("Bad host in rest config: %q instead of foo-host", pf.config.Host)
   496  	}
   497  	if pf.method != "POST" {
   498  		t.Errorf("Bad method %q instead of POST", pf.method)
   499  	}
   500  
   501  	expectedPath := "/namespaces/kube-system/pods/virtlet-foo42/portforward"
   502  	if pf.url == nil {
   503  		t.Errorf("pf.url is not set")
   504  	} else if pf.url.Path != expectedPath {
   505  		t.Errorf("Bad expectedPath: %q instead of %q", pf.url.Path, expectedPath)
   506  	}
   507  
   508  	expectedPortStr := "59000:5900 :5901 :5902"
   509  	if pf.ports != expectedPortStr {
   510  		t.Errorf("Bad requested port forward list: %q instead of %q", pf.ports, expectedPortStr)
   511  	}
   512  
   513  	expectedPorts := []*ForwardedPort{
   514  		{
   515  			LocalPort:  59000,
   516  			RemotePort: 5900,
   517  		},
   518  		{
   519  			LocalPort:  4243, // 1st "random" port
   520  			RemotePort: 5901,
   521  		},
   522  		{
   523  			LocalPort:  4244, // 2nd "random" port
   524  			RemotePort: 5902,
   525  		},
   526  	}
   527  	if !reflect.DeepEqual(expectedPorts, portsToForward) {
   528  		t.Errorf("Bad ports:\n%s", spew.Sdump(portsToForward))
   529  	}
   530  
   531  	if stopCh == nil {
   532  		t.Error("Stop channel is nil")
   533  	} else {
   534  		close(stopCh)
   535  	}
   536  }
   537  
   538  // TODO: test not finding Virtlet pod
   539  // TODO: add test for 'virsh' command
   540  // TODO: don't require --node on a single-Virtlet-node clusters