github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/pod_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  	"io/ioutil"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/util/wait"
    29  	"k8s.io/apimachinery/pkg/watch"
    30  
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    33  	schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    35  	"github.com/GoogleContainerTools/skaffold/testutil"
    36  	testEvent "github.com/GoogleContainerTools/skaffold/testutil/event"
    37  )
    38  
    39  func TestAutomaticPortForwardPod(t *testing.T) {
    40  	tests := []struct {
    41  		description     string
    42  		pods            []*v1.Pod
    43  		forwarder       *testForwarder
    44  		availablePorts  []int
    45  		expectedPorts   []int
    46  		expectedEntries map[string]*portForwardEntry
    47  		shouldErr       bool
    48  	}{
    49  		{
    50  			description:    "single container port",
    51  			availablePorts: []int{8080},
    52  			expectedPorts:  []int{8080},
    53  			expectedEntries: map[string]*portForwardEntry{
    54  				"owner-containername-namespace-portname-8080": {
    55  					resourceVersion: 1,
    56  					podName:         "podname",
    57  					containerName:   "containername",
    58  					resource: latest.PortForwardResource{
    59  						Type:      "pod",
    60  						Name:      "podname",
    61  						Namespace: "namespace",
    62  						Port:      schemautil.FromInt(8080),
    63  						Address:   "127.0.0.1",
    64  						LocalPort: 0,
    65  					},
    66  					ownerReference:         "owner",
    67  					automaticPodForwarding: true,
    68  					portName:               "portname",
    69  					localPort:              8080,
    70  				},
    71  			},
    72  			pods: []*v1.Pod{
    73  				{
    74  					ObjectMeta: metav1.ObjectMeta{
    75  						Name:            "podname",
    76  						ResourceVersion: "1",
    77  						Namespace:       "namespace",
    78  					},
    79  					Spec: v1.PodSpec{
    80  						Containers: []v1.Container{
    81  							{
    82  								Name: "containername",
    83  								Ports: []v1.ContainerPort{
    84  									{
    85  										ContainerPort: 8080,
    86  										Name:          "portname",
    87  									},
    88  								},
    89  							},
    90  						},
    91  					},
    92  				},
    93  			},
    94  		},
    95  		{
    96  			description:    "unavailable container port",
    97  			availablePorts: []int{9000},
    98  			expectedPorts:  []int{9000},
    99  			expectedEntries: map[string]*portForwardEntry{
   100  				"owner-containername-namespace-portname-8080": {
   101  					resourceVersion: 1,
   102  					podName:         "podname",
   103  					resource: latest.PortForwardResource{
   104  						Type:      "pod",
   105  						Name:      "podname",
   106  						Namespace: "namespace",
   107  						Port:      schemautil.FromInt(8080),
   108  						Address:   "127.0.0.1",
   109  						LocalPort: 0,
   110  					},
   111  					ownerReference:         "owner",
   112  					automaticPodForwarding: true,
   113  					containerName:          "containername",
   114  					portName:               "portname",
   115  					localPort:              9000,
   116  				},
   117  			},
   118  			pods: []*v1.Pod{
   119  				{
   120  					ObjectMeta: metav1.ObjectMeta{
   121  						Name:            "podname",
   122  						ResourceVersion: "1",
   123  						Namespace:       "namespace",
   124  					},
   125  					Spec: v1.PodSpec{
   126  						Containers: []v1.Container{
   127  							{
   128  								Name: "containername",
   129  								Ports: []v1.ContainerPort{
   130  									{
   131  										ContainerPort: 8080,
   132  										Name:          "portname",
   133  									},
   134  								},
   135  							},
   136  						},
   137  					},
   138  				},
   139  			},
   140  		},
   141  		{
   142  			description:     "bad resource version",
   143  			availablePorts:  []int{8080},
   144  			expectedPorts:   nil,
   145  			shouldErr:       true,
   146  			expectedEntries: nil,
   147  			pods: []*v1.Pod{
   148  				{
   149  					ObjectMeta: metav1.ObjectMeta{
   150  						Name:            "podname",
   151  						ResourceVersion: "10000000000a",
   152  						Namespace:       "namespace",
   153  					},
   154  					Spec: v1.PodSpec{
   155  						Containers: []v1.Container{
   156  							{
   157  								Name: "containername",
   158  								Ports: []v1.ContainerPort{
   159  									{
   160  										ContainerPort: 8080,
   161  										Name:          "portname",
   162  									},
   163  								},
   164  							},
   165  						},
   166  					},
   167  				},
   168  			},
   169  		},
   170  		{
   171  			description:    "two different container ports",
   172  			availablePorts: []int{8080, 50051},
   173  			expectedPorts:  []int{8080, 50051},
   174  			expectedEntries: map[string]*portForwardEntry{
   175  				"owner-containername-namespace-portname-8080": {
   176  					resourceVersion: 1,
   177  					podName:         "podname",
   178  					containerName:   "containername",
   179  					resource: latest.PortForwardResource{
   180  						Type:      "pod",
   181  						Name:      "podname",
   182  						Namespace: "namespace",
   183  						Port:      schemautil.FromInt(8080),
   184  						Address:   "127.0.0.1",
   185  						LocalPort: 0,
   186  					},
   187  					ownerReference:         "owner",
   188  					portName:               "portname",
   189  					automaticPodForwarding: true,
   190  					localPort:              8080,
   191  				},
   192  				"owner-containername2-namespace2-portname2-50051": {
   193  					resourceVersion: 1,
   194  					podName:         "podname2",
   195  					containerName:   "containername2",
   196  					resource: latest.PortForwardResource{
   197  						Type:      "pod",
   198  						Name:      "podname2",
   199  						Namespace: "namespace2",
   200  						Port:      schemautil.FromInt(50051),
   201  						Address:   "127.0.0.1",
   202  						LocalPort: 0,
   203  					},
   204  					ownerReference:         "owner",
   205  					portName:               "portname2",
   206  					automaticPodForwarding: true,
   207  					localPort:              50051,
   208  				},
   209  			},
   210  			pods: []*v1.Pod{
   211  				{
   212  					ObjectMeta: metav1.ObjectMeta{
   213  						Name:            "podname",
   214  						ResourceVersion: "1",
   215  						Namespace:       "namespace",
   216  					},
   217  					Spec: v1.PodSpec{
   218  						Containers: []v1.Container{
   219  							{
   220  								Name: "containername",
   221  								Ports: []v1.ContainerPort{
   222  									{
   223  										ContainerPort: 8080,
   224  										Name:          "portname",
   225  									},
   226  								},
   227  							},
   228  						},
   229  					},
   230  				},
   231  				{
   232  					ObjectMeta: metav1.ObjectMeta{
   233  						Name:            "podname2",
   234  						ResourceVersion: "1",
   235  						Namespace:       "namespace2",
   236  					},
   237  					Spec: v1.PodSpec{
   238  						Containers: []v1.Container{
   239  							{
   240  								Name: "containername2",
   241  								Ports: []v1.ContainerPort{
   242  									{
   243  										ContainerPort: 50051,
   244  										Name:          "portname2",
   245  									},
   246  								},
   247  							},
   248  						},
   249  					},
   250  				},
   251  			},
   252  		},
   253  		{
   254  			description:    "two same container ports",
   255  			availablePorts: []int{8080, 9000},
   256  			expectedPorts:  []int{8080, 9000},
   257  			expectedEntries: map[string]*portForwardEntry{
   258  				"owner-containername-namespace-portname-8080": {
   259  					resourceVersion: 1,
   260  					podName:         "podname",
   261  					containerName:   "containername",
   262  					portName:        "portname",
   263  					resource: latest.PortForwardResource{
   264  						Type:      "pod",
   265  						Name:      "podname",
   266  						Namespace: "namespace",
   267  						Port:      schemautil.FromInt(8080),
   268  						Address:   "127.0.0.1",
   269  						LocalPort: 0,
   270  					},
   271  					ownerReference:         "owner",
   272  					automaticPodForwarding: true,
   273  					localPort:              8080,
   274  				},
   275  				"owner-containername2-namespace2-portname2-8080": {
   276  					resourceVersion: 1,
   277  					podName:         "podname2",
   278  					containerName:   "containername2",
   279  					portName:        "portname2",
   280  					resource: latest.PortForwardResource{
   281  						Type:      "pod",
   282  						Name:      "podname2",
   283  						Namespace: "namespace2",
   284  						Port:      schemautil.FromInt(8080),
   285  						Address:   "127.0.0.1",
   286  						LocalPort: 0,
   287  					},
   288  					ownerReference:         "owner",
   289  					automaticPodForwarding: true,
   290  					localPort:              9000,
   291  				},
   292  			},
   293  			pods: []*v1.Pod{
   294  				{
   295  					ObjectMeta: metav1.ObjectMeta{
   296  						Name:            "podname",
   297  						ResourceVersion: "1",
   298  						Namespace:       "namespace",
   299  					},
   300  					Spec: v1.PodSpec{
   301  						Containers: []v1.Container{
   302  							{
   303  								Name: "containername",
   304  								Ports: []v1.ContainerPort{
   305  									{
   306  										ContainerPort: 8080,
   307  										Name:          "portname",
   308  									},
   309  								},
   310  							},
   311  						},
   312  					},
   313  				},
   314  				{
   315  					ObjectMeta: metav1.ObjectMeta{
   316  						Name:            "podname2",
   317  						ResourceVersion: "1",
   318  						Namespace:       "namespace2",
   319  					},
   320  					Spec: v1.PodSpec{
   321  						Containers: []v1.Container{
   322  							{
   323  								Name: "containername2",
   324  								Ports: []v1.ContainerPort{
   325  									{
   326  										ContainerPort: 8080,
   327  										Name:          "portname2",
   328  									},
   329  								},
   330  							},
   331  						},
   332  					},
   333  				},
   334  			},
   335  		},
   336  		{
   337  			description:    "updated pod gets port forwarded",
   338  			availablePorts: []int{8080},
   339  			expectedPorts:  []int{8080},
   340  			expectedEntries: map[string]*portForwardEntry{
   341  				"owner-containername-namespace-portname-8080": {
   342  					resourceVersion: 2,
   343  					podName:         "podname",
   344  					containerName:   "containername",
   345  					portName:        "portname",
   346  					resource: latest.PortForwardResource{
   347  						Type:      "pod",
   348  						Name:      "podname",
   349  						Namespace: "namespace",
   350  						Port:      schemautil.FromInt(8080),
   351  						Address:   "127.0.0.1",
   352  						LocalPort: 0,
   353  					},
   354  					ownerReference:         "owner",
   355  					automaticPodForwarding: true,
   356  					localPort:              8080,
   357  				},
   358  			},
   359  			pods: []*v1.Pod{
   360  				{
   361  					ObjectMeta: metav1.ObjectMeta{
   362  						Name:            "podname",
   363  						ResourceVersion: "1",
   364  						Namespace:       "namespace",
   365  					},
   366  					Spec: v1.PodSpec{
   367  						Containers: []v1.Container{
   368  							{
   369  								Name: "containername",
   370  								Ports: []v1.ContainerPort{
   371  									{
   372  										ContainerPort: 8080,
   373  										Name:          "portname",
   374  									},
   375  								},
   376  							},
   377  						},
   378  					},
   379  				},
   380  				{
   381  					ObjectMeta: metav1.ObjectMeta{
   382  						Name:            "podname",
   383  						ResourceVersion: "2",
   384  						Namespace:       "namespace",
   385  					},
   386  					Spec: v1.PodSpec{
   387  						Containers: []v1.Container{
   388  							{
   389  								Name: "containername",
   390  								Ports: []v1.ContainerPort{
   391  									{
   392  										ContainerPort: 8080,
   393  										Name:          "portname",
   394  									},
   395  								},
   396  							},
   397  						},
   398  					},
   399  				},
   400  			},
   401  		},
   402  	}
   403  	for _, test := range tests {
   404  		testutil.Run(t, test.description, func(t *testutil.T) {
   405  			testEvent.InitializeState([]latest.Pipeline{{}})
   406  			taken := map[int]struct{}{}
   407  			t.Override(&retrieveAvailablePort, mockRetrieveAvailablePort(util.Loopback, taken, test.availablePorts))
   408  			t.Override(&topLevelOwnerKey, func(context.Context, metav1.Object, string, string) string { return "owner" })
   409  
   410  			if test.forwarder == nil {
   411  				test.forwarder = newTestForwarder()
   412  			}
   413  			entryManager := NewEntryManager(nil)
   414  			entryManager.entryForwarder = test.forwarder
   415  
   416  			p := NewWatchingPodForwarder(entryManager, "", kubernetes.NewImageList(), allPorts)
   417  			p.Start(context.Background(), ioutil.Discard, nil)
   418  			for _, pod := range test.pods {
   419  				err := p.portForwardPod(context.Background(), pod)
   420  				t.CheckError(test.shouldErr, err)
   421  			}
   422  
   423  			t.CheckDeepEqual(test.expectedPorts, test.forwarder.forwardedPorts.List())
   424  
   425  			// cmp.Diff cannot access unexported fields, so use reflect.DeepEqual here directly
   426  			for k, v := range test.expectedEntries {
   427  				if frv, found := test.forwarder.forwardedResources.Load(k); !found {
   428  					t.Errorf("Forwarded entries missing key %v, value %v", k, v)
   429  				} else if !reflect.DeepEqual(v, frv) {
   430  					t.Errorf("Forwarded entries mismatch for key %v: Expected %v, Actual  %v", k, v, frv)
   431  				}
   432  			}
   433  		})
   434  	}
   435  }
   436  
   437  func TestStartPodForwarder(t *testing.T) {
   438  	pod := &v1.Pod{
   439  		ObjectMeta: metav1.ObjectMeta{
   440  			Namespace:       "default",
   441  			ResourceVersion: "9",
   442  		},
   443  		Spec: v1.PodSpec{
   444  			Containers: []v1.Container{{
   445  				Name:  "mycontainer",
   446  				Image: "image",
   447  				Ports: []v1.ContainerPort{{
   448  					Name:          "myport",
   449  					ContainerPort: 8080,
   450  				}},
   451  			}},
   452  		},
   453  		Status: v1.PodStatus{
   454  			Phase: v1.PodRunning,
   455  		},
   456  	}
   457  
   458  	tests := []struct {
   459  		description   string
   460  		entryExpected bool
   461  		event         kubernetes.PodEvent
   462  	}{
   463  		{
   464  			description:   "pod modified event",
   465  			entryExpected: true,
   466  			event: kubernetes.PodEvent{
   467  				Type: watch.Modified,
   468  				Pod:  pod,
   469  			},
   470  		},
   471  		{
   472  			description: "event is deleted",
   473  			event: kubernetes.PodEvent{
   474  				Type: watch.Deleted,
   475  				Pod:  pod,
   476  			},
   477  		},
   478  	}
   479  
   480  	for _, test := range tests {
   481  		testutil.Run(t, test.description, func(t *testutil.T) {
   482  			testEvent.InitializeState([]latest.Pipeline{{}})
   483  			t.Override(&topLevelOwnerKey, func(context.Context, metav1.Object, string, string) string { return "owner" })
   484  			t.Override(&newPodWatcher, func(kubernetes.PodSelector) kubernetes.PodWatcher {
   485  				return &fakePodWatcher{
   486  					events: []kubernetes.PodEvent{test.event},
   487  				}
   488  			})
   489  
   490  			imageList := kubernetes.NewImageList()
   491  			imageList.Add("image")
   492  
   493  			fakeForwarder := newTestForwarder()
   494  			entryManager := NewEntryManager(fakeForwarder)
   495  
   496  			p := NewWatchingPodForwarder(entryManager, "", imageList, allPorts)
   497  			p.Start(context.Background(), ioutil.Discard, nil)
   498  
   499  			// wait for the pod resource to be forwarded
   500  			err := wait.PollImmediate(10*time.Millisecond, 100*time.Millisecond, func() (bool, error) {
   501  				_, ok := fakeForwarder.forwardedResources.Load("owner-mycontainer-default-myport-8080")
   502  				return ok, nil
   503  			})
   504  			if err != nil && test.entryExpected {
   505  				t.Fatalf("expected entry wasn't forwarded: %v", err)
   506  			}
   507  		})
   508  	}
   509  }
   510  
   511  type fakePodWatcher struct {
   512  	events   []kubernetes.PodEvent
   513  	receiver chan<- kubernetes.PodEvent
   514  }
   515  
   516  func (f *fakePodWatcher) Register(receiver chan<- kubernetes.PodEvent) {
   517  	f.receiver = receiver
   518  }
   519  
   520  func (f *fakePodWatcher) Deregister(_ chan<- kubernetes.PodEvent) {} // noop
   521  
   522  func (f *fakePodWatcher) Start(_ context.Context, _ string, _ []string) (func(), error) {
   523  	go func() {
   524  		for _, event := range f.events {
   525  			f.receiver <- event
   526  		}
   527  	}()
   528  
   529  	return func() {}, nil
   530  }