istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/workload/workload.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 workload
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"path/filepath"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	authenticationv1 "k8s.io/api/authentication/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	"istio.io/api/annotation"
    35  	"istio.io/api/label"
    36  	meshconfig "istio.io/api/mesh/v1alpha1"
    37  	networkingv1alpha3 "istio.io/api/networking/v1alpha3"
    38  	clientv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
    39  	"istio.io/istio/istioctl/pkg/cli"
    40  	"istio.io/istio/istioctl/pkg/clioptions"
    41  	"istio.io/istio/istioctl/pkg/completion"
    42  	istioctlutil "istio.io/istio/istioctl/pkg/util"
    43  	"istio.io/istio/operator/pkg/tpath"
    44  	"istio.io/istio/pilot/pkg/model"
    45  	"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
    46  	"istio.io/istio/pkg/config/constants"
    47  	"istio.io/istio/pkg/config/schema/gvk"
    48  	"istio.io/istio/pkg/config/validation/agent"
    49  	"istio.io/istio/pkg/kube"
    50  	"istio.io/istio/pkg/kube/labels"
    51  	"istio.io/istio/pkg/log"
    52  	netutil "istio.io/istio/pkg/util/net"
    53  	"istio.io/istio/pkg/util/protomarshal"
    54  	"istio.io/istio/pkg/util/shellescape"
    55  )
    56  
    57  var (
    58  	// TODO refactor away from package vars and add more UTs
    59  	tokenDuration  int64
    60  	name           string
    61  	serviceAccount string
    62  	filename       string
    63  	outputDir      string
    64  	clusterID      string
    65  	ingressIP      string
    66  	internalIP     string
    67  	externalIP     string
    68  	ingressSvc     string
    69  	autoRegister   bool
    70  	dnsCapture     bool
    71  	ports          []string
    72  	resourceLabels []string
    73  	annotations    []string
    74  	namespace      string
    75  )
    76  
    77  const (
    78  	istioEastWestGatewayServiceName = "istio-eastwestgateway"
    79  	filePerms                       = os.FileMode(0o744)
    80  )
    81  
    82  func Cmd(ctx cli.Context) *cobra.Command {
    83  	namespace = ctx.Namespace()
    84  	workloadCmd := &cobra.Command{
    85  		Use:   "workload",
    86  		Short: "Commands to assist in configuring and deploying workloads running on VMs and other non-Kubernetes environments",
    87  		Example: `  # workload group yaml generation
    88    istioctl x workload group create
    89  
    90    # workload entry configuration generation
    91    istioctl x workload entry configure`,
    92  	}
    93  	workloadCmd.AddCommand(groupCommand(ctx))
    94  	workloadCmd.AddCommand(entryCommand(ctx))
    95  	return workloadCmd
    96  }
    97  
    98  func groupCommand(ctx cli.Context) *cobra.Command {
    99  	groupCmd := &cobra.Command{
   100  		Use:     "group",
   101  		Short:   "Commands dealing with WorkloadGroup resources",
   102  		Example: "  istioctl x workload group create --name foo --namespace bar --labels app=foobar",
   103  	}
   104  	groupCmd.AddCommand(createCommand(ctx))
   105  	return groupCmd
   106  }
   107  
   108  func entryCommand(ctx cli.Context) *cobra.Command {
   109  	entryCmd := &cobra.Command{
   110  		Use:     "entry",
   111  		Short:   "Commands dealing with WorkloadEntry resources",
   112  		Example: "  istioctl x workload entry configure -f workloadgroup.yaml -o outputDir",
   113  	}
   114  	entryCmd.AddCommand(configureCommand(ctx))
   115  	return entryCmd
   116  }
   117  
   118  func createCommand(ctx cli.Context) *cobra.Command {
   119  	createCmd := &cobra.Command{
   120  		Use:   "create",
   121  		Short: "Creates a WorkloadGroup resource that provides a template for associated WorkloadEntries",
   122  		Long: `Creates a WorkloadGroup resource that provides a template for associated WorkloadEntries.
   123  The default output is serialized YAML, which can be piped into 'kubectl apply -f -' to send the artifact to the API Server.`,
   124  		Example: "  istioctl x workload group create --name foo --namespace bar --labels app=foo,bar=baz " +
   125  			"--ports grpc=3550,http=8080 --annotations annotation=foobar --serviceAccount sa",
   126  		Args: func(cmd *cobra.Command, args []string) error {
   127  			if name == "" {
   128  				return fmt.Errorf("expecting a workload name")
   129  			}
   130  			if namespace == "" {
   131  				return fmt.Errorf("expecting a workload namespace")
   132  			}
   133  			return nil
   134  		},
   135  		RunE: func(cmd *cobra.Command, args []string) error {
   136  			u := &unstructured.Unstructured{
   137  				Object: map[string]any{
   138  					"apiVersion": gvk.WorkloadGroup.GroupVersion(),
   139  					"kind":       gvk.WorkloadGroup.Kind,
   140  					"metadata": map[string]any{
   141  						"name":      name,
   142  						"namespace": namespace,
   143  					},
   144  				},
   145  			}
   146  			spec := &networkingv1alpha3.WorkloadGroup{
   147  				Metadata: &networkingv1alpha3.WorkloadGroup_ObjectMeta{
   148  					Labels:      convertToStringMap(resourceLabels),
   149  					Annotations: convertToStringMap(annotations),
   150  				},
   151  				Template: &networkingv1alpha3.WorkloadEntry{
   152  					Ports:          convertToUnsignedInt32Map(ports),
   153  					ServiceAccount: serviceAccount,
   154  				},
   155  			}
   156  			wgYAML, err := generateWorkloadGroupYAML(u, spec)
   157  			if err != nil {
   158  				return err
   159  			}
   160  			_, err = cmd.OutOrStdout().Write(wgYAML)
   161  			return err
   162  		},
   163  	}
   164  	createCmd.PersistentFlags().StringVar(&name, "name", "", "The name of the workload group")
   165  	createCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "The namespace that the workload instances will belong to")
   166  	createCmd.PersistentFlags().StringSliceVarP(&resourceLabels, "labels", "l", nil, "The labels to apply to the workload instances; e.g. -l env=prod,vers=2")
   167  	createCmd.PersistentFlags().StringSliceVarP(&annotations, "annotations", "a", nil, "The annotations to apply to the workload instances")
   168  	createCmd.PersistentFlags().StringSliceVarP(&ports, "ports", "p", nil, "The incoming ports exposed by the workload instance")
   169  	createCmd.PersistentFlags().StringVarP(&serviceAccount, "serviceAccount", "s", "default", "The service identity to associate with the workload instances")
   170  	_ = createCmd.RegisterFlagCompletionFunc("serviceAccount", func(
   171  		cmd *cobra.Command, args []string, toComplete string,
   172  	) ([]string, cobra.ShellCompDirective) {
   173  		return completion.ValidServiceAccountArgs(cmd, ctx, args, toComplete)
   174  	})
   175  	return createCmd
   176  }
   177  
   178  func generateWorkloadGroupYAML(u *unstructured.Unstructured, spec *networkingv1alpha3.WorkloadGroup) ([]byte, error) {
   179  	iSpec, err := unstructureIstioType(spec)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	u.Object["spec"] = iSpec
   184  
   185  	wgYAML, err := yaml.Marshal(u.Object)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	return wgYAML, nil
   190  }
   191  
   192  func configureCommand(ctx cli.Context) *cobra.Command {
   193  	var opts clioptions.ControlPlaneOptions
   194  
   195  	configureCmd := &cobra.Command{
   196  		Use:   "configure",
   197  		Short: "Generates all the required configuration files for a workload instance running on a VM or non-Kubernetes environment",
   198  		Long: `Generates all the required configuration files for workload instance on a VM or non-Kubernetes environment from a WorkloadGroup artifact.
   199  This includes a MeshConfig resource, the cluster.env file, and necessary certificates and security tokens.
   200  Configure requires either the WorkloadGroup artifact path or its location on the API server.`,
   201  		Example: `  # configure example using a local WorkloadGroup artifact
   202    istioctl x workload entry configure -f workloadgroup.yaml -o config
   203  
   204    # configure example using the API server
   205    istioctl x workload entry configure --name foo --namespace bar -o config`,
   206  		Args: func(cmd *cobra.Command, args []string) error {
   207  			if filename == "" && (name == "" || namespace == "") {
   208  				return fmt.Errorf("expecting a WorkloadGroup artifact file or the name and namespace of an existing WorkloadGroup")
   209  			}
   210  			if outputDir == "" {
   211  				return fmt.Errorf("expecting an output directory")
   212  			}
   213  			return nil
   214  		},
   215  		RunE: func(cmd *cobra.Command, args []string) error {
   216  			kubeClient, err := ctx.CLIClientWithRevision(opts.Revision)
   217  			if err != nil {
   218  				return err
   219  			}
   220  
   221  			wg := &clientv1alpha3.WorkloadGroup{}
   222  			if filename != "" {
   223  				if err := readWorkloadGroup(filename, wg); err != nil {
   224  					return err
   225  				}
   226  			} else {
   227  				wg, err = kubeClient.Istio().NetworkingV1alpha3().WorkloadGroups(namespace).Get(context.Background(), name, metav1.GetOptions{})
   228  				// errors if the requested workload group does not exist in the given namespace
   229  				if err != nil {
   230  					return fmt.Errorf("workloadgroup %s not found in namespace %s: %v", name, namespace, err)
   231  				}
   232  			}
   233  
   234  			// extract the cluster ID from the injector config (.Values.global.multiCluster.clusterName)
   235  			if !validateFlagIsSetManuallyOrNot(cmd, "clusterID") {
   236  				// extract the cluster ID from the injector config if it is not set by user
   237  				clusterName, err := extractClusterIDFromInjectionConfig(kubeClient, ctx.IstioNamespace())
   238  				if err != nil {
   239  					return fmt.Errorf("failed to automatically determine the --clusterID: %v", err)
   240  				}
   241  				if clusterName != "" {
   242  					clusterID = clusterName
   243  				}
   244  			}
   245  
   246  			if err = createConfig(kubeClient, wg, ctx.IstioNamespace(), clusterID, ingressIP, internalIP, externalIP, outputDir, cmd.OutOrStderr()); err != nil {
   247  				return err
   248  			}
   249  			fmt.Printf("Configuration generation into directory %s was successful\n", outputDir)
   250  			return nil
   251  		},
   252  		PreRunE: func(cmd *cobra.Command, args []string) error {
   253  			if len(internalIP) > 0 && len(externalIP) > 0 {
   254  				return fmt.Errorf("the flags --internalIP and --externalIP are mutually exclusive")
   255  			}
   256  			return nil
   257  		},
   258  	}
   259  	configureCmd.PersistentFlags().StringVarP(&filename, "file", "f", "", "filename of the WorkloadGroup artifact. Leave this field empty if using the API server")
   260  	configureCmd.PersistentFlags().StringVar(&name, "name", "", "The name of the workload group")
   261  	configureCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "The namespace that the workload instances belong to")
   262  	configureCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "Output directory for generated files")
   263  	configureCmd.PersistentFlags().StringVar(&clusterID, "clusterID", "", "The ID used to identify the cluster")
   264  	configureCmd.PersistentFlags().Int64Var(&tokenDuration, "tokenDuration", 3600, "The token duration in seconds (default: 1 hour)")
   265  	configureCmd.PersistentFlags().StringVar(&ingressSvc, "ingressService", istioEastWestGatewayServiceName, "Name of the Service to be"+
   266  		" used as the ingress gateway, in the format <service>.<namespace>. If no namespace is provided, the default "+ctx.IstioNamespace()+
   267  		" namespace will be used.")
   268  	configureCmd.PersistentFlags().StringVar(&ingressIP, "ingressIP", "", "IP address of the ingress gateway")
   269  	configureCmd.PersistentFlags().BoolVar(&autoRegister, "autoregister", false, "Creates a WorkloadEntry upon connection to istiod (if enabled in pilot).")
   270  	configureCmd.PersistentFlags().BoolVar(&dnsCapture, "capture-dns", true, "Enables the capture of outgoing DNS packets on port 53, redirecting to istio-agent")
   271  	configureCmd.PersistentFlags().StringVar(&internalIP, "internalIP", "", "Internal IP address of the workload")
   272  	configureCmd.PersistentFlags().StringVar(&externalIP, "externalIP", "", "External IP address of the workload")
   273  	opts.AttachControlPlaneFlags(configureCmd)
   274  	return configureCmd
   275  }
   276  
   277  // Reads a WorkloadGroup yaml. Additionally populates default values if unset
   278  // TODO: add WorkloadGroup validation in pkg/config/validation
   279  func readWorkloadGroup(filename string, wg *clientv1alpha3.WorkloadGroup) error {
   280  	f, err := os.ReadFile(filename)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	if err = yaml.Unmarshal(f, wg); err != nil {
   285  		return err
   286  	}
   287  	// fill empty structs
   288  	if wg.Spec.Metadata == nil {
   289  		wg.Spec.Metadata = &networkingv1alpha3.WorkloadGroup_ObjectMeta{}
   290  	}
   291  	if wg.Spec.Template == nil {
   292  		wg.Spec.Template = &networkingv1alpha3.WorkloadEntry{}
   293  	}
   294  	// default service account for an empty field is "default"
   295  	if wg.Spec.Template.ServiceAccount == "" {
   296  		wg.Spec.Template.ServiceAccount = "default"
   297  	}
   298  	return nil
   299  }
   300  
   301  // Creates all the relevant config for the given workload group and cluster
   302  func createConfig(kubeClient kube.CLIClient, wg *clientv1alpha3.WorkloadGroup, istioNamespace, clusterID, ingressIP, internalIP,
   303  	externalIP string, outputDir string, out io.Writer,
   304  ) error {
   305  	if err := os.MkdirAll(outputDir, filePerms); err != nil {
   306  		return err
   307  	}
   308  	var (
   309  		err         error
   310  		proxyConfig *meshconfig.ProxyConfig
   311  	)
   312  	revision := kubeClient.Revision()
   313  	if proxyConfig, err = createMeshConfig(kubeClient, wg, istioNamespace, clusterID, outputDir, revision); err != nil {
   314  		return err
   315  	}
   316  	if err := createClusterEnv(wg, proxyConfig, istioNamespace, revision, internalIP, externalIP, outputDir); err != nil {
   317  		return err
   318  	}
   319  	if err := createCertsTokens(kubeClient, wg, outputDir, out); err != nil {
   320  		return err
   321  	}
   322  	if err := createHosts(kubeClient, istioNamespace, ingressIP, outputDir, revision); err != nil {
   323  		return err
   324  	}
   325  	return nil
   326  }
   327  
   328  // Write cluster.env into the given directory
   329  func createClusterEnv(wg *clientv1alpha3.WorkloadGroup, config *meshconfig.ProxyConfig, istioNamespace, revision, internalIP, externalIP, dir string) error {
   330  	we := wg.Spec.Template
   331  	ports := []string{}
   332  	for _, v := range we.Ports {
   333  		ports = append(ports, fmt.Sprint(v))
   334  	}
   335  	// respect the inbound port annotation and capture all traffic if no inbound ports are set
   336  	portBehavior := "*"
   337  	if len(ports) > 0 {
   338  		portBehavior = strings.Join(ports, ",")
   339  	}
   340  
   341  	// 22: ssh is extremely common for VMs, and we do not want to make VM inaccessible if there is an issue
   342  	// 15090: prometheus
   343  	// 15021/15020: agent
   344  	excludePorts := "22,15090,15021"
   345  	if config.StatusPort != 15090 && config.StatusPort != 15021 {
   346  		if config.StatusPort != 0 {
   347  			// Explicit status port set, use that
   348  			excludePorts += fmt.Sprintf(",%d", config.StatusPort)
   349  		} else {
   350  			// use default status port
   351  			excludePorts += ",15020"
   352  		}
   353  	}
   354  	// default attributes and service name, namespace, ports, service account, service CIDR
   355  	overrides := map[string]string{
   356  		"ISTIO_INBOUND_PORTS":       portBehavior,
   357  		"ISTIO_NAMESPACE":           wg.Namespace,
   358  		"ISTIO_SERVICE":             fmt.Sprintf("%s.%s", wg.Name, wg.Namespace),
   359  		"ISTIO_SERVICE_CIDR":        "*",
   360  		"ISTIO_LOCAL_EXCLUDE_PORTS": excludePorts,
   361  		"SERVICE_ACCOUNT":           we.ServiceAccount,
   362  	}
   363  
   364  	if isRevisioned(revision) {
   365  		overrides["CA_ADDR"] = IstiodAddr(istioNamespace, revision)
   366  	}
   367  	if len(internalIP) > 0 {
   368  		overrides["ISTIO_SVC_IP"] = internalIP
   369  	} else if len(externalIP) > 0 {
   370  		overrides["ISTIO_SVC_IP"] = externalIP
   371  		overrides["REWRITE_PROBE_LEGACY_LOCALHOST_DESTINATION"] = "true"
   372  	}
   373  
   374  	// clusterEnv will use proxyMetadata from the proxyConfig + overrides specific to the WorkloadGroup and cmd args
   375  	// this is similar to the way the injector sets all values proxyConfig.proxyMetadata to the Pod's env
   376  	clusterEnv := map[string]string{}
   377  	for _, metaMap := range []map[string]string{config.ProxyMetadata, overrides} {
   378  		for k, v := range metaMap {
   379  			clusterEnv[k] = v
   380  		}
   381  	}
   382  
   383  	return os.WriteFile(filepath.Join(dir, "cluster.env"), []byte(mapToString(clusterEnv)), filePerms)
   384  }
   385  
   386  // Get and store the needed certificate and token. The certificate comes from the CA root cert, and
   387  // the token is generated by kubectl under the workload group's namespace and service account
   388  // TODO: Make the following accurate when using the Kubernetes certificate signer
   389  func createCertsTokens(kubeClient kube.CLIClient, wg *clientv1alpha3.WorkloadGroup, dir string, out io.Writer) error {
   390  	rootCert, err := kubeClient.Kube().CoreV1().ConfigMaps(wg.Namespace).Get(context.Background(), controller.CACertNamespaceConfigMap, metav1.GetOptions{})
   391  	// errors if the requested configmap does not exist in the given namespace
   392  	if err != nil {
   393  		return fmt.Errorf("configmap %s was not found in namespace %s: %v", controller.CACertNamespaceConfigMap, wg.Namespace, err)
   394  	}
   395  	if err = os.WriteFile(filepath.Join(dir, "root-cert.pem"), []byte(rootCert.Data[constants.CACertNamespaceConfigMapDataName]), filePerms); err != nil {
   396  		return err
   397  	}
   398  
   399  	serviceAccount := wg.Spec.Template.ServiceAccount
   400  	tokenPath := filepath.Join(dir, "istio-token")
   401  	token := &authenticationv1.TokenRequest{
   402  		// ObjectMeta isn't required in real k8s, but needed for tests
   403  		ObjectMeta: metav1.ObjectMeta{
   404  			Name:      serviceAccount,
   405  			Namespace: wg.Namespace,
   406  		},
   407  		Spec: authenticationv1.TokenRequestSpec{
   408  			Audiences:         []string{"istio-ca"},
   409  			ExpirationSeconds: &tokenDuration,
   410  		},
   411  	}
   412  	tokenReq, err := kubeClient.Kube().CoreV1().ServiceAccounts(wg.Namespace).CreateToken(context.Background(), serviceAccount, token, metav1.CreateOptions{})
   413  	// errors if the token could not be created with the given service account in the given namespace
   414  	if err != nil {
   415  		return fmt.Errorf("could not create a token under service account %s in namespace %s: %v", serviceAccount, wg.Namespace, err)
   416  	}
   417  	if err := os.WriteFile(tokenPath, []byte(tokenReq.Status.Token), filePerms); err != nil {
   418  		return err
   419  	}
   420  	fmt.Fprintf(out, "Warning: a security token for namespace %q and service account %q has been generated and "+
   421  		"stored at %q\n", wg.Namespace, serviceAccount, tokenPath)
   422  	return nil
   423  }
   424  
   425  func createMeshConfig(kubeClient kube.CLIClient, wg *clientv1alpha3.WorkloadGroup, istioNamespace, clusterID, dir,
   426  	revision string,
   427  ) (*meshconfig.ProxyConfig, error) {
   428  	istioCM := "istio"
   429  	// Case with multiple control planes
   430  	if isRevisioned(revision) {
   431  		istioCM = fmt.Sprintf("%s-%s", istioCM, revision)
   432  	}
   433  	istio, err := kubeClient.Kube().CoreV1().ConfigMaps(istioNamespace).Get(context.Background(), istioCM, metav1.GetOptions{})
   434  	// errors if the requested configmap does not exist in the given namespace
   435  	if err != nil {
   436  		return nil, fmt.Errorf("configmap %s was not found in namespace %s: %v", istioCM, istioNamespace, err)
   437  	}
   438  	// fill some fields before applying the yaml to prevent errors later
   439  	meshConfig := &meshconfig.MeshConfig{
   440  		DefaultConfig: &meshconfig.ProxyConfig{
   441  			ProxyMetadata: map[string]string{},
   442  		},
   443  	}
   444  	if err := protomarshal.ApplyYAML(istio.Data[istioctlutil.ConfigMapKey], meshConfig); err != nil {
   445  		return nil, err
   446  	}
   447  	if isRevisioned(revision) && meshConfig.DefaultConfig.DiscoveryAddress == "" {
   448  		meshConfig.DefaultConfig.DiscoveryAddress = IstiodAddr(istioNamespace, revision)
   449  	}
   450  
   451  	// performing separate map-merge, apply seems to completely overwrite all metadata
   452  	proxyMetadata := meshConfig.DefaultConfig.ProxyMetadata
   453  
   454  	// support proxy.istio.io/config on the WorkloadGroup, in the WorkloadGroup spec
   455  	for _, annotations := range []map[string]string{wg.Annotations, wg.Spec.Metadata.Annotations} {
   456  		if pcYaml, ok := annotations[annotation.ProxyConfig.Name]; ok {
   457  			if err := protomarshal.ApplyYAML(pcYaml, meshConfig.DefaultConfig); err != nil {
   458  				return nil, err
   459  			}
   460  			for k, v := range meshConfig.DefaultConfig.ProxyMetadata {
   461  				proxyMetadata[k] = v
   462  			}
   463  		}
   464  	}
   465  
   466  	meshConfig.DefaultConfig.ProxyMetadata = proxyMetadata
   467  
   468  	lbls := map[string]string{}
   469  	for k, v := range wg.Spec.Metadata.Labels {
   470  		lbls[k] = v
   471  	}
   472  	// case where a user provided custom workload group has labels in the workload entry template field
   473  	we := wg.Spec.Template
   474  	if len(we.Labels) > 0 {
   475  		fmt.Printf("Labels should be set in the metadata. The following WorkloadEntry labels will override metadata labels: %s\n", we.Labels)
   476  		for k, v := range we.Labels {
   477  			lbls[k] = v
   478  		}
   479  	}
   480  
   481  	meshConfig.DefaultConfig.ReadinessProbe = wg.Spec.Probe
   482  
   483  	md := meshConfig.DefaultConfig.ProxyMetadata
   484  	if md == nil {
   485  		md = map[string]string{}
   486  		meshConfig.DefaultConfig.ProxyMetadata = md
   487  	}
   488  	md["CANONICAL_SERVICE"], md["CANONICAL_REVISION"] = labels.CanonicalService(lbls, wg.Name)
   489  	md["POD_NAMESPACE"] = wg.Namespace
   490  	md["SERVICE_ACCOUNT"] = we.ServiceAccount
   491  	md["TRUST_DOMAIN"] = meshConfig.TrustDomain
   492  
   493  	md["ISTIO_META_CLUSTER_ID"] = clusterID
   494  	md["ISTIO_META_MESH_ID"] = meshConfig.DefaultConfig.MeshId
   495  	md["ISTIO_META_NETWORK"] = we.Network
   496  	if portsStr := marshalWorkloadEntryPodPorts(we.Ports); portsStr != "" {
   497  		md["ISTIO_META_POD_PORTS"] = portsStr
   498  	}
   499  	md["ISTIO_META_WORKLOAD_NAME"] = wg.Name
   500  	lbls[label.ServiceCanonicalName.Name] = md["CANONICAL_SERVICE"]
   501  	lbls[label.ServiceCanonicalRevision.Name] = md["CANONICAL_REVISION"]
   502  	if labelsJSON, err := json.Marshal(lbls); err == nil {
   503  		md["ISTIO_METAJSON_LABELS"] = string(labelsJSON)
   504  	}
   505  
   506  	// TODO the defaults should be controlled by meshConfig/proxyConfig; if flags not given to the command proxyCOnfig takes precedence
   507  	if dnsCapture {
   508  		md["ISTIO_META_DNS_CAPTURE"] = strconv.FormatBool(dnsCapture)
   509  	}
   510  	if autoRegister {
   511  		md["ISTIO_META_AUTO_REGISTER_GROUP"] = wg.Name
   512  	}
   513  
   514  	proxyConfig, err := protomarshal.ToJSONMap(meshConfig.DefaultConfig)
   515  	if err != nil {
   516  		return nil, err
   517  	}
   518  
   519  	proxyYAML, err := yaml.Marshal(map[string]any{"defaultConfig": proxyConfig})
   520  	if err != nil {
   521  		return nil, err
   522  	}
   523  
   524  	return meshConfig.DefaultConfig, os.WriteFile(filepath.Join(dir, "mesh.yaml"), proxyYAML, filePerms)
   525  }
   526  
   527  func marshalWorkloadEntryPodPorts(p map[string]uint32) string {
   528  	var out []model.PodPort
   529  	for name, port := range p {
   530  		out = append(out, model.PodPort{Name: name, ContainerPort: int(port)})
   531  	}
   532  	if len(out) == 0 {
   533  		return ""
   534  	}
   535  	sort.Slice(out, func(i, j int) bool {
   536  		return out[i].Name < out[j].Name
   537  	})
   538  	str, err := json.Marshal(out)
   539  	if err != nil {
   540  		return ""
   541  	}
   542  	return string(str)
   543  }
   544  
   545  // Retrieves the external IP of the ingress-gateway for the hosts file additions
   546  func createHosts(kubeClient kube.CLIClient, istioNamespace, ingressIP, dir string, revision string) error {
   547  	// try to infer the ingress IP if the provided one is invalid
   548  	if agent.ValidateIPAddress(ingressIP) != nil {
   549  		p := strings.Split(ingressSvc, ".")
   550  		ingressNs := istioNamespace
   551  		if len(p) == 2 {
   552  			ingressSvc = p[0]
   553  			ingressNs = p[1]
   554  		}
   555  		ingress, err := kubeClient.Kube().CoreV1().Services(ingressNs).Get(context.Background(), ingressSvc, metav1.GetOptions{})
   556  		if err == nil {
   557  			if ingress.Status.LoadBalancer.Ingress != nil && len(ingress.Status.LoadBalancer.Ingress) > 0 {
   558  				ingressIP = ingress.Status.LoadBalancer.Ingress[0].IP
   559  			} else if len(ingress.Spec.ExternalIPs) > 0 {
   560  				ingressIP = ingress.Spec.ExternalIPs[0]
   561  			}
   562  			// TODO: add case where the load balancer is a DNS name
   563  		}
   564  	}
   565  
   566  	var hosts string
   567  	if netutil.IsValidIPAddress(ingressIP) {
   568  		hosts = fmt.Sprintf("%s %s\n", ingressIP, IstiodHost(istioNamespace, revision))
   569  	} else {
   570  		log.Warnf("Could not auto-detect IP for %s/%s. Use --ingressIP to manually specify the Gateway address to reach istiod from the VM.",
   571  			IstiodHost(istioNamespace, revision), istioNamespace)
   572  	}
   573  	return os.WriteFile(filepath.Join(dir, "hosts"), []byte(hosts), filePerms)
   574  }
   575  
   576  func isRevisioned(revision string) bool {
   577  	return revision != "" && revision != "default"
   578  }
   579  
   580  func IstiodHost(ns string, revision string) string {
   581  	istiod := "istiod"
   582  	if isRevisioned(revision) {
   583  		istiod = fmt.Sprintf("%s-%s", istiod, revision)
   584  	}
   585  	return fmt.Sprintf("%s.%s.svc", istiod, ns)
   586  }
   587  
   588  func IstiodAddr(ns, revision string) string {
   589  	// TODO make port configurable
   590  	return fmt.Sprintf("%s:%d", IstiodHost(ns, revision), 15012)
   591  }
   592  
   593  // Returns a map with each k,v entry on a new line
   594  func mapToString(m map[string]string) string {
   595  	lines := []string{}
   596  	for k, v := range m {
   597  		lines = append(lines, fmt.Sprintf("%s=%s", k, shellescape.Quote(v)))
   598  	}
   599  	sort.Strings(lines)
   600  	return strings.Join(lines, "\n") + "\n"
   601  }
   602  
   603  // extractClusterIDFromInjectionConfig can extract clusterID from injection configmap
   604  func extractClusterIDFromInjectionConfig(kubeClient kube.CLIClient, istioNamespace string) (string, error) {
   605  	injectionConfigMap := "istio-sidecar-injector"
   606  	// Case with multiple control planes
   607  	revision := kubeClient.Revision()
   608  	if isRevisioned(revision) {
   609  		injectionConfigMap = fmt.Sprintf("%s-%s", injectionConfigMap, revision)
   610  	}
   611  	istioInjectionCM, err := kubeClient.Kube().CoreV1().ConfigMaps(istioNamespace).Get(context.Background(), injectionConfigMap, metav1.GetOptions{})
   612  	if err != nil {
   613  		return "", fmt.Errorf("fetch injection template: %v", err)
   614  	}
   615  
   616  	var injectedCMValues map[string]any
   617  	if err := json.Unmarshal([]byte(istioInjectionCM.Data[istioctlutil.ValuesConfigMapKey]), &injectedCMValues); err != nil {
   618  		return "", err
   619  	}
   620  	v, f, err := tpath.GetFromStructPath(injectedCMValues, "global.multiCluster.clusterName")
   621  	if err != nil {
   622  		return "", err
   623  	}
   624  	vs, ok := v.(string)
   625  	if !f || !ok {
   626  		return "", fmt.Errorf("could not retrieve global.multiCluster.clusterName from injection config")
   627  	}
   628  	return vs, nil
   629  }
   630  
   631  // Because we are placing into an Unstructured, place as a map instead
   632  // of structured Istio types.  (The go-client can handle the structured data, but the
   633  // fake go-client used for mocking cannot.)
   634  func unstructureIstioType(spec any) (map[string]any, error) {
   635  	b, err := yaml.Marshal(spec)
   636  	if err != nil {
   637  		return nil, err
   638  	}
   639  	iSpec := map[string]any{}
   640  	err = yaml.Unmarshal(b, &iSpec)
   641  	if err != nil {
   642  		return nil, err
   643  	}
   644  	return iSpec, nil
   645  }
   646  
   647  func convertToUnsignedInt32Map(s []string) map[string]uint32 {
   648  	out := make(map[string]uint32, len(s))
   649  	for _, l := range s {
   650  		k, v := splitEqual(l)
   651  		u64, err := strconv.ParseUint(v, 10, 32)
   652  		if err != nil {
   653  			log.Errorf("failed to convert to uint32: %v", err)
   654  		}
   655  		out[k] = uint32(u64)
   656  	}
   657  	return out
   658  }
   659  
   660  func convertToStringMap(s []string) map[string]string {
   661  	out := make(map[string]string, len(s))
   662  	for _, l := range s {
   663  		k, v := splitEqual(l)
   664  		out[k] = v
   665  	}
   666  	return out
   667  }
   668  
   669  // splitEqual splits key=value string into key,value. if no = is found
   670  // the whole string is the key and value is empty.
   671  func splitEqual(str string) (string, string) {
   672  	idx := strings.Index(str, "=")
   673  	var k string
   674  	var v string
   675  	if idx >= 0 {
   676  		k = str[:idx]
   677  		v = str[idx+1:]
   678  	} else {
   679  		k = str
   680  	}
   681  	return k, v
   682  }
   683  
   684  // validateFlagIsSetManuallyOrNot can validate that a persistent flag is set manually or not by user for given command
   685  func validateFlagIsSetManuallyOrNot(istioCmd *cobra.Command, flagName string) bool {
   686  	if istioCmd != nil {
   687  		allPersistentFlagSet := istioCmd.PersistentFlags()
   688  		if flagName != "" {
   689  			return allPersistentFlagSet.Changed(flagName)
   690  		}
   691  	}
   692  	return false
   693  }