istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/common/deployment/echos.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 deployment
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/hashicorp/go-multierror"
    23  	"golang.org/x/sync/errgroup"
    24  
    25  	"istio.io/api/annotation"
    26  	"istio.io/api/label"
    27  	"istio.io/istio/pkg/config/constants"
    28  	"istio.io/istio/pkg/test"
    29  	"istio.io/istio/pkg/test/framework/components/ambient"
    30  	"istio.io/istio/pkg/test/framework/components/echo"
    31  	"istio.io/istio/pkg/test/framework/components/echo/common/ports"
    32  	"istio.io/istio/pkg/test/framework/components/echo/deployment"
    33  	"istio.io/istio/pkg/test/framework/components/namespace"
    34  	"istio.io/istio/pkg/test/framework/resource"
    35  	"istio.io/istio/pkg/test/scopes"
    36  )
    37  
    38  // Config for new echo deployment.
    39  type Config struct {
    40  	// Echos is the target Echos for the newly created echo apps. If nil, a new Echos
    41  	// instance will be created.
    42  	Echos *Echos
    43  
    44  	// NamespaceCount indicates the number of echo namespaces to be generated.
    45  	// Ignored if Namespaces is non-empty. Defaults to 1.
    46  	NamespaceCount int
    47  
    48  	// Namespaces is the user-provided list of echo namespaces. If empty, NamespaceCount
    49  	// namespaces will be generated.
    50  	Namespaces []namespace.Getter
    51  
    52  	// NoExternalNamespace if true, no external namespace will be generated and no external echo
    53  	// instance will be deployed. Ignored if ExternalNamespace is non-nil.
    54  	NoExternalNamespace bool
    55  
    56  	// ExternalNamespace the namespace to use for the external deployment. If nil, a namespace
    57  	// will be generated unless NoExternalNamespace is specified.
    58  	ExternalNamespace namespace.Getter
    59  
    60  	// IncludeExtAuthz if enabled, an additional ext-authz container will be included in the deployment.
    61  	// This is mainly used to test the CUSTOM authorization policy when the ext-authz server is deployed
    62  	// locally with the application container in the same pod.
    63  	IncludeExtAuthz bool
    64  
    65  	// Custom allows for configuring custom echo deployments. If a deployment's namespace
    66  	// is nil, it will be created in all namespaces. Otherwise, it must match one of the
    67  	// namespaces configured above.
    68  	//
    69  	// Custom echo instances will be accessible from the `All` field in the namespace(s) under which they
    70  	// were created.
    71  	Configs echo.ConfigGetter
    72  }
    73  
    74  // AddConfigs appends to the configs to be deployed
    75  func (c *Config) AddConfigs(configs []echo.Config) *Config {
    76  	var existing echo.ConfigGetter
    77  	if c.Configs != nil {
    78  		existing = c.Configs
    79  	}
    80  	c.Configs = func() []echo.Config {
    81  		var out []echo.Config
    82  		if existing != nil {
    83  			out = append(out, existing()...)
    84  		}
    85  		return append(out, configs...)
    86  	}
    87  	return c
    88  }
    89  
    90  func (c *Config) fillDefaults(ctx resource.Context) error {
    91  	// Create the namespaces concurrently.
    92  	g, _ := errgroup.WithContext(context.TODO())
    93  
    94  	if c.Echos == nil {
    95  		c.Echos = &Echos{}
    96  	}
    97  
    98  	if c.Configs == nil {
    99  		defaultConfigs := c.DefaultEchoConfigs(ctx)
   100  		c.Configs = echo.ConfigFuture(&defaultConfigs)
   101  	}
   102  
   103  	// Verify the namespace for any custom deployments.
   104  	for _, config := range c.Configs.Get() {
   105  		if config.Namespace != nil {
   106  			found := false
   107  			for _, ns := range c.Namespaces {
   108  				if config.Namespace.Name() == ns.Get().Name() {
   109  					found = true
   110  					break
   111  				}
   112  			}
   113  			if !found {
   114  				return fmt.Errorf("custom echo deployment %s uses unconfigured namespace %s",
   115  					config.NamespacedName().String(), config.NamespaceName())
   116  			}
   117  		}
   118  	}
   119  
   120  	if len(c.Namespaces) > 0 {
   121  		c.NamespaceCount = len(c.Namespaces)
   122  	} else if c.NamespaceCount <= 0 {
   123  		c.NamespaceCount = 1
   124  	}
   125  
   126  	nsLabels := map[string]string{}
   127  	if ctx.Settings().Ambient {
   128  		nsLabels["istio.io/dataplane-mode"] = "ambient"
   129  	}
   130  
   131  	// Create the echo namespaces.
   132  	if len(c.Namespaces) == 0 {
   133  		c.Namespaces = make([]namespace.Getter, c.NamespaceCount)
   134  		if c.NamespaceCount == 1 {
   135  			// If only using a single namespace, preserve the "echo" prefix.
   136  			g.Go(func() error {
   137  				ns, err := namespace.New(ctx, namespace.Config{
   138  					Inject: !ctx.Settings().AmbientEverywhere,
   139  					Prefix: "echo",
   140  					Labels: nsLabels,
   141  				})
   142  				if err != nil {
   143  					return err
   144  				}
   145  				c.Namespaces[0] = namespace.Future(&ns)
   146  				return nil
   147  			})
   148  		} else {
   149  			for i := 0; i < c.NamespaceCount; i++ {
   150  				i := i
   151  				g.Go(func() error {
   152  					ns, err := namespace.New(ctx, namespace.Config{
   153  						Prefix: fmt.Sprintf("echo%d", i+1),
   154  						Inject: true,
   155  					})
   156  					if err != nil {
   157  						return err
   158  					}
   159  					c.Namespaces[i] = namespace.Future(&ns)
   160  					return nil
   161  				})
   162  			}
   163  		}
   164  	}
   165  
   166  	// Create the external namespace, if necessary.
   167  	if c.ExternalNamespace == nil && !c.NoExternalNamespace {
   168  		g.Go(func() error {
   169  			ns, err := namespace.New(ctx, namespace.Config{
   170  				Prefix: "external",
   171  				Inject: false,
   172  			})
   173  			if err != nil {
   174  				return err
   175  			}
   176  			c.ExternalNamespace = namespace.Future(&ns)
   177  			return nil
   178  		})
   179  	}
   180  
   181  	// Wait for the namespaces to be created.
   182  	return g.Wait()
   183  }
   184  
   185  func (c *Config) DefaultEchoConfigs(t resource.Context) []echo.Config {
   186  	var defaultConfigs []echo.Config
   187  
   188  	disableAutomountSAToken := true
   189  	if t.Settings().Revisions.Minimum() < "1.16" {
   190  		disableAutomountSAToken = false
   191  	}
   192  
   193  	a := echo.Config{
   194  		Service:                 ASvc,
   195  		ServiceAccount:          true,
   196  		Ports:                   ports.All(),
   197  		Subsets:                 []echo.SubsetConfig{{}},
   198  		Locality:                "region.zone.subzone",
   199  		IncludeExtAuthz:         c.IncludeExtAuthz,
   200  		DisableAutomountSAToken: disableAutomountSAToken,
   201  	}
   202  
   203  	b := echo.Config{
   204  		Service:         BSvc,
   205  		ServiceAccount:  true,
   206  		Ports:           ports.All(),
   207  		Subsets:         []echo.SubsetConfig{{}},
   208  		IncludeExtAuthz: c.IncludeExtAuthz,
   209  	}
   210  
   211  	cSvc := echo.Config{
   212  		Service:         CSvc,
   213  		ServiceAccount:  true,
   214  		Ports:           ports.All(),
   215  		Subsets:         []echo.SubsetConfig{{}},
   216  		IncludeExtAuthz: c.IncludeExtAuthz,
   217  	}
   218  
   219  	headless := echo.Config{
   220  		Service:         HeadlessSvc,
   221  		ServiceAccount:  true,
   222  		Headless:        true,
   223  		Ports:           ports.Headless(),
   224  		Subsets:         []echo.SubsetConfig{{}},
   225  		IncludeExtAuthz: c.IncludeExtAuthz,
   226  	}
   227  
   228  	stateful := echo.Config{
   229  		Service:         StatefulSetSvc,
   230  		ServiceAccount:  true,
   231  		Headless:        true,
   232  		StatefulSet:     true,
   233  		Ports:           ports.Headless(),
   234  		Subsets:         []echo.SubsetConfig{{}},
   235  		IncludeExtAuthz: c.IncludeExtAuthz,
   236  	}
   237  
   238  	naked := echo.Config{
   239  		Service:        NakedSvc,
   240  		ServiceAccount: true,
   241  		Ports:          ports.All(),
   242  		Subsets: []echo.SubsetConfig{
   243  			{
   244  				Annotations: map[string]string{annotation.SidecarInject.Name: "false"},
   245  				Labels: map[string]string{
   246  					label.SidecarInject.Name:     "false",
   247  					constants.DataplaneModeLabel: constants.DataplaneModeNone,
   248  				},
   249  			},
   250  		},
   251  	}
   252  
   253  	tProxy := echo.Config{
   254  		Service:        TproxySvc,
   255  		ServiceAccount: true,
   256  		Ports:          ports.All(),
   257  		Subsets: []echo.SubsetConfig{{
   258  			Annotations: map[string]string{annotation.SidecarInterceptionMode.Name: "TPROXY"},
   259  			Labels: map[string]string{
   260  				constants.DataplaneModeLabel: constants.DataplaneModeNone,
   261  			},
   262  		}},
   263  		IncludeExtAuthz: c.IncludeExtAuthz,
   264  	}
   265  
   266  	vmSvc := echo.Config{
   267  		Service:         VMSvc,
   268  		ServiceAccount:  true,
   269  		Ports:           ports.All(),
   270  		DeployAsVM:      true,
   271  		AutoRegisterVM:  true,
   272  		Subsets:         []echo.SubsetConfig{{}},
   273  		IncludeExtAuthz: c.IncludeExtAuthz,
   274  	}
   275  
   276  	defaultConfigs = append(defaultConfigs, a, b, cSvc, headless, stateful, naked, tProxy, vmSvc)
   277  
   278  	if t.Settings().EnableDualStack {
   279  		dSvc := echo.Config{
   280  			Service:         DSvc,
   281  			ServiceAccount:  true,
   282  			Ports:           ports.All(),
   283  			Subsets:         []echo.SubsetConfig{{}},
   284  			IncludeExtAuthz: c.IncludeExtAuthz,
   285  			IPFamilies:      "IPv6, IPv4",
   286  			IPFamilyPolicy:  "RequireDualStack",
   287  			DualStack:       true,
   288  		}
   289  		eSvc := echo.Config{
   290  			Service:         ESvc,
   291  			ServiceAccount:  true,
   292  			Ports:           ports.All(),
   293  			Subsets:         []echo.SubsetConfig{{}},
   294  			IncludeExtAuthz: c.IncludeExtAuthz,
   295  			IPFamilies:      "IPv6",
   296  			IPFamilyPolicy:  "SingleStack",
   297  			DualStack:       true,
   298  		}
   299  		defaultConfigs = append(defaultConfigs, dSvc, eSvc)
   300  	}
   301  
   302  	sotw := `{"proxyMetadata": {"ISTIO_DELTA_XDS": "false"}}`
   303  
   304  	if !t.Settings().Skip(echo.Sotw) {
   305  		sotw := echo.Config{
   306  			Service:        SotwSvc,
   307  			ServiceAccount: true,
   308  			Ports:          ports.All(),
   309  			Subsets: []echo.SubsetConfig{{
   310  				Labels:      map[string]string{label.SidecarInject.Name: "true"},
   311  				Annotations: map[string]string{annotation.ProxyConfig.Name: sotw},
   312  			}},
   313  		}
   314  		defaultConfigs = append(defaultConfigs, sotw)
   315  	}
   316  
   317  	if !t.Clusters().IsMulticluster() {
   318  		// TODO when agent handles secure control-plane connection for grpc-less, deploy to "remote" clusters
   319  		proxylessGRPC := echo.Config{
   320  			Service:        ProxylessGRPCSvc,
   321  			ServiceAccount: true,
   322  			Ports:          ports.All(),
   323  			Subsets: []echo.SubsetConfig{
   324  				{
   325  					Labels:      map[string]string{label.SidecarInject.Name: "true"},
   326  					Annotations: map[string]string{annotation.InjectTemplates.Name: "grpc-agent"},
   327  				},
   328  			},
   329  		}
   330  		defaultConfigs = append(defaultConfigs, proxylessGRPC)
   331  	}
   332  
   333  	if t.Settings().Ambient {
   334  		if t.Settings().AmbientEverywhere {
   335  			for i, config := range defaultConfigs {
   336  				if !config.HasSidecar() && !config.IsProxylessGRPC() {
   337  					scopes.Framework.Infof("adding waypoint to %s", config.NamespacedName())
   338  					defaultConfigs[i].ServiceWaypointProxy = "shared"
   339  					defaultConfigs[i].WorkloadWaypointProxy = "shared"
   340  				}
   341  			}
   342  		} else {
   343  			waypointed := echo.Config{
   344  				Service:               WaypointSvc,
   345  				ServiceAccount:        true,
   346  				Ports:                 ports.All(),
   347  				ServiceWaypointProxy:  "shared",
   348  				WorkloadWaypointProxy: "shared",
   349  				Subsets: []echo.SubsetConfig{{
   350  					Labels: map[string]string{label.SidecarInject.Name: "false"},
   351  				}},
   352  			}
   353  			defaultConfigs = append(defaultConfigs, waypointed)
   354  		}
   355  
   356  		captured := echo.Config{
   357  			Service:        CapturedSvc,
   358  			ServiceAccount: true,
   359  			Ports:          ports.All(),
   360  			Subsets: []echo.SubsetConfig{{
   361  				Labels: map[string]string{
   362  					label.SidecarInject.Name:     "false",
   363  					constants.AmbientRedirection: constants.AmbientRedirectionEnabled,
   364  				},
   365  			}},
   366  		}
   367  		defaultConfigs = append(defaultConfigs, captured)
   368  	}
   369  
   370  	return defaultConfigs
   371  }
   372  
   373  // View of an Echos deployment.
   374  type View interface {
   375  	// Echos returns the underlying Echos deployment for this view.
   376  	Echos() *Echos
   377  }
   378  
   379  var (
   380  	_ View = &SingleNamespaceView{}
   381  	_ View = &TwoNamespaceView{}
   382  	_ View = &Echos{}
   383  )
   384  
   385  // SingleNamespaceView is a simplified view of Echos for tests that only require a single namespace.
   386  type SingleNamespaceView struct {
   387  	// Include the echos at the top-level, so there is no need for accessing sub-structures.
   388  	EchoNamespace
   389  
   390  	// External (out-of-mesh) deployments
   391  	External External
   392  
   393  	// All echo instances
   394  	All echo.Services
   395  
   396  	echos *Echos
   397  }
   398  
   399  func (v *SingleNamespaceView) Echos() *Echos {
   400  	return v.echos
   401  }
   402  
   403  // TwoNamespaceView is a simplified view of Echos for tests that require 2 namespaces.
   404  type TwoNamespaceView struct {
   405  	// Ns1 contains the echo deployments in the first namespace
   406  	Ns1 EchoNamespace
   407  
   408  	// Ns2 contains the echo deployments in the second namespace
   409  	Ns2 EchoNamespace
   410  
   411  	// Ns1AndNs2 contains just the echo services in Ns1 and Ns2 (excludes External).
   412  	Ns1AndNs2 echo.Services
   413  
   414  	// External (out-of-mesh) deployments
   415  	External External
   416  
   417  	// All echo instances
   418  	All echo.Services
   419  
   420  	echos *Echos
   421  }
   422  
   423  func (v *TwoNamespaceView) Echos() *Echos {
   424  	return v.echos
   425  }
   426  
   427  // Echos is a common set of echo deployments to support integration testing.
   428  type Echos struct {
   429  	// NS is the list of echo namespaces.
   430  	NS []EchoNamespace
   431  
   432  	// External (out-of-mesh) deployments
   433  	External External
   434  
   435  	// All echo instances.
   436  	All echo.Services
   437  }
   438  
   439  func (e *Echos) Echos() *Echos {
   440  	return e
   441  }
   442  
   443  // New echo deployment with the given configuration.
   444  func New(ctx resource.Context, cfg Config) (*Echos, error) {
   445  	if err := cfg.fillDefaults(ctx); err != nil {
   446  		return nil, err
   447  	}
   448  
   449  	apps := cfg.Echos
   450  	apps.NS = make([]EchoNamespace, len(cfg.Namespaces))
   451  	for i, ns := range cfg.Namespaces {
   452  		apps.NS[i].Namespace = ns.Get()
   453  	}
   454  	if !cfg.NoExternalNamespace {
   455  		apps.External.Namespace = cfg.ExternalNamespace.Get()
   456  	}
   457  
   458  	builder := deployment.New(ctx).WithClusters(ctx.Clusters()...)
   459  	for _, n := range apps.NS {
   460  		builder = n.build(builder, cfg)
   461  	}
   462  
   463  	if !cfg.NoExternalNamespace {
   464  		builder = apps.External.Build(ctx, builder)
   465  	}
   466  
   467  	echos, err := builder.Build()
   468  	if err != nil {
   469  		return nil, err
   470  	}
   471  
   472  	if ctx.Settings().Ambient {
   473  
   474  		waypointProxies := make(map[string]ambient.WaypointProxy)
   475  
   476  		for _, echo := range echos {
   477  			svcwp := echo.Config().ServiceWaypointProxy
   478  			wlwp := echo.Config().WorkloadWaypointProxy
   479  			var err error
   480  			if svcwp != "" {
   481  				if _, found := waypointProxies[svcwp]; !found {
   482  					waypointProxies[svcwp], err = ambient.NewWaypointProxy(ctx, echo.Config().Namespace, svcwp)
   483  					if err != nil {
   484  						return nil, err
   485  					}
   486  				}
   487  			}
   488  			if wlwp != "" {
   489  				if _, found := waypointProxies[wlwp]; !found {
   490  					waypointProxies[wlwp], err = ambient.NewWaypointProxy(ctx, echo.Config().Namespace, wlwp)
   491  					if err != nil {
   492  						return nil, err
   493  					}
   494  				}
   495  			}
   496  
   497  		}
   498  	}
   499  
   500  	apps.All = echos.Services()
   501  
   502  	g := multierror.Group{}
   503  	for i := 0; i < len(apps.NS); i++ {
   504  		i := i
   505  		g.Go(func() error {
   506  			return apps.NS[i].loadValues(ctx, echos, apps)
   507  		})
   508  	}
   509  
   510  	if !cfg.NoExternalNamespace {
   511  		apps.External.LoadValues(echos)
   512  	}
   513  
   514  	if err := g.Wait().ErrorOrNil(); err != nil {
   515  		return nil, err
   516  	}
   517  
   518  	return apps, nil
   519  }
   520  
   521  // NewOrFail calls New and fails if an error is returned.
   522  func NewOrFail(t test.Failer, ctx resource.Context, cfg Config) *Echos {
   523  	t.Helper()
   524  	out, err := New(ctx, cfg)
   525  	if err != nil {
   526  		t.Fatal(err)
   527  	}
   528  	return out
   529  }
   530  
   531  // SingleNamespaceView converts this Echos into a SingleNamespaceView.
   532  func (e *Echos) SingleNamespaceView() SingleNamespaceView {
   533  	return SingleNamespaceView{
   534  		EchoNamespace: e.NS[0],
   535  		External:      e.External,
   536  		All:           e.NS[0].All.Append(e.External.All.Services()),
   537  		echos:         e,
   538  	}
   539  }
   540  
   541  // TwoNamespaceView converts this Echos into a TwoNamespaceView.
   542  func (e *Echos) TwoNamespaceView() TwoNamespaceView {
   543  	ns1AndNs2 := e.NS[0].All.Append(e.NS[1].All)
   544  	return TwoNamespaceView{
   545  		Ns1:       e.NS[0],
   546  		Ns2:       e.NS[1],
   547  		Ns1AndNs2: ns1AndNs2,
   548  		External:  e.External,
   549  		All:       ns1AndNs2.Append(e.External.All.Services()),
   550  		echos:     e,
   551  	}
   552  }
   553  
   554  func serviceEntryPorts() []echo.Port {
   555  	var res []echo.Port
   556  	for _, p := range ports.All().GetServicePorts() {
   557  		if strings.HasPrefix(p.Name, "auto") {
   558  			// The protocol needs to be set in common.EchoPorts to configure the echo deployment
   559  			// But for service entry, we want to ensure we set it to "" which will use sniffing
   560  			p.Protocol = ""
   561  		}
   562  		res = append(res, p)
   563  	}
   564  	return res
   565  }
   566  
   567  // SetupSingleNamespace calls Setup and returns a SingleNamespaceView.
   568  func SetupSingleNamespace(view *SingleNamespaceView, cfg Config) resource.SetupFn {
   569  	cfg.NamespaceCount = 1
   570  	return func(ctx resource.Context) error {
   571  		// Perform a setup with 1 namespace.
   572  		var apps Echos
   573  		if err := Setup(&apps, cfg)(ctx); err != nil {
   574  			return err
   575  		}
   576  
   577  		// Store the view.
   578  		*view = apps.SingleNamespaceView()
   579  		return nil
   580  	}
   581  }
   582  
   583  // SetupTwoNamespaces calls Setup and returns a TwoNamespaceView.
   584  func SetupTwoNamespaces(view *TwoNamespaceView, cfg Config) resource.SetupFn {
   585  	cfg.NamespaceCount = 2
   586  	return func(ctx resource.Context) error {
   587  		// Perform a setup with 2 namespaces.
   588  		var apps Echos
   589  		if err := Setup(&apps, cfg)(ctx); err != nil {
   590  			return err
   591  		}
   592  
   593  		// Store the view.
   594  		*view = apps.TwoNamespaceView()
   595  		return nil
   596  	}
   597  }
   598  
   599  // Setup function for writing to a global deployment variable.
   600  func Setup(apps *Echos, cfg Config) resource.SetupFn {
   601  	return func(ctx resource.Context) error {
   602  		// Set the target for the deployments.
   603  		cfg.Echos = apps
   604  
   605  		_, err := New(ctx, cfg)
   606  		if err != nil {
   607  			return err
   608  		}
   609  
   610  		return nil
   611  	}
   612  }