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  }