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  }