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  }