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 }