istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/kube/deployment.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 kube
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/netip"
    21  	"os"
    22  	"path"
    23  	"path/filepath"
    24  	"strings"
    25  	"text/template"
    26  	"time"
    27  
    28  	"github.com/hashicorp/go-multierror"
    29  	appsv1 "k8s.io/api/apps/v1"
    30  	corev1 "k8s.io/api/core/v1"
    31  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	"k8s.io/client-go/kubernetes"
    35  
    36  	"istio.io/api/label"
    37  	meshconfig "istio.io/api/mesh/v1alpha1"
    38  	istioctlcmd "istio.io/istio/istioctl/pkg/workload"
    39  	"istio.io/istio/pkg/config/constants"
    40  	"istio.io/istio/pkg/config/protocol"
    41  	"istio.io/istio/pkg/log"
    42  	echoCommon "istio.io/istio/pkg/test/echo/common"
    43  	"istio.io/istio/pkg/test/env"
    44  	"istio.io/istio/pkg/test/framework/components/echo"
    45  	"istio.io/istio/pkg/test/framework/components/environment/kube"
    46  	"istio.io/istio/pkg/test/framework/components/istio"
    47  	"istio.io/istio/pkg/test/framework/components/istioctl"
    48  	"istio.io/istio/pkg/test/framework/components/namespace"
    49  	"istio.io/istio/pkg/test/framework/resource"
    50  	"istio.io/istio/pkg/test/framework/resource/config/apply"
    51  	"istio.io/istio/pkg/test/scopes"
    52  	"istio.io/istio/pkg/test/util/file"
    53  	"istio.io/istio/pkg/test/util/retry"
    54  	"istio.io/istio/pkg/test/util/tmpl"
    55  	"istio.io/istio/pkg/util/protomarshal"
    56  )
    57  
    58  const (
    59  	// for proxyless we add a special gRPC server that doesn't get configured with xDS for test-runner use
    60  	grpcMagicPort = 17171
    61  	// for non-Go implementations of gRPC echo, this is the port used to forward non-gRPC requests to the Go server
    62  	grpcFallbackPort = 17777
    63  )
    64  
    65  var echoKubeTemplatesDir = path.Join(env.IstioSrc, "pkg/test/framework/components/echo/kube/templates")
    66  
    67  func getTemplate(tmplFilePath string) *template.Template {
    68  	yamlPath := path.Join(echoKubeTemplatesDir, tmplFilePath)
    69  	if filepath.IsAbs(tmplFilePath) {
    70  		yamlPath = tmplFilePath
    71  	}
    72  	return tmpl.MustParse(file.MustAsString(yamlPath))
    73  }
    74  
    75  var _ workloadHandler = &deployment{}
    76  
    77  type deployment struct {
    78  	ctx             resource.Context
    79  	cfg             echo.Config
    80  	shouldCreateWLE bool
    81  }
    82  
    83  func newDeployment(ctx resource.Context, cfg echo.Config) (*deployment, error) {
    84  	if !cfg.Cluster.IsConfig() && cfg.DeployAsVM {
    85  		return nil, fmt.Errorf("cannot deploy %s/%s as VM on non-config %s",
    86  			cfg.Namespace.Name(),
    87  			cfg.Service,
    88  			cfg.Cluster.Name())
    89  	}
    90  
    91  	if cfg.DeployAsVM {
    92  		if err := createVMConfig(ctx, cfg); err != nil {
    93  			return nil, fmt.Errorf("failed creating vm config for %s/%s: %v",
    94  				cfg.Namespace.Name(),
    95  				cfg.Service,
    96  				err)
    97  		}
    98  	}
    99  
   100  	deploymentYAML, err := GenerateDeployment(ctx, cfg, ctx.Settings())
   101  	if err != nil {
   102  		return nil, fmt.Errorf("failed generating echo deployment YAML for %s/%s: %v",
   103  			cfg.Namespace.Name(),
   104  			cfg.Service, err)
   105  	}
   106  
   107  	// Apply the deployment to the configured cluster.
   108  	if err = ctx.ConfigKube(cfg.Cluster).
   109  		YAML(cfg.Namespace.Name(), deploymentYAML).
   110  		Apply(apply.NoCleanup); err != nil {
   111  		return nil, fmt.Errorf("failed deploying echo %s to cluster %s: %v",
   112  			cfg.ClusterLocalFQDN(), cfg.Cluster.Name(), err)
   113  	}
   114  
   115  	return &deployment{
   116  		ctx:             ctx,
   117  		cfg:             cfg,
   118  		shouldCreateWLE: cfg.DeployAsVM && !cfg.AutoRegisterVM,
   119  	}, nil
   120  }
   121  
   122  // Restart performs restarts of all the pod of the deployment.
   123  // This is analogous to `kubectl rollout restart` on the echo deployment and waits for
   124  // `kubectl rollout status` to complete before returning, but uses direct API calls.
   125  func (d *deployment) Restart() error {
   126  	var errs error
   127  	var deploymentNames []string
   128  	for _, s := range d.cfg.Subsets {
   129  		// TODO(Monkeyanator) move to common place so doesn't fall out of sync with templates
   130  		deploymentNames = append(deploymentNames, fmt.Sprintf("%s-%s", d.cfg.Service, s.Version))
   131  	}
   132  	curTimestamp := time.Now().Format(time.RFC3339)
   133  	for _, deploymentName := range deploymentNames {
   134  		patchOpts := metav1.PatchOptions{}
   135  		patchData := fmt.Sprintf(`{
   136  			"spec": {
   137  				"template": {
   138  					"metadata": {
   139  						"annotations": {
   140  							"kubectl.kubernetes.io/restartedAt": %q
   141  						}
   142  					}
   143  				}
   144  			}
   145  		}`, curTimestamp) // e.g., “2006-01-02T15:04:05Z07:00”
   146  		var err error
   147  		appsv1Client := d.cfg.Cluster.Kube().AppsV1()
   148  
   149  		if d.cfg.IsStatefulSet() {
   150  			_, err = appsv1Client.StatefulSets(d.cfg.Namespace.Name()).Patch(context.TODO(), deploymentName,
   151  				types.StrategicMergePatchType, []byte(patchData), patchOpts)
   152  		} else {
   153  			_, err = appsv1Client.Deployments(d.cfg.Namespace.Name()).Patch(context.TODO(), deploymentName,
   154  				types.StrategicMergePatchType, []byte(patchData), patchOpts)
   155  		}
   156  		if err != nil {
   157  			errs = multierror.Append(errs, fmt.Errorf("failed to rollout restart %v/%v: %v (timestamp:%q)", d.cfg.Namespace.Name(), deploymentName, err, curTimestamp))
   158  			continue
   159  		}
   160  
   161  		if err := retry.UntilSuccess(func() error {
   162  			if d.cfg.IsStatefulSet() {
   163  				sts, err := appsv1Client.StatefulSets(d.cfg.Namespace.Name()).Get(context.TODO(), deploymentName, metav1.GetOptions{})
   164  				if err != nil {
   165  					return err
   166  				}
   167  				if sts.Spec.Replicas == nil || !statefulsetComplete(sts) {
   168  					return fmt.Errorf("rollout is not yet done (updated replicas:%v)", sts.Status.UpdatedReplicas)
   169  				}
   170  			} else {
   171  				dep, err := appsv1Client.Deployments(d.cfg.Namespace.Name()).Get(context.TODO(), deploymentName, metav1.GetOptions{})
   172  				if err != nil {
   173  					return err
   174  				}
   175  				if dep.Spec.Replicas == nil || !deploymentComplete(dep) {
   176  					return fmt.Errorf("rollout is not yet done (updated replicas: %v)", dep.Status.UpdatedReplicas)
   177  				}
   178  			}
   179  			return nil
   180  		}, retry.Timeout(60*time.Second), retry.Delay(2*time.Second)); err != nil {
   181  			errs = multierror.Append(errs, fmt.Errorf("failed to wait rollout status for %v/%v: %v",
   182  				d.cfg.Namespace.Name(), deploymentName, err))
   183  		}
   184  	}
   185  	return errs
   186  }
   187  
   188  func (d *deployment) WorkloadReady(w *workload) {
   189  	if !d.shouldCreateWLE {
   190  		return
   191  	}
   192  
   193  	// Deploy the workload entry to the primary cluster. We will read WorkloadEntry across clusters.
   194  	wle := d.workloadEntryYAML(w)
   195  	if err := d.ctx.ConfigKube(d.cfg.Cluster.Primary()).
   196  		YAML(d.cfg.Namespace.Name(), wle).
   197  		Apply(apply.NoCleanup); err != nil {
   198  		log.Warnf("failed deploying echo WLE for %s/%s to primary cluster: %v",
   199  			d.cfg.Namespace.Name(),
   200  			d.cfg.Service,
   201  			err)
   202  	}
   203  }
   204  
   205  func (d *deployment) WorkloadNotReady(w *workload) {
   206  	if !d.shouldCreateWLE {
   207  		return
   208  	}
   209  
   210  	wle := d.workloadEntryYAML(w)
   211  	if err := d.ctx.ConfigKube(d.cfg.Cluster.Primary()).YAML(d.cfg.Namespace.Name(), wle).Delete(); err != nil {
   212  		log.Warnf("failed deleting echo WLE for %s/%s from primary cluster: %v",
   213  			d.cfg.Namespace.Name(),
   214  			d.cfg.Service,
   215  			err)
   216  	}
   217  }
   218  
   219  func (d *deployment) workloadEntryYAML(w *workload) string {
   220  	name := w.pod.Name
   221  	podIP := w.pod.Status.PodIP
   222  	sa := serviceAccount(d.cfg)
   223  	network := d.cfg.Cluster.NetworkName()
   224  	service := d.cfg.Service
   225  	version := w.pod.Labels[constants.TestVMVersionLabel]
   226  
   227  	return fmt.Sprintf(`
   228  apiVersion: networking.istio.io/v1alpha3
   229  kind: WorkloadEntry
   230  metadata:
   231    name: %s
   232  spec:
   233    address: %s
   234    serviceAccount: %s
   235    network: %q
   236    labels:
   237      app: %s
   238      version: %s
   239  `, name, podIP, sa, network, service, version)
   240  }
   241  
   242  func GenerateDeployment(ctx resource.Context, cfg echo.Config, settings *resource.Settings) (string, error) {
   243  	if settings == nil {
   244  		var err error
   245  		settings, err = resource.SettingsFromCommandLine("template")
   246  		if err != nil {
   247  			return "", err
   248  		}
   249  	}
   250  
   251  	params, err := deploymentParams(ctx, cfg, settings)
   252  	if err != nil {
   253  		return "", err
   254  	}
   255  
   256  	deploy := getTemplate(deploymentTemplateFile)
   257  	if cfg.DeployAsVM {
   258  		deploy = getTemplate(vmDeploymentTemplateFile)
   259  	}
   260  
   261  	return tmpl.Execute(deploy, params)
   262  }
   263  
   264  func GenerateService(cfg echo.Config) (string, error) {
   265  	params := serviceParams(cfg)
   266  	return tmpl.Execute(getTemplate(serviceTemplateFile), params)
   267  }
   268  
   269  var VMImages = map[echo.VMDistro]string{
   270  	echo.UbuntuBionic: "app_sidecar_ubuntu_bionic",
   271  	echo.UbuntuNoble:  "app_sidecar_ubuntu_noble",
   272  	echo.Debian12:     "app_sidecar_debian_12",
   273  	echo.Rockylinux9:  "app_sidecar_rockylinux_9",
   274  }
   275  
   276  // ArmVMImages is the subset of images that work on arm64. These fail because Istio's arm64 build has a higher GLIBC requirement
   277  var ArmVMImages = map[echo.VMDistro]string{
   278  	echo.UbuntuNoble: "app_sidecar_ubuntu_noble",
   279  	echo.Debian12:    "app_sidecar_debian_12",
   280  	echo.Rockylinux9: "app_sidecar_rockylinux_9",
   281  }
   282  
   283  var RevVMImages = func() map[string]echo.VMDistro {
   284  	r := map[string]echo.VMDistro{}
   285  	for k, v := range VMImages {
   286  		r[v] = k
   287  	}
   288  	return r
   289  }()
   290  
   291  // getVMOverrideForIstiodDNS returns the DNS alias to use for istiod on VMs. VMs always access
   292  // istiod via the east-west gateway, even though they are installed on the same cluster as istiod.
   293  func getVMOverrideForIstiodDNS(ctx resource.Context, cfg echo.Config) (istioHost string, istioIP string) {
   294  	if ctx == nil {
   295  		return
   296  	}
   297  
   298  	ist, err := istio.Get(ctx)
   299  	if err != nil {
   300  		log.Warnf("VM config failed to get Istio component for %s: %v", cfg.Cluster.Name(), err)
   301  		return
   302  	}
   303  
   304  	// Generate the istiod host the same way as istioctl.
   305  	istioNS := ist.Settings().SystemNamespace
   306  	istioRevision := getIstioRevision(cfg.Namespace)
   307  	istioHost = istioctlcmd.IstiodHost(istioNS, istioRevision)
   308  
   309  	istioIPAddr := ist.EastWestGatewayFor(cfg.Cluster).DiscoveryAddresses()[0].Addr()
   310  	if !istioIPAddr.IsValid() {
   311  		log.Warnf("VM config failed to get east-west gateway IP for %s", cfg.Cluster.Name())
   312  		istioHost, istioIP = "", ""
   313  	} else {
   314  		istioIP = istioIPAddr.String()
   315  	}
   316  	return
   317  }
   318  
   319  func deploymentParams(ctx resource.Context, cfg echo.Config, settings *resource.Settings) (map[string]any, error) {
   320  	supportStartupProbe := cfg.Cluster.MinKubeVersion(0)
   321  	imagePullSecretName, err := settings.Image.PullSecretName()
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  
   326  	containerPorts := getContainerPorts(cfg)
   327  	appContainers := []map[string]any{{
   328  		"Name":           appContainerName,
   329  		"ImageFullPath":  settings.EchoImage, // This overrides image hub/tag if it's not empty.
   330  		"ContainerPorts": containerPorts,
   331  	}}
   332  
   333  	// Only use the custom image for proxyless gRPC instances. This will bind the gRPC ports on one container
   334  	// and all other ports on another. Additionally, we bind one port for communication between the custom image
   335  	// container, and the regular Go server.
   336  	if cfg.IsProxylessGRPC() && settings.CustomGRPCEchoImage != "" {
   337  		var grpcPorts, otherPorts echoCommon.PortList
   338  		for _, port := range containerPorts {
   339  			if port.Protocol == protocol.GRPC {
   340  				grpcPorts = append(grpcPorts, port)
   341  			} else {
   342  				otherPorts = append(otherPorts, port)
   343  			}
   344  		}
   345  		otherPorts = append(otherPorts, &echoCommon.Port{
   346  			Name:     "grpc-fallback",
   347  			Protocol: protocol.GRPC,
   348  			Port:     grpcFallbackPort,
   349  		})
   350  		appContainers[0]["ContainerPorts"] = otherPorts
   351  		appContainers = append(appContainers, map[string]any{
   352  			"Name":           "custom-grpc-" + appContainerName,
   353  			"ImageFullPath":  settings.CustomGRPCEchoImage, // This overrides image hub/tag if it's not empty.
   354  			"ContainerPorts": grpcPorts,
   355  			"FallbackPort":   grpcFallbackPort,
   356  		})
   357  	}
   358  
   359  	if cfg.WorkloadWaypointProxy != "" {
   360  		for _, subset := range cfg.Subsets {
   361  			if subset.Labels == nil {
   362  				subset.Labels = make(map[string]string)
   363  			}
   364  			subset.Labels[constants.AmbientUseWaypointLabel] = cfg.WorkloadWaypointProxy
   365  		}
   366  	}
   367  
   368  	params := map[string]any{
   369  		"ImageHub":                settings.Image.Hub,
   370  		"ImageTag":                settings.Image.Tag,
   371  		"ImagePullPolicy":         settings.Image.PullPolicy,
   372  		"ImagePullSecretName":     imagePullSecretName,
   373  		"Service":                 cfg.Service,
   374  		"StatefulSet":             cfg.StatefulSet,
   375  		"ProxylessGRPC":           cfg.IsProxylessGRPC(),
   376  		"GRPCMagicPort":           grpcMagicPort,
   377  		"Locality":                cfg.Locality,
   378  		"ServiceAccount":          cfg.ServiceAccount,
   379  		"DisableAutomountSAToken": cfg.DisableAutomountSAToken,
   380  		"AppContainers":           appContainers,
   381  		"ContainerPorts":          containerPorts,
   382  		"Subsets":                 cfg.Subsets,
   383  		"TLSSettings":             cfg.TLSSettings,
   384  		"Cluster":                 cfg.Cluster.Name(),
   385  		"ReadinessTCPPort":        cfg.ReadinessTCPPort,
   386  		"ReadinessGRPCPort":       cfg.ReadinessGRPCPort,
   387  		"StartupProbe":            supportStartupProbe,
   388  		"IncludeExtAuthz":         cfg.IncludeExtAuthz,
   389  		"Revisions":               settings.Revisions.TemplateMap(),
   390  		"Compatibility":           settings.Compatibility,
   391  		"WorkloadClass":           cfg.WorkloadClass(),
   392  		"OverlayIstioProxy":       canCreateIstioProxy(settings.Revisions.Minimum()) && !settings.Ambient,
   393  		"Ambient":                 settings.Ambient,
   394  	}
   395  
   396  	vmIstioHost, vmIstioIP := "", ""
   397  	if cfg.IsVM() {
   398  		vmImage := VMImages[cfg.VMDistro]
   399  		_, knownImage := RevVMImages[cfg.VMDistro]
   400  		if vmImage == "" {
   401  			if knownImage {
   402  				vmImage = cfg.VMDistro
   403  			} else {
   404  				vmImage = VMImages[echo.DefaultVMDistro]
   405  			}
   406  			log.Debugf("no image for distro %s, defaulting to %s", cfg.VMDistro, echo.DefaultVMDistro)
   407  		}
   408  
   409  		vmIstioHost, vmIstioIP = getVMOverrideForIstiodDNS(ctx, cfg)
   410  
   411  		params["VM"] = map[string]any{
   412  			"Image":     vmImage,
   413  			"IstioHost": vmIstioHost,
   414  			"IstioIP":   vmIstioIP,
   415  		}
   416  	}
   417  
   418  	return params, nil
   419  }
   420  
   421  func serviceParams(cfg echo.Config) map[string]any {
   422  	if cfg.ServiceWaypointProxy != "" {
   423  		if cfg.ServiceLabels == nil {
   424  			cfg.ServiceLabels = make(map[string]string)
   425  		}
   426  		cfg.ServiceLabels[constants.AmbientUseWaypointLabel] = cfg.ServiceWaypointProxy
   427  	}
   428  	return map[string]any{
   429  		"Service":        cfg.Service,
   430  		"Headless":       cfg.Headless,
   431  		"ServiceAccount": cfg.ServiceAccount,
   432  		"ServicePorts":   cfg.Ports.GetServicePorts(),
   433  		"ServiceLabels":  cfg.ServiceLabels,
   434  		"IPFamilies":     cfg.IPFamilies,
   435  		"IPFamilyPolicy": cfg.IPFamilyPolicy,
   436  	}
   437  }
   438  
   439  // createVMConfig sets up a Service account,
   440  func createVMConfig(ctx resource.Context, cfg echo.Config) error {
   441  	istioCtl, err := istioctl.New(ctx, istioctl.Config{Cluster: cfg.Cluster})
   442  	if err != nil {
   443  		return err
   444  	}
   445  	// generate config files for VM bootstrap
   446  	dirname := fmt.Sprintf("%s-vm-config-", cfg.Service)
   447  	dir, err := ctx.CreateDirectory(dirname)
   448  	if err != nil {
   449  		return err
   450  	}
   451  
   452  	wg := tmpl.MustEvaluate(`
   453  apiVersion: networking.istio.io/v1alpha3
   454  kind: WorkloadGroup
   455  metadata:
   456    name: {{.name}}
   457    namespace: {{.namespace}}
   458  spec:
   459    metadata:
   460      labels:
   461        app: {{.name}}
   462        test.istio.io/class: {{ .workloadClass }}
   463    template:
   464      serviceAccount: {{.serviceAccount}}
   465      network: "{{.network}}"
   466    probe:
   467      failureThreshold: 5
   468      httpGet:
   469        path: /
   470        port: 8080
   471      periodSeconds: 2
   472      successThreshold: 1
   473      timeoutSeconds: 2
   474  
   475  `, map[string]string{
   476  		"name":           cfg.Service,
   477  		"namespace":      cfg.Namespace.Name(),
   478  		"serviceAccount": serviceAccount(cfg),
   479  		"network":        cfg.Cluster.NetworkName(),
   480  		"workloadClass":  cfg.WorkloadClass(),
   481  	})
   482  
   483  	// Push the WorkloadGroup for auto-registration
   484  	if cfg.AutoRegisterVM {
   485  		if err := ctx.ConfigKube(cfg.Cluster).
   486  			YAML(cfg.Namespace.Name(), wg).
   487  			Apply(apply.NoCleanup); err != nil {
   488  			return err
   489  		}
   490  	}
   491  
   492  	if cfg.ServiceAccount {
   493  		// create service account, the next workload command will use it to generate a token
   494  		err = createServiceAccount(cfg.Cluster.Kube(), cfg.Namespace.Name(), serviceAccount(cfg))
   495  		if err != nil && !kerrors.IsAlreadyExists(err) {
   496  			return err
   497  		}
   498  	}
   499  
   500  	if err := os.WriteFile(path.Join(dir, "workloadgroup.yaml"), []byte(wg), 0o600); err != nil {
   501  		return err
   502  	}
   503  
   504  	ist, err := istio.Get(ctx)
   505  	if err != nil {
   506  		return err
   507  	}
   508  	// this will wait until the eastwest gateway has an IP before running the next command
   509  	istiodAddr, err := ist.RemoteDiscoveryAddressFor(cfg.Cluster)
   510  	if err != nil {
   511  		return err
   512  	}
   513  
   514  	var subsetDir string
   515  	for _, subset := range cfg.Subsets {
   516  		subsetDir, err = os.MkdirTemp(dir, subset.Version+"-")
   517  		if err != nil {
   518  			return err
   519  		}
   520  		cmd := []string{
   521  			"x", "workload", "entry", "configure",
   522  			"-f", path.Join(dir, "workloadgroup.yaml"),
   523  			"-o", subsetDir,
   524  		}
   525  		if ctx.Clusters().IsMulticluster() {
   526  			// When VMs talk about "cluster", they refer to the cluster they connect to for discovery
   527  			cmd = append(cmd, "--clusterID", cfg.Cluster.Name())
   528  		}
   529  		if cfg.AutoRegisterVM {
   530  			cmd = append(cmd, "--autoregister")
   531  		}
   532  		if !ctx.Environment().(*kube.Environment).Settings().LoadBalancerSupported {
   533  			// LoadBalancer may not be supported and the command doesn't have NodePort fallback logic that the tests do
   534  			cmd = append(cmd, "--ingressIP", istiodAddr.Addr().String())
   535  		}
   536  		if rev := getIstioRevision(cfg.Namespace); len(rev) > 0 {
   537  			cmd = append(cmd, "--revision", rev)
   538  		}
   539  		// make sure namespace controller has time to create root-cert ConfigMap
   540  		if err := retry.UntilSuccess(func() error {
   541  			stdout, stderr, err := istioCtl.Invoke(cmd)
   542  			if err != nil {
   543  				return fmt.Errorf("%v:\nstdout: %s\nstderr: %s", err, stdout, stderr)
   544  			}
   545  			return nil
   546  		}, retry.Timeout(20*time.Second)); err != nil {
   547  			return err
   548  		}
   549  
   550  		// support proxyConfig customizations on VMs via annotation in the echo API.
   551  		for k, v := range subset.Annotations {
   552  			if k == "proxy.istio.io/config" {
   553  				if err := patchProxyConfigFile(path.Join(subsetDir, "mesh.yaml"), v); err != nil {
   554  					return fmt.Errorf("failed patching proxyconfig: %v", err)
   555  				}
   556  			}
   557  		}
   558  
   559  		if err := customizeVMEnvironment(ctx, cfg, path.Join(subsetDir, "cluster.env"), istiodAddr); err != nil {
   560  			return fmt.Errorf("failed customizing cluster.env: %v", err)
   561  		}
   562  
   563  		// push bootstrap config as a ConfigMap so we can mount it on our "vm" pods
   564  		cmData := map[string][]byte{}
   565  		generatedFiles, err := os.ReadDir(subsetDir)
   566  		if err != nil {
   567  			return err
   568  		}
   569  		for _, file := range generatedFiles {
   570  			if file.IsDir() {
   571  				continue
   572  			}
   573  			cmData[file.Name()], err = os.ReadFile(path.Join(subsetDir, file.Name()))
   574  			if err != nil {
   575  				return err
   576  			}
   577  		}
   578  		cmName := fmt.Sprintf("%s-%s-vm-bootstrap", cfg.Service, subset.Version)
   579  		cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: cmName}, BinaryData: cmData}
   580  		_, err = cfg.Cluster.Kube().CoreV1().ConfigMaps(cfg.Namespace.Name()).Create(context.TODO(), cm, metav1.CreateOptions{})
   581  		if err != nil && !kerrors.IsAlreadyExists(err) {
   582  			return fmt.Errorf("failed creating configmap %s: %v", cm.Name, err)
   583  		}
   584  	}
   585  
   586  	// push the generated token as a Secret (only need one, they should be identical)
   587  	token, err := os.ReadFile(path.Join(subsetDir, "istio-token"))
   588  	if err != nil {
   589  		return err
   590  	}
   591  	secret := &corev1.Secret{
   592  		ObjectMeta: metav1.ObjectMeta{
   593  			Name:      cfg.Service + "-istio-token",
   594  			Namespace: cfg.Namespace.Name(),
   595  		},
   596  		Data: map[string][]byte{
   597  			"istio-token": token,
   598  		},
   599  	}
   600  	if _, err := cfg.Cluster.Kube().CoreV1().Secrets(cfg.Namespace.Name()).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
   601  		if kerrors.IsAlreadyExists(err) {
   602  			if _, err := cfg.Cluster.Kube().CoreV1().Secrets(cfg.Namespace.Name()).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil {
   603  				return fmt.Errorf("failed updating secret %s: %v", secret.Name, err)
   604  			}
   605  		} else {
   606  			return fmt.Errorf("failed creating secret %s: %v", secret.Name, err)
   607  		}
   608  	}
   609  
   610  	return nil
   611  }
   612  
   613  func patchProxyConfigFile(file string, overrides string) error {
   614  	config, err := readMeshConfig(file)
   615  	if err != nil {
   616  		return err
   617  	}
   618  	overrideYAML := "defaultConfig:\n"
   619  	overrideYAML += istio.Indent(overrides, "  ")
   620  	if err := protomarshal.ApplyYAML(overrideYAML, config.DefaultConfig); err != nil {
   621  		return err
   622  	}
   623  	outYAML, err := protomarshal.ToYAML(config)
   624  	if err != nil {
   625  		return err
   626  	}
   627  	return os.WriteFile(file, []byte(outYAML), 0o744)
   628  }
   629  
   630  func readMeshConfig(file string) (*meshconfig.MeshConfig, error) {
   631  	baseYAML, err := os.ReadFile(file)
   632  	if err != nil {
   633  		return nil, err
   634  	}
   635  	config := &meshconfig.MeshConfig{}
   636  	if err := protomarshal.ApplyYAML(string(baseYAML), config); err != nil {
   637  		return nil, err
   638  	}
   639  	return config, nil
   640  }
   641  
   642  func createServiceAccount(client kubernetes.Interface, ns string, serviceAccount string) error {
   643  	scopes.Framework.Debugf("Creating service account for: %s/%s", ns, serviceAccount)
   644  	_, err := client.CoreV1().ServiceAccounts(ns).Create(context.TODO(), &corev1.ServiceAccount{
   645  		ObjectMeta: metav1.ObjectMeta{Name: serviceAccount},
   646  	}, metav1.CreateOptions{})
   647  	return err
   648  }
   649  
   650  // getContainerPorts converts the ports to a port list of container ports.
   651  // Adds ports for health/readiness if necessary.
   652  func getContainerPorts(cfg echo.Config) echoCommon.PortList {
   653  	ports := cfg.Ports
   654  	containerPorts := make(echoCommon.PortList, 0, len(ports))
   655  	var healthPort *echoCommon.Port
   656  	var readyPort *echoCommon.Port
   657  	for _, p := range ports {
   658  		// Add the port to the set of application ports.
   659  		cport := &echoCommon.Port{
   660  			Name:        p.Name,
   661  			Protocol:    p.Protocol,
   662  			Port:        p.WorkloadPort,
   663  			TLS:         p.TLS,
   664  			ServerFirst: p.ServerFirst,
   665  			InstanceIP:  p.InstanceIP,
   666  			LocalhostIP: p.LocalhostIP,
   667  		}
   668  		containerPorts = append(containerPorts, cport)
   669  
   670  		switch p.Protocol {
   671  		case protocol.GRPC:
   672  			if cfg.IsProxylessGRPC() {
   673  				cport.XDSServer = true
   674  			}
   675  			continue
   676  		case protocol.HTTP:
   677  			if p.WorkloadPort == httpReadinessPort {
   678  				readyPort = cport
   679  			}
   680  		default:
   681  			if p.WorkloadPort == tcpHealthPort {
   682  				healthPort = cport
   683  			}
   684  		}
   685  	}
   686  
   687  	// If we haven't added the readiness/health ports, do so now.
   688  	if readyPort == nil {
   689  		containerPorts = append(containerPorts, &echoCommon.Port{
   690  			Name:     "http-readiness-port",
   691  			Protocol: protocol.HTTP,
   692  			Port:     httpReadinessPort,
   693  		})
   694  	}
   695  	if healthPort == nil {
   696  		containerPorts = append(containerPorts, &echoCommon.Port{
   697  			Name:     "tcp-health-port",
   698  			Protocol: protocol.HTTP,
   699  			Port:     tcpHealthPort,
   700  		})
   701  	}
   702  
   703  	// gives something the test runner to connect to without being in the mesh
   704  	if cfg.IsProxylessGRPC() {
   705  		containerPorts = append(containerPorts, &echoCommon.Port{
   706  			Name:        "grpc-magic-port",
   707  			Protocol:    protocol.GRPC,
   708  			Port:        grpcMagicPort,
   709  			LocalhostIP: true,
   710  		})
   711  	}
   712  	return containerPorts
   713  }
   714  
   715  func customizeVMEnvironment(ctx resource.Context, cfg echo.Config, clusterEnv string, istiodAddr netip.AddrPort) error {
   716  	f, err := os.OpenFile(clusterEnv, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
   717  	if err != nil {
   718  		return fmt.Errorf("failed opening %s: %v", clusterEnv, err)
   719  	}
   720  	defer f.Close()
   721  
   722  	if cfg.VMEnvironment != nil {
   723  		for k, v := range cfg.VMEnvironment {
   724  			addition := fmt.Sprintf("%s=%s\n", k, v)
   725  			_, err = f.WriteString(addition)
   726  			if err != nil {
   727  				return fmt.Errorf("failed writing %q to %s: %v", addition, clusterEnv, err)
   728  			}
   729  		}
   730  	}
   731  	if !ctx.Environment().(*kube.Environment).Settings().LoadBalancerSupported {
   732  		// customize cluster.env with NodePort mapping
   733  		_, err = f.WriteString(fmt.Sprintf("ISTIO_PILOT_PORT=%d\n", istiodAddr.Port()))
   734  		if err != nil {
   735  			return err
   736  		}
   737  	}
   738  	return err
   739  }
   740  
   741  func canCreateIstioProxy(version resource.IstioVersion) bool {
   742  	// if no revision specified create the istio-proxy
   743  	if string(version) == "" {
   744  		return true
   745  	}
   746  	if minor := strings.Split(string(version), ".")[1]; minor > "8" || len(minor) > 1 {
   747  		return true
   748  	}
   749  	return false
   750  }
   751  
   752  func getIstioRevision(n namespace.Instance) string {
   753  	nsLabels, err := n.Labels()
   754  	if err != nil {
   755  		log.Warnf("failed fetching labels for %s; assuming no-revision (can cause failures): %v", n.Name(), err)
   756  		return ""
   757  	}
   758  	return nsLabels[label.IoIstioRev.Name]
   759  }
   760  
   761  func statefulsetComplete(statefulset *appsv1.StatefulSet) bool {
   762  	return statefulset.Status.UpdatedReplicas == *(statefulset.Spec.Replicas) &&
   763  		statefulset.Status.Replicas == *(statefulset.Spec.Replicas) &&
   764  		statefulset.Status.AvailableReplicas == *(statefulset.Spec.Replicas) &&
   765  		statefulset.Status.ReadyReplicas == *(statefulset.Spec.Replicas) &&
   766  		statefulset.Status.ObservedGeneration >= statefulset.Generation
   767  }
   768  
   769  func deploymentComplete(deployment *appsv1.Deployment) bool {
   770  	return deployment.Status.UpdatedReplicas == *(deployment.Spec.Replicas) &&
   771  		deployment.Status.Replicas == *(deployment.Spec.Replicas) &&
   772  		deployment.Status.AvailableReplicas == *(deployment.Spec.Replicas) &&
   773  		deployment.Status.ReadyReplicas == *(deployment.Spec.Replicas) &&
   774  		deployment.Status.ObservedGeneration >= deployment.Generation
   775  }