| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package inject |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| import ( |
| "github.com/google/go-cmp/cmp" |
| "google.golang.org/protobuf/testing/protocmp" |
| "google.golang.org/protobuf/types/known/durationpb" |
| "google.golang.org/protobuf/types/known/structpb" |
| "istio.io/api/annotation" |
| meshapi "istio.io/api/mesh/v1alpha1" |
| proxyConfig "istio.io/api/networking/v1beta1" |
| corev1 "k8s.io/api/core/v1" |
| "k8s.io/apimachinery/pkg/runtime" |
| ) |
| |
| import ( |
| opconfig "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/features" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| "github.com/apache/dubbo-go-pixiu/pilot/test/util" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/constants" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/mesh" |
| "github.com/apache/dubbo-go-pixiu/pkg/kube" |
| "github.com/apache/dubbo-go-pixiu/pkg/test" |
| "github.com/apache/dubbo-go-pixiu/pkg/util/sets" |
| ) |
| |
| // TestInjection tests both the mutating webhook and kube-inject. It does this by sharing the same input and output |
| // test files and running through the two different code paths. |
| func TestInjection(t *testing.T) { |
| type testCase struct { |
| in string |
| want string |
| setFlags []string |
| inFilePath string |
| mesh func(m *meshapi.MeshConfig) |
| skipWebhook bool |
| expectedError string |
| expectedLog string |
| setup func(t test.Failer) |
| } |
| cases := []testCase{ |
| // verify cni |
| { |
| in: "hello.yaml", |
| want: "hello.yaml.cni.injected", |
| setFlags: []string{ |
| "components.cni.enabled=true", |
| "values.istio_cni.chained=true", |
| "values.global.network=network1", |
| }, |
| }, |
| { |
| in: "hello.yaml", |
| want: "hello.yaml.proxyImageName.injected", |
| setFlags: []string{ |
| "values.global.proxy.image=proxyTest", |
| }, |
| }, |
| { |
| in: "hello.yaml", |
| want: "hello-tproxy.yaml.injected", |
| mesh: func(m *meshapi.MeshConfig) { |
| m.DefaultConfig.InterceptionMode = meshapi.ProxyConfig_TPROXY |
| }, |
| }, |
| { |
| in: "hello.yaml", |
| want: "hello-always.yaml.injected", |
| setFlags: []string{"values.global.imagePullPolicy=Always"}, |
| }, |
| { |
| in: "hello.yaml", |
| want: "hello-never.yaml.injected", |
| setFlags: []string{"values.global.imagePullPolicy=Never"}, |
| }, |
| { |
| in: "enable-core-dump.yaml", |
| want: "enable-core-dump.yaml.injected", |
| setFlags: []string{"values.global.proxy.enableCoreDump=true"}, |
| }, |
| { |
| in: "format-duration.yaml", |
| want: "format-duration.yaml.injected", |
| mesh: func(m *meshapi.MeshConfig) { |
| m.DefaultConfig.DrainDuration = durationpb.New(time.Second * 23) |
| m.DefaultConfig.ParentShutdownDuration = durationpb.New(time.Second * 42) |
| }, |
| }, |
| { |
| // Verifies that parameters are applied properly when no annotations are provided. |
| in: "traffic-params.yaml", |
| want: "traffic-params.yaml.injected", |
| setFlags: []string{ |
| `values.global.proxy.includeIPRanges=127.0.0.1/24,10.96.0.1/24`, |
| `values.global.proxy.excludeIPRanges=10.96.0.2/24,10.96.0.3/24`, |
| `values.global.proxy.excludeInboundPorts=4,5,6`, |
| `values.global.proxy.statusPort=0`, |
| }, |
| }, |
| { |
| // Verifies that the status params behave properly. |
| in: "status_params.yaml", |
| want: "status_params.yaml.injected", |
| setFlags: []string{ |
| `values.global.proxy.statusPort=123`, |
| `values.global.proxy.readinessInitialDelaySeconds=100`, |
| `values.global.proxy.readinessPeriodSeconds=200`, |
| `values.global.proxy.readinessFailureThreshold=300`, |
| }, |
| }, |
| { |
| // Verifies that the kubevirtInterfaces list are applied properly from parameters.. |
| in: "kubevirtInterfaces.yaml", |
| want: "kubevirtInterfaces.yaml.injected", |
| setFlags: []string{ |
| `values.global.proxy.statusPort=123`, |
| `values.global.proxy.readinessInitialDelaySeconds=100`, |
| `values.global.proxy.readinessPeriodSeconds=200`, |
| `values.global.proxy.readinessFailureThreshold=300`, |
| }, |
| }, |
| { |
| // Verifies that global.imagePullSecrets are applied properly |
| in: "hello.yaml", |
| want: "hello-image-secrets-in-values.yaml.injected", |
| inFilePath: "hello-image-secrets-in-values.iop.yaml", |
| }, |
| { |
| // Verifies that global.imagePullSecrets are appended properly |
| in: "hello-image-pull-secret.yaml", |
| want: "hello-multiple-image-secrets.yaml.injected", |
| inFilePath: "hello-image-secrets-in-values.iop.yaml", |
| }, |
| { |
| // Verifies that global.podDNSSearchNamespaces are applied properly |
| in: "hello.yaml", |
| want: "hello-template-in-values.yaml.injected", |
| inFilePath: "hello-template-in-values.iop.yaml", |
| }, |
| { |
| // Verifies that global.mountMtlsCerts is applied properly |
| in: "hello.yaml", |
| want: "hello-mount-mtls-certs.yaml.injected", |
| setFlags: []string{`values.global.mountMtlsCerts=true`}, |
| }, |
| { |
| // Verifies that k8s.v1.cni.cncf.io/networks is set to istio-cni when not chained |
| in: "hello.yaml", |
| want: "hello-cncf-networks.yaml.injected", |
| setFlags: []string{ |
| `components.cni.enabled=true`, |
| `values.istio_cni.chained=false`, |
| }, |
| }, |
| { |
| // Verifies that istio-cni is appended to k8s.v1.cni.cncf.io/networks flat value if set |
| in: "hello-existing-cncf-networks.yaml", |
| want: "hello-existing-cncf-networks.yaml.injected", |
| setFlags: []string{ |
| `components.cni.enabled=true`, |
| `values.istio_cni.chained=false`, |
| }, |
| }, |
| { |
| // Verifies that istio-cni is appended to k8s.v1.cni.cncf.io/networks JSON value |
| in: "hello-existing-cncf-networks-json.yaml", |
| want: "hello-existing-cncf-networks-json.yaml.injected", |
| setFlags: []string{ |
| `components.cni.enabled=true`, |
| `values.istio_cni.chained=false`, |
| }, |
| }, |
| { |
| // Verifies that HoldApplicationUntilProxyStarts in MeshConfig puts sidecar in front |
| in: "hello.yaml", |
| want: "hello.proxyHoldsApplication.yaml.injected", |
| setFlags: []string{ |
| `values.global.proxy.holdApplicationUntilProxyStarts=true`, |
| }, |
| }, |
| { |
| // Verifies that HoldApplicationUntilProxyStarts in MeshConfig puts sidecar in front |
| in: "hello-probes.yaml", |
| want: "hello-probes.proxyHoldsApplication.yaml.injected", |
| setFlags: []string{ |
| `values.global.proxy.holdApplicationUntilProxyStarts=true`, |
| }, |
| }, |
| { |
| // Verifies that HoldApplicationUntilProxyStarts in proxyconfig sets lifecycle hook |
| in: "hello-probes-proxyHoldApplication-ProxyConfig.yaml", |
| want: "hello-probes-proxyHoldApplication-ProxyConfig.yaml.injected", |
| }, |
| { |
| // Verifies that HoldApplicationUntilProxyStarts=false in proxyconfig 'OR's with MeshConfig setting |
| in: "hello-probes-noProxyHoldApplication-ProxyConfig.yaml", |
| want: "hello-probes-noProxyHoldApplication-ProxyConfig.yaml.injected", |
| setFlags: []string{ |
| `values.global.proxy.holdApplicationUntilProxyStarts=true`, |
| }, |
| }, |
| { |
| // A test with no pods is not relevant for webhook |
| in: "hello-service.yaml", |
| want: "hello-service.yaml.injected", |
| skipWebhook: true, |
| }, |
| { |
| // Cronjob is tricky for webhook test since the spec is different. Since the real code will |
| // get a pod anyways, the test isn't too useful for webhook anyways. |
| in: "cronjob.yaml", |
| want: "cronjob.yaml.injected", |
| skipWebhook: true, |
| }, |
| { |
| in: "traffic-annotations-bad-includeipranges.yaml", |
| expectedError: "includeipranges", |
| }, |
| { |
| in: "traffic-annotations-bad-excludeipranges.yaml", |
| expectedError: "excludeipranges", |
| }, |
| { |
| in: "traffic-annotations-bad-includeinboundports.yaml", |
| expectedError: "includeinboundports", |
| }, |
| { |
| in: "traffic-annotations-bad-excludeinboundports.yaml", |
| expectedError: "excludeinboundports", |
| }, |
| { |
| in: "traffic-annotations-bad-excludeoutboundports.yaml", |
| expectedError: "excludeoutboundports", |
| }, |
| { |
| in: "hello.yaml", |
| want: "hello-no-seccontext.yaml.injected", |
| setup: func(t test.Failer) { |
| test.SetBoolForTest(t, &features.EnableLegacyFSGroupInjection, false) |
| test.SetEnvForTest(t, "ENABLE_LEGACY_FSGROUP_INJECTION", "false") |
| }, |
| }, |
| { |
| in: "traffic-annotations.yaml", |
| want: "traffic-annotations.yaml.injected", |
| mesh: func(m *meshapi.MeshConfig) { |
| if m.DefaultConfig.ProxyMetadata == nil { |
| m.DefaultConfig.ProxyMetadata = map[string]string{} |
| } |
| m.DefaultConfig.ProxyMetadata["ISTIO_META_TLS_CLIENT_KEY"] = "/etc/identity/client/keys/client-key.pem" |
| }, |
| }, |
| { |
| in: "proxy-override.yaml", |
| want: "proxy-override.yaml.injected", |
| }, |
| { |
| in: "explicit-security-context.yaml", |
| want: "explicit-security-context.yaml.injected", |
| }, |
| { |
| in: "only-proxy-container.yaml", |
| want: "only-proxy-container.yaml.injected", |
| }, |
| { |
| in: "proxy-override-args.yaml", |
| want: "proxy-override-args.yaml.injected", |
| }, |
| { |
| in: "custom-template.yaml", |
| want: "custom-template.yaml.injected", |
| inFilePath: "custom-template.iop.yaml", |
| }, |
| { |
| in: "tcp-probes.yaml", |
| want: "tcp-probes.yaml.injected", |
| }, |
| { |
| in: "tcp-probes.yaml", |
| want: "tcp-probes-disabled.yaml.injected", |
| setup: func(t test.Failer) { |
| test.SetBoolForTest(t, &features.RewriteTCPProbes, false) |
| }, |
| }, |
| { |
| in: "hello-host-network-with-ns.yaml", |
| want: "hello-host-network-with-ns.yaml.injected", |
| expectedLog: "Skipping injection because Deployment \"sample/hello-host-network\" has host networking enabled", |
| }, |
| } |
| // Keep track of tests we add options above |
| // We will search for all test files and skip these ones |
| alreadyTested := sets.New() |
| for _, t := range cases { |
| if t.want != "" { |
| alreadyTested.Insert(t.want) |
| } else { |
| alreadyTested.Insert(t.in + ".injected") |
| } |
| } |
| files, err := os.ReadDir("testdata/inject") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(files) < 3 { |
| t.Fatalf("Didn't find test files - something must have gone wrong") |
| } |
| // Automatically add any other test files in the folder. This ensures we don't |
| // forget to add to this list, that we don't have duplicates, etc |
| // Keep track of all golden files so we can ensure we don't have unused ones later |
| allOutputFiles := sets.New() |
| for _, f := range files { |
| if strings.HasSuffix(f.Name(), ".injected") { |
| allOutputFiles.Insert(f.Name()) |
| } |
| if strings.HasSuffix(f.Name(), ".iop.yaml") { |
| continue |
| } |
| if !strings.HasSuffix(f.Name(), ".yaml") { |
| continue |
| } |
| want := f.Name() + ".injected" |
| if alreadyTested.Contains(want) { |
| continue |
| } |
| cases = append(cases, testCase{in: f.Name(), want: want}) |
| } |
| |
| // Precompute injection settings. This may seem like a premature optimization, but due to the size of |
| // YAMLs, with -race this was taking >10min in some cases to generate! |
| if util.Refresh() { |
| writeInjectionSettings(t, "default", nil, "") |
| for i, c := range cases { |
| if c.setFlags != nil || c.inFilePath != "" { |
| writeInjectionSettings(t, fmt.Sprintf("%s.%d", c.in, i), c.setFlags, c.inFilePath) |
| } |
| } |
| } |
| // Preload default settings. Computation here is expensive, so this speeds the tests up substantially |
| defaultTemplate, defaultValues, defaultMesh := readInjectionSettings(t, "default") |
| for i, c := range cases { |
| i, c := i, c |
| testName := fmt.Sprintf("[%02d] %s", i, c.want) |
| if c.expectedError != "" { |
| testName = fmt.Sprintf("[%02d] %s", i, c.in) |
| } |
| t.Run(testName, func(t *testing.T) { |
| if c.setup != nil { |
| c.setup(t) |
| } else { |
| // Tests with custom setup modify global state and cannot run in parallel |
| t.Parallel() |
| } |
| |
| mc, err := mesh.DeepCopyMeshConfig(defaultMesh) |
| if err != nil { |
| t.Fatal(err) |
| } |
| sidecarTemplate, valuesConfig := defaultTemplate, defaultValues |
| if c.setFlags != nil || c.inFilePath != "" { |
| sidecarTemplate, valuesConfig, mc = readInjectionSettings(t, fmt.Sprintf("%s.%d", c.in, i)) |
| } |
| if c.mesh != nil { |
| c.mesh(mc) |
| } |
| |
| inputFilePath := "testdata/inject/" + c.in |
| wantFilePath := "testdata/inject/" + c.want |
| in, err := os.Open(inputFilePath) |
| if err != nil { |
| t.Fatalf("Failed to open %q: %v", inputFilePath, err) |
| } |
| t.Cleanup(func() { |
| _ = in.Close() |
| }) |
| |
| // First we test kube-inject. This will run exactly what kube-inject does, and write output to the golden files |
| t.Run("kube-inject", func(t *testing.T) { |
| var got bytes.Buffer |
| logs := make([]string, 0) |
| warn := func(s string) { |
| logs = append(logs, s) |
| t.Log(s) |
| } |
| if err = IntoResourceFile(nil, sidecarTemplate.Templates, valuesConfig, "", mc, in, &got, warn); err != nil { |
| if c.expectedError != "" { |
| if !strings.Contains(strings.ToLower(err.Error()), c.expectedError) { |
| t.Fatalf("expected error %q got %q", c.expectedError, err) |
| } |
| return |
| } |
| t.Fatalf("IntoResourceFile(%v) returned an error: %v", inputFilePath, err) |
| } |
| if c.expectedError != "" { |
| t.Fatalf("expected error but got none") |
| } |
| if c.expectedLog != "" { |
| hasExpectedLog := false |
| for _, log := range logs { |
| if strings.Contains(log, c.expectedLog) { |
| hasExpectedLog = true |
| break |
| } |
| } |
| if !hasExpectedLog { |
| t.Fatal("expected log but got none") |
| } |
| } |
| |
| // The version string is a maintenance pain for this test. Strip the version string before comparing. |
| gotBytes := util.StripVersion(got.Bytes()) |
| wantBytes := util.ReadGoldenFile(t, gotBytes, wantFilePath) |
| |
| util.CompareBytes(t, gotBytes, wantBytes, wantFilePath) |
| }) |
| |
| // Exit early if we don't need to test webhook. We can skip errors since its redundant |
| // and painful to test here. |
| if c.expectedError != "" || c.skipWebhook { |
| return |
| } |
| // Next run the webhook test. This one is a bit trickier as the webhook operates |
| // on Pods, but the inputs are Deployments/StatefulSets/etc. As a result, we need |
| // to convert these to pods, then run the injection This test will *not* |
| // overwrite golden files, as we do not have identical textual output as |
| // kube-inject. Instead, we just compare the desired/actual pod specs. |
| t.Run("webhook", func(t *testing.T) { |
| webhook := &Webhook{ |
| Config: sidecarTemplate, |
| meshConfig: mc, |
| env: &model.Environment{ |
| PushContext: &model.PushContext{ |
| ProxyConfigs: &model.ProxyConfigs{}, |
| }, |
| }, |
| valuesConfig: valuesConfig, |
| revision: "default", |
| } |
| // Split multi-part yaml documents. Input and output will have the same number of parts. |
| inputYAMLs := splitYamlFile(inputFilePath, t) |
| wantYAMLs := splitYamlFile(wantFilePath, t) |
| for i := 0; i < len(inputYAMLs); i++ { |
| t.Run(fmt.Sprintf("yamlPart[%d]", i), func(t *testing.T) { |
| runWebhook(t, webhook, inputYAMLs[i], wantYAMLs[i], true) |
| }) |
| } |
| }) |
| }) |
| } |
| |
| // Make sure we don't have any stale test data leftover, as it can cause confusion. |
| for _, c := range cases { |
| delete(allOutputFiles, c.want) |
| } |
| if len(allOutputFiles) != 0 { |
| t.Fatalf("stale golden files found: %v", allOutputFiles.UnsortedList()) |
| } |
| } |
| |
| func testInjectionTemplate(t *testing.T, template, input, expected string) { |
| t.Helper() |
| tmpl, err := ParseTemplates(map[string]string{SidecarTemplateName: template}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| webhook := &Webhook{ |
| Config: &Config{ |
| Templates: tmpl, |
| Policy: InjectionPolicyEnabled, |
| DefaultTemplates: []string{SidecarTemplateName}, |
| }, |
| env: &model.Environment{ |
| PushContext: &model.PushContext{ |
| ProxyConfigs: &model.ProxyConfigs{}, |
| }, |
| }, |
| } |
| runWebhook(t, webhook, []byte(input), []byte(expected), false) |
| } |
| |
| func TestMultipleInjectionTemplates(t *testing.T) { |
| p, err := ParseTemplates(map[string]string{ |
| "sidecar": ` |
| spec: |
| containers: |
| - name: istio-proxy |
| image: proxy |
| `, |
| "init": ` |
| spec: |
| initContainers: |
| - name: istio-init |
| image: proxy |
| `, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| webhook := &Webhook{ |
| Config: &Config{ |
| Templates: p, |
| Aliases: map[string][]string{"both": {"sidecar", "init"}}, |
| Policy: InjectionPolicyEnabled, |
| }, |
| env: &model.Environment{ |
| PushContext: &model.PushContext{ |
| ProxyConfigs: &model.ProxyConfigs{}, |
| }, |
| }, |
| } |
| |
| input := ` |
| apiVersion: v1 |
| kind: Pod |
| metadata: |
| name: hello |
| annotations: |
| inject.istio.io/templates: sidecar,init |
| spec: |
| containers: |
| - name: hello |
| image: "fake.docker.io/google-samples/hello-go-gke:1.0" |
| ` |
| inputAlias := ` |
| apiVersion: v1 |
| kind: Pod |
| metadata: |
| name: hello |
| annotations: |
| inject.istio.io/templates: both |
| spec: |
| containers: |
| - name: hello |
| image: "fake.docker.io/google-samples/hello-go-gke:1.0" |
| ` |
| // nolint: lll |
| expected := ` |
| apiVersion: v1 |
| kind: Pod |
| metadata: |
| annotations: |
| inject.istio.io/templates: %s |
| prometheus.io/path: /stats/prometheus |
| prometheus.io/port: "0" |
| prometheus.io/scrape: "true" |
| 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}' |
| name: hello |
| spec: |
| initContainers: |
| - name: istio-init |
| image: proxy |
| containers: |
| - name: hello |
| image: fake.docker.io/google-samples/hello-go-gke:1.0 |
| - name: istio-proxy |
| image: proxy |
| ` |
| runWebhook(t, webhook, []byte(input), []byte(fmt.Sprintf(expected, "sidecar,init")), false) |
| runWebhook(t, webhook, []byte(inputAlias), []byte(fmt.Sprintf(expected, "both")), false) |
| } |
| |
| // TestStrategicMerge ensures we can use https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md |
| // directives in the injection template |
| func TestStrategicMerge(t *testing.T) { |
| testInjectionTemplate(t, |
| ` |
| metadata: |
| labels: |
| $patch: replace |
| foo: bar |
| spec: |
| containers: |
| - name: injected |
| image: "fake.docker.io/google-samples/hello-go-gke:1.1" |
| `, |
| ` |
| apiVersion: v1 |
| kind: Pod |
| metadata: |
| name: hello |
| labels: |
| key: value |
| spec: |
| containers: |
| - name: hello |
| image: "fake.docker.io/google-samples/hello-go-gke:1.0" |
| `, |
| |
| // We expect resources to only have limits, since we had the "replace" directive. |
| // nolint: lll |
| ` |
| apiVersion: v1 |
| kind: Pod |
| metadata: |
| annotations: |
| prometheus.io/path: /stats/prometheus |
| prometheus.io/port: "0" |
| prometheus.io/scrape: "true" |
| labels: |
| foo: bar |
| name: hello |
| spec: |
| containers: |
| - name: injected |
| image: "fake.docker.io/google-samples/hello-go-gke:1.1" |
| - name: hello |
| image: "fake.docker.io/google-samples/hello-go-gke:1.0" |
| `) |
| } |
| |
| func runWebhook(t *testing.T, webhook *Webhook, inputYAML []byte, wantYAML []byte, idempotencyCheck bool) { |
| // Convert the input YAML to a deployment. |
| inputRaw, err := FromRawToObject(inputYAML) |
| if err != nil { |
| t.Fatal(err) |
| } |
| inputPod := objectToPod(t, inputRaw) |
| |
| // Convert the wanted YAML to a deployment. |
| wantRaw, err := FromRawToObject(wantYAML) |
| if err != nil { |
| t.Fatal(err) |
| } |
| wantPod := objectToPod(t, wantRaw) |
| |
| // Generate the patch. At runtime, the webhook would actually generate the patch against the |
| // pod configuration. But since our input files are deployments, rather than actual pod instances, |
| // we have to apply the patch to the template portion of the deployment only. |
| templateJSON := convertToJSON(inputPod, t) |
| got := webhook.inject(&kube.AdmissionReview{ |
| Request: &kube.AdmissionRequest{ |
| Object: runtime.RawExtension{ |
| Raw: templateJSON, |
| }, |
| Namespace: jsonToUnstructured(inputYAML, t).GetNamespace(), |
| }, |
| }, "") |
| var gotPod *corev1.Pod |
| // Apply the generated patch to the template. |
| if got.Patch != nil { |
| patchedPod := &corev1.Pod{} |
| patch := prettyJSON(got.Patch, t) |
| patchedTemplateJSON := applyJSONPatch(templateJSON, patch, t) |
| if err := json.Unmarshal(patchedTemplateJSON, patchedPod); err != nil { |
| t.Fatal(err) |
| } |
| gotPod = patchedPod |
| } else { |
| gotPod = inputPod |
| } |
| |
| if err := normalizeAndCompareDeployments(gotPod, wantPod, false, t); err != nil { |
| t.Fatal(err) |
| } |
| if idempotencyCheck { |
| t.Run("idempotency", func(t *testing.T) { |
| if err := normalizeAndCompareDeployments(gotPod, wantPod, true, t); err != nil { |
| t.Fatal(err) |
| } |
| }) |
| } |
| } |
| |
| func TestSkipUDPPorts(t *testing.T) { |
| cases := []struct { |
| c corev1.Container |
| ports []string |
| }{ |
| { |
| c: corev1.Container{ |
| Ports: []corev1.ContainerPort{}, |
| }, |
| }, |
| { |
| c: corev1.Container{ |
| Ports: []corev1.ContainerPort{ |
| { |
| ContainerPort: 80, |
| Protocol: corev1.ProtocolTCP, |
| }, |
| { |
| ContainerPort: 8080, |
| Protocol: corev1.ProtocolTCP, |
| }, |
| }, |
| }, |
| ports: []string{"80", "8080"}, |
| }, |
| { |
| c: corev1.Container{ |
| Ports: []corev1.ContainerPort{ |
| { |
| ContainerPort: 53, |
| Protocol: corev1.ProtocolTCP, |
| }, |
| { |
| ContainerPort: 53, |
| Protocol: corev1.ProtocolUDP, |
| }, |
| }, |
| }, |
| ports: []string{"53"}, |
| }, |
| { |
| c: corev1.Container{ |
| Ports: []corev1.ContainerPort{ |
| { |
| ContainerPort: 80, |
| Protocol: corev1.ProtocolTCP, |
| }, |
| { |
| ContainerPort: 53, |
| Protocol: corev1.ProtocolUDP, |
| }, |
| }, |
| }, |
| ports: []string{"80"}, |
| }, |
| { |
| c: corev1.Container{ |
| Ports: []corev1.ContainerPort{ |
| { |
| ContainerPort: 53, |
| Protocol: corev1.ProtocolUDP, |
| }, |
| }, |
| }, |
| }, |
| } |
| for i := range cases { |
| expectPorts := cases[i].ports |
| ports := getPortsForContainer(cases[i].c) |
| if len(ports) != len(expectPorts) { |
| t.Fatalf("unexpect ports result for case %d", i) |
| } |
| for j := 0; j < len(ports); j++ { |
| if ports[j] != expectPorts[j] { |
| t.Fatalf("unexpect ports result for case %d: expect %v, got %v", i, expectPorts, ports) |
| } |
| } |
| } |
| } |
| |
| func TestCleanProxyConfig(t *testing.T) { |
| overrides := mesh.DefaultProxyConfig() |
| overrides.ConfigPath = "/foo/bar" |
| overrides.DrainDuration = durationpb.New(7 * time.Second) |
| overrides.ProxyMetadata = map[string]string{ |
| "foo": "barr", |
| } |
| explicit := mesh.DefaultProxyConfig() |
| explicit.ConfigPath = constants.ConfigPathDir |
| explicit.DrainDuration = durationpb.New(45 * time.Second) |
| cases := []struct { |
| name string |
| proxy *meshapi.ProxyConfig |
| expect string |
| }{ |
| { |
| "default", |
| mesh.DefaultProxyConfig(), |
| `{}`, |
| }, |
| { |
| "explicit default", |
| explicit, |
| `{}`, |
| }, |
| { |
| "overrides", |
| overrides, |
| `{"configPath":"/foo/bar","drainDuration":"7s","proxyMetadata":{"foo":"barr"}}`, |
| }, |
| } |
| for _, tt := range cases { |
| t.Run(tt.name, func(t *testing.T) { |
| got := protoToJSON(tt.proxy) |
| if got != tt.expect { |
| t.Fatalf("incorrect output: got %v, expected %v", got, tt.expect) |
| } |
| roundTrip, err := mesh.ApplyProxyConfig(got, mesh.DefaultMeshConfig()) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !cmp.Equal(roundTrip.GetDefaultConfig(), tt.proxy, protocmp.Transform()) { |
| t.Fatalf("round trip is not identical: got \n%+v, expected \n%+v", *roundTrip.GetDefaultConfig(), tt.proxy) |
| } |
| }) |
| } |
| } |
| |
| func TestAppendMultusNetwork(t *testing.T) { |
| cases := []struct { |
| name string |
| in string |
| want string |
| }{ |
| { |
| name: "empty", |
| in: "", |
| want: "istio-cni", |
| }, |
| { |
| name: "flat-single", |
| in: "macvlan-conf-1", |
| want: "macvlan-conf-1, istio-cni", |
| }, |
| { |
| name: "flat-multiple", |
| in: "macvlan-conf-1, macvlan-conf-2", |
| want: "macvlan-conf-1, macvlan-conf-2, istio-cni", |
| }, |
| { |
| name: "json-single", |
| in: `[{"name": "macvlan-conf-1"}]`, |
| want: `[{"name": "macvlan-conf-1"}, {"name": "istio-cni"}]`, |
| }, |
| { |
| name: "json-multiple", |
| in: `[{"name": "macvlan-conf-1"}, {"name": "macvlan-conf-2"}]`, |
| want: `[{"name": "macvlan-conf-1"}, {"name": "macvlan-conf-2"}, {"name": "istio-cni"}]`, |
| }, |
| { |
| name: "json-multiline", |
| in: `[ |
| {"name": "macvlan-conf-1"}, |
| {"name": "macvlan-conf-2"} |
| ]`, |
| want: `[ |
| {"name": "macvlan-conf-1"}, |
| {"name": "macvlan-conf-2"} |
| , {"name": "istio-cni"}]`, |
| }, |
| { |
| name: "json-multiline-additional-fields", |
| in: `[ |
| {"name": "macvlan-conf-1", "another-field": "another-value"}, |
| {"name": "macvlan-conf-2"} |
| ]`, |
| want: `[ |
| {"name": "macvlan-conf-1", "another-field": "another-value"}, |
| {"name": "macvlan-conf-2"} |
| , {"name": "istio-cni"}]`, |
| }, |
| { |
| name: "json-preconfigured-istio-cni", |
| in: `[ |
| {"name": "macvlan-conf-1"}, |
| {"name": "macvlan-conf-2"}, |
| {"name": "istio-cni", "config": "additional-config"}, |
| ]`, |
| want: `[ |
| {"name": "macvlan-conf-1"}, |
| {"name": "macvlan-conf-2"}, |
| {"name": "istio-cni", "config": "additional-config"}, |
| ]`, |
| }, |
| } |
| |
| for _, tc := range cases { |
| t.Run(tc.name, func(t *testing.T) { |
| t.Parallel() |
| actual := appendMultusNetwork(tc.in, "istio-cni") |
| if actual != tc.want { |
| t.Fatalf("Unexpected result.\nExpected:\n%v\nActual:\n%v", tc.want, actual) |
| } |
| t.Run("idempotency", func(t *testing.T) { |
| actual := appendMultusNetwork(actual, "istio-cni") |
| if actual != tc.want { |
| t.Fatalf("Function is not idempotent.\nExpected:\n%v\nActual:\n%v", tc.want, actual) |
| } |
| }) |
| }) |
| } |
| } |
| |
| func Test_updateClusterEnvs(t *testing.T) { |
| type args struct { |
| container *corev1.Container |
| newKVs map[string]string |
| } |
| tests := []struct { |
| name string |
| args args |
| want *corev1.Container |
| }{ |
| { |
| args: args{ |
| container: &corev1.Container{}, |
| newKVs: parseInjectEnvs("/inject/net/network1/cluster/cluster1"), |
| }, |
| want: &corev1.Container{ |
| Env: []corev1.EnvVar{ |
| { |
| Name: "ISTIO_META_CLUSTER_ID", |
| Value: "cluster1", |
| }, |
| { |
| Name: "ISTIO_META_NETWORK", |
| Value: "network1", |
| }, |
| }, |
| }, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| updateClusterEnvs(tt.args.container, tt.args.newKVs) |
| if !cmp.Equal(tt.args.container.Env, tt.want.Env) { |
| t.Fatalf("updateClusterEnvs got \n%+v, expected \n%+v", tt.args.container.Env, tt.want.Env) |
| } |
| }) |
| } |
| } |
| |
| func TestQuantityConversion(t *testing.T) { |
| for _, tt := range []struct { |
| in string |
| out int |
| err error |
| }{ |
| { |
| in: "4000m", |
| out: 4, |
| }, |
| { |
| in: "6500m", |
| out: 7, |
| }, |
| { |
| in: "200mi", |
| err: errors.New("unable to parse"), |
| }, |
| } { |
| t.Run(tt.in, func(t *testing.T) { |
| got, err := quantityToConcurrency(tt.in) |
| if err != nil { |
| if tt.err == nil { |
| t.Errorf("expected no error, got %v", err) |
| } |
| } else { |
| if tt.out != got { |
| t.Errorf("got %v, want %v", got, tt.out) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestProxyImage(t *testing.T) { |
| val := func(hub string, tag interface{}) *opconfig.Values { |
| t, _ := structpb.NewValue(tag) |
| return &opconfig.Values{ |
| Global: &opconfig.GlobalConfig{ |
| Hub: hub, |
| Tag: t, |
| }, |
| } |
| } |
| pc := func(imageType string) *proxyConfig.ProxyImage { |
| return &proxyConfig.ProxyImage{ |
| ImageType: imageType, |
| } |
| } |
| |
| ann := func(imageType string) map[string]string { |
| if imageType == "" { |
| return nil |
| } |
| return map[string]string{ |
| annotation.SidecarProxyImageType.Name: imageType, |
| } |
| } |
| |
| for _, tt := range []struct { |
| desc string |
| v *opconfig.Values |
| pc *proxyConfig.ProxyImage |
| ann map[string]string |
| want string |
| }{ |
| { |
| desc: "vals-only-int-tag", |
| v: val("docker.io/istio", 11), |
| want: "docker.io/istio/proxyv2:11", |
| }, |
| { |
| desc: "pc overrides imageType - float tag", |
| v: val("docker.io/istio", 1.12), |
| pc: pc("distroless"), |
| want: "docker.io/istio/proxyv2:1.12-distroless", |
| }, |
| { |
| desc: "annotation overrides imageType", |
| v: val("gcr.io/gke-release/asm", "1.11.2-asm.17"), |
| ann: ann("distroless"), |
| want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17-distroless", |
| }, |
| { |
| desc: "pc and annotation overrides imageType", |
| v: val("gcr.io/gke-release/asm", "1.11.2-asm.17"), |
| pc: pc("distroless"), |
| ann: ann("debug"), |
| want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17-debug", |
| }, |
| { |
| desc: "pc and annotation overrides imageType, ann is default", |
| v: val("gcr.io/gke-release/asm", "1.11.2-asm.17"), |
| pc: pc("debug"), |
| ann: ann("default"), |
| want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17", |
| }, |
| { |
| desc: "pc overrides imageType with default, tag also has image type", |
| v: val("gcr.io/gke-release/asm", "1.11.2-asm.17-distroless"), |
| pc: pc("default"), |
| want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17", |
| }, |
| { |
| desc: "ann overrides imageType with default, tag also has image type", |
| v: val("gcr.io/gke-release/asm", "1.11.2-asm.17-distroless"), |
| ann: ann("default"), |
| want: "gcr.io/gke-release/asm/proxyv2:1.11.2-asm.17", |
| }, |
| { |
| desc: "pc overrides imageType, tag also has image type", |
| v: val("docker.io/istio", "1.12-debug"), |
| pc: pc("distroless"), |
| want: "docker.io/istio/proxyv2:1.12-distroless", |
| }, |
| { |
| desc: "annotation overrides imageType, tag also has the same image type", |
| v: val("docker.io/istio", "1.12-distroless"), |
| ann: ann("distroless"), |
| want: "docker.io/istio/proxyv2:1.12-distroless", |
| }, |
| { |
| desc: "unusual tag should work", |
| v: val("private-repo/istio", "1.12-this-is-unusual-tag"), |
| want: "private-repo/istio/proxyv2:1.12-this-is-unusual-tag", |
| }, |
| { |
| desc: "unusual tag should work, default override", |
| v: val("private-repo/istio", "1.12-this-is-unusual-tag-distroless"), |
| pc: pc("default"), |
| want: "private-repo/istio/proxyv2:1.12-this-is-unusual-tag", |
| }, |
| { |
| desc: "annotation overrides imageType with unusual tag", |
| v: val("private-repo/istio", "1.12-this-is-unusual-tag"), |
| ann: ann("distroless"), |
| want: "private-repo/istio/proxyv2:1.12-this-is-unusual-tag-distroless", |
| }, |
| } { |
| t.Run(tt.desc, func(t *testing.T) { |
| got := ProxyImage(tt.v, tt.pc, tt.ann) |
| if got != tt.want { |
| t.Errorf("got: <%s>, want <%s> <== value(%v) proxyConfig(%v) ann(%v)", got, tt.want, tt.v, tt.pc, tt.ann) |
| } |
| }) |
| } |
| } |