istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/plugin/plugin_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package plugin 16 17 import ( 18 "fmt" 19 "net/http" 20 "net/http/httptest" 21 "reflect" 22 "testing" 23 24 "github.com/containernetworking/cni/pkg/skel" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 29 "istio.io/api/annotation" 30 "istio.io/api/label" 31 "istio.io/istio/pkg/config/constants" 32 "istio.io/istio/pkg/kube" 33 "istio.io/istio/pkg/test/util/assert" 34 ) 35 36 const ( 37 testPodName = "testPod" 38 testNSName = "testNS" 39 testSandboxDirectory = "/tmp" 40 invalidVersion = "0.1.0" 41 preVersion = "0.2.0" 42 ) 43 44 var mockConfTmpl = `{ 45 "cniVersion": "%s", 46 "name": "istio-plugin-sample-test", 47 "type": "sample", 48 "capabilities": { 49 "testCapability": false 50 }, 51 "ipam": { 52 "type": "testIPAM" 53 }, 54 "dns": { 55 "nameservers": ["testNameServer"], 56 "domain": "testDomain", 57 "search": ["testSearch"], 58 "options": ["testOption"] 59 }, 60 "prevResult": { 61 "cniversion": "%s", 62 "interfaces": [ 63 { 64 "name": "%s", 65 "sandbox": "%s" 66 } 67 ], 68 "ips": [ 69 { 70 "version": "4", 71 "address": "10.0.0.2/24", 72 "gateway": "10.0.0.1", 73 "interface": 0 74 } 75 ], 76 "routes": [] 77 78 }, 79 "log_level": "debug", 80 "cni_event_address": "%s", 81 "ambient_enabled": %t, 82 "kubernetes": { 83 "k8s_api_root": "APIRoot", 84 "kubeconfig": "testK8sConfig", 85 "intercept_type": "%s", 86 "node_name": "testNodeName", 87 "exclude_namespaces": ["testExcludeNS"], 88 "cni_bin_dir": "/testDirectory" 89 } 90 }` 91 92 type mockInterceptRuleMgr struct { 93 lastRedirect []*Redirect 94 } 95 96 func buildMockConf(ambientEnabled bool, eventURL string) string { 97 return fmt.Sprintf( 98 mockConfTmpl, 99 "1.0.0", 100 "1.0.0", 101 "eth0", 102 testSandboxDirectory, 103 eventURL, 104 ambientEnabled, 105 "mock", 106 ) 107 } 108 109 func buildFakePodAndNSForClient() (*corev1.Pod, *corev1.Namespace) { 110 proxy := corev1.Container{Name: "mockContainer"} 111 app := corev1.Container{Name: "foo-init"} 112 fakePod := &corev1.Pod{ 113 TypeMeta: metav1.TypeMeta{ 114 APIVersion: "core/v1", 115 Kind: "Pod", 116 }, 117 ObjectMeta: metav1.ObjectMeta{ 118 Name: testPodName, 119 Namespace: testNSName, 120 Annotations: map[string]string{}, 121 }, 122 Spec: corev1.PodSpec{ 123 Containers: []corev1.Container{app, proxy}, 124 }, 125 } 126 127 fakeNS := &corev1.Namespace{ 128 TypeMeta: metav1.TypeMeta{ 129 APIVersion: "core/v1", 130 Kind: "Namespace", 131 }, 132 ObjectMeta: metav1.ObjectMeta{ 133 Name: testNSName, 134 Namespace: "", 135 Labels: map[string]string{}, 136 }, 137 } 138 139 return fakePod, fakeNS 140 } 141 142 func (mrdir *mockInterceptRuleMgr) Program(podName, netns string, redirect *Redirect) error { 143 mrdir.lastRedirect = append(mrdir.lastRedirect, redirect) 144 return nil 145 } 146 147 // returns the test server URL and a dispose func for the test server 148 func setupCNIEventClientWithMockServer(serverErr bool) (string, func() bool) { 149 cniAddServerCalled := false 150 // replace the global CNI client with mock 151 newCNIClient = func(address, path string) CNIEventClient { 152 c := http.DefaultClient 153 154 eventC := CNIEventClient{ 155 client: c, 156 url: address + path, 157 } 158 return eventC 159 } 160 161 testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 162 cniAddServerCalled = true 163 if serverErr { 164 res.WriteHeader(http.StatusInternalServerError) 165 res.Write([]byte("server not happy")) 166 return 167 } 168 res.WriteHeader(http.StatusOK) 169 res.Write([]byte("server happy")) 170 })) 171 172 return testServer.URL, func() bool { 173 testServer.Close() 174 return cniAddServerCalled 175 } 176 } 177 178 func buildCmdArgs(stdinData, podName, podNamespace string) *skel.CmdArgs { 179 return &skel.CmdArgs{ 180 ContainerID: "testContainerID", 181 Netns: testSandboxDirectory, 182 IfName: "eth0", 183 Args: fmt.Sprintf("K8S_POD_NAMESPACE=%s;K8S_POD_NAME=%s", podNamespace, podName), 184 Path: "/tmp", 185 StdinData: []byte(stdinData), 186 } 187 } 188 189 func testCmdAddExpectFail(t *testing.T, stdinData string, objects ...runtime.Object) *mockInterceptRuleMgr { 190 args := buildCmdArgs(stdinData, testPodName, testNSName) 191 192 conf, err := parseConfig(args.StdinData) 193 if err != nil { 194 t.Fatalf("config parse failed with error: %v", err) 195 } 196 197 // Create a kube client 198 client := kube.NewFakeClient(objects...) 199 200 mockRedir := &mockInterceptRuleMgr{} 201 err = doAddRun(args, conf, client.Kube(), mockRedir) 202 if err == nil { 203 t.Fatal("expected to fail, but did not!") 204 } 205 206 return mockRedir 207 } 208 209 func testDoAddRun(t *testing.T, stdinData, nsName string, objects ...runtime.Object) *mockInterceptRuleMgr { 210 args := buildCmdArgs(stdinData, testPodName, nsName) 211 212 conf, err := parseConfig(args.StdinData) 213 if err != nil { 214 t.Fatalf("config parse failed with error: %v", err) 215 } 216 217 // Create a kube client 218 client := kube.NewFakeClient(objects...) 219 220 mockRedir := &mockInterceptRuleMgr{} 221 err = doAddRun(args, conf, client.Kube(), mockRedir) 222 if err != nil { 223 t.Fatalf("failed with error: %v", err) 224 } 225 226 return mockRedir 227 } 228 229 func TestCmdAddAmbientEnabledOnNS(t *testing.T) { 230 url, serverClose := setupCNIEventClientWithMockServer(false) 231 232 cniConf := buildMockConf(true, url) 233 234 pod, ns := buildFakePodAndNSForClient() 235 ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient} 236 237 testDoAddRun(t, cniConf, testNSName, pod, ns) 238 239 wasCalled := serverClose() 240 // Pod in namespace with enabled ambient label, should be added to mesh 241 assert.Equal(t, wasCalled, true) 242 } 243 244 func TestCmdAddAmbientEnabledOnNSServerFails(t *testing.T) { 245 url, serverClose := setupCNIEventClientWithMockServer(true) 246 247 cniConf := buildMockConf(true, url) 248 249 pod, ns := buildFakePodAndNSForClient() 250 ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient} 251 252 testCmdAddExpectFail(t, cniConf, pod, ns) 253 254 wasCalled := serverClose() 255 // server called, but errored 256 assert.Equal(t, wasCalled, true) 257 } 258 259 func TestCmdAddPodWithProxySidecarAmbientEnabledNS(t *testing.T) { 260 url, serverClose := setupCNIEventClientWithMockServer(false) 261 262 cniConf := buildMockConf(true, url) 263 264 pod, ns := buildFakePodAndNSForClient() 265 266 proxy := corev1.Container{Name: "istio-proxy"} 267 app := corev1.Container{Name: "app"} 268 269 pod.Spec.Containers = []corev1.Container{app, proxy} 270 pod.ObjectMeta.Annotations = map[string]string{annotation.SidecarStatus.Name: "true"} 271 ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient} 272 273 testDoAddRun(t, cniConf, testNSName, pod, ns) 274 275 wasCalled := serverClose() 276 // Pod has sidecar annotation from injector, should not be added to mesh 277 assert.Equal(t, wasCalled, false) 278 } 279 280 func TestCmdAddPodWithGenericSidecar(t *testing.T) { 281 url, serverClose := setupCNIEventClientWithMockServer(false) 282 283 cniConf := buildMockConf(true, url) 284 285 pod, ns := buildFakePodAndNSForClient() 286 287 proxy := corev1.Container{Name: "istio-proxy"} 288 app := corev1.Container{Name: "app"} 289 290 pod.Spec.Containers = []corev1.Container{app, proxy} 291 ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient} 292 293 testDoAddRun(t, cniConf, testNSName, pod, ns) 294 295 wasCalled := serverClose() 296 // Pod should be added to ambient mesh 297 assert.Equal(t, wasCalled, true) 298 } 299 300 func TestCmdAddPodDisabledLabel(t *testing.T) { 301 url, serverClose := setupCNIEventClientWithMockServer(false) 302 303 cniConf := buildMockConf(true, url) 304 305 pod, ns := buildFakePodAndNSForClient() 306 307 app := corev1.Container{Name: "app"} 308 ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient} 309 pod.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeNone} 310 pod.Spec.Containers = []corev1.Container{app} 311 312 testDoAddRun(t, cniConf, testNSName, pod, ns) 313 314 wasCalled := serverClose() 315 // Pod has an explicit opt-out label, should not be added to ambient mesh 316 assert.Equal(t, wasCalled, false) 317 } 318 319 func TestCmdAddPodEnabledNamespaceDisabled(t *testing.T) { 320 url, serverClose := setupCNIEventClientWithMockServer(false) 321 322 cniConf := buildMockConf(true, url) 323 324 pod, ns := buildFakePodAndNSForClient() 325 326 app := corev1.Container{Name: "app"} 327 pod.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient} 328 pod.Spec.Containers = []corev1.Container{app} 329 330 testDoAddRun(t, cniConf, testNSName, pod, ns) 331 332 wasCalled := serverClose() 333 assert.Equal(t, wasCalled, true) 334 } 335 336 func TestCmdAddPodInExcludedNamespace(t *testing.T) { 337 url, serverClose := setupCNIEventClientWithMockServer(false) 338 339 cniConf := buildMockConf(true, url) 340 341 excludedNS := "testExcludeNS" 342 pod, ns := buildFakePodAndNSForClient() 343 344 app := corev1.Container{Name: "app"} 345 ns.ObjectMeta.Name = excludedNS 346 ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.AmbientRedirectionEnabled} 347 348 pod.ObjectMeta.Namespace = excludedNS 349 pod.Spec.Containers = []corev1.Container{app} 350 351 testDoAddRun(t, cniConf, excludedNS, pod, ns) 352 353 wasCalled := serverClose() 354 // If the pod is being added to a namespace that is explicitly excluded by plugin config denylist 355 // it should never be added, even if the namespace has the annotation 356 assert.Equal(t, wasCalled, false) 357 } 358 359 func TestCmdAdd(t *testing.T) { 360 pod, ns := buildFakePodAndNSForClient() 361 testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 362 } 363 364 func TestCmdAddTwoContainersWithAnnotation(t *testing.T) { 365 pod, ns := buildFakePodAndNSForClient() 366 367 pod.Spec.Containers[0].Name = "mockContainer" 368 pod.Spec.Containers[1].Name = "istio-proxy" 369 pod.ObjectMeta.Annotations[injectAnnotationKey] = "false" 370 371 testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 372 } 373 374 func TestCmdAddTwoContainersWithLabel(t *testing.T) { 375 pod, ns := buildFakePodAndNSForClient() 376 pod.Spec.Containers[0].Name = "mockContainer" 377 pod.Spec.Containers[1].Name = "istio-proxy" 378 pod.ObjectMeta.Annotations[label.SidecarInject.Name] = "false" 379 380 testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 381 } 382 383 func TestCmdAddTwoContainers(t *testing.T) { 384 pod, ns := buildFakePodAndNSForClient() 385 386 pod.Spec.Containers[0].Name = "mockContainer" 387 pod.Spec.Containers[1].Name = "istio-proxy" 388 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 389 390 mockIntercept := testDoAddRun(t, buildMockConf(false, ""), testNSName, pod, ns) 391 392 if len(mockIntercept.lastRedirect) == 0 { 393 t.Fatalf("expected nsenterFunc to be called") 394 } 395 r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1] 396 if r.includeInboundPorts != "*" { 397 t.Fatalf("expect includeInboundPorts has value '*' set by istio, actual %v", r.includeInboundPorts) 398 } 399 } 400 401 func TestCmdAddTwoContainersWithStarInboundPort(t *testing.T) { 402 pod, ns := buildFakePodAndNSForClient() 403 404 pod.Spec.Containers[0].Name = "mockContainer" 405 pod.Spec.Containers[1].Name = "istio-proxy" 406 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 407 pod.ObjectMeta.Annotations[includeInboundPortsKey] = "*" 408 409 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 410 411 if len(mockIntercept.lastRedirect) != 1 { 412 t.Fatalf("expected nsenterFunc to be called") 413 } 414 r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1] 415 if r.includeInboundPorts != "*" { 416 t.Fatalf("expect includeInboundPorts is '*', actual %v", r.includeInboundPorts) 417 } 418 } 419 420 func TestCmdAddTwoContainersWithEmptyInboundPort(t *testing.T) { 421 pod, ns := buildFakePodAndNSForClient() 422 423 pod.Spec.Containers[0].Name = "mockContainer" 424 pod.Spec.Containers[1].Name = "istio-proxy" 425 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 426 pod.ObjectMeta.Annotations[includeInboundPortsKey] = "" 427 428 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 429 430 if len(mockIntercept.lastRedirect) != 1 { 431 t.Fatalf("expected nsenterFunc to be called") 432 } 433 r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1] 434 if r.includeInboundPorts != "" { 435 t.Fatalf("expect includeInboundPorts is \"\", actual %v", r.includeInboundPorts) 436 } 437 } 438 439 func TestCmdAddTwoContainersWithEmptyExcludeInboundPort(t *testing.T) { 440 pod, ns := buildFakePodAndNSForClient() 441 pod.Spec.Containers[0].Name = "mockContainer" 442 pod.Spec.Containers[1].Name = "istio-proxy" 443 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 444 pod.ObjectMeta.Annotations[excludeInboundPortsKey] = "" 445 446 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 447 448 if len(mockIntercept.lastRedirect) != 1 { 449 t.Fatalf("expected nsenterFunc to be called") 450 } 451 r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1] 452 if r.excludeInboundPorts != "15020,15021,15090" { 453 t.Fatalf("expect excludeInboundPorts is \"15090\", actual %v", r.excludeInboundPorts) 454 } 455 } 456 457 func TestCmdAddTwoContainersWithExplictExcludeInboundPort(t *testing.T) { 458 pod, ns := buildFakePodAndNSForClient() 459 pod.Spec.Containers[0].Name = "mockContainer" 460 pod.Spec.Containers[1].Name = "istio-proxy" 461 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 462 pod.ObjectMeta.Annotations[excludeInboundPortsKey] = "3306" 463 464 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 465 466 if len(mockIntercept.lastRedirect) == 0 { 467 t.Fatalf("expected nsenterFunc to be called") 468 } 469 r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1] 470 if r.excludeInboundPorts != "3306,15020,15021,15090" { 471 t.Fatalf("expect excludeInboundPorts is \"3306,15090\", actual %v", r.excludeInboundPorts) 472 } 473 } 474 475 func TestCmdAddTwoContainersWithoutSideCar(t *testing.T) { 476 pod, ns := buildFakePodAndNSForClient() 477 pod.Spec.Containers[0].Name = "mockContainer" 478 pod.Spec.Containers[1].Name = "istio-proxy" 479 480 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 481 482 if len(mockIntercept.lastRedirect) != 0 { 483 t.Fatalf("Didnt Expect nsenterFunc to be called because this pod does not contain a sidecar") 484 } 485 } 486 487 func TestCmdAddExcludePod(t *testing.T) { 488 pod, ns := buildFakePodAndNSForClient() 489 490 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), "testExcludeNS", pod, ns) 491 if len(mockIntercept.lastRedirect) != 0 { 492 t.Fatalf("failed to exclude pod") 493 } 494 } 495 496 func TestCmdAddExcludePodWithIstioInitContainer(t *testing.T) { 497 pod, ns := buildFakePodAndNSForClient() 498 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 499 pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{Name: "istio-init"}) 500 501 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 502 503 if len(mockIntercept.lastRedirect) != 0 { 504 t.Fatalf("failed to exclude pod") 505 } 506 } 507 508 func TestCmdAddExcludePodWithEnvoyDisableEnv(t *testing.T) { 509 pod, ns := buildFakePodAndNSForClient() 510 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 511 pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ 512 Name: "istio-init", 513 Env: []corev1.EnvVar{{Name: "DISABLE_ENVOY", Value: "true"}}, 514 }) 515 516 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 517 518 if len(mockIntercept.lastRedirect) != 0 { 519 t.Fatalf("failed to exclude pod") 520 } 521 } 522 523 func TestCmdAddNoPrevResult(t *testing.T) { 524 confNoPrevResult := `{ 525 "cniVersion": "1.0.0", 526 "name": "istio-plugin-sample-test", 527 "type": "sample", 528 "runtimeconfig": { 529 "sampleconfig": [] 530 }, 531 "loglevel": "debug", 532 "ambient_enabled": %t, 533 "kubernetes": { 534 "k8sapiroot": "APIRoot", 535 "kubeconfig": "testK8sConfig", 536 "nodename": "testNodeName", 537 "excludenamespaces": "testNS", 538 "cnibindir": "/testDirectory" 539 } 540 }` 541 542 pod, ns := buildFakePodAndNSForClient() 543 testDoAddRun(t, fmt.Sprintf(confNoPrevResult, false), testNSName, pod, ns) 544 testDoAddRun(t, fmt.Sprintf(confNoPrevResult, true), testNSName, pod, ns) 545 } 546 547 func TestCmdAddEnableDualStack(t *testing.T) { 548 pod, ns := buildFakePodAndNSForClient() 549 pod.ObjectMeta.Annotations[sidecarStatusKey] = "true" 550 pod.Spec.Containers = []corev1.Container{ 551 { 552 Name: "istio-proxy", 553 Env: []corev1.EnvVar{{Name: "ISTIO_DUAL_STACK", Value: "true"}}, 554 }, {Name: "mockContainer"}, 555 } 556 557 mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns) 558 559 if len(mockIntercept.lastRedirect) == 0 { 560 t.Fatalf("expected nsenterFunc to be called") 561 } 562 r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1] 563 if !r.dualStack { 564 t.Fatalf("expect dualStack is true, actual %v", r.dualStack) 565 } 566 } 567 568 func Test_dedupPorts(t *testing.T) { 569 type args struct { 570 ports []string 571 } 572 tests := []struct { 573 name string 574 args args 575 want []string 576 }{ 577 { 578 name: "No duplicates", 579 args: args{ports: []string{"1234", "2345"}}, 580 want: []string{"1234", "2345"}, 581 }, 582 { 583 name: "Sequential Duplicates", 584 args: args{ports: []string{"1234", "1234", "2345", "2345"}}, 585 want: []string{"1234", "2345"}, 586 }, 587 { 588 name: "Mixed Duplicates", 589 args: args{ports: []string{"1234", "2345", "1234", "2345"}}, 590 want: []string{"1234", "2345"}, 591 }, 592 { 593 name: "Empty", 594 args: args{ports: []string{}}, 595 want: []string{}, 596 }, 597 { 598 name: "Non-parseable", 599 args: args{ports: []string{"abcd", "2345", "abcd"}}, 600 want: []string{"abcd", "2345"}, 601 }, 602 } 603 for _, tt := range tests { 604 t.Run(tt.name, func(t *testing.T) { 605 if got := dedupPorts(tt.args.ports); !reflect.DeepEqual(got, tt.want) { 606 t.Errorf("dedupPorts() = %v, want %v", got, tt.want) 607 } 608 }) 609 } 610 }