github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/resource_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  	"context"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  	v1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	"k8s.io/client-go/kubernetes"
    34  	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
    35  
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label"
    38  	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
    39  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    40  	schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
    41  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    42  	"github.com/GoogleContainerTools/skaffold/testutil"
    43  	testEvent "github.com/GoogleContainerTools/skaffold/testutil/event"
    44  )
    45  
    46  type testForwarder struct {
    47  	forwardedResources sync.Map
    48  	forwardedPorts     util.PortSet
    49  }
    50  
    51  func (f *testForwarder) Forward(ctx context.Context, pfe *portForwardEntry) error {
    52  	f.forwardedResources.Store(pfe.key(), pfe)
    53  	f.forwardedPorts.Set(pfe.localPort)
    54  	return nil
    55  }
    56  
    57  func (f *testForwarder) Monitor(*portForwardEntry, func()) {}
    58  
    59  func (f *testForwarder) Terminate(pfe *portForwardEntry) {
    60  	f.forwardedResources.Delete(pfe.key())
    61  	f.forwardedPorts.Delete(pfe.localPort)
    62  }
    63  
    64  func (f *testForwarder) Start(io.Writer) {}
    65  
    66  func newTestForwarder() *testForwarder {
    67  	return &testForwarder{}
    68  }
    69  
    70  func mockRetrieveAvailablePort(_ string, taken map[int]struct{}, availablePorts []int) func(string, int, *util.PortSet) int {
    71  	// Return first available port in ports that isn't taken
    72  	var lock sync.Mutex
    73  	return func(string, int, *util.PortSet) int {
    74  		for _, p := range availablePorts {
    75  			lock.Lock()
    76  			if _, ok := taken[p]; ok {
    77  				lock.Unlock()
    78  				continue
    79  			}
    80  			taken[p] = struct{}{}
    81  			lock.Unlock()
    82  			return p
    83  		}
    84  		return -1
    85  	}
    86  }
    87  
    88  func TestStart(t *testing.T) {
    89  	svc1 := &latest.PortForwardResource{
    90  		Type:      constants.Service,
    91  		Name:      "svc1",
    92  		Namespace: "default",
    93  		Port:      schemautil.FromInt(8080),
    94  	}
    95  
    96  	svc2 := &latest.PortForwardResource{
    97  		Type:      constants.Service,
    98  		Name:      "svc2",
    99  		Namespace: "default",
   100  		Port:      schemautil.FromInt(9000),
   101  	}
   102  
   103  	tests := []struct {
   104  		description    string
   105  		resources      []*latest.PortForwardResource
   106  		availablePorts []int
   107  		expected       map[string]*portForwardEntry
   108  	}{
   109  		{
   110  			description:    "forward two services",
   111  			resources:      []*latest.PortForwardResource{svc1, svc2},
   112  			availablePorts: []int{8080, 9000},
   113  			expected: map[string]*portForwardEntry{
   114  				"service-svc1-default-8080": {
   115  					resource:  *svc1,
   116  					localPort: 8080,
   117  				},
   118  				"service-svc2-default-9000": {
   119  					resource:  *svc2,
   120  					localPort: 9000,
   121  				},
   122  			},
   123  		},
   124  	}
   125  	for _, test := range tests {
   126  		testutil.Run(t, test.description, func(t *testutil.T) {
   127  			testEvent.InitializeState([]latest.Pipeline{{}})
   128  			t.Override(&retrieveAvailablePort, mockRetrieveAvailablePort(util.Loopback, map[int]struct{}{}, test.availablePorts))
   129  			t.Override(&retrieveServices, func(context.Context, string, []string, string) ([]*latest.PortForwardResource, error) {
   130  				return test.resources, nil
   131  			})
   132  
   133  			fakeForwarder := newTestForwarder()
   134  			entryManager := NewEntryManager(fakeForwarder)
   135  
   136  			rf := NewServicesForwarder(entryManager, "", "")
   137  			if err := rf.Start(context.Background(), ioutil.Discard, []string{"test"}); err != nil {
   138  				t.Fatalf("error starting resource forwarder: %v", err)
   139  			}
   140  
   141  			// poll up to 10 seconds for the resources to be forwarded
   142  			err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   143  				return len(test.expected) == length(&fakeForwarder.forwardedResources), nil
   144  			})
   145  			if err != nil {
   146  				t.Fatalf("expected entries didn't match actual entries.\nExpected: %v\n  Actual: %v", test.expected, print(&fakeForwarder.forwardedResources))
   147  			}
   148  		})
   149  	}
   150  }
   151  
   152  func TestGetCurrentEntryFunc(t *testing.T) {
   153  	tests := []struct {
   154  		description        string
   155  		forwardedResources map[string]*portForwardEntry
   156  		availablePorts     []int
   157  		resource           latest.PortForwardResource
   158  		expectedReq        int
   159  		expected           *portForwardEntry
   160  	}{
   161  		{
   162  			description: "port forward service",
   163  			resource: latest.PortForwardResource{
   164  				Type: "service",
   165  				Name: "serviceName",
   166  				Port: schemautil.FromInt(8080),
   167  			},
   168  			availablePorts: []int{8080},
   169  			expectedReq:    8080,
   170  			expected:       newPortForwardEntry(0, latest.PortForwardResource{}, "", "", "", "", 8080, false),
   171  		}, {
   172  			description: "should not request system ports (1-1023)",
   173  			resource: latest.PortForwardResource{
   174  				Type: "service",
   175  				Name: "serviceName",
   176  				Port: schemautil.FromInt(80),
   177  			},
   178  			availablePorts: []int{8080},
   179  			expectedReq:    0, // no local port requested as port 80 is a system port
   180  			expected:       newPortForwardEntry(0, latest.PortForwardResource{}, "", "", "", "", 8080, false),
   181  		}, {
   182  			description: "port forward existing deployment",
   183  			resource: latest.PortForwardResource{
   184  				Type:      "deployment",
   185  				Namespace: "default",
   186  				Name:      "depName",
   187  				Port:      schemautil.FromInt(8080),
   188  			},
   189  			forwardedResources: map[string]*portForwardEntry{
   190  				"deployment-depName-default-8080": {
   191  					resource: latest.PortForwardResource{
   192  						Type:      "deployment",
   193  						Namespace: "default",
   194  						Name:      "depName",
   195  						Port:      schemautil.FromInt(8080),
   196  					},
   197  					localPort: 9000,
   198  				},
   199  			},
   200  			expectedReq: -1, // retrieveAvailablePort should not be called as there is an assigned localPort
   201  			expected:    newPortForwardEntry(0, latest.PortForwardResource{}, "", "", "", "", 9000, false),
   202  		},
   203  	}
   204  
   205  	for _, test := range tests {
   206  		testutil.Run(t, test.description, func(t *testutil.T) {
   207  			t.Override(&retrieveAvailablePort, func(addr string, req int, ps *util.PortSet) int {
   208  				t.CheckDeepEqual(test.expectedReq, req)
   209  				return mockRetrieveAvailablePort(util.Loopback, map[int]struct{}{}, test.availablePorts)(addr, req, ps)
   210  			})
   211  
   212  			entryManager := NewEntryManager(newTestForwarder())
   213  			entryManager.forwardedResources = sync.Map{}
   214  			for k, v := range test.forwardedResources {
   215  				entryManager.forwardedResources.Store(k, v)
   216  			}
   217  			rf := NewServicesForwarder(entryManager, "", "")
   218  			actualEntry := rf.getCurrentEntry(test.resource)
   219  
   220  			expectedEntry := test.expected
   221  			expectedEntry.resource = test.resource
   222  			t.CheckDeepEqual(expectedEntry, actualEntry, cmp.AllowUnexported(portForwardEntry{}, sync.Mutex{}))
   223  		})
   224  	}
   225  }
   226  
   227  func TestUserDefinedResources(t *testing.T) {
   228  	svc := &latest.PortForwardResource{
   229  		Type:      constants.Service,
   230  		Name:      "svc1",
   231  		Namespace: "test",
   232  		Port:      schemautil.FromInt(8080),
   233  	}
   234  
   235  	tests := []struct {
   236  		description       string
   237  		userResources     []*latest.PortForwardResource
   238  		namespaces        []string
   239  		expectedResources []string
   240  	}{
   241  		{
   242  			description: "pod should be found",
   243  			userResources: []*latest.PortForwardResource{
   244  				{Type: constants.Pod, Name: "pod", Port: schemautil.FromInt(9000)},
   245  			},
   246  			namespaces: []string{"test"},
   247  			expectedResources: []string{
   248  				"pod-pod-test-9000",
   249  			},
   250  		},
   251  		{
   252  			description: "pod not available",
   253  			userResources: []*latest.PortForwardResource{
   254  				{Type: constants.Pod, Name: "pod", Port: schemautil.FromInt(9000)},
   255  			},
   256  			namespaces:        []string{"test", "some"},
   257  			expectedResources: []string{},
   258  		},
   259  		{
   260  			userResources: []*latest.PortForwardResource{
   261  				{Type: constants.Pod, Name: "pod", Port: schemautil.FromInt(9000)},
   262  				{Type: constants.Pod, Name: "pod", Namespace: "some", Port: schemautil.FromInt(9001)},
   263  			},
   264  			namespaces: []string{"test", "some"},
   265  			expectedResources: []string{
   266  				"pod-pod-some-9001",
   267  			},
   268  		},
   269  		{
   270  			description: "pod should be found with namespace with template",
   271  			userResources: []*latest.PortForwardResource{
   272  				{Type: constants.Pod, Name: "pod", Namespace: "some-with-template-{{ .FOO }}", Port: schemautil.FromInt(9000)},
   273  			},
   274  			namespaces: []string{"test"},
   275  			expectedResources: []string{
   276  				"pod-pod-some-with-template-bar-9000",
   277  			},
   278  		},
   279  		{
   280  			description: "pod should be found with namespace with template",
   281  			userResources: []*latest.PortForwardResource{
   282  				{Type: constants.Pod, Name: "pod", Namespace: "some-with-template-{{ .FOO }}", Port: schemautil.FromInt(9000)},
   283  			},
   284  			namespaces: []string{"test", "another"},
   285  			expectedResources: []string{
   286  				"pod-pod-some-with-template-bar-9000",
   287  			},
   288  		},
   289  		{
   290  			description: "pod should be found with name with template",
   291  			userResources: []*latest.PortForwardResource{
   292  				{Type: constants.Pod, Name: "pod-{{ .FOO }}", Port: schemautil.FromInt(9000)},
   293  			},
   294  			namespaces: []string{"test"},
   295  			expectedResources: []string{
   296  				"pod-pod-bar-test-9000",
   297  			},
   298  		},
   299  		{
   300  			description: "pod should be found with name with template",
   301  			userResources: []*latest.PortForwardResource{
   302  				{Type: constants.Pod, Name: "pod-{{ .FOO }}", Namespace: "some-ns", Port: schemautil.FromInt(9000)},
   303  			},
   304  			namespaces: []string{"test", "another"},
   305  			expectedResources: []string{
   306  				"pod-pod-bar-some-ns-9000",
   307  			},
   308  		},
   309  	}
   310  
   311  	for _, test := range tests {
   312  		testutil.Run(t, test.description, func(t *testutil.T) {
   313  			testEvent.InitializeState([]latest.Pipeline{{}})
   314  			t.Override(&retrieveAvailablePort, mockRetrieveAvailablePort(util.Loopback, map[int]struct{}{}, []int{8080, 9000}))
   315  			t.Override(&retrieveServices, func(context.Context, string, []string, string) ([]*latest.PortForwardResource, error) {
   316  				return []*latest.PortForwardResource{svc}, nil
   317  			})
   318  
   319  			fakeForwarder := newTestForwarder()
   320  			entryManager := NewEntryManager(fakeForwarder)
   321  
   322  			util.OSEnviron = func() []string {
   323  				return []string{"FOO=bar"}
   324  			}
   325  
   326  			rf := NewUserDefinedForwarder(entryManager, "", test.userResources)
   327  			if err := rf.Start(context.Background(), ioutil.Discard, test.namespaces); err != nil {
   328  				t.Fatalf("error starting resource forwarder: %v", err)
   329  			}
   330  
   331  			// poll up to 10 seconds for the resources to be forwarded
   332  			err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   333  				return len(test.expectedResources) == length(&fakeForwarder.forwardedResources), nil
   334  			})
   335  			for _, key := range test.expectedResources {
   336  				pfe, found := fakeForwarder.forwardedResources.Load(key)
   337  				t.CheckTrue(found)
   338  				t.CheckNotNil(pfe)
   339  			}
   340  			if err != nil {
   341  				t.Fatalf("expected entries didn't match actual entries.\nExpected: %v\n  Actual: %v", test.expectedResources, print(&fakeForwarder.forwardedResources))
   342  			}
   343  		})
   344  	}
   345  }
   346  
   347  func mockClient(m kubernetes.Interface) func(string) (kubernetes.Interface, error) {
   348  	return func(string) (kubernetes.Interface, error) {
   349  		return m, nil
   350  	}
   351  }
   352  
   353  func TestRetrieveServices(t *testing.T) {
   354  	tests := []struct {
   355  		description string
   356  		namespaces  []string
   357  		services    []*v1.Service
   358  		expected    []*latest.PortForwardResource
   359  	}{
   360  		{
   361  			description: "multiple services in multiple namespaces",
   362  			namespaces:  []string{"test", "test1"},
   363  			services: []*v1.Service{
   364  				{
   365  					ObjectMeta: metav1.ObjectMeta{
   366  						Name:      "svc1",
   367  						Namespace: "test",
   368  						Labels: map[string]string{
   369  							label.RunIDLabel: "9876-6789",
   370  						},
   371  					},
   372  					Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 8080}}},
   373  				}, {
   374  					ObjectMeta: metav1.ObjectMeta{
   375  						Name:      "svc2",
   376  						Namespace: "test1",
   377  						Labels: map[string]string{
   378  							label.RunIDLabel: "9876-6789",
   379  						},
   380  					},
   381  					Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 8081}}},
   382  				},
   383  			},
   384  			expected: []*latest.PortForwardResource{{
   385  				Type:      constants.Service,
   386  				Name:      "svc1",
   387  				Namespace: "test",
   388  				Port:      schemautil.FromInt(8080),
   389  				Address:   "127.0.0.1",
   390  				LocalPort: 0,
   391  			}, {
   392  				Type:      constants.Service,
   393  				Name:      "svc2",
   394  				Namespace: "test1",
   395  				Port:      schemautil.FromInt(8081),
   396  				Address:   "127.0.0.1",
   397  				LocalPort: 0,
   398  			}},
   399  		}, {
   400  			description: "no services in given namespace",
   401  			namespaces:  []string{"randon"},
   402  			services: []*v1.Service{
   403  				{
   404  					ObjectMeta: metav1.ObjectMeta{
   405  						Name:      "svc1",
   406  						Namespace: "test",
   407  						Labels: map[string]string{
   408  							label.RunIDLabel: "9876-6789",
   409  						},
   410  					},
   411  					Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 8080}}},
   412  				},
   413  			},
   414  		}, {
   415  			description: "services present but does not expose any port",
   416  			namespaces:  []string{"test"},
   417  			services: []*v1.Service{
   418  				{
   419  					ObjectMeta: metav1.ObjectMeta{
   420  						Name:      "svc1",
   421  						Namespace: "test",
   422  						Labels: map[string]string{
   423  							label.RunIDLabel: "9876-6789",
   424  						},
   425  					},
   426  				},
   427  			},
   428  		},
   429  	}
   430  
   431  	for _, test := range tests {
   432  		testutil.Run(t, test.description, func(t *testutil.T) {
   433  			objs := make([]runtime.Object, len(test.services))
   434  			for i, s := range test.services {
   435  				objs[i] = s
   436  			}
   437  			client := fakekubeclientset.NewSimpleClientset(objs...)
   438  			t.Override(&kubernetesclient.Client, mockClient(client))
   439  
   440  			actual, err := retrieveServiceResources(context.Background(), fmt.Sprintf("%s=9876-6789", label.RunIDLabel), test.namespaces, "")
   441  
   442  			t.CheckNoError(err)
   443  			t.CheckDeepEqual(test.expected, actual)
   444  		})
   445  	}
   446  }