istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/cmd/mesh/manifest-generate_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 mesh 16 17 import ( 18 "archive/tar" 19 "compress/gzip" 20 "encoding/json" 21 "fmt" 22 "io" 23 "io/fs" 24 "os" 25 "path" 26 "path/filepath" 27 "reflect" 28 "strings" 29 "testing" 30 31 "github.com/google/go-cmp/cmp" 32 . "github.com/onsi/gomega" 33 v1 "k8s.io/api/admissionregistration/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 klabels "k8s.io/apimachinery/pkg/labels" 36 37 "istio.io/istio/operator/pkg/compare" 38 "istio.io/istio/operator/pkg/helmreconciler" 39 "istio.io/istio/operator/pkg/manifest" 40 "istio.io/istio/operator/pkg/name" 41 "istio.io/istio/operator/pkg/object" 42 "istio.io/istio/operator/pkg/util" 43 "istio.io/istio/operator/pkg/util/clog" 44 tutil "istio.io/istio/pilot/test/util" 45 "istio.io/istio/pkg/file" 46 "istio.io/istio/pkg/kube" 47 "istio.io/istio/pkg/test" 48 "istio.io/istio/pkg/test/env" 49 "istio.io/istio/pkg/test/util/assert" 50 "istio.io/istio/pkg/version" 51 ) 52 53 const ( 54 testIstioDiscoveryChartPath = "charts/istio-control/istio-discovery/templates" 55 operatorSubdirFilePath = "manifests" 56 ) 57 58 // chartSourceType defines where charts used in the test come from. 59 type chartSourceType string 60 61 var ( 62 operatorRootDir = filepath.Join(env.IstioSrc, "operator") 63 64 // testDataDir contains the directory for manifest-generate test data 65 testDataDir = filepath.Join(operatorRootDir, "cmd/mesh/testdata/manifest-generate") 66 67 // Snapshot charts are in testdata/manifest-generate/data-snapshot 68 snapshotCharts = func() chartSourceType { 69 d, err := os.MkdirTemp("", "data-snapshot-*") 70 if err != nil { 71 panic(fmt.Errorf("failed to make temp dir: %v", err)) 72 } 73 f, err := os.Open("testdata/manifest-generate/data-snapshot.tar.gz") 74 if err != nil { 75 panic(fmt.Errorf("failed to read data snapshot: %v", err)) 76 } 77 if err := extract(f, d); err != nil { 78 panic(fmt.Errorf("failed to extract data snapshot: %v", err)) 79 } 80 return chartSourceType(filepath.Join(d, "manifests")) 81 }() 82 // Compiled in charts come from assets.gen.go 83 compiledInCharts chartSourceType = "COMPILED" 84 _ = compiledInCharts 85 // Live charts come from manifests/ 86 liveCharts = chartSourceType(filepath.Join(env.IstioSrc, operatorSubdirFilePath)) 87 ) 88 89 type testGroup []struct { 90 desc string 91 // Small changes to the input profile produce large changes to the golden output 92 // files. This makes it difficult to spot meaningful changes in pull requests. 93 // By default we hide these changes to make developers life's a bit easier. However, 94 // it is still useful to sometimes override this behavior and show the full diff. 95 // When this flag is true, use an alternative file suffix that is not hidden by 96 // default GitHub in pull requests. 97 showOutputFileInPullRequest bool 98 flags string 99 noInput bool 100 outputDir string 101 fileSelect []string 102 diffSelect string 103 diffIgnore string 104 chartSource chartSourceType 105 } 106 107 func init() { 108 kubeClientFunc = func() (kube.CLIClient, error) { 109 return nil, nil 110 } 111 } 112 113 func extract(gzipStream io.Reader, destination string) error { 114 uncompressedStream, err := gzip.NewReader(gzipStream) 115 if err != nil { 116 return fmt.Errorf("create gzip reader: %v", err) 117 } 118 119 tarReader := tar.NewReader(uncompressedStream) 120 121 for { 122 header, err := tarReader.Next() 123 if err == io.EOF { 124 break 125 } 126 if err != nil { 127 return fmt.Errorf("next: %v", err) 128 } 129 130 dest := filepath.Join(destination, header.Name) 131 switch header.Typeflag { 132 case tar.TypeDir: 133 if _, err := os.Stat(dest); err != nil { 134 if err := os.Mkdir(dest, 0o755); err != nil { 135 return fmt.Errorf("mkdir: %v", err) 136 } 137 } 138 case tar.TypeReg: 139 // Create containing folder if not present 140 dir := path.Dir(dest) 141 if _, err := os.Stat(dir); err != nil { 142 if err := os.MkdirAll(dir, 0o755); err != nil { 143 return err 144 } 145 } 146 outFile, err := os.Create(dest) 147 if err != nil { 148 return fmt.Errorf("create: %v", err) 149 } 150 if _, err := io.Copy(outFile, tarReader); err != nil { 151 return fmt.Errorf("copy: %v", err) 152 } 153 outFile.Close() 154 default: 155 return fmt.Errorf("unknown type: %v in %v", header.Typeflag, header.Name) 156 } 157 } 158 return nil 159 } 160 161 func copyDir(src string, dest string) error { 162 return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { 163 if err != nil { 164 return err 165 } 166 167 outpath := filepath.Join(dest, strings.TrimPrefix(path, src)) 168 169 if info.IsDir() { 170 os.MkdirAll(outpath, info.Mode()) 171 return nil 172 } 173 cpErr := file.AtomicCopy(path, filepath.Dir(outpath), filepath.Base(outpath)) 174 if cpErr != nil { 175 return cpErr 176 } 177 178 return nil 179 }) 180 } 181 182 func TestMain(m *testing.M) { 183 code := m.Run() 184 // Cleanup uncompress snapshot charts 185 os.RemoveAll(string(snapshotCharts)) 186 os.Exit(code) 187 } 188 189 func TestManifestGenerateComponentHubTag(t *testing.T) { 190 g := NewWithT(t) 191 192 objs, err := runManifestCommands("component_hub_tag", "", liveCharts, []string{"templates/deployment.yaml"}) 193 if err != nil { 194 t.Fatal(err) 195 } 196 197 tests := []struct { 198 deploymentName string 199 containerName string 200 want string 201 }{ 202 { 203 deploymentName: "istio-ingressgateway", 204 containerName: "istio-proxy", 205 want: "istio-spec.hub/proxyv2:istio-spec.tag", 206 }, 207 { 208 deploymentName: "istiod", 209 containerName: "discovery", 210 want: "component.pilot.hub/pilot:2", 211 }, 212 } 213 214 for _, tt := range tests { 215 for _, os := range objs { 216 containerName := tt.deploymentName 217 if tt.containerName != "" { 218 containerName = tt.containerName 219 } 220 container := mustGetContainer(g, os, tt.deploymentName, containerName) 221 g.Expect(container).Should(HavePathValueEqual(PathValue{"image", tt.want})) 222 } 223 } 224 } 225 226 func TestManifestGenerateGateways(t *testing.T) { 227 g := NewWithT(t) 228 229 flags := "-s components.ingressGateways.[0].k8s.resources.requests.memory=999Mi " + 230 "-s components.ingressGateways.[name:user-ingressgateway].k8s.resources.requests.cpu=555m" 231 232 objss, err := runManifestCommands("gateways", flags, liveCharts, nil) 233 if err != nil { 234 t.Fatal(err) 235 } 236 237 for _, objs := range objss { 238 g.Expect(objs.kind(name.HPAStr).size()).Should(Equal(3)) 239 g.Expect(objs.kind(name.PDBStr).size()).Should(Equal(3)) 240 g.Expect(objs.kind(name.ServiceStr).labels("istio=ingressgateway").size()).Should(Equal(3)) 241 g.Expect(objs.kind(name.RoleStr).nameMatches(".*gateway.*").size()).Should(Equal(3)) 242 g.Expect(objs.kind(name.RoleBindingStr).nameMatches(".*gateway.*").size()).Should(Equal(3)) 243 g.Expect(objs.kind(name.SAStr).nameMatches(".*gateway.*").size()).Should(Equal(3)) 244 245 dobj := mustGetDeployment(g, objs, "istio-ingressgateway") 246 d := dobj.Unstructured() 247 c := dobj.Container("istio-proxy") 248 g.Expect(d).Should(HavePathValueContain(PathValue{"spec.template.metadata.labels", toMap("service.istio.io/canonical-revision:21")})) 249 g.Expect(d).Should(HavePathValueContain(PathValue{"metadata.labels", toMap("aaa:aaa-val,bbb:bbb-val")})) 250 g.Expect(c).Should(HavePathValueEqual(PathValue{"resources.requests.cpu", "111m"})) 251 g.Expect(c).Should(HavePathValueEqual(PathValue{"resources.requests.memory", "999Mi"})) 252 253 dobj = mustGetDeployment(g, objs, "user-ingressgateway") 254 d = dobj.Unstructured() 255 c = dobj.Container("istio-proxy") 256 g.Expect(d).Should(HavePathValueContain(PathValue{"metadata.labels", toMap("ccc:ccc-val,ddd:ddd-val")})) 257 g.Expect(c).Should(HavePathValueEqual(PathValue{"resources.requests.cpu", "555m"})) 258 g.Expect(c).Should(HavePathValueEqual(PathValue{"resources.requests.memory", "888Mi"})) 259 260 dobj = mustGetDeployment(g, objs, "ilb-gateway") 261 d = dobj.Unstructured() 262 c = dobj.Container("istio-proxy") 263 s := mustGetService(g, objs, "ilb-gateway").Unstructured() 264 g.Expect(d).Should(HavePathValueContain(PathValue{"metadata.labels", toMap("app:istio-ingressgateway,istio:ingressgateway,release: istio")})) 265 g.Expect(c).Should(HavePathValueEqual(PathValue{"resources.requests.cpu", "333m"})) 266 g.Expect(c).Should(HavePathValueEqual(PathValue{"env.[name:PILOT_CERT_PROVIDER].value", "foobar"})) 267 g.Expect(s).Should(HavePathValueContain(PathValue{"metadata.annotations", toMap("cloud.google.com/load-balancer-type: internal")})) 268 g.Expect(s).Should(HavePathValueContain(PathValue{"spec.ports.[0]", portVal("grpc-pilot-mtls", 15011, -1)})) 269 g.Expect(s).Should(HavePathValueContain(PathValue{"spec.ports.[1]", portVal("tcp-citadel-grpc-tls", 8060, 8060)})) 270 g.Expect(s).Should(HavePathValueContain(PathValue{"spec.ports.[2]", portVal("tcp-dns", 5353, -1)})) 271 272 for _, o := range objs.kind(name.HPAStr).objSlice { 273 ou := o.Unstructured() 274 g.Expect(ou).Should(HavePathValueEqual(PathValue{"spec.minReplicas", int64(1)})) 275 g.Expect(ou).Should(HavePathValueEqual(PathValue{"spec.maxReplicas", int64(5)})) 276 } 277 278 checkRoleBindingsReferenceRoles(g, objs) 279 } 280 } 281 282 func TestManifestGenerateWithDuplicateMutatingWebhookConfig(t *testing.T) { 283 testResourceFile := "duplicate_mwc" 284 285 testCases := []struct { 286 name string 287 force bool 288 assertFunc func(g *WithT, objs *ObjectSet, err error) 289 }{ 290 { 291 name: "Duplicate MutatingWebhookConfiguration should be allowed when --force is enabled", 292 force: true, 293 assertFunc: func(g *WithT, objs *ObjectSet, err error) { 294 g.Expect(err).Should(BeNil()) 295 g.Expect(objs.kind(name.MutatingWebhookConfigurationStr).size()).Should(Equal(3)) 296 }, 297 }, 298 { 299 name: "Duplicate MutatingWebhookConfiguration should not be allowed when --force is disabled", 300 force: false, 301 assertFunc: func(g *WithT, objs *ObjectSet, err error) { 302 g.Expect(err.Error()).To(ContainSubstring("Webhook overlaps with others")) 303 g.Expect(objs).Should(BeNil()) 304 }, 305 }, 306 } 307 308 recreateSimpleTestEnv() 309 310 tmpDir := t.TempDir() 311 tmpCharts := chartSourceType(filepath.Join(tmpDir, operatorSubdirFilePath)) 312 err := copyDir(string(liveCharts), string(tmpCharts)) 313 if err != nil { 314 t.Fatal(err) 315 } 316 317 rs, err := readFile(filepath.Join(testDataDir, "input-extra-resources", testResourceFile+".yaml")) 318 if err != nil { 319 t.Fatal(err) 320 } 321 322 err = writeFile(filepath.Join(tmpDir, operatorSubdirFilePath+"/"+testIstioDiscoveryChartPath+"/"+testResourceFile+".yaml"), []byte(rs)) 323 if err != nil { 324 t.Fatal(err) 325 } 326 327 for _, tc := range testCases { 328 t.Run(tc.name, func(t *testing.T) { 329 g := NewWithT(t) 330 objs, err := fakeControllerReconcile(testResourceFile, tmpCharts, &helmreconciler.Options{Force: tc.force, SkipPrune: true}) 331 tc.assertFunc(g, objs, err) 332 }) 333 } 334 } 335 336 func TestManifestGenerateDefaultWithRevisionedWebhook(t *testing.T) { 337 runRevisionedWebhookTest(t, "minimal-revisioned", "default_tag") 338 } 339 340 func TestManifestGenerateFailedDefaultInstallation(t *testing.T) { 341 runRevisionedWebhookTest(t, "minimal", "default_installation_failed") 342 } 343 344 func runRevisionedWebhookTest(t *testing.T, testResourceFile, whSource string) { 345 t.Helper() 346 recreateSimpleTestEnv() 347 tmpDir := t.TempDir() 348 tmpCharts := chartSourceType(filepath.Join(tmpDir, operatorSubdirFilePath)) 349 err := copyDir(string(liveCharts), string(tmpCharts)) 350 if err != nil { 351 t.Fatal(err) 352 } 353 354 // Add a default tag which is the webhook that will be processed post-install 355 rs, err := readFile(filepath.Join(testDataDir, "input-extra-resources", whSource+".yaml")) 356 if err != nil { 357 t.Fatal(err) 358 } 359 360 err = writeFile(filepath.Join(tmpDir, operatorSubdirFilePath+"/"+testIstioDiscoveryChartPath+"/"+testResourceFile+".yaml"), []byte(rs)) 361 if err != nil { 362 t.Fatal(err) 363 } 364 _, err = fakeControllerReconcile(testResourceFile, tmpCharts, &helmreconciler.Options{Force: false, SkipPrune: true}) 365 assert.NoError(t, err) 366 367 // Install a default revision should not cause any error 368 minimal := "minimal" 369 _, err = fakeControllerReconcile(minimal, tmpCharts, &helmreconciler.Options{Force: false, SkipPrune: true}) 370 assert.NoError(t, err) 371 } 372 373 func TestManifestGenerateIstiodRemote(t *testing.T) { 374 g := NewWithT(t) 375 376 objss, err := runManifestCommands("istiod_remote", "", liveCharts, nil) 377 if err != nil { 378 t.Fatal(err) 379 } 380 381 for _, objs := range objss { 382 // check core CRDs exists 383 g.Expect(objs.kind(name.CRDStr).nameEquals("destinationrules.networking.istio.io")).Should(Not(BeNil())) 384 g.Expect(objs.kind(name.CRDStr).nameEquals("gateways.networking.istio.io")).Should(Not(BeNil())) 385 g.Expect(objs.kind(name.CRDStr).nameEquals("sidecars.networking.istio.io")).Should(Not(BeNil())) 386 g.Expect(objs.kind(name.CRDStr).nameEquals("virtualservices.networking.istio.io")).Should(Not(BeNil())) 387 g.Expect(objs.kind(name.CRDStr).nameEquals("adapters.config.istio.io")).Should(BeNil()) 388 g.Expect(objs.kind(name.CRDStr).nameEquals("authorizationpolicies.security.istio.io")).Should(Not(BeNil())) 389 390 g.Expect(objs.kind(name.CMStr).nameEquals("istio-sidecar-injector")).Should(Not(BeNil())) 391 g.Expect(objs.kind(name.ServiceStr).nameEquals("istiod")).Should(Not(BeNil())) 392 g.Expect(objs.kind(name.SAStr).nameEquals("istio-reader-service-account")).Should(Not(BeNil())) 393 394 mwc := mustGetMutatingWebhookConfiguration(g, objs, "istio-sidecar-injector").Unstructured() 395 g.Expect(mwc).Should(HavePathValueEqual(PathValue{"webhooks.[0].clientConfig.url", "https://xxx:15017/inject"})) 396 397 ep := mustGetEndpoint(g, objs, "istiod-remote").Unstructured() 398 g.Expect(ep).Should(HavePathValueEqual(PathValue{"subsets.[0].addresses.[0]", endpointSubsetAddressVal("", "169.10.112.88", "")})) 399 g.Expect(ep).Should(HavePathValueContain(PathValue{"subsets.[0].ports.[0]", portVal("tcp-istiod", 15012, -1)})) 400 401 checkClusterRoleBindingsReferenceRoles(g, objs) 402 } 403 } 404 405 func TestPrune(t *testing.T) { 406 recreateSimpleTestEnv() 407 tmpDir := t.TempDir() 408 tmpCharts := chartSourceType(filepath.Join(tmpDir, operatorSubdirFilePath)) 409 err := copyDir(string(liveCharts), string(tmpCharts)) 410 if err != nil { 411 t.Fatal(err) 412 } 413 414 rs, err := readFile(filepath.Join(testDataDir, "input-extra-resources", "envoyfilter"+".yaml")) 415 if err != nil { 416 t.Fatal(err) 417 } 418 419 err = writeFile(filepath.Join(tmpDir, operatorSubdirFilePath+"/"+testIstioDiscoveryChartPath+"/"+"default.yaml"), []byte(rs)) 420 if err != nil { 421 t.Fatal(err) 422 } 423 _, err = fakeControllerReconcile("default", tmpCharts, &helmreconciler.Options{ 424 Force: false, 425 SkipPrune: false, 426 Log: clog.NewDefaultLogger(), 427 }) 428 assert.NoError(t, err) 429 430 // Install a default revision should not cause any error 431 objs, err := fakeControllerReconcile("empty", tmpCharts, &helmreconciler.Options{ 432 Force: false, 433 SkipPrune: false, 434 Log: clog.NewDefaultLogger(), 435 }) 436 assert.NoError(t, err) 437 438 for _, s := range helmreconciler.PrunedResourcesSchemas() { 439 remainedObjs := objs.kind(s.Kind) 440 if remainedObjs.size() == 0 { 441 continue 442 } 443 for _, v := range remainedObjs.objMap { 444 // exclude operator objects, which will not be pruned 445 if strings.Contains(v.Name, "istio-operator") { 446 continue 447 } 448 t.Fatalf("obj %s/%s is not pruned", v.Namespace, v.Name) 449 } 450 } 451 } 452 453 func TestManifestGenerateAllOff(t *testing.T) { 454 g := NewWithT(t) 455 m, _, err := generateManifest("all_off", "", liveCharts, nil) 456 if err != nil { 457 t.Fatal(err) 458 } 459 objs, err := parseObjectSetFromManifest(m) 460 if err != nil { 461 t.Fatal(err) 462 } 463 g.Expect(objs.size()).Should(Equal(0)) 464 } 465 466 func TestManifestGenerateFlagsMinimalProfile(t *testing.T) { 467 g := NewWithT(t) 468 // Change profile from empty to minimal using flag. 469 m, _, err := generateManifest("empty", "-s profile=minimal", liveCharts, []string{"templates/deployment.yaml"}) 470 if err != nil { 471 t.Fatal(err) 472 } 473 objs, err := parseObjectSetFromManifest(m) 474 if err != nil { 475 t.Fatal(err) 476 } 477 // minimal profile always has istiod, empty does not. 478 mustGetDeployment(g, objs, "istiod") 479 } 480 481 func TestManifestGenerateFlagsSetHubTag(t *testing.T) { 482 g := NewWithT(t) 483 m, _, err := generateManifest("minimal", "-s hub=foo -s tag=bar", liveCharts, []string{"templates/deployment.yaml"}) 484 if err != nil { 485 t.Fatal(err) 486 } 487 objs, err := parseObjectSetFromManifest(m) 488 if err != nil { 489 t.Fatal(err) 490 } 491 492 dobj := mustGetDeployment(g, objs, "istiod") 493 494 c := dobj.Container("discovery") 495 g.Expect(c).Should(HavePathValueEqual(PathValue{"image", "foo/pilot:bar"})) 496 } 497 498 func TestManifestGenerateFlagsSetValues(t *testing.T) { 499 g := NewWithT(t) 500 m, _, err := generateManifest("default", "-s values.global.proxy.image=myproxy -s values.global.proxy.includeIPRanges=172.30.0.0/16,172.21.0.0/16", liveCharts, 501 []string{"templates/deployment.yaml", "templates/istiod-injector-configmap.yaml"}) 502 if err != nil { 503 t.Fatal(err) 504 } 505 objs, err := parseObjectSetFromManifest(m) 506 if err != nil { 507 t.Fatal(err) 508 } 509 dobj := mustGetDeployment(g, objs, "istio-ingressgateway") 510 511 c := dobj.Container("istio-proxy") 512 g.Expect(c).Should(HavePathValueEqual(PathValue{"image", "gcr.io/istio-testing/myproxy:latest"})) 513 514 cm := objs.kind("ConfigMap").nameEquals("istio-sidecar-injector").Unstructured() 515 // TODO: change values to some nicer format rather than text block. 516 g.Expect(cm).Should(HavePathValueMatchRegex(PathValue{"data.values", `.*"includeIPRanges"\: "172\.30\.0\.0/16,172\.21\.0\.0/16".*`})) 517 } 518 519 func TestManifestGenerateFlags(t *testing.T) { 520 flagOutputDir := t.TempDir() 521 flagOutputValuesDir := t.TempDir() 522 runTestGroup(t, testGroup{ 523 { 524 desc: "all_on", 525 diffIgnore: "ConfigMap:*:istio", 526 showOutputFileInPullRequest: true, 527 }, 528 { 529 desc: "flag_values_enable_egressgateway", 530 diffSelect: "Service:*:istio-egressgateway", 531 fileSelect: []string{"templates/service.yaml"}, 532 flags: "--set values.gateways.istio-egressgateway.enabled=true", 533 noInput: true, 534 }, 535 { 536 desc: "flag_output", 537 flags: "-o " + flagOutputDir, 538 diffSelect: "Deployment:*:istiod", 539 fileSelect: []string{"templates/deployment.yaml"}, 540 outputDir: flagOutputDir, 541 }, 542 { 543 desc: "flag_output_set_values", 544 diffSelect: "Deployment:*:istio-ingressgateway", 545 flags: "-s values.global.proxy.image=mynewproxy -o " + flagOutputValuesDir, 546 fileSelect: []string{"templates/deployment.yaml"}, 547 outputDir: flagOutputValuesDir, 548 noInput: true, 549 }, 550 { 551 desc: "flag_force", 552 diffSelect: "no:resources:selected", 553 fileSelect: []string{""}, 554 flags: "--force", 555 }, 556 }) 557 } 558 559 func TestManifestGeneratePilot(t *testing.T) { 560 runTestGroup(t, testGroup{ 561 { 562 desc: "pilot_default", 563 diffIgnore: "CustomResourceDefinition:*:*,ConfigMap:*:istio", 564 }, 565 { 566 desc: "pilot_k8s_settings", 567 diffSelect: "Deployment:*:istiod,HorizontalPodAutoscaler:*:istiod", 568 fileSelect: []string{"templates/deployment.yaml", "templates/autoscale.yaml"}, 569 }, 570 { 571 desc: "pilot_override_values", 572 diffSelect: "Deployment:*:istiod,HorizontalPodAutoscaler:*:istiod", 573 fileSelect: []string{"templates/deployment.yaml", "templates/autoscale.yaml"}, 574 }, 575 { 576 desc: "pilot_override_kubernetes", 577 diffSelect: "Deployment:*:istiod, Service:*:istiod,MutatingWebhookConfiguration:*:istio-sidecar-injector,ServiceAccount:*:istio-reader-service-account", 578 fileSelect: []string{ 579 "templates/deployment.yaml", "templates/mutatingwebhook.yaml", 580 "templates/service.yaml", "templates/reader-serviceaccount.yaml", 581 }, 582 }, 583 // TODO https://github.com/istio/istio/issues/22347 this is broken for overriding things to default value 584 // This can be seen from REGISTRY_ONLY not applying 585 { 586 desc: "pilot_merge_meshconfig", 587 diffSelect: "ConfigMap:*:istio$", 588 fileSelect: []string{"templates/configmap.yaml", "templates/_helpers.tpl"}, 589 }, 590 { 591 desc: "pilot_disable_tracing", 592 diffSelect: "ConfigMap:*:istio$", 593 }, 594 { 595 desc: "autoscaling_ingress_v2", 596 diffSelect: "HorizontalPodAutoscaler:*:istiod,HorizontalPodAutoscaler:*:istio-ingressgateway", 597 fileSelect: []string{"templates/autoscale.yaml"}, 598 }, 599 { 600 desc: "autoscaling_v2", 601 diffSelect: "HorizontalPodAutoscaler:*:istiod,HorizontalPodAutoscaler:*:istio-ingressgateway", 602 fileSelect: []string{"templates/autoscale.yaml"}, 603 }, 604 }) 605 } 606 607 func TestManifestGenerateGateway(t *testing.T) { 608 runTestGroup(t, testGroup{ 609 { 610 desc: "ingressgateway_k8s_settings", 611 diffSelect: "Deployment:*:istio-ingressgateway, Service:*:istio-ingressgateway", 612 }, 613 }) 614 } 615 616 func TestManifestGenerateZtunnel(t *testing.T) { 617 runTestGroup(t, testGroup{ 618 { 619 desc: "ztunnel", 620 diffSelect: "DaemonSet:*:ztunnel", 621 }, 622 }) 623 } 624 625 // TestManifestGenerateHelmValues tests whether enabling components through the values passthrough interface works as 626 // expected i.e. without requiring enablement also in IstioOperator API. 627 func TestManifestGenerateHelmValues(t *testing.T) { 628 runTestGroup(t, testGroup{ 629 { 630 desc: "helm_values_enablement", 631 diffSelect: "Deployment:*:istio-egressgateway, Service:*:istio-egressgateway", 632 }, 633 }) 634 } 635 636 func TestManifestGenerateOrdered(t *testing.T) { 637 // Since this is testing the special case of stable YAML output order, it 638 // does not use the established test group pattern 639 inPath := filepath.Join(testDataDir, "input/all_on.yaml") 640 got1, err := runManifestGenerate([]string{inPath}, "", snapshotCharts, nil) 641 if err != nil { 642 t.Fatal(err) 643 } 644 got2, err := runManifestGenerate([]string{inPath}, "", snapshotCharts, nil) 645 if err != nil { 646 t.Fatal(err) 647 } 648 649 if got1 != got2 { 650 fmt.Printf("%s", util.YAMLDiff(got1, got2)) 651 t.Errorf("stable_manifest: Manifest generation is not producing stable text output.") 652 } 653 } 654 655 func TestManifestGenerateFlagAliases(t *testing.T) { 656 inPath := filepath.Join(testDataDir, "input/all_on.yaml") 657 gotSet, err := runManifestGenerate([]string{inPath}, "--set revision=foo", snapshotCharts, []string{"templates/deployment.yaml"}) 658 if err != nil { 659 t.Fatal(err) 660 } 661 gotAlias, err := runManifestGenerate([]string{inPath}, "--revision=foo", snapshotCharts, []string{"templates/deployment.yaml"}) 662 if err != nil { 663 t.Fatal(err) 664 } 665 666 if gotAlias != gotSet { 667 t.Errorf("Flag aliases not producing same output: with --set: \n\n%s\n\nWith alias:\n\n%s\nDiff:\n\n%s\n", 668 gotSet, gotAlias, util.YAMLDiff(gotSet, gotAlias)) 669 } 670 } 671 672 func TestMultiICPSFiles(t *testing.T) { 673 inPathBase := filepath.Join(testDataDir, "input/all_off.yaml") 674 inPathOverride := filepath.Join(testDataDir, "input/helm_values_enablement.yaml") 675 got, err := runManifestGenerate([]string{inPathBase, inPathOverride}, "", snapshotCharts, []string{"templates/deployment.yaml", "templates/service.yaml"}) 676 if err != nil { 677 t.Fatal(err) 678 } 679 outPath := filepath.Join(testDataDir, "output/helm_values_enablement"+goldenFileSuffixHideChangesInReview) 680 681 want, err := readFile(outPath) 682 if err != nil { 683 t.Fatal(err) 684 } 685 diffSelect := "Deployment:*:istio-egressgateway, Service:*:istio-egressgateway" 686 got, err = compare.FilterManifest(got, diffSelect, "") 687 if err != nil { 688 t.Errorf("error selecting from output manifest: %v", err) 689 } 690 diff := compare.YAMLCmp(got, want) 691 if diff != "" { 692 t.Errorf("`manifest generate` diff = %s", diff) 693 } 694 } 695 696 func TestBareSpec(t *testing.T) { 697 inPathBase := filepath.Join(testDataDir, "input/bare_spec.yaml") 698 _, err := runManifestGenerate([]string{inPathBase}, "", liveCharts, []string{"templates/deployment.yaml"}) 699 if err != nil { 700 t.Fatal(err) 701 } 702 } 703 704 func TestMultipleSpecOneFile(t *testing.T) { 705 inPathBase := filepath.Join(testDataDir, "input/multiple_iops.yaml") 706 _, err := runManifestGenerate([]string{inPathBase}, "", liveCharts, []string{"templates/deployment.yaml"}) 707 if !strings.Contains(err.Error(), "contains multiple IstioOperator CRs, only one per file is supported") { 708 t.Fatalf("got %v, expected error for file with multiple IOPs", err) 709 } 710 } 711 712 func TestBareValues(t *testing.T) { 713 inPathBase := filepath.Join(testDataDir, "input/bare_values.yaml") 714 // As long as the generate doesn't panic, we pass it. bare_values.yaml doesn't 715 // overlay well because JSON doesn't handle null values, and our charts 716 // don't expect values to be blown away. 717 _, _ = runManifestGenerate([]string{inPathBase}, "", liveCharts, []string{"templates/deployment.yaml"}) 718 } 719 720 func TestBogusControlPlaneSec(t *testing.T) { 721 inPathBase := filepath.Join(testDataDir, "input/bogus_cps.yaml") 722 _, err := runManifestGenerate([]string{inPathBase}, "", liveCharts, []string{"templates/deployment.yaml"}) 723 if err != nil { 724 t.Fatal(err) 725 } 726 } 727 728 func TestInstallPackagePath(t *testing.T) { 729 runTestGroup(t, testGroup{ 730 { 731 // Use some arbitrary small test input (pilot only) since we are testing the local filesystem code here, not 732 // manifest generation. 733 desc: "install_package_path", 734 diffSelect: "Deployment:*:istiod", 735 flags: "--set installPackagePath=" + string(liveCharts), 736 }, 737 { 738 // Specify both charts and profile from local filesystem. 739 desc: "install_package_path", 740 diffSelect: "Deployment:*:istiod", 741 flags: fmt.Sprintf("--set installPackagePath=%s --set profile=%s/profiles/default.yaml", string(liveCharts), string(liveCharts)), 742 }, 743 }) 744 } 745 746 // TestTrailingWhitespace ensures there are no trailing spaces in the manifests 747 // This is important because `kubectl edit` and other commands will get escaped if they are present 748 // making it hard to read/edit 749 func TestTrailingWhitespace(t *testing.T) { 750 got, err := runManifestGenerate([]string{}, "--set values.gateways.istio-egressgateway.enabled=true", liveCharts, nil) 751 if err != nil { 752 t.Fatal(err) 753 } 754 lines := strings.Split(got, "\n") 755 for i, l := range lines { 756 if strings.HasSuffix(l, " ") { 757 t.Errorf("Line %v has a trailing space: [%v]. Context: %v", i, l, strings.Join(lines[i-25:i+25], "\n")) 758 } 759 } 760 } 761 762 func validateReferentialIntegrity(t *testing.T, objs object.K8sObjects, cname string, deploymentSelector map[string]string) { 763 t.Run(cname, func(t *testing.T) { 764 deployment := mustFindObject(t, objs, cname, name.DeploymentStr) 765 service := mustFindObject(t, objs, cname, name.ServiceStr) 766 pdb := mustFindObject(t, objs, cname, name.PDBStr) 767 hpa := mustFindObject(t, objs, cname, name.HPAStr) 768 podLabels := mustGetLabels(t, deployment, "spec.template.metadata.labels") 769 // Check all selectors align 770 mustSelect(t, mustGetLabels(t, pdb, "spec.selector.matchLabels"), podLabels) 771 mustSelect(t, mustGetLabels(t, service, "spec.selector"), podLabels) 772 mustSelect(t, mustGetLabels(t, deployment, "spec.selector.matchLabels"), podLabels) 773 if hpaName := mustGetPath(t, hpa, "spec.scaleTargetRef.name"); cname != hpaName { 774 t.Fatalf("HPA does not match deployment: %v != %v", cname, hpaName) 775 } 776 777 serviceAccountName := mustGetPath(t, deployment, "spec.template.spec.serviceAccountName").(string) 778 mustFindObject(t, objs, serviceAccountName, name.SAStr) 779 780 // Check we aren't changing immutable fields. This only matters for in place upgrade (non revision) 781 // This one is not a selector, it must be an exact match 782 if sel := mustGetLabels(t, deployment, "spec.selector.matchLabels"); !reflect.DeepEqual(deploymentSelector, sel) { 783 t.Fatalf("Depployment selectors are immutable, but changed since 1.5. Was %v, now is %v", deploymentSelector, sel) 784 } 785 }) 786 } 787 788 // This test enforces that objects that reference other objects do so properly, such as Service selecting deployment 789 func TestConfigSelectors(t *testing.T) { 790 selectors := []string{ 791 "templates/deployment.yaml", 792 "templates/service.yaml", 793 "templates/poddisruptionbudget.yaml", 794 "templates/autoscale.yaml", 795 "templates/serviceaccount.yaml", 796 } 797 got, err := runManifestGenerate([]string{}, "--set values.gateways.istio-egressgateway.enabled=true", liveCharts, selectors) 798 if err != nil { 799 t.Fatal(err) 800 } 801 objs, err := object.ParseK8sObjectsFromYAMLManifest(got) 802 if err != nil { 803 t.Fatal(err) 804 } 805 gotRev, e := runManifestGenerate([]string{}, "--set revision=canary", liveCharts, selectors) 806 if e != nil { 807 t.Fatal(e) 808 } 809 objsRev, err := object.ParseK8sObjectsFromYAMLManifest(gotRev) 810 if err != nil { 811 t.Fatal(err) 812 } 813 814 istiod15Selector := map[string]string{ 815 "istio": "pilot", 816 } 817 istiodCanary16Selector := map[string]string{ 818 "app": "istiod", 819 "istio.io/rev": "canary", 820 } 821 ingress15Selector := map[string]string{ 822 "app": "istio-ingressgateway", 823 "istio": "ingressgateway", 824 } 825 egress15Selector := map[string]string{ 826 "app": "istio-egressgateway", 827 "istio": "egressgateway", 828 } 829 830 // Validate references within the same deployment 831 validateReferentialIntegrity(t, objs, "istiod", istiod15Selector) 832 validateReferentialIntegrity(t, objs, "istio-ingressgateway", ingress15Selector) 833 validateReferentialIntegrity(t, objs, "istio-egressgateway", egress15Selector) 834 validateReferentialIntegrity(t, objsRev, "istiod-canary", istiodCanary16Selector) 835 836 t.Run("cross revision", func(t *testing.T) { 837 // Istiod revisions have complicated cross revision implications. We should assert these are correct 838 // First we fetch all the objects for our default install 839 cname := "istiod" 840 deployment := mustFindObject(t, objs, cname, name.DeploymentStr) 841 service := mustFindObject(t, objs, cname, name.ServiceStr) 842 pdb := mustFindObject(t, objs, cname, name.PDBStr) 843 podLabels := mustGetLabels(t, deployment, "spec.template.metadata.labels") 844 845 // Next we fetch all the objects for a revision install 846 nameRev := "istiod-canary" 847 deploymentRev := mustFindObject(t, objsRev, nameRev, name.DeploymentStr) 848 hpaRev := mustFindObject(t, objsRev, nameRev, name.HPAStr) 849 serviceRev := mustFindObject(t, objsRev, nameRev, name.ServiceStr) 850 pdbRev := mustFindObject(t, objsRev, nameRev, name.PDBStr) 851 podLabelsRev := mustGetLabels(t, deploymentRev, "spec.template.metadata.labels") 852 853 // Make sure default and revisions do not cross 854 mustNotSelect(t, mustGetLabels(t, serviceRev, "spec.selector"), podLabels) 855 mustNotSelect(t, mustGetLabels(t, service, "spec.selector"), podLabelsRev) 856 mustNotSelect(t, mustGetLabels(t, pdbRev, "spec.selector.matchLabels"), podLabels) 857 mustNotSelect(t, mustGetLabels(t, pdb, "spec.selector.matchLabels"), podLabelsRev) 858 859 // Make sure the scaleTargetRef points to the correct Deployment 860 if hpaName := mustGetPath(t, hpaRev, "spec.scaleTargetRef.name"); nameRev != hpaName { 861 t.Fatalf("HPA does not match deployment: %v != %v", nameRev, hpaName) 862 } 863 864 // Check selection of previous versions . This only matters for in place upgrade (non revision) 865 podLabels15 := map[string]string{ 866 "app": "istiod", 867 "istio": "pilot", 868 } 869 mustSelect(t, mustGetLabels(t, service, "spec.selector"), podLabels15) 870 mustNotSelect(t, mustGetLabels(t, serviceRev, "spec.selector"), podLabels15) 871 mustSelect(t, mustGetLabels(t, pdb, "spec.selector.matchLabels"), podLabels15) 872 mustNotSelect(t, mustGetLabels(t, pdbRev, "spec.selector.matchLabels"), podLabels15) 873 }) 874 } 875 876 // TestLDFlags checks whether building mesh command with 877 // -ldflags "-X istio.io/istio/pkg/version.buildHub=myhub -X istio.io/istio/pkg/version.buildVersion=mytag" 878 // results in these values showing up in a generated manifest. 879 func TestLDFlags(t *testing.T) { 880 tmpHub, tmpTag := version.DockerInfo.Hub, version.DockerInfo.Tag 881 defer func() { 882 version.DockerInfo.Hub, version.DockerInfo.Tag = tmpHub, tmpTag 883 }() 884 version.DockerInfo.Hub = "testHub" 885 version.DockerInfo.Tag = "testTag" 886 l := clog.NewConsoleLogger(os.Stdout, os.Stderr, installerScope) 887 _, iop, err := manifest.GenerateConfig(nil, []string{"installPackagePath=" + string(liveCharts)}, true, nil, l) 888 if err != nil { 889 t.Fatal(err) 890 } 891 if iop.Spec.Hub != version.DockerInfo.Hub || iop.Spec.Tag.GetStringValue() != version.DockerInfo.Tag { 892 t.Fatalf("DockerInfoHub, DockerInfoTag got: %s,%s, want: %s, %s", iop.Spec.Hub, iop.Spec.Tag, version.DockerInfo.Hub, version.DockerInfo.Tag) 893 } 894 } 895 896 func runTestGroup(t *testing.T, tests testGroup) { 897 for _, tt := range tests { 898 tt := tt 899 t.Run(tt.desc, func(t *testing.T) { 900 t.Parallel() 901 inPath := filepath.Join(testDataDir, "input", tt.desc+".yaml") 902 outputSuffix := goldenFileSuffixHideChangesInReview 903 if tt.showOutputFileInPullRequest { 904 outputSuffix = goldenFileSuffixShowChangesInReview 905 } 906 outPath := filepath.Join(testDataDir, "output", tt.desc+outputSuffix) 907 908 var filenames []string 909 if !tt.noInput { 910 filenames = []string{inPath} 911 } 912 913 csource := snapshotCharts 914 if tt.chartSource != "" { 915 csource = tt.chartSource 916 } 917 got, err := runManifestGenerate(filenames, tt.flags, csource, tt.fileSelect) 918 if err != nil { 919 t.Fatal(err) 920 } 921 922 if tt.outputDir != "" { 923 got, err = util.ReadFilesWithFilter(tt.outputDir, func(fileName string) bool { 924 return strings.HasSuffix(fileName, ".yaml") 925 }) 926 if err != nil { 927 t.Fatal(err) 928 } 929 } 930 931 diffSelect := "*:*:*" 932 if tt.diffSelect != "" { 933 diffSelect = tt.diffSelect 934 got, err = compare.FilterManifest(got, diffSelect, "") 935 if err != nil { 936 t.Errorf("error selecting from output manifest: %v", err) 937 } 938 } 939 940 tutil.RefreshGoldenFile(t, []byte(got), outPath) 941 942 want, err := readFile(outPath) 943 if err != nil { 944 t.Fatal(err) 945 } 946 947 if got != want { 948 diff, err := compare.ManifestDiffWithRenameSelectIgnore(got, want, 949 "", diffSelect, tt.diffIgnore, false) 950 if err != nil { 951 t.Fatal(err) 952 } 953 if diff != "" { 954 t.Fatalf("%s: got:\n%s\nwant:\n%s\n(-got, +want)\n%s\n", tt.desc, "", "", diff) 955 } 956 t.Fatalf(cmp.Diff(got, want)) 957 } 958 }) 959 } 960 } 961 962 // nolint: unparam 963 func generateManifest(inFile, flags string, chartSource chartSourceType, fileSelect []string) (string, object.K8sObjects, error) { 964 inPath := filepath.Join(testDataDir, "input", inFile+".yaml") 965 manifest, err := runManifestGenerate([]string{inPath}, flags, chartSource, fileSelect) 966 if err != nil { 967 return "", nil, fmt.Errorf("error %s: %s", err, manifest) 968 } 969 objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest) 970 return manifest, objs, err 971 } 972 973 // runManifestGenerate runs the manifest generate command. If filenames is set, passes the given filenames as -f flag, 974 // flags is passed to the command verbatim. If you set both flags and path, make sure to not use -f in flags. 975 func runManifestGenerate(filenames []string, flags string, chartSource chartSourceType, fileSelect []string) (string, error) { 976 return runManifestCommand("generate", filenames, flags, chartSource, fileSelect) 977 } 978 979 func mustGetWebhook(t test.Failer, obj object.K8sObject) []v1.MutatingWebhook { 980 t.Helper() 981 path := mustGetPath(t, obj, "webhooks") 982 by, err := json.Marshal(path) 983 if err != nil { 984 t.Fatal(err) 985 } 986 var mwh []v1.MutatingWebhook 987 if err := json.Unmarshal(by, &mwh); err != nil { 988 t.Fatal(err) 989 } 990 return mwh 991 } 992 993 func getWebhooks(t *testing.T, setFlags string, webhookName string) []v1.MutatingWebhook { 994 t.Helper() 995 got, err := runManifestGenerate([]string{}, setFlags, liveCharts, []string{"templates/mutatingwebhook.yaml"}) 996 if err != nil { 997 t.Fatal(err) 998 } 999 objs, err := object.ParseK8sObjectsFromYAMLManifest(got) 1000 if err != nil { 1001 t.Fatal(err) 1002 } 1003 return mustGetWebhook(t, mustFindObject(t, objs, webhookName, name.MutatingWebhookConfigurationStr)) 1004 } 1005 1006 func getWebhooksFromYaml(t *testing.T, yml string) []v1.MutatingWebhook { 1007 t.Helper() 1008 objs, err := object.ParseK8sObjectsFromYAMLManifest(yml) 1009 if err != nil { 1010 t.Fatal(err) 1011 } 1012 if len(objs) != 1 { 1013 t.Fatal("expected one webhook") 1014 } 1015 return mustGetWebhook(t, *objs[0]) 1016 } 1017 1018 type LabelSet struct { 1019 namespace, pod klabels.Set 1020 } 1021 1022 func mergeWebhooks(whs ...[]v1.MutatingWebhook) []v1.MutatingWebhook { 1023 res := []v1.MutatingWebhook{} 1024 for _, wh := range whs { 1025 res = append(res, wh...) 1026 } 1027 return res 1028 } 1029 1030 const ( 1031 // istioctl manifest generate --set values.sidecarInjectorWebhook.useLegacySelectors=true 1032 legacyDefaultInjector = ` 1033 apiVersion: admissionregistration.k8s.io/v1 1034 kind: MutatingWebhookConfiguration 1035 metadata: 1036 name: istio-sidecar-injector 1037 webhooks: 1038 - name: sidecar-injector.istio.io 1039 clientConfig: 1040 service: 1041 name: istiod 1042 namespace: istio-system 1043 path: "/inject" 1044 sideEffects: None 1045 rules: 1046 - operations: [ "CREATE" ] 1047 apiGroups: [""] 1048 apiVersions: ["v1"] 1049 resources: ["pods"] 1050 failurePolicy: Fail 1051 admissionReviewVersions: ["v1beta1", "v1"] 1052 namespaceSelector: 1053 matchLabels: 1054 istio-injection: enabled 1055 objectSelector: 1056 matchExpressions: 1057 - key: "sidecar.istio.io/inject" 1058 operator: NotIn 1059 values: 1060 - "false" 1061 ` 1062 1063 // istioctl manifest generate --set values.sidecarInjectorWebhook.useLegacySelectors=true --set revision=canary 1064 legacyRevisionInjector = ` 1065 apiVersion: admissionregistration.k8s.io/v1 1066 kind: MutatingWebhookConfiguration 1067 metadata: 1068 name: istio-sidecar-injector-canary 1069 webhooks: 1070 - name: sidecar-injector.istio.io 1071 clientConfig: 1072 service: 1073 name: istiod-canary 1074 namespace: istio-system 1075 path: "/inject" 1076 sideEffects: None 1077 rules: 1078 - operations: [ "CREATE" ] 1079 apiGroups: [""] 1080 apiVersions: ["v1"] 1081 resources: ["pods"] 1082 failurePolicy: Fail 1083 admissionReviewVersions: ["v1beta1", "v1"] 1084 namespaceSelector: 1085 matchExpressions: 1086 - key: istio-injection 1087 operator: DoesNotExist 1088 - key: istio.io/rev 1089 operator: In 1090 values: 1091 - canary 1092 objectSelector: 1093 matchExpressions: 1094 - key: "sidecar.istio.io/inject" 1095 operator: NotIn 1096 values: 1097 - "false" 1098 ` 1099 ) 1100 1101 // This test checks the mutating webhook selectors behavior, especially with interaction with revisions 1102 func TestWebhookSelector(t *testing.T) { 1103 // Setup various labels to be tested 1104 empty := klabels.Set{} 1105 revLabel := klabels.Set{"istio.io/rev": "canary"} 1106 legacyAndRevLabel := klabels.Set{"istio-injection": "enabled", "istio.io/rev": "canary"} 1107 legacyDisabledAndRevLabel := klabels.Set{"istio-injection": "disabled", "istio.io/rev": "canary"} 1108 legacyLabel := klabels.Set{"istio-injection": "enabled"} 1109 legacyLabelDisabled := klabels.Set{"istio-injection": "disabled"} 1110 1111 objEnabled := klabels.Set{"sidecar.istio.io/inject": "true"} 1112 objDisable := klabels.Set{"sidecar.istio.io/inject": "false"} 1113 objEnabledAndRev := klabels.Set{"sidecar.istio.io/inject": "true", "istio.io/rev": "canary"} 1114 objDisableAndRev := klabels.Set{"sidecar.istio.io/inject": "false", "istio.io/rev": "canary"} 1115 1116 defaultWebhook := getWebhooks(t, "", "istio-sidecar-injector") 1117 revWebhook := getWebhooks(t, "--set revision=canary", "istio-sidecar-injector-canary") 1118 autoWebhook := getWebhooks(t, "--set values.sidecarInjectorWebhook.enableNamespacesByDefault=true", "istio-sidecar-injector") 1119 legacyWebhook := getWebhooksFromYaml(t, legacyDefaultInjector) 1120 legacyRevWebhook := getWebhooksFromYaml(t, legacyRevisionInjector) 1121 1122 // predicate is used to filter out "obvious" test cases, to avoid enumerating all cases 1123 // nolint: unparam 1124 predicate := func(ls LabelSet) (string, bool) { 1125 if ls.namespace.Get("istio-injection") == "disabled" { 1126 return "", true 1127 } 1128 if ls.pod.Get("sidecar.istio.io/inject") == "false" { 1129 return "", true 1130 } 1131 return "", false 1132 } 1133 1134 // We test the cross product namespace and pod labels: 1135 // 1. revision label (istio.io/rev) 1136 // 2. inject label true (istio-injection on namespace, sidecar.istio.io/inject on pod) 1137 // 3. inject label false 1138 // 4. inject label true and revision label 1139 // 5. inject label false and revision label 1140 // 6. no label 1141 // However, we filter out all the disable cases, leaving us with a reasonable number of cases 1142 testLabels := []LabelSet{} 1143 for _, namespaceLabel := range []klabels.Set{empty, revLabel, legacyLabel, legacyLabelDisabled, legacyAndRevLabel, legacyDisabledAndRevLabel} { 1144 for _, podLabel := range []klabels.Set{empty, revLabel, objEnabled, objDisable, objEnabledAndRev, objDisableAndRev} { 1145 testLabels = append(testLabels, LabelSet{namespaceLabel, podLabel}) 1146 } 1147 } 1148 type assertion struct { 1149 namespaceLabel klabels.Set 1150 objectLabel klabels.Set 1151 match string 1152 } 1153 baseAssertions := []assertion{ 1154 {empty, empty, ""}, 1155 {empty, revLabel, "istiod-canary"}, 1156 {empty, objEnabled, "istiod"}, 1157 {empty, objEnabledAndRev, "istiod-canary"}, 1158 {revLabel, empty, "istiod-canary"}, 1159 {revLabel, revLabel, "istiod-canary"}, 1160 {revLabel, objEnabled, "istiod-canary"}, 1161 {revLabel, objEnabledAndRev, "istiod-canary"}, 1162 {legacyLabel, empty, "istiod"}, 1163 {legacyLabel, objEnabled, "istiod"}, 1164 {legacyAndRevLabel, empty, "istiod"}, 1165 {legacyAndRevLabel, objEnabled, "istiod"}, 1166 1167 // The behavior of these is a bit odd; they are explicitly selecting a revision but getting 1168 // the default Unfortunately, the legacy webhook selectors would select these, cause 1169 // duplicate injection, so we defer to the namespace label. 1170 {legacyLabel, revLabel, "istiod"}, 1171 {legacyAndRevLabel, revLabel, "istiod"}, 1172 {legacyLabel, objEnabledAndRev, "istiod"}, 1173 {legacyAndRevLabel, objEnabledAndRev, "istiod"}, 1174 } 1175 cases := []struct { 1176 name string 1177 webhooks []v1.MutatingWebhook 1178 checks []assertion 1179 }{ 1180 { 1181 name: "base", 1182 webhooks: mergeWebhooks(defaultWebhook, revWebhook), 1183 checks: baseAssertions, 1184 }, 1185 { 1186 // This is exactly the same as above, but empty/empty matches 1187 name: "auto injection", 1188 webhooks: mergeWebhooks(autoWebhook, revWebhook), 1189 checks: append([]assertion{{empty, empty, "istiod"}}, baseAssertions...), 1190 }, 1191 { 1192 // Upgrade from a legacy webhook to a new revision based 1193 // Note: we don't need non revision legacy -> non revision, since it will overwrite the webhook 1194 name: "revision upgrade", 1195 webhooks: mergeWebhooks(legacyWebhook, revWebhook), 1196 checks: append([]assertion{ 1197 {empty, objEnabled, ""}, // Legacy one requires namespace label 1198 }, baseAssertions...), 1199 }, 1200 { 1201 // Use new default webhook, while we still have a legacy revision one around. 1202 name: "inplace upgrade", 1203 webhooks: mergeWebhooks(defaultWebhook, legacyRevWebhook), 1204 checks: append([]assertion{ 1205 {empty, revLabel, ""}, // Legacy one requires namespace label 1206 {empty, objEnabledAndRev, ""}, // Legacy one requires namespace label 1207 }, baseAssertions...), 1208 }, 1209 } 1210 for _, tt := range cases { 1211 t.Run(tt.name, func(t *testing.T) { 1212 whs := tt.webhooks 1213 for _, s := range testLabels { 1214 t.Run(fmt.Sprintf("ns:%v pod:%v", s.namespace, s.pod), func(t *testing.T) { 1215 found := "" 1216 match := 0 1217 for i, wh := range whs { 1218 sn := wh.ClientConfig.Service.Name 1219 matches := selectorMatches(t, wh.NamespaceSelector, s.namespace) && selectorMatches(t, wh.ObjectSelector, s.pod) 1220 if matches && found != "" { 1221 // There must be exactly one match, or we will double inject. 1222 t.Fatalf("matched multiple webhooks. Had %v, matched %v", found, sn) 1223 } 1224 if matches { 1225 found = sn 1226 match = i 1227 } 1228 } 1229 // If our predicate can tell us the expected match, use that 1230 if want, ok := predicate(s); ok { 1231 if want != found { 1232 t.Fatalf("expected webhook to go to service %q, found %q", want, found) 1233 } 1234 return 1235 } 1236 // Otherwise, look through our assertions for a matching one, and check that 1237 for _, w := range tt.checks { 1238 if w.namespaceLabel.String() == s.namespace.String() && w.objectLabel.String() == s.pod.String() { 1239 if found != w.match { 1240 if found != "" { 1241 t.Fatalf("expected webhook to go to service %q, found %q (from match %d)\nNamespace selector: %v\nObject selector: %v)", 1242 w.match, found, match, whs[match].NamespaceSelector.MatchExpressions, whs[match].ObjectSelector.MatchExpressions) 1243 } else { 1244 t.Fatalf("expected webhook to go to service %q, found %q", w.match, found) 1245 } 1246 } 1247 return 1248 } 1249 } 1250 // If none match, a test case is missing for the label set. 1251 t.Fatalf("no assertion for namespace=%v pod=%v", s.namespace, s.pod) 1252 }) 1253 } 1254 }) 1255 } 1256 } 1257 1258 func selectorMatches(t *testing.T, selector *metav1.LabelSelector, labels klabels.Set) bool { 1259 t.Helper() 1260 // From webhook spec: "Default to the empty LabelSelector, which matches everything." 1261 if selector == nil { 1262 return true 1263 } 1264 s, err := metav1.LabelSelectorAsSelector(selector) 1265 if err != nil { 1266 t.Fatal(err) 1267 } 1268 return s.Matches(labels) 1269 } 1270 1271 func TestSidecarTemplate(t *testing.T) { 1272 runTestGroup(t, testGroup{ 1273 { 1274 desc: "sidecar_template", 1275 diffSelect: "ConfigMap:*:istio-sidecar-injector", 1276 }, 1277 }) 1278 }