istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/istio/kube.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 istio
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"net/netip"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/hashicorp/go-multierror"
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/api/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	"sigs.k8s.io/yaml"
    35  
    36  	"istio.io/api/annotation"
    37  	"istio.io/api/label"
    38  	opAPI "istio.io/api/operator/v1alpha1"
    39  	"istio.io/istio/istioctl/cmd"
    40  	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    41  	"istio.io/istio/operator/pkg/manifest"
    42  	istiokube "istio.io/istio/pkg/kube"
    43  	"istio.io/istio/pkg/kube/inject"
    44  	"istio.io/istio/pkg/test"
    45  	"istio.io/istio/pkg/test/cert/ca"
    46  	testenv "istio.io/istio/pkg/test/env"
    47  	"istio.io/istio/pkg/test/framework/components/cluster"
    48  	kubecluster "istio.io/istio/pkg/test/framework/components/cluster/kube"
    49  	"istio.io/istio/pkg/test/framework/components/environment/kube"
    50  	"istio.io/istio/pkg/test/framework/components/istio/ingress"
    51  	"istio.io/istio/pkg/test/framework/components/istioctl"
    52  	"istio.io/istio/pkg/test/framework/resource"
    53  	"istio.io/istio/pkg/test/framework/resource/config/apply"
    54  	"istio.io/istio/pkg/test/framework/resource/config/cleanup"
    55  	testKube "istio.io/istio/pkg/test/kube"
    56  	"istio.io/istio/pkg/test/scopes"
    57  	"istio.io/istio/pkg/test/util/file"
    58  	"istio.io/istio/pkg/test/util/retry"
    59  )
    60  
    61  // TODO: dynamically generate meshID to support multi-tenancy tests
    62  const (
    63  	meshID        = "testmesh0"
    64  	istiodSvcName = "istiod"
    65  	istiodLabel   = "pilot"
    66  )
    67  
    68  var (
    69  	// the retry options for waiting for an individual component to be ready
    70  	componentDeployTimeout = retry.Timeout(1 * time.Minute)
    71  	componentDeployDelay   = retry.BackoffDelay(200 * time.Millisecond)
    72  
    73  	_ io.Closer       = &istioImpl{}
    74  	_ Instance        = &istioImpl{}
    75  	_ resource.Dumper = &istioImpl{}
    76  )
    77  
    78  type istioImpl struct {
    79  	id                   resource.ID
    80  	cfg                  Config
    81  	ctx                  resource.Context
    82  	env                  *kube.Environment
    83  	externalControlPlane bool
    84  	installer            *installer
    85  	*meshConfig
    86  	injectConfig *injectConfig
    87  
    88  	mu sync.Mutex
    89  	// ingress components, indexed first by cluster name and then by gateway name.
    90  	ingress map[string]map[string]ingress.Instance
    91  	istiod  map[string]istiokube.PortForwarder
    92  	values  OperatorValues
    93  	workDir string
    94  	iopFiles
    95  }
    96  
    97  type iopInfo struct {
    98  	file string
    99  	spec *opAPI.IstioOperatorSpec
   100  }
   101  
   102  type iopFiles struct {
   103  	primaryIOP  iopInfo
   104  	configIOP   iopInfo
   105  	remoteIOP   iopInfo
   106  	gatewayIOP  iopInfo
   107  	eastwestIOP iopInfo
   108  }
   109  
   110  // ID implements resource.Instance
   111  func (i *istioImpl) ID() resource.ID {
   112  	return i.id
   113  }
   114  
   115  func (i *istioImpl) Settings() Config {
   116  	return i.cfg
   117  }
   118  
   119  func (i *istioImpl) Ingresses() ingress.Instances {
   120  	var out ingress.Instances
   121  	for _, c := range i.ctx.Clusters().Kube() {
   122  		// call IngressFor in-case initialization is needed.
   123  		out = append(out, i.IngressFor(c))
   124  	}
   125  	return out
   126  }
   127  
   128  func (i *istioImpl) IngressFor(c cluster.Cluster) ingress.Instance {
   129  	ingressServiceName := defaultIngressServiceName
   130  	ingressServiceNamespace := i.cfg.SystemNamespace
   131  	ingressServiceLabel := defaultIngressIstioLabel
   132  	if serviceNameOverride := i.cfg.IngressGatewayServiceName; serviceNameOverride != "" {
   133  		ingressServiceName = serviceNameOverride
   134  	}
   135  	if serviceNamespaceOverride := i.cfg.IngressGatewayServiceNamespace; serviceNamespaceOverride != "" {
   136  		ingressServiceNamespace = serviceNamespaceOverride
   137  	}
   138  	if serviceLabelOverride := i.cfg.IngressGatewayIstioLabel; serviceLabelOverride != "" {
   139  		ingressServiceLabel = fmt.Sprintf("istio=%s", serviceLabelOverride)
   140  	}
   141  	name := types.NamespacedName{Name: ingressServiceName, Namespace: ingressServiceNamespace}
   142  	return i.CustomIngressFor(c, name, ingressServiceLabel)
   143  }
   144  
   145  func (i *istioImpl) EastWestGatewayFor(c cluster.Cluster) ingress.Instance {
   146  	name := types.NamespacedName{Name: eastWestIngressServiceName, Namespace: i.cfg.SystemNamespace}
   147  	return i.CustomIngressFor(c, name, eastWestIngressIstioLabel)
   148  }
   149  
   150  func (i *istioImpl) CustomIngressFor(c cluster.Cluster, service types.NamespacedName, labelSelector string) ingress.Instance {
   151  	i.mu.Lock()
   152  	defer i.mu.Unlock()
   153  	if c.Kind() != cluster.Kubernetes {
   154  		c = c.Primary()
   155  	}
   156  
   157  	if i.ingress[c.Name()] == nil {
   158  		i.ingress[c.Name()] = map[string]ingress.Instance{}
   159  	}
   160  	if _, ok := i.ingress[c.Name()][labelSelector]; !ok {
   161  		ingr := newIngress(i.ctx, ingressConfig{
   162  			Cluster:       c,
   163  			Service:       service,
   164  			LabelSelector: labelSelector,
   165  		})
   166  		if closer, ok := ingr.(io.Closer); ok {
   167  			i.ctx.Cleanup(func() { _ = closer.Close() })
   168  		}
   169  		i.ingress[c.Name()][labelSelector] = ingr
   170  	}
   171  	return i.ingress[c.Name()][labelSelector]
   172  }
   173  
   174  func (i *istioImpl) PodIPsFor(c cluster.Cluster, namespace string, label string) ([]corev1.PodIP, error) {
   175  	// Find the pod with the specified label in the specified namespace
   176  	fetchFn := testKube.NewSinglePodFetch(c, namespace, label)
   177  	pods, err := testKube.WaitUntilPodsAreReady(fetchFn)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	pod := pods[0]
   183  	return pod.Status.PodIPs, nil
   184  }
   185  
   186  func (i *istioImpl) InternalDiscoveryAddressFor(c cluster.Cluster) (string, error) {
   187  	i.mu.Lock()
   188  	defer i.mu.Unlock()
   189  	if e, f := i.istiod[c.Name()]; f {
   190  		return e.Address(), nil
   191  	}
   192  	// Find the istiod pod and service, and start forwarding a local port.
   193  	fetchFn := testKube.NewSinglePodFetch(c, i.cfg.SystemNamespace, "istio=pilot")
   194  	pods, err := testKube.WaitUntilPodsAreReady(fetchFn)
   195  	if err != nil {
   196  		return "", err
   197  	}
   198  	pod := pods[0]
   199  	fw, err := c.NewPortForwarder(pod.Name, pod.Namespace, "", 0, 15012)
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  
   204  	if err := fw.Start(); err != nil {
   205  		return "", err
   206  	}
   207  	return fw.Address(), nil
   208  }
   209  
   210  func (i *istioImpl) RemoteDiscoveryAddressFor(cluster cluster.Cluster) (netip.AddrPort, error) {
   211  	var addr netip.AddrPort
   212  	primary := cluster.Primary()
   213  	if !primary.IsConfig() {
   214  		// istiod is exposed via LoadBalancer since we won't have ingress outside of a cluster;a cluster that is;
   215  		// a control cluster, but not config cluster is supposed to simulate istiod outside of k8s or "external"
   216  		address, err := retry.UntilComplete(func() (any, bool, error) {
   217  			addrs, outcome, err := getRemoteServiceAddresses(i.env.Settings(), primary, i.cfg.SystemNamespace, istiodLabel, istiodSvcName, discoveryPort)
   218  			return addrs[0], outcome, err
   219  		}, getAddressTimeout, getAddressDelay)
   220  		if err != nil {
   221  			return netip.AddrPort{}, err
   222  		}
   223  		addr = address.(netip.AddrPort)
   224  	} else {
   225  		name := types.NamespacedName{Name: eastWestIngressServiceName, Namespace: i.cfg.SystemNamespace}
   226  		addr = i.CustomIngressFor(primary, name, eastWestIngressIstioLabel).DiscoveryAddresses()[0]
   227  	}
   228  	if !addr.IsValid() {
   229  		return netip.AddrPort{}, fmt.Errorf("failed to get ingress IP for %s", primary.Name())
   230  	}
   231  	return addr, nil
   232  }
   233  
   234  func (i *istioImpl) Values() (OperatorValues, error) {
   235  	return i.values, nil
   236  }
   237  
   238  func (i *istioImpl) ValuesOrFail(test.Failer) OperatorValues {
   239  	return i.values
   240  }
   241  
   242  func newKube(ctx resource.Context, cfg Config) (Instance, error) {
   243  	cfg.fillDefaults(ctx)
   244  
   245  	scopes.Framework.Infof("=== Istio Component Config ===")
   246  	scopes.Framework.Infof("\n%s", cfg.String())
   247  	scopes.Framework.Infof("================================")
   248  
   249  	// Top-level work dir for Istio deployment.
   250  	workDir, err := ctx.CreateTmpDirectory("istio-deployment")
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  
   255  	// Generate common IstioOperator yamls for different cluster types (primary, remote, remote-config)
   256  	iopFiles, err := genCommonOperatorFiles(ctx, cfg, workDir)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	iop, err := genDefaultOperator(ctx, cfg, iopFiles.primaryIOP.file)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  
   266  	// Populate the revisions for the control plane.
   267  	var revisions resource.RevVerMap
   268  	if !cfg.DeployIstio {
   269  		// Using a pre-installed control plane. Get the revisions from the
   270  		// command-line.
   271  		revisions = ctx.Settings().Revisions
   272  	} else if len(iop.Spec.Revision) > 0 {
   273  		// Use revisions from the default control plane operator.
   274  		revisions = resource.RevVerMap{
   275  			iop.Spec.Revision: "",
   276  		}
   277  	}
   278  
   279  	i := &istioImpl{
   280  		env:                  ctx.Environment().(*kube.Environment),
   281  		cfg:                  cfg,
   282  		ctx:                  ctx,
   283  		workDir:              workDir,
   284  		values:               iop.Spec.Values.Fields,
   285  		installer:            newInstaller(ctx, workDir),
   286  		meshConfig:           &meshConfig{configMap: *newConfigMap(ctx, cfg.SystemNamespace, revisions)},
   287  		injectConfig:         &injectConfig{configMap: *newConfigMap(ctx, cfg.SystemNamespace, revisions)},
   288  		iopFiles:             iopFiles,
   289  		ingress:              map[string]map[string]ingress.Instance{},
   290  		istiod:               map[string]istiokube.PortForwarder{},
   291  		externalControlPlane: ctx.AllClusters().IsExternalControlPlane(),
   292  	}
   293  
   294  	t0 := time.Now()
   295  	defer func() {
   296  		ctx.RecordTraceEvent("istio-deploy", time.Since(t0).Seconds())
   297  	}()
   298  	i.id = ctx.TrackResource(i)
   299  
   300  	if !cfg.DeployIstio {
   301  		scopes.Framework.Info("skipping deployment as specified in the config")
   302  		return i, nil
   303  	}
   304  
   305  	// For multicluster, create and push the CA certs to all clusters to establish a shared root of trust.
   306  	if i.env.IsMultiCluster() {
   307  		if err := i.deployCACerts(); err != nil {
   308  			return nil, err
   309  		}
   310  	}
   311  
   312  	// First install remote-config clusters.
   313  	// We do this first because the external istiod needs to read the config cluster at startup.
   314  	for _, c := range ctx.Clusters().Kube().Configs().Remotes() {
   315  		if err = i.installConfigCluster(c); err != nil {
   316  			return i, err
   317  		}
   318  	}
   319  
   320  	// Install control plane clusters (can be external or primary).
   321  	errG := multierror.Group{}
   322  	for _, c := range ctx.AllClusters().Kube().Primaries() {
   323  		c := c
   324  		errG.Go(func() error {
   325  			return i.installControlPlaneCluster(c)
   326  		})
   327  	}
   328  	if err := errG.Wait().ErrorOrNil(); err != nil {
   329  		scopes.Framework.Errorf("one or more errors occurred installing control-plane clusters: %v", err)
   330  		return i, err
   331  	}
   332  
   333  	// Update config clusters now that external istiod is running.
   334  	for _, c := range ctx.Clusters().Kube().Configs().Remotes() {
   335  		if err = i.reinstallConfigCluster(c); err != nil {
   336  			return i, err
   337  		}
   338  	}
   339  
   340  	// Install (non-config) remote clusters.
   341  	errG = multierror.Group{}
   342  	for _, c := range ctx.Clusters().Kube().Remotes(ctx.Clusters().Configs()...) {
   343  		c := c
   344  		errG.Go(func() error {
   345  			if err := i.installRemoteCluster(c); err != nil {
   346  				return fmt.Errorf("failed installing remote cluster %s: %v", c.Name(), err)
   347  			}
   348  			return nil
   349  		})
   350  	}
   351  	if errs := errG.Wait(); errs != nil {
   352  		return nil, fmt.Errorf("%d errors occurred deploying remote clusters: %v", errs.Len(), errs.ErrorOrNil())
   353  	}
   354  
   355  	if ctx.Clusters().IsMulticluster() {
   356  		// Need to determine if there is a setting to watch cluster secret in config cluster
   357  		// or in external cluster. The flag is named LOCAL_CLUSTER_SECRET_WATCHER and set as
   358  		// an environment variable for istiod.
   359  		watchLocalNamespace := false
   360  		if i.primaryIOP.spec != nil && i.primaryIOP.spec.Values != nil {
   361  			values := OperatorValues(i.primaryIOP.spec.Values.Fields)
   362  			localClusterSecretWatcher := values.GetConfigValue("pilot.env.LOCAL_CLUSTER_SECRET_WATCHER")
   363  			if localClusterSecretWatcher.GetStringValue() == "true" && i.externalControlPlane {
   364  				watchLocalNamespace = true
   365  			}
   366  		}
   367  		if err := i.configureDirectAPIServerAccess(watchLocalNamespace); err != nil {
   368  			return nil, err
   369  		}
   370  	}
   371  
   372  	// Configure gateways for remote clusters.
   373  	for _, c := range ctx.Clusters().Kube().Remotes() {
   374  		c := c
   375  		if i.externalControlPlane || cfg.IstiodlessRemotes {
   376  			// Install ingress and egress gateways
   377  			// These need to be installed as a separate step for external control planes because config clusters are installed
   378  			// before the external control plane cluster. Since remote clusters use gateway injection, we can't install the gateways
   379  			// until after the control plane is running, so we install them here. This is not really necessary for pure (non-config)
   380  			// remote clusters, but it's cleaner to just install gateways as a separate step for all remote clusters.
   381  			if err = i.installRemoteClusterGateways(c); err != nil {
   382  				return i, err
   383  			}
   384  		}
   385  
   386  		// remote clusters only need east-west gateway for multi-network purposes
   387  		if ctx.Environment().IsMultiNetwork() {
   388  			spec := i.remoteIOP.spec
   389  			if c.IsConfig() {
   390  				spec = i.configIOP.spec
   391  			}
   392  			if err := i.deployEastWestGateway(c, spec.Revision, i.eastwestIOP.file); err != nil {
   393  				return i, err
   394  			}
   395  
   396  			// Wait for the eastwestgateway to have a public IP.
   397  			name := types.NamespacedName{Name: eastWestIngressServiceName, Namespace: i.cfg.SystemNamespace}
   398  			_ = i.CustomIngressFor(c, name, eastWestIngressIstioLabel).DiscoveryAddresses()
   399  		}
   400  	}
   401  
   402  	if i.env.IsMultiNetwork() {
   403  		// enable cross network traffic
   404  		for _, c := range ctx.Clusters().Kube().Configs() {
   405  			if err := i.exposeUserServices(c); err != nil {
   406  				return nil, err
   407  			}
   408  		}
   409  	}
   410  
   411  	return i, nil
   412  }
   413  
   414  func initIOPFile(cfg Config, iopFile string, valuesYaml string) (*opAPI.IstioOperatorSpec, error) {
   415  	operatorYaml := cfg.IstioOperatorConfigYAML(valuesYaml)
   416  
   417  	operatorCfg := &iopv1alpha1.IstioOperator{}
   418  	if err := yaml.Unmarshal([]byte(operatorYaml), operatorCfg); err != nil {
   419  		return nil, fmt.Errorf("failed to unmarshal base iop: %v, %v", err, operatorYaml)
   420  	}
   421  	if operatorCfg.Spec == nil {
   422  		operatorCfg.Spec = &opAPI.IstioOperatorSpec{}
   423  	}
   424  
   425  	// marshaling entire operatorCfg causes panic because of *time.Time in ObjectMeta
   426  	outb, err := yaml.Marshal(operatorCfg.Spec)
   427  	if err != nil {
   428  		return nil, fmt.Errorf("failed marshaling iop spec: %v", err)
   429  	}
   430  
   431  	out := fmt.Sprintf(`
   432  apiVersion: install.istio.io/v1alpha1
   433  kind: IstioOperator
   434  spec:
   435  %s`, Indent(string(outb), "  "))
   436  
   437  	if err := os.WriteFile(iopFile, []byte(out), os.ModePerm); err != nil {
   438  		return nil, fmt.Errorf("failed to write iop: %v", err)
   439  	}
   440  
   441  	return operatorCfg.Spec, nil
   442  }
   443  
   444  // installControlPlaneCluster installs the istiod control plane to the given cluster.
   445  // The cluster is considered a "primary" cluster if it is also a "config cluster", in which case components
   446  // like ingress will be installed.
   447  func (i *istioImpl) installControlPlaneCluster(c cluster.Cluster) error {
   448  	scopes.Framework.Infof("setting up %s as control-plane cluster", c.Name())
   449  
   450  	if !c.IsConfig() {
   451  		if err := i.configureRemoteConfigForControlPlane(c); err != nil {
   452  			return err
   453  		}
   454  	}
   455  
   456  	args := commonInstallArgs(i.ctx, i.cfg, c, i.cfg.PrimaryClusterIOPFile, i.primaryIOP.file)
   457  	if i.ctx.Environment().IsMultiCluster() {
   458  		if i.externalControlPlane || i.cfg.IstiodlessRemotes {
   459  			// Enable namespace controller writing to remote clusters
   460  			args.AppendSet("values.pilot.env.EXTERNAL_ISTIOD", "true")
   461  		}
   462  
   463  		// Set the clusterName for the local cluster.
   464  		// This MUST match the clusterName in the remote secret for this cluster.
   465  		clusterName := c.Name()
   466  		if !c.IsConfig() {
   467  			clusterName = c.ConfigName()
   468  		}
   469  		args.AppendSet("values.global.multiCluster.clusterName", clusterName)
   470  	}
   471  
   472  	if err := i.installer.Install(c, args); err != nil {
   473  		return err
   474  	}
   475  
   476  	if c.IsConfig() {
   477  		// this is a traditional primary cluster, install the eastwest gateway
   478  
   479  		// there are a few tests that require special gateway setup which will cause eastwest gateway fail to start
   480  		// exclude these tests from installing eastwest gw for now
   481  		if !i.cfg.DeployEastWestGW {
   482  			return nil
   483  		}
   484  
   485  		if err := i.deployEastWestGateway(c, i.primaryIOP.spec.Revision, i.eastwestIOP.file); err != nil {
   486  			return err
   487  		}
   488  		// Other clusters should only use this for discovery if its a config cluster.
   489  		if err := i.applyIstiodGateway(c, i.primaryIOP.spec.Revision); err != nil {
   490  			return fmt.Errorf("failed applying istiod gateway for cluster %s: %v", c.Name(), err)
   491  		}
   492  		if err := waitForIstioReady(i.ctx, c, i.cfg); err != nil {
   493  			return err
   494  		}
   495  	} else {
   496  		// configure istioctl to run with an external control plane topology.
   497  		istiodAddress, err := i.RemoteDiscoveryAddressFor(c)
   498  		if err != nil {
   499  			return err
   500  		}
   501  		_ = os.Setenv("ISTIOCTL_XDS_ADDRESS", istiodAddress.String())
   502  		_ = os.Setenv("ISTIOCTL_PREFER_EXPERIMENTAL", "true")
   503  		if err := cmd.ConfigAndEnvProcessing(); err != nil {
   504  			return err
   505  		}
   506  	}
   507  
   508  	return nil
   509  }
   510  
   511  // reinstallConfigCluster updates the config cluster installation after the external discovery address is available.
   512  func (i *istioImpl) reinstallConfigCluster(c cluster.Cluster) error {
   513  	scopes.Framework.Infof("updating setup for config cluster %s", c.Name())
   514  	return i.installRemoteCommon(c, i.cfg.ConfigClusterIOPFile, i.configIOP.file, true)
   515  }
   516  
   517  // installConfigCluster installs istio to a cluster that runs workloads and provides Istio configuration.
   518  // The installed components include CRDs, Roles, etc. but not istiod.
   519  func (i *istioImpl) installConfigCluster(c cluster.Cluster) error {
   520  	scopes.Framework.Infof("setting up %s as config cluster", c.Name())
   521  	return i.installRemoteCommon(c, i.cfg.ConfigClusterIOPFile, i.configIOP.file, false)
   522  }
   523  
   524  // installRemoteCluster installs istio to a remote cluster that does not also serve as a config cluster.
   525  func (i *istioImpl) installRemoteCluster(c cluster.Cluster) error {
   526  	scopes.Framework.Infof("setting up %s as remote cluster", c.Name())
   527  	return i.installRemoteCommon(c, i.cfg.RemoteClusterIOPFile, i.remoteIOP.file, true)
   528  }
   529  
   530  // Common install on a either a remote-config or pure remote cluster.
   531  func (i *istioImpl) installRemoteCommon(c cluster.Cluster, defaultsIOPFile, iopFile string, discovery bool) error {
   532  	args := commonInstallArgs(i.ctx, i.cfg, c, defaultsIOPFile, iopFile)
   533  	if i.env.IsMultiCluster() {
   534  		// Set the clusterName for the local cluster.
   535  		// This MUST match the clusterName in the remote secret for this cluster.
   536  		args.AppendSet("values.global.multiCluster.clusterName", c.Name())
   537  	}
   538  
   539  	if discovery {
   540  		// Configure the cluster and network arguments to pass through the injector webhook.
   541  		remoteIstiodAddress, err := i.RemoteDiscoveryAddressFor(c)
   542  		if err != nil {
   543  			return err
   544  		}
   545  		args.AppendSet("values.global.remotePilotAddress", remoteIstiodAddress.Addr().String())
   546  	}
   547  
   548  	if i.externalControlPlane || i.cfg.IstiodlessRemotes {
   549  		args.AppendSet("values.istiodRemote.injectionPath",
   550  			fmt.Sprintf("/inject/net/%s/cluster/%s", c.NetworkName(), c.Name()))
   551  	}
   552  
   553  	if err := i.installer.Install(c, args); err != nil {
   554  		return err
   555  	}
   556  
   557  	return nil
   558  }
   559  
   560  func (i *istioImpl) installRemoteClusterGateways(c cluster.Cluster) error {
   561  	inFilenames := []string{
   562  		filepath.Join(testenv.IstioSrc, IntegrationTestRemoteGatewaysIOP),
   563  	}
   564  	if i.gatewayIOP.file != "" {
   565  		inFilenames = append(inFilenames, i.gatewayIOP.file)
   566  	}
   567  	args := installArgs{
   568  		ComponentName: "ingress and egress gateways",
   569  		Files:         inFilenames,
   570  		Set: []string{
   571  			"values.global.imagePullPolicy=" + i.ctx.Settings().Image.PullPolicy,
   572  		},
   573  	}
   574  
   575  	if err := i.installer.Install(c, args); err != nil {
   576  		return err
   577  	}
   578  
   579  	return nil
   580  }
   581  
   582  func kubeConfigFileForCluster(c cluster.Cluster) (string, error) {
   583  	type Filenamer interface {
   584  		Filename() string
   585  	}
   586  	fn, ok := c.(Filenamer)
   587  	if !ok {
   588  		return "", fmt.Errorf("cluster does not support fetching kube config")
   589  	}
   590  	return fn.Filename(), nil
   591  }
   592  
   593  func commonInstallArgs(ctx resource.Context, cfg Config, c cluster.Cluster, defaultsIOPFile, iopFile string) installArgs {
   594  	if !path.IsAbs(defaultsIOPFile) {
   595  		defaultsIOPFile = filepath.Join(testenv.IstioSrc, defaultsIOPFile)
   596  	}
   597  	baseIOP := cfg.BaseIOPFile
   598  	if !path.IsAbs(baseIOP) {
   599  		baseIOP = filepath.Join(testenv.IstioSrc, baseIOP)
   600  	}
   601  
   602  	args := installArgs{
   603  		Files: []string{
   604  			baseIOP,
   605  			defaultsIOPFile,
   606  			iopFile,
   607  		},
   608  		Set: []string{
   609  			"hub=" + ctx.Settings().Image.Hub,
   610  			"tag=" + ctx.Settings().Image.Tag,
   611  			"values.global.imagePullPolicy=" + ctx.Settings().Image.PullPolicy,
   612  			"values.global.variant=" + ctx.Settings().Image.Variant,
   613  		},
   614  	}
   615  
   616  	if ctx.Environment().IsMultiNetwork() && c.NetworkName() != "" {
   617  		args.AppendSet("values.global.meshID", meshID)
   618  		args.AppendSet("values.global.network", c.NetworkName())
   619  	}
   620  
   621  	// Include all user-specified values and configuration options.
   622  	if cfg.EnableCNI {
   623  		args.AppendSet("components.cni.enabled", "true")
   624  	}
   625  
   626  	if ctx.Settings().EnableDualStack {
   627  		args.AppendSet("values.pilot.env.ISTIO_DUAL_STACK", "true")
   628  		args.AppendSet("meshConfig.defaultConfig.proxyMetadata.ISTIO_DUAL_STACK", "true")
   629  		args.AppendSet("values.gateways.istio-ingressgateway.ipFamilyPolicy", "RequireDualStack")
   630  		args.AppendSet("values.gateways.istio-egressgateway.ipFamilyPolicy", "RequireDualStack")
   631  	}
   632  
   633  	// Include all user-specified values.
   634  	for k, v := range cfg.Values {
   635  		args.AppendSet("values."+k, v)
   636  	}
   637  
   638  	for k, v := range cfg.OperatorOptions {
   639  		args.AppendSet(k, v)
   640  	}
   641  	return args
   642  }
   643  
   644  func waitForIstioReady(ctx resource.Context, c cluster.Cluster, cfg Config) error {
   645  	if !cfg.SkipWaitForValidationWebhook {
   646  		// Wait for webhook to come online. The only reliable way to do that is to see if we can submit invalid config.
   647  		if err := waitForValidationWebhook(ctx, c, cfg); err != nil {
   648  			return err
   649  		}
   650  	}
   651  	return nil
   652  }
   653  
   654  // configureDirectAPIServiceAccessBetweenClusters - create a remote secret of cluster `c` and place
   655  // the secret in all `from` clusters
   656  func (i *istioImpl) configureDirectAPIServiceAccessBetweenClusters(c cluster.Cluster, from ...cluster.Cluster) error {
   657  	// Create a secret.
   658  	secret, err := i.CreateRemoteSecret(i.ctx, c)
   659  	if err != nil {
   660  		return fmt.Errorf("failed creating remote secret for cluster %s: %v", c.Name(), err)
   661  	}
   662  	if err := i.ctx.ConfigKube(from...).
   663  		YAML(i.cfg.SystemNamespace, secret).
   664  		Apply(apply.NoCleanup); err != nil {
   665  		return fmt.Errorf("failed applying remote secret to clusters: %v", err)
   666  	}
   667  	return nil
   668  }
   669  
   670  func getTargetClusterListForCluster(targetClusters []cluster.Cluster, c cluster.Cluster) []cluster.Cluster {
   671  	var outClusters []cluster.Cluster
   672  	scopes.Framework.Infof("Secret from cluster: %s will be placed in following clusters", c.Name())
   673  	for _, cc := range targetClusters {
   674  		// if cc is an external cluster, config cluster's secret should have already been
   675  		// placed on the cluster, or the given cluster is the same as the cluster in
   676  		// the target list. Only when c is not config cluster, cc is not external cluster
   677  		// and the given cluster is not the same as the target, c's secret goes onto cc.
   678  		if (!c.IsConfig() || !cc.IsExternalControlPlane()) && c.Name() != cc.Name() {
   679  			scopes.Framework.Infof("Target cluster: %s", cc.Name())
   680  			outClusters = append(outClusters, cc)
   681  		}
   682  	}
   683  	return outClusters
   684  }
   685  
   686  func (i *istioImpl) configureDirectAPIServerAccess(watchLocalNamespace bool) error {
   687  	var targetClusters []cluster.Cluster
   688  	if watchLocalNamespace {
   689  		// when configured to watch istiod local namespace, secrets go to the external cluster
   690  		// and primary clusters
   691  		targetClusters = i.ctx.AllClusters().Primaries()
   692  	} else {
   693  		// when configured to watch istiod config namespace, secrets go to config clusters
   694  		targetClusters = i.ctx.AllClusters().Configs()
   695  	}
   696  
   697  	// Now look through entire mesh, create secret for every cluster other than external control plane and
   698  	// place the secret into the target clusters.
   699  	for _, c := range i.ctx.Clusters().Kube().MeshClusters() {
   700  		theTargetClusters := getTargetClusterListForCluster(targetClusters, c)
   701  		if len(theTargetClusters) > 0 {
   702  			if err := i.configureDirectAPIServiceAccessBetweenClusters(c, theTargetClusters...); err != nil {
   703  				return fmt.Errorf("failed providing primary cluster access for remote cluster %s: %v", c.Name(), err)
   704  			}
   705  		}
   706  	}
   707  	return nil
   708  }
   709  
   710  func (i *istioImpl) CreateRemoteSecret(ctx resource.Context, c cluster.Cluster, opts ...string) (string, error) {
   711  	istioCtl, err := istioctl.New(ctx, istioctl.Config{
   712  		Cluster: c,
   713  	})
   714  	if err != nil {
   715  		return "", err
   716  	}
   717  	istioctlCmd := []string{
   718  		"create-remote-secret",
   719  		"--name", c.Name(),
   720  		"--namespace", i.cfg.SystemNamespace,
   721  		"--manifests", filepath.Join(testenv.IstioSrc, "manifests"),
   722  	}
   723  	istioctlCmd = append(istioctlCmd, opts...)
   724  
   725  	scopes.Framework.Infof("Creating remote secret for cluster %s %v", c.Name(), istioctlCmd)
   726  	out, _, err := istioCtl.Invoke(istioctlCmd)
   727  	if err != nil {
   728  		return "", fmt.Errorf("create remote secret failed for cluster %s: %v", c.Name(), err)
   729  	}
   730  	return out, nil
   731  }
   732  
   733  func (i *istioImpl) deployCACerts() error {
   734  	certsDir := filepath.Join(i.workDir, "cacerts")
   735  	if err := os.Mkdir(certsDir, 0o700); err != nil {
   736  		return err
   737  	}
   738  
   739  	root, err := ca.NewRoot(certsDir)
   740  	if err != nil {
   741  		return fmt.Errorf("failed creating the root CA: %v", err)
   742  	}
   743  
   744  	for _, c := range i.env.Clusters() {
   745  		// Create a subdir for the cluster certs.
   746  		clusterDir := filepath.Join(certsDir, c.Name())
   747  		if err := os.Mkdir(clusterDir, 0o700); err != nil {
   748  			return err
   749  		}
   750  
   751  		// Create the new extensions config for the CA
   752  		caConfig, err := ca.NewIstioConfig(i.cfg.SystemNamespace)
   753  		if err != nil {
   754  			return err
   755  		}
   756  
   757  		// Create the certs for the cluster.
   758  		clusterCA, err := ca.NewIntermediate(clusterDir, caConfig, root)
   759  		if err != nil {
   760  			return fmt.Errorf("failed creating intermediate CA for cluster %s: %v", c.Name(), err)
   761  		}
   762  
   763  		// Create the CA secret for this cluster. Istio will use these certs for its CA rather
   764  		// than its autogenerated self-signed root.
   765  		secret, err := clusterCA.NewIstioCASecret()
   766  		if err != nil {
   767  			return fmt.Errorf("failed creating intermediate CA secret for cluster %s: %v", c.Name(), err)
   768  		}
   769  
   770  		// Create the system namespace.
   771  		var nsLabels map[string]string
   772  		if i.env.IsMultiNetwork() {
   773  			nsLabels = map[string]string{label.TopologyNetwork.Name: c.NetworkName()}
   774  		}
   775  		var nsAnnotations map[string]string
   776  		if c.IsRemote() {
   777  			nsAnnotations = map[string]string{
   778  				annotation.TopologyControlPlaneClusters.Name: c.Config().Name(),
   779  				// ^^^ Use config cluster name because external control plane uses config cluster as its cluster ID
   780  			}
   781  		}
   782  		if _, err := c.Kube().CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
   783  			ObjectMeta: metav1.ObjectMeta{
   784  				Labels:      nsLabels,
   785  				Annotations: nsAnnotations,
   786  				Name:        i.cfg.SystemNamespace,
   787  			},
   788  		}, metav1.CreateOptions{}); err != nil {
   789  			if errors.IsAlreadyExists(err) {
   790  				if _, err := c.Kube().CoreV1().Namespaces().Update(context.TODO(), &corev1.Namespace{
   791  					ObjectMeta: metav1.ObjectMeta{
   792  						Labels:      nsLabels,
   793  						Annotations: nsAnnotations,
   794  						Name:        i.cfg.SystemNamespace,
   795  					},
   796  				}, metav1.UpdateOptions{}); err != nil {
   797  					scopes.Framework.Errorf("failed updating namespace %s on cluster %s. This can happen when deploying "+
   798  						"multiple control planes. Error: %v", i.cfg.SystemNamespace, c.Name(), err)
   799  				}
   800  			} else {
   801  				scopes.Framework.Errorf("failed creating namespace %s on cluster %s. This can happen when deploying "+
   802  					"multiple control planes. Error: %v", i.cfg.SystemNamespace, c.Name(), err)
   803  			}
   804  		}
   805  
   806  		// Create the secret for the cacerts.
   807  		if _, err := c.Kube().CoreV1().Secrets(i.cfg.SystemNamespace).Create(context.TODO(), secret,
   808  			metav1.CreateOptions{}); err != nil {
   809  			// no need to do anything if cacerts is already present
   810  			if !errors.IsAlreadyExists(err) {
   811  				scopes.Framework.Errorf("failed to create CA secrets on cluster %s. This can happen when deploying "+
   812  					"multiple control planes. Error: %v", c.Name(), err)
   813  			}
   814  		}
   815  	}
   816  	return nil
   817  }
   818  
   819  // configureRemoteConfigForControlPlane allows istiod in the given external control plane to read resources
   820  // in its remote config cluster by creating the kubeconfig secret pointing to the remote kubeconfig, and the
   821  // service account required to read the secret.
   822  func (i *istioImpl) configureRemoteConfigForControlPlane(c cluster.Cluster) error {
   823  	configCluster := c.Config()
   824  	istioKubeConfig, err := file.AsString(configCluster.(*kubecluster.Cluster).Filename())
   825  	if err != nil {
   826  		scopes.Framework.Infof("error in parsing kubeconfig for %s", configCluster.Name())
   827  		return err
   828  	}
   829  
   830  	scopes.Framework.Infof("configuring external control plane in %s to use config cluster %s", c.Name(), configCluster.Name())
   831  	// ensure system namespace exists
   832  	if _, err = c.Kube().CoreV1().Namespaces().
   833  		Create(context.TODO(), &corev1.Namespace{
   834  			ObjectMeta: metav1.ObjectMeta{
   835  				Name: i.cfg.SystemNamespace,
   836  			},
   837  		}, metav1.CreateOptions{}); err != nil && !errors.IsAlreadyExists(err) {
   838  		return err
   839  	}
   840  	// create kubeconfig secret
   841  	if _, err = c.Kube().CoreV1().Secrets(i.cfg.SystemNamespace).
   842  		Create(context.TODO(), &corev1.Secret{
   843  			ObjectMeta: metav1.ObjectMeta{
   844  				Name:      "istio-kubeconfig",
   845  				Namespace: i.cfg.SystemNamespace,
   846  			},
   847  			Data: map[string][]byte{
   848  				"config": []byte(istioKubeConfig),
   849  			},
   850  		}, metav1.CreateOptions{}); err != nil {
   851  		if errors.IsAlreadyExists(err) { // Allow easier running locally when we run multiple tests in a row
   852  			if _, err := c.Kube().CoreV1().Secrets(i.cfg.SystemNamespace).Update(context.TODO(), &corev1.Secret{
   853  				ObjectMeta: metav1.ObjectMeta{
   854  					Name:      "istio-kubeconfig",
   855  					Namespace: i.cfg.SystemNamespace,
   856  				},
   857  				Data: map[string][]byte{
   858  					"config": []byte(istioKubeConfig),
   859  				},
   860  			}, metav1.UpdateOptions{}); err != nil {
   861  				scopes.Framework.Infof("error updating istio-kubeconfig secret: %v", err)
   862  				return err
   863  			}
   864  		} else {
   865  			scopes.Framework.Infof("error creating istio-kubeconfig secret %v", err)
   866  			return err
   867  		}
   868  	}
   869  	return nil
   870  }
   871  
   872  func (i *istioImpl) UpdateInjectionConfig(t resource.Context, update func(*inject.Config) error, cleanup cleanup.Strategy) error {
   873  	return i.injectConfig.UpdateInjectionConfig(t, update, cleanup)
   874  }
   875  
   876  func (i *istioImpl) InjectionConfig() (*inject.Config, error) {
   877  	return i.injectConfig.InjectConfig()
   878  }
   879  
   880  func genCommonOperatorFiles(ctx resource.Context, cfg Config, workDir string) (i iopFiles, err error) {
   881  	// Generate the istioctl config file for primary clusters
   882  	i.primaryIOP.file = filepath.Join(workDir, "iop.yaml")
   883  	if i.primaryIOP.spec, err = initIOPFile(cfg, i.primaryIOP.file, cfg.ControlPlaneValues); err != nil {
   884  		return iopFiles{}, err
   885  	}
   886  
   887  	// Generate the istioctl config file for remote cluster
   888  	i.remoteIOP.file = filepath.Join(workDir, "remote.yaml")
   889  	if i.remoteIOP.spec, err = initIOPFile(cfg, i.remoteIOP.file, cfg.RemoteClusterValues); err != nil {
   890  		return iopFiles{}, err
   891  	}
   892  
   893  	// Generate the istioctl config file for config cluster
   894  	if ctx.AllClusters().IsExternalControlPlane() {
   895  		i.configIOP.file = filepath.Join(workDir, "config.yaml")
   896  		if i.configIOP.spec, err = initIOPFile(cfg, i.configIOP.file, cfg.ConfigClusterValues); err != nil {
   897  			return iopFiles{}, err
   898  		}
   899  	} else {
   900  		i.configIOP = i.primaryIOP
   901  	}
   902  
   903  	if cfg.GatewayValues != "" {
   904  		i.gatewayIOP.file = filepath.Join(workDir, "custom_gateways.yaml")
   905  		_, err = initIOPFile(cfg, i.gatewayIOP.file, cfg.GatewayValues)
   906  		if err != nil {
   907  			return iopFiles{}, err
   908  		}
   909  	}
   910  	if cfg.EastWestGatewayValues != "" {
   911  		i.eastwestIOP.file = filepath.Join(workDir, "eastwest.yaml")
   912  		_, err = initIOPFile(cfg, i.eastwestIOP.file, cfg.EastWestGatewayValues)
   913  		if err != nil {
   914  			return iopFiles{}, err
   915  		}
   916  	}
   917  
   918  	return
   919  }
   920  
   921  func genDefaultOperator(ctx resource.Context, cfg Config, iopFile string) (*iopv1alpha1.IstioOperator, error) {
   922  	primary := ctx.AllClusters().Configs()[0]
   923  	args := commonInstallArgs(ctx, cfg, primary, cfg.PrimaryClusterIOPFile, iopFile)
   924  
   925  	var stdOut, stdErr bytes.Buffer
   926  	_, iop, err := manifest.GenerateConfig(
   927  		args.Files,
   928  		args.Set,
   929  		false,
   930  		nil,
   931  		cmdLogger(&stdOut, &stdErr))
   932  	if err != nil {
   933  		return nil, fmt.Errorf("failed generating primary manifest: %v", err)
   934  	}
   935  
   936  	return iop, nil
   937  }