istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/config.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 echo 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "strings" 21 "time" 22 23 "github.com/mitchellh/copystructure" 24 "gopkg.in/yaml.v3" 25 26 "istio.io/api/annotation" 27 "istio.io/istio/pkg/config/constants" 28 "istio.io/istio/pkg/config/protocol" 29 "istio.io/istio/pkg/test/echo/common" 30 "istio.io/istio/pkg/test/framework/components/cluster" 31 "istio.io/istio/pkg/test/framework/components/namespace" 32 "istio.io/istio/pkg/test/framework/resource" 33 ) 34 35 // Cluster that can deploy echo instances. 36 // TODO putting this here for now to deal with circular imports, needs to be moved 37 type Cluster interface { 38 cluster.Cluster 39 40 CanDeploy(Config) (Config, bool) 41 } 42 43 // Configurable is and object that has Config. 44 type Configurable interface { 45 Config() Config 46 47 // ServiceName is the name of this service within the namespace. 48 ServiceName() string 49 50 // NamespaceName returns the name of the namespace or "" if the Namespace is nil. 51 NamespaceName() string 52 53 // NamespacedName returns the namespaced name for this service. 54 // Short form for Config().NamespacedName(). 55 NamespacedName() NamespacedName 56 57 // ServiceAccountName returns the service account string for this service. 58 ServiceAccountName() string 59 60 // ClusterLocalFQDN returns the fully qualified domain name for cluster-local host. 61 ClusterLocalFQDN() string 62 63 // ClusterSetLocalFQDN returns the fully qualified domain name for the Kubernetes 64 // Multi-Cluster Services (MCS) Cluster Set host. 65 ClusterSetLocalFQDN() string 66 67 // PortForName is a short form for Config().Ports.MustForName(). 68 PortForName(name string) Port 69 } 70 71 type VMDistro = string 72 73 const ( 74 UbuntuBionic VMDistro = "UbuntuBionic" 75 UbuntuNoble VMDistro = "UbuntuNoble" 76 Debian12 VMDistro = "Debian12" 77 Rockylinux9 VMDistro = "Rockylinux9" 78 79 DefaultVMDistro = UbuntuNoble 80 ) 81 82 // Config defines the options for creating an Echo component. 83 // nolint: maligned 84 type Config struct { 85 // Namespace of the echo Instance. If not provided, a default namespace "apps" is used. 86 Namespace namespace.Instance 87 88 // DefaultHostHeader overrides the default Host header for calls (`service.namespace.svc.cluster.local`) 89 DefaultHostHeader string 90 91 // Domain of the echo Instance. If not provided, a default will be selected. 92 Domain string 93 94 // Service indicates the service name of the Echo application. 95 Service string 96 97 // Version indicates the version path for calls to the Echo application. 98 Version string 99 100 // Locality (k8s only) indicates the locality of the deployed app. 101 Locality string 102 103 // Headless (k8s only) indicates that no ClusterIP should be specified. 104 Headless bool 105 106 // StatefulSet indicates that the pod should be backed by a StatefulSet. This implies Headless=true 107 // as well. 108 StatefulSet bool 109 110 // StaticAddress for some echo implementations is an address locally reachable within 111 // the test framework and from the echo Cluster's network. 112 StaticAddresses []string 113 114 // ServiceAccount (k8s only) indicates that a service account should be created 115 // for the deployment. 116 ServiceAccount bool 117 118 // DisableAutomountSAToken indicates to opt out of auto mounting ServiceAccount's API credentials 119 DisableAutomountSAToken bool 120 121 // Ports for this application. Port numbers may or may not be used, depending 122 // on the implementation. 123 Ports Ports 124 125 // ServiceAnnotations is annotations on service object. 126 ServiceAnnotations map[string]string 127 128 // ServiceLabels is the labels on service object. 129 ServiceLabels map[string]string 130 131 // ReadinessTimeout specifies the timeout that we wait the application to 132 // become ready. 133 ReadinessTimeout time.Duration 134 135 // ReadinessTCPPort if set, use this port for the TCP readiness probe (instead of using a HTTP probe). 136 ReadinessTCPPort string 137 138 // ReadinessGRPCPort if set, use this port for the GRPC readiness probe (instead of using a HTTP probe). 139 ReadinessGRPCPort string 140 141 // Subsets contains the list of Subsets config belonging to this echo 142 // service instance. 143 Subsets []SubsetConfig 144 145 // Cluster to be used in a multicluster environment 146 Cluster cluster.Cluster 147 148 // TLS settings for echo server 149 TLSSettings *common.TLSSettings 150 151 // If enabled, echo will be deployed as a "VM". This means it will run Envoy in the same pod as echo, 152 // disable sidecar injection, etc. 153 // This aims to simulate a VM, but instead of managing the complex test setup of spinning up a VM, 154 // connecting, etc we run it inside a pod. The pod has pretty much all Kubernetes features disabled (DNS and SA token mount) 155 // such that we can adequately simulate a VM and DIY the bootstrapping. 156 DeployAsVM bool 157 158 // If enabled, ISTIO_META_AUTO_REGISTER_GROUP will be set on the VM and the WorkloadEntry will be created automatically. 159 AutoRegisterVM bool 160 161 // The distro to use for a VM. For fake VMs, this maps to docker images. 162 VMDistro VMDistro 163 164 // The set of environment variables to set for `DeployAsVM` instances. 165 VMEnvironment map[string]string 166 167 // If enabled, an additional ext-authz container will be included in the deployment. This is mainly used to test 168 // the CUSTOM authorization policy when the ext-authz server is deployed locally with the application container in 169 // the same pod. 170 IncludeExtAuthz bool 171 172 // IPFamily for the service. This is optional field. Mainly is used for dual stack testing 173 IPFamilies string 174 175 // IPFamilyPolicy. This is optional field. Mainly is used for dual stack testing. 176 IPFamilyPolicy string 177 178 DualStack bool 179 180 // ServiceWaypointProxy specifies if this workload should have an associated Waypoint for service-addressed traffic 181 ServiceWaypointProxy string 182 183 // WorkloadWaypointProxy specifies if this workload should have an associated Waypoint for workload-addressed traffic 184 WorkloadWaypointProxy string 185 } 186 187 // Getter for a custom echo deployment 188 type ConfigGetter func() []Config 189 190 // Get is a utility method that helps in readability of call sites. 191 func (g ConfigGetter) Get() []Config { 192 return g() 193 } 194 195 // Future creates a Getter for a variable the custom echo deployment that will be set at sometime in the future. 196 // This is helpful for configuring a setup chain for a test suite that operates on global variables. 197 func ConfigFuture(custom *[]Config) ConfigGetter { 198 return func() []Config { 199 return *custom 200 } 201 } 202 203 // NamespaceName returns the string name of the namespace. 204 func (c Config) NamespaceName() string { 205 return c.NamespacedName().NamespaceName() 206 } 207 208 // NamespacedName returns the namespaced name for the service. 209 func (c Config) NamespacedName() NamespacedName { 210 return NamespacedName{ 211 Name: c.Service, 212 Namespace: c.Namespace, 213 } 214 } 215 216 func (c Config) AccountName() string { 217 if c.ServiceAccount { 218 return c.Service 219 } 220 return "default" 221 } 222 223 // ServiceAccountName returns the service account name for this service. 224 func (c Config) ServiceAccountName() string { 225 return "cluster.local/ns/" + c.NamespaceName() + "/sa/" + c.Service 226 } 227 228 // SubsetConfig is the config for a group of Subsets (e.g. Kubernetes deployment). 229 type SubsetConfig struct { 230 // The version of the deployment. 231 Version string 232 // Annotations provides metadata hints for deployment of the instance. 233 Annotations map[string]string 234 // Labels provides metadata hints for deployment of the instance. 235 Labels map[string]string 236 // Replicas of this deployment 237 Replicas int 238 239 // TODO: port more into workload config. 240 } 241 242 // String implements the Configuration interface (which implements fmt.Stringer) 243 func (c Config) String() string { 244 return fmt.Sprint("{service: ", c.Service, ", version: ", c.Version, "}") 245 } 246 247 // ClusterLocalFQDN returns the fully qualified domain name for cluster-local host. 248 func (c Config) ClusterLocalFQDN() string { 249 out := c.Service 250 if c.Namespace != nil { 251 out += "." + c.Namespace.Name() + ".svc" 252 } else { 253 out += ".default.svc" 254 } 255 if c.Domain != "" { 256 out += "." + c.Domain 257 } 258 return out 259 } 260 261 // ClusterSetLocalFQDN returns the fully qualified domain name for the Kubernetes 262 // Multi-Cluster Services (MCS) Cluster Set host. 263 func (c Config) ClusterSetLocalFQDN() string { 264 out := c.Service 265 if c.Namespace != nil { 266 out += "." + c.Namespace.Name() + ".svc" 267 } else { 268 out += ".default.svc" 269 } 270 out += "." + constants.DefaultClusterSetLocalDomain 271 return out 272 } 273 274 // HostnameVariants for a Kubernetes service. 275 // Results may be invalid for non k8s. 276 func (c Config) HostnameVariants() []string { 277 ns := c.NamespaceName() 278 if ns == "" { 279 ns = "default" 280 } 281 return []string{ 282 c.Service, 283 c.Service + "." + ns, 284 c.Service + "." + ns + ".svc", 285 c.ClusterLocalFQDN(), 286 } 287 } 288 289 // HostHeader returns the Host header that will be used for calls to this service. 290 func (c Config) HostHeader() string { 291 if c.DefaultHostHeader != "" { 292 return c.DefaultHostHeader 293 } 294 return c.ClusterLocalFQDN() 295 } 296 297 func (c Config) IsHeadless() bool { 298 return c.Headless 299 } 300 301 func (c Config) IsStatefulSet() bool { 302 return c.StatefulSet 303 } 304 305 // IsNaked checks if the config has no sidecar. 306 // Note: instances that mix subsets with and without sidecars are considered 'naked'. 307 func (c Config) IsNaked() bool { 308 for _, s := range c.Subsets { 309 if s.Annotations != nil && s.Annotations[annotation.SidecarInject.Name] == "false" { 310 // Sidecar injection is disabled - it's naked. 311 return true 312 } 313 } 314 return false 315 } 316 317 // IsAllNaked checks if every subset is configured with no sidecar. 318 func (c Config) IsAllNaked() bool { 319 if len(c.Subsets) == 0 { 320 // No subsets - default to not-naked. 321 return false 322 } 323 // if ANY subset has a sidecar, not naked. 324 for _, s := range c.Subsets { 325 if s.Annotations == nil || s.Annotations[annotation.SidecarInject.Name] != "false" { 326 // Sidecar injection is enabled - it's not naked. 327 return false 328 } 329 } 330 // All subsets were annotated indicating no sidecar injection. 331 return true 332 } 333 334 func (c Config) IsProxylessGRPC() bool { 335 // TODO make these check if any subset has a matching annotation 336 return len(c.Subsets) > 0 && c.Subsets[0].Annotations != nil && strings.HasPrefix(c.Subsets[0].Annotations[annotation.InjectTemplates.Name], "grpc-") 337 } 338 339 func (c Config) IsTProxy() bool { 340 // TODO this could be HasCustomInjectionMode 341 return len(c.Subsets) > 0 && c.Subsets[0].Annotations != nil && c.Subsets[0].Annotations[annotation.SidecarInterceptionMode.Name] == "TPROXY" 342 } 343 344 func (c Config) HasAnyWaypointProxy() bool { 345 return c.ServiceWaypointProxy != "" || c.WorkloadWaypointProxy != "" 346 } 347 348 func (c Config) HasServiceAddressedWaypointProxy() bool { 349 return c.ServiceWaypointProxy != "" 350 } 351 352 func (c Config) HasWorkloadAddressedWaypointProxy() bool { 353 return c.WorkloadWaypointProxy != "" 354 } 355 356 func (c Config) HasSidecar() bool { 357 var perPodEnable, perPodDisable bool 358 if len(c.Subsets) > 0 && c.Subsets[0].Labels != nil { 359 perPodEnable = c.Subsets[0].Labels["sidecar.istio.io/inject"] == "true" 360 perPodDisable = c.Subsets[0].Labels["sidecar.istio.io/inject"] == "false" 361 } 362 363 return perPodEnable || (!perPodDisable && c.Namespace != nil && c.Namespace.IsInjected()) 364 } 365 366 func (c Config) IsUncaptured() bool { 367 // TODO this can be more robust to not require labeling initial echo config (check namespace + isWaypoint + not sidecar) 368 return len(c.Subsets) > 0 && c.Subsets[0].Labels != nil && c.Subsets[0].Labels[constants.DataplaneModeLabel] == constants.DataplaneModeNone 369 } 370 371 func (c Config) HasProxyCapabilities() bool { 372 return !c.IsUncaptured() || c.HasSidecar() || c.IsProxylessGRPC() 373 } 374 375 func (c Config) IsVM() bool { 376 return c.DeployAsVM 377 } 378 379 func (c Config) IsSotw() bool { 380 // TODO this doesn't hold if delta is off by default 381 return len(c.Subsets) > 0 && c.Subsets[0].Annotations != nil && strings.Contains(c.Subsets[0].Annotations[annotation.ProxyConfig.Name], "ISTIO_DELTA_XDS") 382 } 383 384 // IsRegularPod returns true if the echo pod is not any of the following: 385 // - VM 386 // - Naked 387 // - Headless 388 // - TProxy 389 // - Multi-Subset 390 // - DualStack Service Pods 391 func (c Config) IsRegularPod() bool { 392 return len(c.Subsets) == 1 && 393 !c.IsVM() && 394 !c.IsTProxy() && 395 !c.IsNaked() && 396 !c.IsHeadless() && 397 !c.IsStatefulSet() && 398 !c.IsProxylessGRPC() && 399 !c.HasServiceAddressedWaypointProxy() && 400 !c.HasWorkloadAddressedWaypointProxy() && 401 !c.ZTunnelCaptured() && 402 !c.DualStack 403 } 404 405 // WaypointClient means the client supports HBONE and does zTunnel redirection. 406 // Currently, only zTunnel captured sources do this. Eventually this might be enabled 407 // for ingress and/or sidecars. 408 func (c Config) WaypointClient() bool { 409 return c.ZTunnelCaptured() && !c.IsUncaptured() 410 } 411 412 // ZTunnelCaptured returns true in ambient enabled namespaces where there is no sidecar 413 func (c Config) ZTunnelCaptured() bool { 414 haveSubsets := len(c.Subsets) > 0 415 if c.Namespace.IsAmbient() && haveSubsets && 416 c.Subsets[0].Labels[constants.DataplaneModeLabel] != constants.DataplaneModeNone && 417 !c.HasSidecar() { 418 return true 419 } 420 return haveSubsets && c.Subsets[0].Annotations[constants.AmbientRedirection] == constants.AmbientRedirectionEnabled 421 } 422 423 // DeepCopy creates a clone of IstioEndpoint. 424 func (c Config) DeepCopy() Config { 425 newc := c 426 newc.Cluster = nil 427 newc = copyInternal(newc).(Config) 428 newc.Cluster = c.Cluster 429 newc.Namespace = c.Namespace 430 return newc 431 } 432 433 func (c Config) IsExternal() bool { 434 return c.HostHeader() != c.ClusterLocalFQDN() 435 } 436 437 const ( 438 defaultService = "echo" 439 defaultVersion = "v1" 440 defaultNamespace = "echo" 441 defaultDomain = constants.DefaultClusterLocalDomain 442 ) 443 444 func (c *Config) FillDefaults(ctx resource.Context) (err error) { 445 if c.Service == "" { 446 c.Service = defaultService 447 } 448 449 if c.Version == "" { 450 c.Version = defaultVersion 451 } 452 453 if c.Domain == "" { 454 c.Domain = defaultDomain 455 } 456 457 if c.VMDistro == "" { 458 c.VMDistro = DefaultVMDistro 459 } 460 if c.StatefulSet { 461 // Statefulset requires headless 462 c.Headless = true 463 } 464 465 // Convert legacy config to workload oritended. 466 if c.Subsets == nil { 467 c.Subsets = []SubsetConfig{ 468 { 469 Version: c.Version, 470 }, 471 } 472 } 473 474 for i := range c.Subsets { 475 if c.Subsets[i].Version == "" { 476 c.Subsets[i].Version = c.Version 477 } 478 } 479 c.addPortIfMissing(protocol.GRPC) 480 // If no namespace was provided, use the default. 481 if c.Namespace == nil && ctx != nil { 482 nsConfig := namespace.Config{ 483 Prefix: defaultNamespace, 484 Inject: true, 485 } 486 if c.Namespace, err = namespace.New(ctx, nsConfig); err != nil { 487 return err 488 } 489 } 490 491 // Make a copy of the ports array. This avoids potential corruption if multiple Echo 492 // Instances share the same underlying ports array. 493 c.Ports = append([]Port{}, c.Ports...) 494 495 // Mark all user-defined ports as used, so the port generator won't assign them. 496 portGen := newPortGenerators() 497 for _, p := range c.Ports { 498 if p.ServicePort > 0 { 499 if portGen.Service.IsUsed(p.ServicePort) { 500 return fmt.Errorf("failed configuring port %s: service port already used %d", p.Name, p.ServicePort) 501 } 502 portGen.Service.SetUsed(p.ServicePort) 503 } 504 if p.WorkloadPort > 0 { 505 if portGen.Instance.IsUsed(p.WorkloadPort) { 506 return fmt.Errorf("failed configuring port %s: instance port already used %d", p.Name, p.WorkloadPort) 507 } 508 portGen.Instance.SetUsed(p.WorkloadPort) 509 } 510 } 511 512 // Second pass: try to make unassigned instance ports match service port. 513 for i, p := range c.Ports { 514 if p.WorkloadPort == 0 && p.ServicePort > 0 && !portGen.Instance.IsUsed(p.ServicePort) { 515 c.Ports[i].WorkloadPort = p.ServicePort 516 portGen.Instance.SetUsed(p.ServicePort) 517 } 518 } 519 520 // Final pass: assign default values for any ports that haven't been specified. 521 for i, p := range c.Ports { 522 if p.ServicePort == 0 { 523 c.Ports[i].ServicePort = portGen.Service.Next(p.Protocol) 524 } 525 if p.WorkloadPort == 0 { 526 c.Ports[i].WorkloadPort = portGen.Instance.Next(p.Protocol) 527 } 528 } 529 530 // If readiness probe is not specified by a test, wait a long time 531 // Waiting forever would cause the test to timeout and lose logs 532 if c.ReadinessTimeout == 0 { 533 c.ReadinessTimeout = DefaultReadinessTimeout() 534 } 535 536 return nil 537 } 538 539 // addPortIfMissing adds a port for the given protocol if none was found. 540 func (c *Config) addPortIfMissing(protocol protocol.Instance) { 541 if _, found := c.Ports.ForProtocol(protocol); !found { 542 c.Ports = append([]Port{ 543 { 544 Name: strings.ToLower(string(protocol)), 545 Protocol: protocol, 546 }, 547 }, c.Ports...) 548 } 549 } 550 551 func copyInternal(v any) any { 552 copied, err := copystructure.Copy(v) 553 if err != nil { 554 // There are 2 locations where errors are generated in copystructure.Copy: 555 // * The reflection walk over the structure fails, which should never happen 556 // * A configurable copy function returns an error. This is only used for copying times, which never returns an error. 557 // Therefore, this should never happen 558 panic(err) 559 } 560 return copied 561 } 562 563 // ParseConfigs unmarshals the given YAML bytes into []Config, using a namespace.Static rather 564 // than attempting to Claim the configured namespace. 565 func ParseConfigs(bytes []byte) ([]Config, error) { 566 // parse into flexible type, so we can remove Namespace and parse that ourselves 567 raw := make([]map[string]any, 0) 568 if err := yaml.Unmarshal(bytes, &raw); err != nil { 569 return nil, err 570 } 571 configs := make([]Config, len(raw)) 572 573 for i, raw := range raw { 574 if ns, ok := raw["Namespace"]; ok { 575 configs[i].Namespace = namespace.Static(fmt.Sprint(ns)) 576 delete(raw, "Namespace") 577 } 578 } 579 580 // unmarshal again after Namespace stripped is stripped, to avoid unmarshal error 581 modifiedBytes, err := json.Marshal(raw) 582 if err != nil { 583 return nil, err 584 } 585 if err := json.Unmarshal(modifiedBytes, &configs); err != nil { 586 return nil, nil 587 } 588 589 return configs, nil 590 } 591 592 // WorkloadClass returns the type of workload a given config is. 593 func (c Config) WorkloadClass() WorkloadClass { 594 if c.IsProxylessGRPC() { 595 return Proxyless 596 } else if c.IsVM() { 597 return VM 598 } else if c.IsTProxy() { 599 return TProxy 600 } else if c.IsNaked() { 601 return Naked 602 } else if c.IsExternal() { 603 return External 604 } else if c.IsStatefulSet() { 605 return StatefulSet 606 } else if c.IsSotw() { 607 return Sotw 608 } else if c.HasAnyWaypointProxy() { 609 return Waypoint 610 } else if c.ZTunnelCaptured() && !c.HasAnyWaypointProxy() { 611 return Captured 612 } 613 if c.IsHeadless() { 614 return Headless 615 } 616 return Standard 617 }