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 }