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