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  }