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 }