istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/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 framework
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/hashicorp/go-multierror"
    23  	"go.uber.org/atomic"
    24  	"golang.org/x/sync/errgroup"
    25  
    26  	"istio.io/istio/pkg/test"
    27  	"istio.io/istio/pkg/test/framework/components/cluster"
    28  	"istio.io/istio/pkg/test/framework/components/istioctl"
    29  	"istio.io/istio/pkg/test/framework/resource"
    30  	"istio.io/istio/pkg/test/framework/resource/config"
    31  	"istio.io/istio/pkg/test/framework/resource/config/apply"
    32  	"istio.io/istio/pkg/test/framework/resource/config/cleanup"
    33  	"istio.io/istio/pkg/test/scopes"
    34  	"istio.io/istio/pkg/test/util/file"
    35  	"istio.io/istio/pkg/test/util/tmpl"
    36  	"istio.io/istio/pkg/test/util/yml"
    37  )
    38  
    39  var _ config.Factory = &configFactory{}
    40  
    41  type configFactory struct {
    42  	ctx      resource.Context
    43  	clusters []cluster.Cluster
    44  	prefix   string
    45  }
    46  
    47  func newConfigFactory(ctx resource.Context, clusters cluster.Clusters) config.Factory {
    48  	if len(clusters) == 0 {
    49  		clusters = ctx.Clusters()
    50  	}
    51  	return &configFactory{
    52  		ctx:      ctx,
    53  		clusters: clusters.Kube(),
    54  	}
    55  }
    56  
    57  // GlobalYAMLWrites records how many YAMLs we have applied from all sources.
    58  // Note: go tests are distinct binaries per test suite, so this is the suite level number of calls
    59  var GlobalYAMLWrites = atomic.NewUint64(0)
    60  
    61  func (c *configFactory) New() config.Plan {
    62  	return &configPlan{
    63  		configFactory: c,
    64  		yamlText:      make(map[string][]string),
    65  	}
    66  }
    67  
    68  func (c *configFactory) YAML(ns string, yamlText ...string) config.Plan {
    69  	return c.New().YAML(ns, yamlText...)
    70  }
    71  
    72  func (c *configFactory) Eval(ns string, args any, yamlTemplates ...string) config.Plan {
    73  	return c.New().Eval(ns, args, yamlTemplates...)
    74  }
    75  
    76  func (c *configFactory) File(ns string, filePaths ...string) config.Plan {
    77  	return c.New().File(ns, filePaths...)
    78  }
    79  
    80  func (c *configFactory) EvalFile(ns string, args any, filePaths ...string) config.Plan {
    81  	return c.New().EvalFile(ns, args, filePaths...)
    82  }
    83  
    84  func (c *configFactory) applyYAML(cleanupStrategy cleanup.Strategy, ns string, yamlText ...string) error {
    85  	if len(c.prefix) == 0 {
    86  		return c.withFilePrefix("apply").(*configFactory).applyYAML(cleanupStrategy, ns, yamlText...)
    87  	}
    88  	GlobalYAMLWrites.Add(uint64(len(yamlText)))
    89  
    90  	// Convert the content to files.
    91  	yamlFiles, err := c.ctx.WriteYAML(c.prefix, yamlText...)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	g, _ := errgroup.WithContext(context.TODO())
    97  	for _, cl := range c.clusters {
    98  		cl := cl
    99  		g.Go(func() error {
   100  			scopes.Framework.Debugf("Applying to %s to namespace %v: %s", cl.StableName(), ns, strings.Join(yamlFiles, ", "))
   101  			if err := cl.ApplyYAMLFiles(ns, yamlFiles...); err != nil {
   102  				return fmt.Errorf("failed applying YAML files %v to ns %s in cluster %s: %v", yamlFiles, ns, cl.Name(), err)
   103  			}
   104  			c.ctx.CleanupStrategy(cleanupStrategy, func() {
   105  				scopes.Framework.Debugf("Deleting from %s: %s", cl.StableName(), strings.Join(yamlFiles, ", "))
   106  				if err := cl.DeleteYAMLFiles(ns, yamlFiles...); err != nil {
   107  					scopes.Framework.Errorf("failed deleting YAML files %v from ns %s in cluster %s: %v", yamlFiles, ns, cl.Name(), err)
   108  				}
   109  			})
   110  			return nil
   111  		})
   112  	}
   113  	return g.Wait()
   114  }
   115  
   116  func (c *configFactory) deleteYAML(ns string, yamlText ...string) error {
   117  	if len(c.prefix) == 0 {
   118  		return c.withFilePrefix("delete").(*configFactory).deleteYAML(ns, yamlText...)
   119  	}
   120  
   121  	// Convert the content to files.
   122  	yamlFiles, err := c.ctx.WriteYAML(c.prefix, yamlText...)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	g, _ := errgroup.WithContext(context.TODO())
   128  	for _, c := range c.clusters {
   129  		c := c
   130  		g.Go(func() error {
   131  			if err := c.DeleteYAMLFiles(ns, yamlFiles...); err != nil {
   132  				return fmt.Errorf("failed deleting YAML from cluster %s: %v", c.Name(), err)
   133  			}
   134  			return nil
   135  		})
   136  	}
   137  	return g.Wait()
   138  }
   139  
   140  func (c *configFactory) WaitForConfig(ctx resource.Context, ns string, yamlText ...string) error {
   141  	var outErr error
   142  	for _, c := range c.ctx.Clusters() {
   143  		ik, err := istioctl.New(ctx, istioctl.Config{Cluster: c})
   144  		if err != nil {
   145  			return err
   146  		}
   147  
   148  		for _, cfg := range yamlText {
   149  			cfg := cfg
   150  
   151  			// TODO(https://github.com/istio/istio/issues/37324): It's currently unsafe
   152  			// to call istioctl concurrently since it relies on the istioctl library
   153  			// (rather than calling the binary from the command line) which uses a number
   154  			// of global variables, which will be overwritten for each call.
   155  			if err := ik.WaitForConfig(ns, cfg); err != nil {
   156  				// Get proxy status for additional debugging
   157  				s, _, _ := ik.Invoke([]string{"ps"})
   158  				outErr = multierror.Append(err, fmt.Errorf("failed waiting for config for cluster %s: err=%v. Proxy status: %v",
   159  					c.StableName(), err, s))
   160  			}
   161  		}
   162  	}
   163  	return outErr
   164  }
   165  
   166  func (c *configFactory) WaitForConfigOrFail(ctx resource.Context, t test.Failer, ns string, yamlText ...string) {
   167  	err := c.WaitForConfig(ctx, ns, yamlText...)
   168  	if err != nil {
   169  		// TODO(https://github.com/istio/istio/issues/37148) fail hard in this case
   170  		t.Log(err)
   171  	}
   172  }
   173  
   174  func (c *configFactory) withFilePrefix(prefix string) config.Factory {
   175  	return &configFactory{
   176  		ctx:      c.ctx,
   177  		prefix:   prefix,
   178  		clusters: c.clusters,
   179  	}
   180  }
   181  
   182  var _ config.Plan = &configPlan{}
   183  
   184  type configPlan struct {
   185  	*configFactory
   186  	yamlText map[string][]string
   187  }
   188  
   189  func (c *configPlan) Copy() config.Plan {
   190  	yamlText := make(map[string][]string, len(c.yamlText))
   191  	for k, v := range c.yamlText {
   192  		yamlText[k] = append([]string{}, v...)
   193  	}
   194  	return &configPlan{
   195  		configFactory: c.configFactory,
   196  		yamlText:      yamlText,
   197  	}
   198  }
   199  
   200  func (c *configPlan) YAML(ns string, yamlText ...string) config.Plan {
   201  	c.yamlText[ns] = append(c.yamlText[ns], splitYAML(yamlText...)...)
   202  	return c
   203  }
   204  
   205  func splitYAML(yamlText ...string) []string {
   206  	var out []string
   207  	for _, doc := range yamlText {
   208  		out = append(out, yml.SplitString(doc)...)
   209  	}
   210  	return out
   211  }
   212  
   213  func (c *configPlan) File(ns string, paths ...string) config.Plan {
   214  	yamlText, err := file.AsStringArray(paths...)
   215  	if err != nil {
   216  		panic(err)
   217  	}
   218  
   219  	return c.YAML(ns, yamlText...)
   220  }
   221  
   222  func (c *configPlan) Eval(ns string, args any, templates ...string) config.Plan {
   223  	return c.YAML(ns, tmpl.MustEvaluateAll(args, templates...)...)
   224  }
   225  
   226  func (c *configPlan) EvalFile(ns string, args any, paths ...string) config.Plan {
   227  	templates, err := file.AsStringArray(paths...)
   228  	if err != nil {
   229  		panic(err)
   230  	}
   231  
   232  	return c.Eval(ns, args, templates...)
   233  }
   234  
   235  func (c *configPlan) Apply(opts ...apply.Option) error {
   236  	// Apply the options.
   237  	options := apply.Options{}
   238  	for _, o := range opts {
   239  		o.Set(&options)
   240  	}
   241  
   242  	// Apply for each namespace concurrently.
   243  	g, _ := errgroup.WithContext(context.TODO())
   244  	for ns, y := range c.yamlText {
   245  		ns, y := ns, y
   246  		g.Go(func() error {
   247  			return c.applyYAML(options.Cleanup, ns, y...)
   248  		})
   249  	}
   250  
   251  	// Wait for all each apply to complete.
   252  	if err := g.Wait(); err != nil {
   253  		return err
   254  	}
   255  
   256  	if options.Wait {
   257  		// TODO: wait for each namespace concurrently once WaitForConfig supports concurrency.
   258  		for ns, y := range c.yamlText {
   259  			if err := c.WaitForConfig(c.ctx, ns, y...); err != nil {
   260  				// TODO(https://github.com/istio/istio/issues/37148) fail hard in this case
   261  				scopes.Framework.Warnf("(Ignored until https://github.com/istio/istio/issues/37148 is fixed) "+
   262  					"failed waiting for YAML %v: %v", y, err)
   263  			}
   264  		}
   265  	}
   266  	return nil
   267  }
   268  
   269  func (c *configPlan) ApplyOrFail(t test.Failer, opts ...apply.Option) {
   270  	t.Helper()
   271  	if err := c.Apply(opts...); err != nil {
   272  		t.Fatal(err)
   273  	}
   274  }
   275  
   276  func (c *configPlan) Delete() error {
   277  	// Delete for each namespace concurrently.
   278  	g, _ := errgroup.WithContext(context.TODO())
   279  	for ns, y := range c.yamlText {
   280  		ns, y := ns, y
   281  		g.Go(func() error {
   282  			return c.deleteYAML(ns, y...)
   283  		})
   284  	}
   285  
   286  	// Wait for all each delete to complete.
   287  	return g.Wait()
   288  }
   289  
   290  func (c *configPlan) DeleteOrFail(t test.Failer) {
   291  	t.Helper()
   292  	if err := c.Delete(); err != nil {
   293  		t.Fatal(err)
   294  	}
   295  }