istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/inject/inject_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 inject 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "os" 22 "strings" 23 "testing" 24 "time" 25 26 "github.com/google/go-cmp/cmp" 27 securityv1 "github.com/openshift/api/security/v1" 28 "google.golang.org/protobuf/testing/protocmp" 29 "google.golang.org/protobuf/types/known/durationpb" 30 "google.golang.org/protobuf/types/known/structpb" 31 corev1 "k8s.io/api/core/v1" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 35 "istio.io/api/annotation" 36 meshapi "istio.io/api/mesh/v1alpha1" 37 proxyConfig "istio.io/api/networking/v1beta1" 38 opconfig "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 39 "istio.io/istio/pilot/pkg/features" 40 "istio.io/istio/pilot/pkg/model" 41 "istio.io/istio/pilot/test/util" 42 "istio.io/istio/pkg/config/constants" 43 "istio.io/istio/pkg/config/mesh" 44 "istio.io/istio/pkg/kube" 45 "istio.io/istio/pkg/kube/kubetypes" 46 "istio.io/istio/pkg/kube/multicluster" 47 istiolog "istio.io/istio/pkg/log" 48 "istio.io/istio/pkg/platform" 49 "istio.io/istio/pkg/test" 50 "istio.io/istio/pkg/util/sets" 51 ) 52 53 // TestInjection tests both the mutating webhook and kube-inject. It does this by sharing the same input and output 54 // test files and running through the two different code paths. 55 func TestInjection(t *testing.T) { 56 type testCase struct { 57 in string 58 want string 59 setFlags []string 60 inFilePath string 61 mesh func(m *meshapi.MeshConfig) 62 skipWebhook bool 63 skipInjection bool 64 expectedError string 65 expectedLog string 66 setup func(t test.Failer) 67 } 68 cases := []testCase{ 69 // verify cni 70 { 71 in: "hello.yaml", 72 want: "hello.yaml.cni.injected", 73 setFlags: []string{ 74 "components.cni.enabled=true", 75 "values.istio_cni.provider=default", 76 "values.global.network=network1", 77 }, 78 }, 79 { 80 in: "hello.yaml", 81 want: "hello.yaml.proxyImageName.injected", 82 setFlags: []string{ 83 "values.global.proxy.image=proxyTest", 84 }, 85 }, 86 { 87 in: "hello.yaml", 88 want: "hello-tproxy.yaml.injected", 89 mesh: func(m *meshapi.MeshConfig) { 90 m.DefaultConfig.InterceptionMode = meshapi.ProxyConfig_TPROXY 91 }, 92 }, 93 { 94 in: "hello.yaml", 95 want: "hello-always.yaml.injected", 96 setFlags: []string{"values.global.imagePullPolicy=Always"}, 97 }, 98 { 99 in: "hello.yaml", 100 want: "hello-never.yaml.injected", 101 setFlags: []string{"values.global.imagePullPolicy=Never"}, 102 }, 103 { 104 in: "enable-core-dump.yaml", 105 want: "enable-core-dump.yaml.injected", 106 setFlags: []string{"values.global.proxy.enableCoreDump=true"}, 107 }, 108 { 109 in: "format-duration.yaml", 110 want: "format-duration.yaml.injected", 111 mesh: func(m *meshapi.MeshConfig) { 112 m.DefaultConfig.DrainDuration = durationpb.New(time.Second * 23) 113 }, 114 }, 115 { 116 // Verifies that parameters are applied properly when no annotations are provided. 117 in: "traffic-params.yaml", 118 want: "traffic-params.yaml.injected", 119 setFlags: []string{ 120 `values.global.proxy.includeIPRanges=127.0.0.1/24,10.96.0.1/24`, 121 `values.global.proxy.excludeIPRanges=10.96.0.2/24,10.96.0.3/24`, 122 `values.global.proxy.excludeInboundPorts=4,5,6`, 123 `values.global.proxy.statusPort=0`, 124 }, 125 }, 126 { 127 // Verifies that the status params behave properly. 128 in: "status_params.yaml", 129 want: "status_params.yaml.injected", 130 setFlags: []string{ 131 `values.global.proxy.statusPort=123`, 132 `values.global.proxy.readinessInitialDelaySeconds=100`, 133 `values.global.proxy.readinessPeriodSeconds=200`, 134 `values.global.proxy.readinessFailureThreshold=300`, 135 }, 136 }, 137 { 138 // Verifies that the kubevirtInterfaces list are applied properly from parameters.. 139 in: "kubevirtInterfaces.yaml", 140 want: "kubevirtInterfaces.yaml.injected", 141 setFlags: []string{ 142 `values.global.proxy.statusPort=123`, 143 `values.global.proxy.readinessInitialDelaySeconds=100`, 144 `values.global.proxy.readinessPeriodSeconds=200`, 145 `values.global.proxy.readinessFailureThreshold=300`, 146 }, 147 }, 148 { 149 // Verifies that global.imagePullSecrets are applied properly 150 in: "hello.yaml", 151 want: "hello-image-secrets-in-values.yaml.injected", 152 inFilePath: "hello-image-secrets-in-values.iop.yaml", 153 }, 154 { 155 // Verifies that global.imagePullSecrets are appended properly 156 in: "hello-image-pull-secret.yaml", 157 want: "hello-multiple-image-secrets.yaml.injected", 158 inFilePath: "hello-image-secrets-in-values.iop.yaml", 159 }, 160 { 161 // Verifies that global.podDNSSearchNamespaces are applied properly 162 in: "hello.yaml", 163 want: "hello-template-in-values.yaml.injected", 164 inFilePath: "hello-template-in-values.iop.yaml", 165 }, 166 { 167 // Verifies that global.mountMtlsCerts is applied properly 168 in: "hello.yaml", 169 want: "hello-mount-mtls-certs.yaml.injected", 170 setFlags: []string{`values.global.mountMtlsCerts=true`}, 171 }, 172 { 173 // Verifies that k8s.v1.cni.cncf.io/networks is set to istio-cni when not chained 174 in: "hello.yaml", 175 want: "hello-cncf-networks.yaml.injected", 176 setFlags: []string{ 177 `components.cni.enabled=true`, 178 `values.istio_cni.provider=multus`, 179 }, 180 }, 181 { 182 // Verifies that istio-cni is appended to k8s.v1.cni.cncf.io/networks flat value if set 183 in: "hello-existing-cncf-networks.yaml", 184 want: "hello-existing-cncf-networks.yaml.injected", 185 setFlags: []string{ 186 `components.cni.enabled=true`, 187 `values.istio_cni.provider=multus`, 188 }, 189 }, 190 { 191 // Verifies that istio-cni is appended to k8s.v1.cni.cncf.io/networks JSON value 192 in: "hello-existing-cncf-networks-json.yaml", 193 want: "hello-existing-cncf-networks-json.yaml.injected", 194 setFlags: []string{ 195 `components.cni.enabled=true`, 196 `values.istio_cni.provider=multus`, 197 }, 198 }, 199 { 200 // Verifies that HoldApplicationUntilProxyStarts in MeshConfig puts sidecar in front 201 in: "hello.yaml", 202 want: "hello.proxyHoldsApplication.yaml.injected", 203 setFlags: []string{ 204 `values.global.proxy.holdApplicationUntilProxyStarts=true`, 205 }, 206 }, 207 { 208 // Verifies that HoldApplicationUntilProxyStarts in MeshConfig puts sidecar in front 209 in: "hello-probes.yaml", 210 want: "hello-probes.proxyHoldsApplication.yaml.injected", 211 setFlags: []string{ 212 `values.global.proxy.holdApplicationUntilProxyStarts=true`, 213 }, 214 }, 215 { 216 // Verifies that HoldApplicationUntilProxyStarts in proxyconfig sets lifecycle hook 217 in: "hello-probes-proxyHoldApplication-ProxyConfig.yaml", 218 want: "hello-probes-proxyHoldApplication-ProxyConfig.yaml.injected", 219 }, 220 { 221 // Verifies that HoldApplicationUntilProxyStarts=false in proxyconfig 'OR's with MeshConfig setting 222 in: "hello-probes-noProxyHoldApplication-ProxyConfig.yaml", 223 want: "hello-probes-noProxyHoldApplication-ProxyConfig.yaml.injected", 224 setFlags: []string{ 225 `values.global.proxy.holdApplicationUntilProxyStarts=true`, 226 }, 227 }, 228 { 229 // A test with no pods is not relevant for webhook 230 in: "hello-service.yaml", 231 want: "hello-service.yaml.injected", 232 skipWebhook: true, 233 }, 234 { 235 // Cronjob is tricky for webhook test since the spec is different. Since the real code will 236 // get a pod anyways, the test isn't too useful for webhook anyways. 237 in: "cronjob.yaml", 238 want: "cronjob.yaml.injected", 239 skipWebhook: true, 240 }, 241 { 242 in: "traffic-annotations-bad-includeipranges.yaml", 243 expectedError: "includeipranges", 244 }, 245 { 246 in: "traffic-annotations-bad-excludeipranges.yaml", 247 expectedError: "excludeipranges", 248 }, 249 { 250 in: "traffic-annotations-bad-includeinboundports.yaml", 251 expectedError: "includeinboundports", 252 }, 253 { 254 in: "traffic-annotations-bad-excludeinboundports.yaml", 255 expectedError: "excludeinboundports", 256 }, 257 { 258 in: "traffic-annotations-bad-excludeoutboundports.yaml", 259 expectedError: "excludeoutboundports", 260 }, 261 { 262 in: "traffic-annotations.yaml", 263 want: "traffic-annotations.yaml.injected", 264 mesh: func(m *meshapi.MeshConfig) { 265 if m.DefaultConfig.ProxyMetadata == nil { 266 m.DefaultConfig.ProxyMetadata = map[string]string{} 267 } 268 m.DefaultConfig.ProxyMetadata["ISTIO_META_TLS_CLIENT_KEY"] = "/etc/identity/client/keys/client-key.pem" 269 }, 270 }, 271 { 272 in: "proxy-override.yaml", 273 want: "proxy-override.yaml.injected", 274 }, 275 { 276 in: "explicit-security-context.yaml", 277 want: "explicit-security-context.yaml.injected", 278 }, 279 { 280 in: "only-proxy-container.yaml", 281 want: "only-proxy-container.yaml.injected", 282 }, 283 { 284 in: "proxy-override-args.yaml", 285 want: "proxy-override-args.yaml.injected", 286 }, 287 { 288 in: "proxy-override-runas.yaml", 289 want: "proxy-override-runas.yaml.injected", 290 }, 291 { 292 in: "proxy-override-runas.yaml", 293 want: "proxy-override-runas.yaml.cni.injected", 294 setFlags: []string{ 295 "components.cni.enabled=true", 296 }, 297 }, 298 { 299 in: "proxy-override-runas.yaml", 300 want: "proxy-override-runas.yaml.tproxy.injected", 301 mesh: func(m *meshapi.MeshConfig) { 302 m.DefaultConfig.InterceptionMode = meshapi.ProxyConfig_TPROXY 303 }, 304 }, 305 { 306 in: "proxy-override-args.yaml", 307 want: "proxy-override-args-native.yaml.injected", 308 setup: func(t test.Failer) { 309 test.SetEnvForTest(t, features.EnableNativeSidecars.Name, "true") 310 }, 311 }, 312 { 313 in: "gateway.yaml", 314 want: "gateway.yaml.injected", 315 }, 316 { 317 in: "gateway.yaml", 318 want: "gateway.yaml.injected", 319 setup: func(t test.Failer) { 320 test.SetEnvForTest(t, features.EnableNativeSidecars.Name, "true") 321 }, 322 }, 323 { 324 in: "native-sidecar.yaml", 325 want: "native-sidecar.yaml.injected", 326 setup: func(t test.Failer) { 327 test.SetEnvForTest(t, features.EnableNativeSidecars.Name, "true") 328 }, 329 }, 330 { 331 in: "custom-template.yaml", 332 want: "custom-template.yaml.injected", 333 inFilePath: "custom-template.iop.yaml", 334 }, 335 { 336 in: "tcp-probes.yaml", 337 want: "tcp-probes.yaml.injected", 338 }, 339 { 340 in: "hello-host-network-with-ns.yaml", 341 want: "hello-host-network-with-ns.yaml.injected", 342 expectedLog: "Skipping injection because Deployment \"sample/hello-host-network\" has host networking enabled", 343 }, 344 { 345 // Verifies ISTIO_KUBE_APP_PROBERS are correctly merged during multiple injections. 346 in: "merge-probers.yaml", 347 want: "merge-probers.yaml.injected", 348 setFlags: []string{ 349 `values.global.proxy.holdApplicationUntilProxyStarts=true`, 350 }, 351 }, 352 { 353 in: "hello-tracing-disabled.yaml", 354 want: "hello-tracing-disabled.yaml.injected", 355 mesh: func(m *meshapi.MeshConfig) { 356 m.DefaultConfig.Tracing = &meshapi.Tracing{} 357 }, 358 }, 359 { 360 in: "truncate-canonical-name-pod.yaml", 361 want: "truncate-canonical-name-pod.yaml.injected", 362 }, 363 { 364 in: "truncate-canonical-name-custom-controller-pod.yaml", 365 want: "truncate-canonical-name-custom-controller-pod.yaml.injected", 366 }, 367 { 368 // Test injection on OpenShift. Currently kube-inject does not work, only test webhook 369 in: "hello-openshift.yaml", 370 want: "hello-openshift.yaml.injected", 371 setFlags: []string{ 372 "components.cni.enabled=true", 373 }, 374 skipInjection: true, 375 setup: func(t test.Failer) { 376 test.SetEnvForTest(t, platform.Platform.Name, platform.OpenShift) 377 }, 378 }, 379 { 380 // Validates localhost probes get injected correctly 381 in: "hello-probes-localhost.yaml", 382 want: "hello-probes-localhost.yaml.injected", 383 mesh: func(m *meshapi.MeshConfig) { 384 m.InboundTrafficPolicy = &meshapi.MeshConfig_InboundTrafficPolicy{ 385 Mode: meshapi.MeshConfig_InboundTrafficPolicy_LOCALHOST, 386 } 387 }, 388 }, 389 } 390 // Keep track of tests we add options above 391 // We will search for all test files and skip these ones 392 alreadyTested := sets.New[string]() 393 for _, t := range cases { 394 if t.want != "" { 395 alreadyTested.Insert(t.want) 396 } else { 397 alreadyTested.Insert(t.in + ".injected") 398 } 399 } 400 files, err := os.ReadDir("testdata/inject") 401 if err != nil { 402 t.Fatal(err) 403 } 404 if len(files) < 3 { 405 t.Fatalf("Didn't find test files - something must have gone wrong") 406 } 407 // Automatically add any other test files in the folder. This ensures we don't 408 // forget to add to this list, that we don't have duplicates, etc 409 // Keep track of all golden files so we can ensure we don't have unused ones later 410 allOutputFiles := sets.New[string]() 411 for _, f := range files { 412 if strings.HasSuffix(f.Name(), ".injected") { 413 allOutputFiles.Insert(f.Name()) 414 } 415 if strings.HasSuffix(f.Name(), ".iop.yaml") { 416 continue 417 } 418 if !strings.HasSuffix(f.Name(), ".yaml") { 419 continue 420 } 421 want := f.Name() + ".injected" 422 if alreadyTested.Contains(want) { 423 continue 424 } 425 cases = append(cases, testCase{in: f.Name(), want: want}) 426 } 427 428 // Precompute injection settings. This may seem like a premature optimization, but due to the size of 429 // YAMLs, with -race this was taking >10min in some cases to generate! 430 if util.Refresh() { 431 cleanupOldFiles(t) 432 writeInjectionSettings(t, "default", nil, "") 433 for i, c := range cases { 434 if c.setFlags != nil || c.inFilePath != "" { 435 writeInjectionSettings(t, fmt.Sprintf("%s.%d", c.in, i), c.setFlags, c.inFilePath) 436 } 437 } 438 } 439 // Preload default settings. Computation here is expensive, so this speeds the tests up substantially 440 defaultTemplate, defaultValues, defaultMesh := readInjectionSettings(t, "default") 441 for i, c := range cases { 442 i, c := i, c 443 testName := fmt.Sprintf("[%02d] %s", i, c.want) 444 if c.expectedError != "" { 445 testName = fmt.Sprintf("[%02d] %s", i, c.in) 446 } 447 t.Run(testName, func(t *testing.T) { 448 if c.setup != nil { 449 c.setup(t) 450 } else { 451 // Tests with custom setup modify global state and cannot run in parallel 452 t.Parallel() 453 } 454 455 mc, err := mesh.DeepCopyMeshConfig(defaultMesh) 456 if err != nil { 457 t.Fatal(err) 458 } 459 sidecarTemplate, valuesConfig := defaultTemplate, defaultValues 460 if c.setFlags != nil || c.inFilePath != "" { 461 sidecarTemplate, valuesConfig, mc = readInjectionSettings(t, fmt.Sprintf("%s.%d", c.in, i)) 462 } 463 if c.mesh != nil { 464 c.mesh(mc) 465 } 466 467 inputFilePath := "testdata/inject/" + c.in 468 wantFilePath := "testdata/inject/" + c.want 469 in, err := os.Open(inputFilePath) 470 if err != nil { 471 t.Fatalf("Failed to open %q: %v", inputFilePath, err) 472 } 473 t.Cleanup(func() { 474 _ = in.Close() 475 }) 476 477 // First we test kube-inject. This will run exactly what kube-inject does, and write output to the golden files 478 t.Run("kube-inject", func(t *testing.T) { 479 if c.skipInjection { 480 return 481 } 482 483 var got bytes.Buffer 484 logs := make([]string, 0) 485 warn := func(s string) { 486 logs = append(logs, s) 487 t.Log(s) 488 } 489 if err = IntoResourceFile(nil, sidecarTemplate.Templates, valuesConfig, "", mc, in, &got, warn); err != nil { 490 if c.expectedError != "" { 491 if !strings.Contains(strings.ToLower(err.Error()), c.expectedError) { 492 t.Fatalf("expected error %q got %q", c.expectedError, err) 493 } 494 return 495 } 496 t.Fatalf("IntoResourceFile(%v) returned an error: %v", inputFilePath, err) 497 } 498 if c.expectedError != "" { 499 t.Fatalf("expected error but got none") 500 } 501 if c.expectedLog != "" { 502 hasExpectedLog := false 503 for _, log := range logs { 504 if strings.Contains(log, c.expectedLog) { 505 hasExpectedLog = true 506 break 507 } 508 } 509 if !hasExpectedLog { 510 t.Fatal("expected log but got none") 511 } 512 } 513 514 // The version string is a maintenance pain for this test. Strip the version string before comparing. 515 gotBytes := util.StripVersion(got.Bytes()) 516 wantBytes := util.ReadGoldenFile(t, gotBytes, wantFilePath) 517 518 util.CompareBytes(t, gotBytes, wantBytes, wantFilePath) 519 }) 520 521 // Exit early if we don't need to test webhook. We can skip errors since its redundant 522 // and painful to test here. 523 if c.expectedError != "" || c.skipWebhook { 524 return 525 } 526 // Next run the webhook test. This one is a bit trickier as the webhook operates 527 // on Pods, but the inputs are Deployments/StatefulSets/etc. As a result, we need 528 // to convert these to pods, then run the injection This test will *not* 529 // overwrite golden files, as we do not have identical textual output as 530 // kube-inject. Instead, we just compare the desired/actual pod specs. 531 t.Run("webhook", func(t *testing.T) { 532 env := &model.Environment{} 533 env.SetPushContext(&model.PushContext{ 534 ProxyConfigs: &model.ProxyConfigs{}, 535 }) 536 537 multi := multicluster.NewFakeController() 538 client := kube.NewFakeClient( 539 &corev1.Namespace{ 540 ObjectMeta: metav1.ObjectMeta{ 541 Name: "test-ns", 542 Annotations: map[string]string{ 543 securityv1.UIDRangeAnnotation: "1000620000/10000", 544 securityv1.SupplementalGroupsAnnotation: "1000620000/10000", 545 }, 546 }, 547 }) 548 549 webhook := &Webhook{ 550 Config: sidecarTemplate, 551 meshConfig: mc, 552 env: env, 553 valuesConfig: valuesConfig, 554 revision: "default", 555 namespaces: multicluster.BuildMultiClusterKclientComponent[*corev1.Namespace](multi, kubetypes.Filter{}), 556 } 557 558 stop := test.NewStop(t) 559 multi.Add(constants.DefaultClusterName, client, stop) 560 client.RunAndWait(stop) 561 562 // Split multi-part yaml documents. Input and output will have the same number of parts. 563 inputYAMLs := splitYamlFile(inputFilePath, t) 564 wantYAMLs := splitYamlFile(wantFilePath, t) 565 for i := 0; i < len(inputYAMLs); i++ { 566 t.Run(fmt.Sprintf("yamlPart[%d]", i), func(t *testing.T) { 567 runWebhook(t, webhook, inputYAMLs[i], wantYAMLs[i], true) 568 }) 569 } 570 }) 571 }) 572 } 573 574 // Make sure we don't have any stale test data leftover, as it can cause confusion. 575 for _, c := range cases { 576 delete(allOutputFiles, c.want) 577 } 578 if len(allOutputFiles) != 0 { 579 t.Fatalf("stale golden files found: %v", allOutputFiles.UnsortedList()) 580 } 581 } 582 583 func testInjectionTemplate(t *testing.T, template, input, expected string) { 584 t.Helper() 585 tmpl, err := ParseTemplates(map[string]string{SidecarTemplateName: template}) 586 if err != nil { 587 t.Fatal(err) 588 } 589 env := &model.Environment{} 590 env.SetPushContext(&model.PushContext{ 591 ProxyConfigs: &model.ProxyConfigs{}, 592 }) 593 webhook := &Webhook{ 594 Config: &Config{ 595 Templates: tmpl, 596 Policy: InjectionPolicyEnabled, 597 DefaultTemplates: []string{SidecarTemplateName}, 598 }, 599 env: env, 600 } 601 runWebhook(t, webhook, []byte(input), []byte(expected), false) 602 } 603 604 func TestMultipleInjectionTemplates(t *testing.T) { 605 p, err := ParseTemplates(map[string]string{ 606 "sidecar": ` 607 spec: 608 containers: 609 - name: istio-proxy 610 image: proxy 611 `, 612 "init": ` 613 spec: 614 initContainers: 615 - name: istio-init 616 image: proxy 617 `, 618 }) 619 if err != nil { 620 t.Fatal(err) 621 } 622 env := &model.Environment{} 623 env.SetPushContext(&model.PushContext{ 624 ProxyConfigs: &model.ProxyConfigs{}, 625 }) 626 webhook := &Webhook{ 627 Config: &Config{ 628 Templates: p, 629 Aliases: map[string][]string{"both": {"sidecar", "init"}}, 630 Policy: InjectionPolicyEnabled, 631 }, 632 env: env, 633 } 634 635 input := ` 636 apiVersion: v1 637 kind: Pod 638 metadata: 639 name: hello 640 annotations: 641 inject.istio.io/templates: sidecar,init 642 spec: 643 containers: 644 - name: hello 645 image: "fake.docker.io/google-samples/hello-go-gke:1.0" 646 ` 647 inputAlias := ` 648 apiVersion: v1 649 kind: Pod 650 metadata: 651 name: hello 652 annotations: 653 inject.istio.io/templates: both 654 spec: 655 containers: 656 - name: hello 657 image: "fake.docker.io/google-samples/hello-go-gke:1.0" 658 ` 659 // nolint: lll 660 expected := ` 661 apiVersion: v1 662 kind: Pod 663 metadata: 664 annotations: 665 inject.istio.io/templates: %s 666 prometheus.io/path: /stats/prometheus 667 prometheus.io/port: "0" 668 prometheus.io/scrape: "true" 669 sidecar.istio.io/status: '{"version":"","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-data","istio-podinfo","istio-token","istiod-ca-cert"],"imagePullSecrets":null}' 670 name: hello 671 spec: 672 initContainers: 673 - name: istio-init 674 image: proxy 675 containers: 676 - name: hello 677 image: fake.docker.io/google-samples/hello-go-gke:1.0 678 - name: istio-proxy 679 image: proxy 680 ` 681 runWebhook(t, webhook, []byte(input), []byte(fmt.Sprintf(expected, "sidecar,init")), false) 682 runWebhook(t, webhook, []byte(inputAlias), []byte(fmt.Sprintf(expected, "both")), false) 683 } 684 685 // TestStrategicMerge ensures we can use https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md 686 // directives in the injection template 687 func TestStrategicMerge(t *testing.T) { 688 testInjectionTemplate(t, 689 ` 690 metadata: 691 labels: 692 $patch: replace 693 foo: bar 694 spec: 695 containers: 696 - name: injected 697 image: "fake.docker.io/google-samples/hello-go-gke:1.1" 698 `, 699 ` 700 apiVersion: v1 701 kind: Pod 702 metadata: 703 name: hello 704 labels: 705 key: value 706 spec: 707 containers: 708 - name: hello 709 image: "fake.docker.io/google-samples/hello-go-gke:1.0" 710 `, 711 712 // We expect resources to only have limits, since we had the "replace" directive. 713 // nolint: lll 714 ` 715 apiVersion: v1 716 kind: Pod 717 metadata: 718 annotations: 719 prometheus.io/path: /stats/prometheus 720 prometheus.io/port: "0" 721 prometheus.io/scrape: "true" 722 labels: 723 foo: bar 724 name: hello 725 spec: 726 containers: 727 - name: injected 728 image: "fake.docker.io/google-samples/hello-go-gke:1.1" 729 - name: hello 730 image: "fake.docker.io/google-samples/hello-go-gke:1.0" 731 `) 732 } 733 734 func runWebhook(t *testing.T, webhook *Webhook, inputYAML []byte, wantYAML []byte, idempotencyCheck bool) { 735 // Convert the input YAML to a deployment. 736 inputRaw, err := FromRawToObject(inputYAML) 737 if err != nil { 738 t.Fatal(err) 739 } 740 inputPod := objectToPod(t, inputRaw) 741 742 // Convert the wanted YAML to a deployment. 743 wantRaw, err := FromRawToObject(wantYAML) 744 if err != nil { 745 t.Fatal(err) 746 } 747 wantPod := objectToPod(t, wantRaw) 748 749 // Generate the patch. At runtime, the webhook would actually generate the patch against the 750 // pod configuration. But since our input files are deployments, rather than actual pod instances, 751 // we have to apply the patch to the template portion of the deployment only. 752 templateJSON := convertToJSON(inputPod, t) 753 got := webhook.inject(&kube.AdmissionReview{ 754 Request: &kube.AdmissionRequest{ 755 Object: runtime.RawExtension{ 756 Raw: templateJSON, 757 }, 758 Namespace: jsonToUnstructured(inputYAML, t).GetNamespace(), 759 }, 760 }, "") 761 var gotPod *corev1.Pod 762 // Apply the generated patch to the template. 763 if got.Patch != nil { 764 patchedPod := &corev1.Pod{} 765 patch := prettyJSON(got.Patch, t) 766 patchedTemplateJSON := applyJSONPatch(templateJSON, patch, t) 767 if err := json.Unmarshal(patchedTemplateJSON, patchedPod); err != nil { 768 t.Fatal(err) 769 } 770 gotPod = patchedPod 771 } else { 772 gotPod = inputPod 773 } 774 775 if err := normalizeAndCompareDeployments(gotPod, wantPod, false, t); err != nil { 776 t.Fatal(err) 777 } 778 if idempotencyCheck { 779 t.Run("idempotency", func(t *testing.T) { 780 if err := normalizeAndCompareDeployments(gotPod, wantPod, true, t); err != nil { 781 t.Fatal(err) 782 } 783 }) 784 } 785 } 786 787 func TestSkipUDPPorts(t *testing.T) { 788 cases := []struct { 789 c corev1.Container 790 ports []string 791 }{ 792 { 793 c: corev1.Container{ 794 Ports: []corev1.ContainerPort{}, 795 }, 796 }, 797 { 798 c: corev1.Container{ 799 Ports: []corev1.ContainerPort{ 800 { 801 ContainerPort: 80, 802 Protocol: corev1.ProtocolTCP, 803 }, 804 { 805 ContainerPort: 8080, 806 Protocol: corev1.ProtocolTCP, 807 }, 808 }, 809 }, 810 ports: []string{"80", "8080"}, 811 }, 812 { 813 c: corev1.Container{ 814 Ports: []corev1.ContainerPort{ 815 { 816 ContainerPort: 53, 817 Protocol: corev1.ProtocolTCP, 818 }, 819 { 820 ContainerPort: 53, 821 Protocol: corev1.ProtocolUDP, 822 }, 823 }, 824 }, 825 ports: []string{"53"}, 826 }, 827 { 828 c: corev1.Container{ 829 Ports: []corev1.ContainerPort{ 830 { 831 ContainerPort: 80, 832 Protocol: corev1.ProtocolTCP, 833 }, 834 { 835 ContainerPort: 53, 836 Protocol: corev1.ProtocolUDP, 837 }, 838 }, 839 }, 840 ports: []string{"80"}, 841 }, 842 { 843 c: corev1.Container{ 844 Ports: []corev1.ContainerPort{ 845 { 846 ContainerPort: 53, 847 Protocol: corev1.ProtocolUDP, 848 }, 849 }, 850 }, 851 }, 852 } 853 for i := range cases { 854 expectPorts := cases[i].ports 855 ports := getPortsForContainer(cases[i].c) 856 if len(ports) != len(expectPorts) { 857 t.Fatalf("unexpected ports result for case %d", i) 858 } 859 for j := 0; j < len(ports); j++ { 860 if ports[j] != expectPorts[j] { 861 t.Fatalf("unexpected ports result for case %d: expect %v, got %v", i, expectPorts, ports) 862 } 863 } 864 } 865 } 866 867 func TestCleanProxyConfig(t *testing.T) { 868 overrides := mesh.DefaultProxyConfig() 869 overrides.ConfigPath = "/foo/bar" 870 overrides.DrainDuration = durationpb.New(7 * time.Second) 871 overrides.ProxyMetadata = map[string]string{ 872 "foo": "barr", 873 } 874 explicit := mesh.DefaultProxyConfig() 875 explicit.ConfigPath = constants.ConfigPathDir 876 explicit.DrainDuration = durationpb.New(45 * time.Second) 877 cases := []struct { 878 name string 879 proxy *meshapi.ProxyConfig 880 expect string 881 }{ 882 { 883 "default", 884 mesh.DefaultProxyConfig(), 885 `{}`, 886 }, 887 { 888 "explicit default", 889 explicit, 890 `{}`, 891 }, 892 { 893 "overrides", 894 overrides, 895 `{"configPath":"/foo/bar","drainDuration":"7s","proxyMetadata":{"foo":"barr"}}`, 896 }, 897 } 898 for _, tt := range cases { 899 t.Run(tt.name, func(t *testing.T) { 900 got := protoToJSON(tt.proxy) 901 if got != tt.expect { 902 t.Fatalf("incorrect output: got %v, expected %v", got, tt.expect) 903 } 904 roundTrip, err := mesh.ApplyProxyConfig(got, mesh.DefaultMeshConfig()) 905 if err != nil { 906 t.Fatal(err) 907 } 908 if !cmp.Equal(roundTrip.GetDefaultConfig(), tt.proxy, protocmp.Transform()) { 909 t.Fatalf("round trip is not identical: got \n%+v, expected \n%+v", *roundTrip.GetDefaultConfig(), tt.proxy) 910 } 911 }) 912 } 913 } 914 915 func TestAppendMultusNetwork(t *testing.T) { 916 cases := []struct { 917 name string 918 in string 919 want string 920 }{ 921 { 922 name: "empty", 923 in: "", 924 want: "istio-cni", 925 }, 926 { 927 name: "flat-single", 928 in: "macvlan-conf-1", 929 want: "macvlan-conf-1, istio-cni", 930 }, 931 { 932 name: "flat-multiple", 933 in: "macvlan-conf-1, macvlan-conf-2", 934 want: "macvlan-conf-1, macvlan-conf-2, istio-cni", 935 }, 936 { 937 name: "json-single", 938 in: `[{"name": "macvlan-conf-1"}]`, 939 want: `[{"name": "macvlan-conf-1"}, {"name": "istio-cni"}]`, 940 }, 941 { 942 name: "json-multiple", 943 in: `[{"name": "macvlan-conf-1"}, {"name": "macvlan-conf-2"}]`, 944 want: `[{"name": "macvlan-conf-1"}, {"name": "macvlan-conf-2"}, {"name": "istio-cni"}]`, 945 }, 946 { 947 name: "json-multiline", 948 in: `[ 949 {"name": "macvlan-conf-1"}, 950 {"name": "macvlan-conf-2"} 951 ]`, 952 want: `[ 953 {"name": "macvlan-conf-1"}, 954 {"name": "macvlan-conf-2"} 955 , {"name": "istio-cni"}]`, 956 }, 957 { 958 name: "json-multiline-additional-fields", 959 in: `[ 960 {"name": "macvlan-conf-1", "another-field": "another-value"}, 961 {"name": "macvlan-conf-2"} 962 ]`, 963 want: `[ 964 {"name": "macvlan-conf-1", "another-field": "another-value"}, 965 {"name": "macvlan-conf-2"} 966 , {"name": "istio-cni"}]`, 967 }, 968 { 969 name: "json-preconfigured-istio-cni", 970 in: `[ 971 {"name": "macvlan-conf-1"}, 972 {"name": "macvlan-conf-2"}, 973 {"name": "istio-cni", "config": "additional-config"}, 974 ]`, 975 want: `[ 976 {"name": "macvlan-conf-1"}, 977 {"name": "macvlan-conf-2"}, 978 {"name": "istio-cni", "config": "additional-config"}, 979 ]`, 980 }, 981 } 982 983 for _, tc := range cases { 984 tc := tc 985 t.Run(tc.name, func(t *testing.T) { 986 t.Parallel() 987 actual := appendMultusNetwork(tc.in, "istio-cni") 988 if actual != tc.want { 989 t.Fatalf("Unexpected result.\nExpected:\n%v\nActual:\n%v", tc.want, actual) 990 } 991 t.Run("idempotency", func(t *testing.T) { 992 actual := appendMultusNetwork(actual, "istio-cni") 993 if actual != tc.want { 994 t.Fatalf("Function is not idempotent.\nExpected:\n%v\nActual:\n%v", tc.want, actual) 995 } 996 }) 997 }) 998 } 999 } 1000 1001 func Test_updateClusterEnvs(t *testing.T) { 1002 type args struct { 1003 container *corev1.Container 1004 newKVs map[string]string 1005 } 1006 tests := []struct { 1007 name string 1008 args args 1009 want *corev1.Container 1010 }{ 1011 { 1012 args: args{ 1013 container: &corev1.Container{}, 1014 newKVs: parseInjectEnvs("/inject/net/network1/cluster/cluster1"), 1015 }, 1016 want: &corev1.Container{ 1017 Env: []corev1.EnvVar{ 1018 { 1019 Name: "ISTIO_META_CLUSTER_ID", 1020 Value: "cluster1", 1021 }, 1022 { 1023 Name: "ISTIO_META_NETWORK", 1024 Value: "network1", 1025 }, 1026 }, 1027 }, 1028 }, 1029 } 1030 for _, tt := range tests { 1031 t.Run(tt.name, func(t *testing.T) { 1032 updateClusterEnvs(tt.args.container, tt.args.newKVs) 1033 if !cmp.Equal(tt.args.container.Env, tt.want.Env) { 1034 t.Fatalf("updateClusterEnvs got \n%+v, expected \n%+v", tt.args.container.Env, tt.want.Env) 1035 } 1036 }) 1037 } 1038 } 1039 1040 func TestProxyImage(t *testing.T) { 1041 val := func(hub string, tag any) *opconfig.Values { 1042 t, _ := structpb.NewValue(tag) 1043 return &opconfig.Values{ 1044 Global: &opconfig.GlobalConfig{ 1045 Hub: hub, 1046 Tag: t, 1047 }, 1048 } 1049 } 1050 pc := func(imageType string) *proxyConfig.ProxyImage { 1051 return &proxyConfig.ProxyImage{ 1052 ImageType: imageType, 1053 } 1054 } 1055 1056 ann := func(imageType string) map[string]string { 1057 if imageType == "" { 1058 return nil 1059 } 1060 return map[string]string{ 1061 annotation.SidecarProxyImageType.Name: imageType, 1062 } 1063 } 1064 1065 for _, tt := range []struct { 1066 desc string 1067 v *opconfig.Values 1068 pc *proxyConfig.ProxyImage 1069 ann map[string]string 1070 want string 1071 }{ 1072 { 1073 desc: "vals-only-int-tag", 1074 v: val("docker.io/istio", 11), 1075 want: "docker.io/istio/proxyv2:11", 1076 }, 1077 { 1078 desc: "pc overrides imageType - float tag", 1079 v: val("docker.io/istio", 1.12), 1080 pc: pc("distroless"), 1081 want: "docker.io/istio/proxyv2:1.12-distroless", 1082 }, 1083 { 1084 desc: "annotation overrides imageType", 1085 v: val("gcr.io/gke-release/asm", "1.11.2-asm.17"), 1086 ann: ann("distroless"), 1087 want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17-distroless", 1088 }, 1089 { 1090 desc: "pc and annotation overrides imageType", 1091 v: val("gcr.io/gke-release/asm", "1.11.2-asm.17"), 1092 pc: pc("distroless"), 1093 ann: ann("debug"), 1094 want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17-debug", 1095 }, 1096 { 1097 desc: "pc and annotation overrides imageType, ann is default", 1098 v: val("gcr.io/gke-release/asm", "1.11.2-asm.17"), 1099 pc: pc("debug"), 1100 ann: ann("default"), 1101 want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17", 1102 }, 1103 { 1104 desc: "pc overrides imageType with default, tag also has image type", 1105 v: val("gcr.io/gke-release/asm", "1.11.2-asm.17-distroless"), 1106 pc: pc("default"), 1107 want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17", 1108 }, 1109 { 1110 desc: "ann overrides imageType with default, tag also has image type", 1111 v: val("gcr.io/gke-release/asm", "1.11.2-asm.17-distroless"), 1112 ann: ann("default"), 1113 want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17", 1114 }, 1115 { 1116 desc: "pc overrides imageType, tag also has image type", 1117 v: val("docker.io/istio", "1.12-debug"), 1118 pc: pc("distroless"), 1119 want: "docker.io/istio/proxyv2:1.12-distroless", 1120 }, 1121 { 1122 desc: "annotation overrides imageType, tag also has the same image type", 1123 v: val("docker.io/istio", "1.12-distroless"), 1124 ann: ann("distroless"), 1125 want: "docker.io/istio/proxyv2:1.12-distroless", 1126 }, 1127 { 1128 desc: "unusual tag should work", 1129 v: val("private-repo/istio", "1.12-this-is-unusual-tag"), 1130 want: "private-repo/istio/proxyv2:1.12-this-is-unusual-tag", 1131 }, 1132 { 1133 desc: "unusual tag should work, default override", 1134 v: val("private-repo/istio", "1.12-this-is-unusual-tag-distroless"), 1135 pc: pc("default"), 1136 want: "private-repo/istio/proxyv2:1.12-this-is-unusual-tag", 1137 }, 1138 { 1139 desc: "annotation overrides imageType with unusual tag", 1140 v: val("private-repo/istio", "1.12-this-is-unusual-tag"), 1141 ann: ann("distroless"), 1142 want: "private-repo/istio/proxyv2:1.12-this-is-unusual-tag-distroless", 1143 }, 1144 } { 1145 t.Run(tt.desc, func(t *testing.T) { 1146 got := ProxyImage(tt.v, tt.pc, tt.ann) 1147 if got != tt.want { 1148 t.Errorf("got: <%s>, want <%s> <== value(%v) proxyConfig(%v) ann(%v)", got, tt.want, tt.v, tt.pc, tt.ann) 1149 } 1150 }) 1151 } 1152 } 1153 1154 func podWithEnv(envCount int) *corev1.Pod { 1155 envs := []corev1.EnvVar{} 1156 for i := 0; i < envCount; i++ { 1157 envs = append(envs, corev1.EnvVar{ 1158 Name: fmt.Sprintf("something-%d", i), 1159 Value: "blah", 1160 }) 1161 } 1162 return &corev1.Pod{ 1163 ObjectMeta: metav1.ObjectMeta{ 1164 Name: "foo", 1165 Namespace: "bar", 1166 }, 1167 Spec: corev1.PodSpec{ 1168 Containers: []corev1.Container{{ 1169 Name: "app", 1170 Image: "fake", 1171 Env: envs, 1172 }}, 1173 }, 1174 } 1175 } 1176 1177 // TestInjection tests both the mutating webhook and kube-inject. It does this by sharing the same input and output 1178 // test files and running through the two different code paths. 1179 func BenchmarkInjection(b *testing.B) { 1180 istiolog.FindScope("default").SetOutputLevel(istiolog.ErrorLevel) 1181 cases := []struct { 1182 name string 1183 in *corev1.Pod 1184 }{ 1185 { 1186 name: "many env vars", 1187 in: podWithEnv(2000), 1188 }, 1189 } 1190 1191 for _, tt := range cases { 1192 b.Run(tt.name, func(b *testing.B) { 1193 // Preload default settings. Computation here is expensive, so this speeds the tests up substantially 1194 sidecarTemplate, valuesConfig, mc := readInjectionSettings(b, "default") 1195 env := &model.Environment{} 1196 env.SetPushContext(&model.PushContext{ 1197 ProxyConfigs: &model.ProxyConfigs{}, 1198 }) 1199 webhook := &Webhook{ 1200 Config: sidecarTemplate, 1201 meshConfig: mc, 1202 env: env, 1203 valuesConfig: valuesConfig, 1204 revision: "default", 1205 } 1206 templateJSON := convertToJSON(tt.in, b) 1207 b.ResetTimer() 1208 for n := 0; n < b.N; n++ { 1209 webhook.inject(&kube.AdmissionReview{ 1210 Request: &kube.AdmissionRequest{ 1211 Object: runtime.RawExtension{ 1212 Raw: templateJSON, 1213 }, 1214 Namespace: tt.in.Namespace, 1215 }, 1216 }, "") 1217 } 1218 }) 1219 } 1220 }