github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/kubectl_forwarder_test.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 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 portforward 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "io/ioutil" 24 "runtime" 25 "sort" 26 "strings" 27 "sync" 28 "testing" 29 "time" 30 31 corev1 "k8s.io/api/core/v1" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 pkgruntime "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/util/intstr" 35 "k8s.io/client-go/kubernetes" 36 "k8s.io/client-go/kubernetes/fake" 37 38 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl" 39 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client" 40 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 41 schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util" 42 "github.com/GoogleContainerTools/skaffold/testutil" 43 ) 44 45 func TestUnavailablePort(t *testing.T) { 46 testutil.Run(t, "", func(t *testutil.T) { 47 t.Override(&waitPortNotFree, 100*time.Millisecond) 48 49 // Return that the port is false, while also 50 // adding a sync group so we know when isPortFree 51 // has been called 52 var portFreeWG sync.WaitGroup 53 portFreeWG.Add(1) 54 t.Override(&isPortFree, func(string, int) bool { 55 portFreeWG.Done() 56 return false 57 }) 58 59 // Create a wait group that will only be 60 // fulfilled when the forward function returns 61 var forwardFunctionWG sync.WaitGroup 62 forwardFunctionWG.Add(1) 63 t.Override(&deferFunc, func() { 64 forwardFunctionWG.Done() 65 }) 66 67 var buf bytes.Buffer 68 k := KubectlForwarder{ 69 out: &buf, 70 } 71 pfe := newPortForwardEntry(0, latest.PortForwardResource{}, "", "", "", "", 8080, false) 72 73 k.Start(&buf) 74 go k.Forward(context.Background(), pfe) 75 76 // wait for isPortFree to be called 77 portFreeWG.Wait() 78 79 // then, end port forwarding and wait for the forward function to return. 80 pfe.terminationLock.Lock() 81 pfe.terminated = true 82 pfe.terminationLock.Unlock() 83 forwardFunctionWG.Wait() 84 85 // read output to make sure logs are expected 86 t.CheckContains("port 8080 is taken", buf.String()) 87 }) 88 } 89 90 func TestTerminate(t *testing.T) { 91 ctx, cancel := context.WithCancel(context.Background()) 92 93 pfe := newPortForwardEntry(0, latest.PortForwardResource{}, "", "", "", "", 8080, false) 94 pfe.cancel = cancel 95 96 k := &KubectlForwarder{} 97 k.Terminate(pfe) 98 if pfe.terminated != true { 99 t.Fatalf("expected pfe.terminated to be true after termination") 100 } 101 if ctx.Err() != context.Canceled { 102 t.Fatalf("expected cancel to be called") 103 } 104 } 105 106 func TestMonitorErrorLogs(t *testing.T) { 107 if runtime.GOOS == "windows" { 108 t.Skip("skip flaky test until it's fixed") 109 } 110 tests := []struct { 111 description string 112 input string 113 cmdRunning bool 114 shouldError bool 115 }{ 116 { 117 description: "no error logs appear", 118 input: "some random logs", 119 cmdRunning: true, 120 }, 121 { 122 description: "match on 'error forwarding port'", 123 input: "error forwarding port 8080", 124 shouldError: true, 125 }, 126 { 127 description: "match on 'unable to forward'", 128 input: "unable to forward 8080", 129 shouldError: true, 130 }, 131 { 132 description: "match on 'error upgrading connection'", 133 input: "error upgrading connection 8080", 134 shouldError: true, 135 }, 136 { 137 description: "match on successful port forwarding message", 138 input: "Forwarding from 127.0.0.1:8080 -> 8080", 139 cmdRunning: true, 140 }, 141 } 142 143 for _, test := range tests { 144 testutil.Run(t, test.description, func(t *testutil.T) { 145 t.Override(&waitErrorLogs, 10*time.Millisecond) 146 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 147 defer cancel() 148 149 cmdStr := "sleep" 150 if runtime.GOOS == "windows" { 151 cmdStr = "timeout" 152 } 153 cmd := kubectl.CommandContext(ctx, cmdStr, "5") 154 if err := cmd.Start(); err != nil { 155 t.Fatalf("error starting command: %v", err) 156 } 157 158 errChan := make(chan error, 1) 159 go func() { 160 logs := strings.NewReader(test.input) 161 162 k := KubectlForwarder{} 163 k.monitorLogs(ctx, logs, cmd, &portForwardEntry{}, errChan) 164 165 errChan <- nil 166 }() 167 168 err := <-errChan 169 t.CheckError(test.shouldError, err) 170 171 // make sure the command is running or killed based on what's expected 172 if test.cmdRunning { 173 assertCmdIsRunning(t, cmd) 174 cmd.Terminate() 175 } else { 176 assertCmdWasKilled(t, cmd) 177 } 178 }) 179 } 180 } 181 182 func assertCmdIsRunning(t *testutil.T, cmd *kubectl.Cmd) { 183 if cmd.ProcessState != nil { 184 t.Fatal("cmd was killed but expected to continue running") 185 } 186 } 187 188 func assertCmdWasKilled(t *testutil.T, cmd *kubectl.Cmd) { 189 if err := cmd.Wait(); err == nil { 190 t.Fatal("cmd was not killed but expected to be killed") 191 } 192 } 193 194 func TestPortForwardArgs(t *testing.T) { 195 tests := []struct { 196 description string 197 input *portForwardEntry 198 servicePod string 199 servicePort int 200 serviceErr error 201 result []string 202 }{ 203 { 204 description: "non-default address", 205 input: newPortForwardEntry(0, latest.PortForwardResource{Type: "pod", Name: "p", Namespace: "ns", Port: schemautil.FromInt(9), Address: "0.0.0.0"}, "", "", "", "", 8080, false), 206 result: []string{"--pod-running-timeout", "1s", "--namespace", "ns", "pod/p", "8080:9", "--address", "0.0.0.0"}, 207 }, 208 { 209 description: "localhost is the default", 210 input: newPortForwardEntry(0, latest.PortForwardResource{Type: "pod", Name: "p", Namespace: "ns", Port: schemautil.FromInt(9), Address: "127.0.0.1"}, "", "", "", "", 8080, false), 211 result: []string{"--pod-running-timeout", "1s", "--namespace", "ns", "pod/p", "8080:9"}, 212 }, 213 { 214 description: "no address", 215 input: newPortForwardEntry(0, latest.PortForwardResource{Type: "pod", Name: "p", Namespace: "ns", Port: schemautil.FromInt(9)}, "", "", "", "", 8080, false), 216 result: []string{"--pod-running-timeout", "1s", "--namespace", "ns", "pod/p", "8080:9"}, 217 }, 218 { 219 description: "service to pod", 220 input: newPortForwardEntry(0, latest.PortForwardResource{Type: "service", Name: "svc", Namespace: "ns", Port: schemautil.FromInt(9)}, "", "", "", "", 8080, false), 221 servicePod: "servicePod", 222 servicePort: 9999, 223 result: []string{"--pod-running-timeout", "1s", "--namespace", "ns", "pod/servicePod", "8080:9999"}, 224 }, 225 { 226 description: "service could not be mapped to pod", 227 input: newPortForwardEntry(0, latest.PortForwardResource{Type: "service", Name: "svc", Namespace: "ns", Port: schemautil.FromInt(9)}, "", "", "", "", 8080, false), 228 serviceErr: errors.New("error"), 229 result: []string{"--pod-running-timeout", "1s", "--namespace", "ns", "service/svc", "8080:9"}, 230 }, 231 } 232 233 for _, test := range tests { 234 testutil.Run(t, test.description, func(t *testutil.T) { 235 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 236 defer cancel() 237 238 t.Override(&findNewestPodForSvc, func(ctx context.Context, kCtx, ns, serviceName string, servicePort schemautil.IntOrString) (string, int, error) { 239 return test.servicePod, test.servicePort, test.serviceErr 240 }) 241 242 args := portForwardArgs(ctx, "", test.input) 243 t.CheckDeepEqual(test.result, args) 244 }) 245 } 246 } 247 248 func TestNewestPodFirst(t *testing.T) { 249 starting := mockPod("starting", nil, time.Now()) 250 starting.Status.Phase = corev1.PodPending 251 new := mockPod("new", nil, time.Now().Add(-time.Minute)) 252 old := mockPod("old", nil, time.Now().Add(-time.Hour)) 253 254 pods := []corev1.Pod{*old, *new, *starting} 255 sort.Slice(pods, newestPodsFirst(pods)) 256 257 expected := []corev1.Pod{*starting, *new, *old} 258 testutil.CheckDeepEqual(t, expected, pods) 259 } 260 261 func TestFindServicePort(t *testing.T) { 262 tests := []struct { 263 description string 264 service *corev1.Service 265 port schemautil.IntOrString 266 shouldErr bool 267 expected corev1.ServicePort 268 }{ 269 { 270 description: "simple case", 271 service: mockService("svc1", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Port: 90, TargetPort: intstr.FromInt(80)}, {Port: 80, TargetPort: intstr.FromInt(8080)}}), 272 port: schemautil.FromInt(80), 273 expected: corev1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}, 274 }, 275 { 276 description: "no ports", 277 service: mockService("svc2", corev1.ServiceTypeLoadBalancer, nil), 278 port: schemautil.FromInt(80), 279 shouldErr: true, 280 }, 281 { 282 description: "no matching ports", 283 service: mockService("svc3", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Port: 90, TargetPort: intstr.FromInt(80)}, {Port: 80, TargetPort: intstr.FromInt(8080)}}), 284 port: schemautil.FromInt(100), 285 shouldErr: true, 286 }, 287 { 288 description: "simple case with service port names", 289 service: mockService("svc1", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Name: "aaa", Port: 90, TargetPort: intstr.FromInt(80)}, {Name: "bbb", Port: 80, TargetPort: intstr.FromInt(8080)}}), 290 port: schemautil.FromString("bbb"), 291 expected: corev1.ServicePort{Name: "bbb", Port: 80, TargetPort: intstr.FromInt(8080)}, 292 }, 293 } 294 for _, test := range tests { 295 testutil.Run(t, test.description, func(t *testutil.T) { 296 result, err := findServicePort(*test.service, test.port) 297 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, result) 298 }) 299 } 300 } 301 302 func TestFindTargetPort(t *testing.T) { 303 tests := []struct { 304 description string 305 servicePort corev1.ServicePort 306 pod corev1.Pod 307 expected int 308 }{ 309 { 310 description: "integer port", 311 servicePort: corev1.ServicePort{TargetPort: intstr.FromInt(8080)}, 312 pod: *mockPod("new", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Time{}), 313 expected: 8080, 314 }, 315 { 316 description: "named port", 317 servicePort: corev1.ServicePort{TargetPort: intstr.FromString("http")}, 318 pod: *mockPod("new", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Time{}), 319 expected: 8080, 320 }, 321 { 322 description: "no port found", 323 servicePort: corev1.ServicePort{TargetPort: intstr.FromString("http")}, 324 pod: *mockPod("new", nil, time.Time{}), 325 expected: -1, 326 }, 327 } 328 for _, test := range tests { 329 testutil.Run(t, test.description, func(t *testutil.T) { 330 result := findTargetPort(test.servicePort, test.pod) 331 t.CheckDeepEqual(test.expected, result) 332 }) 333 } 334 } 335 336 func TestFindNewestPodForService(t *testing.T) { 337 tests := []struct { 338 description string 339 clientResources []pkgruntime.Object 340 clientErr error 341 serviceName string 342 servicePort int 343 shouldErr bool 344 chosenPod string 345 chosenPort int 346 }{ 347 { 348 description: "chooses new with port 8080 via int targetport", 349 clientResources: []pkgruntime.Object{ 350 mockService("svc", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Port: 80, TargetPort: intstr.FromInt(8080)}}), 351 mockPod("new", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Minute)), 352 mockPod("old", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Hour)), 353 }, 354 serviceName: "svc", 355 servicePort: 80, 356 chosenPod: "new", 357 chosenPort: 8080, 358 }, 359 { 360 description: "chooses new with port 8080 via string targetport", 361 clientResources: []pkgruntime.Object{ 362 mockService("svc", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Port: 80, TargetPort: intstr.FromString("http")}}), 363 mockPod("new", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Minute)), 364 mockPod("old", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Hour)), 365 }, 366 serviceName: "svc", 367 servicePort: 80, 368 chosenPod: "new", 369 chosenPort: 8080, 370 }, 371 { 372 description: "service not found", 373 clientResources: []pkgruntime.Object{ 374 mockService("svc", corev1.ServiceTypeClusterIP, []corev1.ServicePort{{Port: 80, TargetPort: intstr.FromInt(8080)}}), 375 mockPod("new", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Minute)), 376 mockPod("old", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Hour)), 377 }, 378 serviceName: "notfound", 379 servicePort: 80, 380 shouldErr: true, 381 chosenPort: -1, 382 }, 383 { 384 description: "port not found", 385 clientResources: []pkgruntime.Object{ 386 mockService("svc", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Port: 80, TargetPort: intstr.FromInt(8080)}}), 387 mockPod("new", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Minute)), 388 mockPod("old", []corev1.ContainerPort{{Name: "http", ContainerPort: 8080}}, time.Now().Add(-time.Hour)), 389 }, 390 serviceName: "svc", 391 servicePort: 90, 392 shouldErr: true, 393 chosenPort: -1, 394 }, 395 { 396 description: "no matching pods", 397 clientResources: []pkgruntime.Object{ 398 mockService("service", corev1.ServiceTypeLoadBalancer, []corev1.ServicePort{{Port: 80, TargetPort: intstr.FromInt(8080)}}), 399 }, 400 serviceName: "svc", 401 servicePort: 90, 402 shouldErr: true, 403 chosenPort: -1, 404 }, 405 { 406 description: "port not found", 407 clientErr: errors.New("injected failure"), 408 serviceName: "svc", 409 servicePort: 90, 410 shouldErr: true, 411 chosenPort: -1, 412 }, 413 } 414 for _, test := range tests { 415 testutil.Run(t, test.description, func(t *testutil.T) { 416 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 417 defer cancel() 418 419 t.Override(&client.Client, func(string) (kubernetes.Interface, error) { 420 return fake.NewSimpleClientset(test.clientResources...), test.clientErr 421 }) 422 423 pod, port, err := findNewestPodForService(ctx, "", "", test.serviceName, schemautil.FromInt(test.servicePort)) 424 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.chosenPod, pod) 425 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.chosenPort, port) 426 }) 427 } 428 } 429 430 func mockService(name string, serviceType corev1.ServiceType, ports []corev1.ServicePort) *corev1.Service { 431 return &corev1.Service{ 432 ObjectMeta: metav1.ObjectMeta{Name: name}, 433 Spec: corev1.ServiceSpec{ 434 Type: serviceType, 435 Ports: ports, 436 }} 437 } 438 439 func mockPod(name string, ports []corev1.ContainerPort, creationTime time.Time) *corev1.Pod { 440 return &corev1.Pod{ 441 ObjectMeta: metav1.ObjectMeta{ 442 Name: name, 443 CreationTimestamp: metav1.NewTime(creationTime), 444 }, 445 Spec: corev1.PodSpec{ 446 Containers: []corev1.Container{{ 447 Name: "container", 448 Ports: ports, 449 }}, 450 }, 451 Status: corev1.PodStatus{ 452 Phase: corev1.PodRunning, 453 }, 454 } 455 } 456 457 func TestStartAndForward(t *testing.T) { 458 tests := []struct { 459 description string 460 startFirst bool 461 }{ 462 { 463 description: "Forward() before Start() errors", 464 startFirst: false, 465 }, { 466 description: "Start() before Forward()", 467 startFirst: true, 468 }, 469 } 470 471 for _, test := range tests { 472 testutil.Run(t, test.description, func(_ *testutil.T) { 473 k := &KubectlForwarder{} 474 if test.startFirst { 475 k.Start(ioutil.Discard) 476 testutil.CheckDeepEqual(t, k.started, int32(1)) 477 } else { 478 err := k.Forward(context.Background(), nil) 479 testutil.CheckError(t, true, err) 480 } 481 }) 482 } 483 } 484 485 func TestForwardReturnsNilOnContextCancelled(t *testing.T) { 486 k := NewKubectlForwarder(&kubectl.CLI{}) 487 k.Start(ioutil.Discard) 488 ctx, cancel := context.WithCancel(context.Background()) 489 done := make(chan struct{}, 1) 490 go func() { 491 pfe := newPortForwardEntry(0, latest.PortForwardResource{}, "", "", "", "", 8080, false) 492 err := k.Forward(ctx, pfe) 493 if err != nil { 494 t.Errorf("expected nil error, got %+v", err) 495 } 496 close(done) 497 }() 498 cancel() 499 select { 500 case <-done: 501 // expected 502 case <-time.After(3 * time.Second): 503 t.Fatalf("forwarder did not return on context cancel") 504 } 505 }