istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/config/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 config 16 17 import ( 18 "flag" 19 "fmt" 20 "os" 21 "strings" 22 23 "go.uber.org/atomic" 24 "gopkg.in/yaml.v3" 25 26 "istio.io/istio/pkg/test/scopes" 27 "istio.io/istio/pkg/test/util/file" 28 ) 29 30 const prefix = "istio.test" 31 32 var ( 33 configFilePath string 34 parsed atomic.Bool 35 ) 36 37 func init() { 38 flag.StringVar(&configFilePath, "istio.test.config", "", "Path to test framework config file") 39 } 40 41 type Value interface { 42 flag.Value 43 // SetConfig will receive either a Map or a []Map 44 SetConfig(any) error 45 } 46 47 func Parsed() bool { 48 return flag.Parsed() && parsed.Load() 49 } 50 51 // Parse overrides any unset command line flags beginning with "istio.test" with values provided 52 // from a YAML file. 53 func Parse() { 54 defer func() { 55 parsed.Store(true) 56 }() 57 if !flag.Parsed() { 58 flag.Parse() 59 } 60 if configFilePath == "" { 61 return 62 } 63 64 cfg, err := readConfig() 65 if err != nil { 66 scopes.Framework.Error(err) 67 return 68 } 69 set := map[string]struct{}{} 70 flag.Visit(func(f *flag.Flag) { 71 set[f.Name] = struct{}{} 72 }) 73 74 flag.VisitAll(func(f *flag.Flag) { 75 var err error 76 defer func() { 77 if err != nil { 78 scopes.Framework.Errorf("failed getting %s from config file: %v", f.Name, err) 79 } 80 }() 81 82 // exclude non-istio flags and flags that were set via command line 83 if !strings.HasPrefix(f.Name, prefix) { 84 return 85 } 86 if _, ok := set[f.Name]; ok { 87 return 88 } 89 90 // grab the map containing the last "." separated key 91 keys := strings.Split(f.Name, ".") 92 parentPath, key := keys[:len(keys)-1], keys[len(keys)-1] 93 parent := cfg 94 for _, k := range parentPath { 95 parent = parent.Map(k) 96 if parent == nil { 97 return 98 } 99 } 100 101 // if the registered flag implements config.Value, and is a non-string type, we can do fancy custom parsing 102 cfgValue, isCfgVal := f.Value.(Value) 103 if cfgMap := parent.Map(key); isCfgVal && len(cfgMap) > 0 { 104 err = cfgValue.SetConfig(cfgMap) 105 } else if cfgSlice := parent.Slice(key); isCfgVal && len(cfgSlice) > 0 { 106 err = cfgValue.SetConfig(cfgSlice) 107 } else if v := parent.String(key); v != "" { 108 // otherwise parse via string (if-set) 109 err = f.Value.Set(v) 110 } 111 }) 112 } 113 114 func readConfig() (Map, error) { 115 path, err := file.NormalizePath(configFilePath) 116 if err != nil { 117 return nil, fmt.Errorf("failed normalizing config file path %q: %v", configFilePath, err) 118 } 119 bytes, err := os.ReadFile(path) 120 if err != nil { 121 return nil, fmt.Errorf("failed reading %s: %v", path, err) 122 } 123 cfg := Map{} 124 if err := yaml.Unmarshal(bytes, cfg); err != nil { 125 return nil, fmt.Errorf("failed unmarshalling %s: %v", path, err) 126 } 127 return cfg, nil 128 }