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 }