k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/config/template_provider.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package config 18 19 import ( 20 "bufio" 21 "bytes" 22 "fmt" 23 "io/fs" 24 "io/ioutil" 25 "os" 26 "reflect" 27 "regexp" 28 "strconv" 29 "strings" 30 "sync" 31 32 template "github.com/google/safetext/yamltemplate" 33 34 goerrors "github.com/go-errors/errors" 35 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 36 "k8s.io/perf-tests/clusterloader2/api" 37 "k8s.io/perf-tests/clusterloader2/pkg/errors" 38 ) 39 40 // TemplateProvider provides object templates. Templates in unstructured form 41 // are served by reading file from given path or by using cache if available. 42 type TemplateProvider struct { 43 fsys fs.FS 44 45 binLock sync.RWMutex 46 binCache map[string][]byte 47 48 templateLock sync.RWMutex 49 templateCache map[string]*template.Template 50 } 51 52 // NewTemplateProvider creates new template provider. 53 func NewTemplateProvider(fsys fs.FS) *TemplateProvider { 54 return &TemplateProvider{ 55 fsys: fsys, 56 binCache: make(map[string][]byte), 57 templateCache: make(map[string]*template.Template), 58 } 59 } 60 61 func (tp *TemplateProvider) getRaw(path string) ([]byte, error) { 62 tp.binLock.RLock() 63 bin, exists := tp.binCache[path] 64 tp.binLock.RUnlock() 65 if !exists { 66 tp.binLock.Lock() 67 defer tp.binLock.Unlock() 68 // Recheck condition. 69 bin, exists = tp.binCache[path] 70 if !exists { 71 var err error 72 bin, err = fs.ReadFile(tp.fsys, path) 73 if err != nil { 74 return []byte{}, fmt.Errorf("reading error: %v", err) 75 } 76 tp.binCache[path] = bin 77 } 78 } 79 return bin, nil 80 } 81 82 // RawToObject creates object from file specified by the given path 83 // or uses cached object if available. 84 func (tp *TemplateProvider) RawToObject(path string) (*unstructured.Unstructured, error) { 85 bin, err := tp.getRaw(path) 86 if err != nil { 87 return nil, err 88 } 89 // Removing all placeholder from template. 90 // This needs to be done due to placeholders not being valid yaml. 91 r, err := regexp.Compile("\\{\\{.*\\}\\}") 92 if err != nil { 93 return nil, fmt.Errorf("regexp creation error: %v", err) 94 } 95 bin = r.ReplaceAll(bin, []byte{}) 96 return convertToObject(bin) 97 } 98 99 func (tp *TemplateProvider) getRawTemplate(path string) (*template.Template, error) { 100 tp.templateLock.RLock() 101 raw, exists := tp.templateCache[path] 102 tp.templateLock.RUnlock() 103 if !exists { 104 tp.templateLock.Lock() 105 defer tp.templateLock.Unlock() 106 // Recheck condition. 107 raw, exists = tp.templateCache[path] 108 if !exists { 109 bin, err := tp.getRaw(path) 110 if err != nil { 111 return nil, err 112 } 113 raw = template.New("").Funcs(GetFuncs(tp.fsys)) 114 raw, err = raw.Parse(string(bin)) 115 if err != nil { 116 return nil, fmt.Errorf("parsing error: %v", err) 117 } 118 tp.templateCache[path] = raw 119 } 120 } 121 return raw, nil 122 } 123 124 func (tp *TemplateProvider) getMappedTemplate(path string, mapping map[string]interface{}) ([]byte, error) { 125 raw, err := tp.getRawTemplate(path) 126 if err != nil { 127 return []byte{}, err 128 } 129 var b bytes.Buffer 130 writer := bufio.NewWriter(&b) 131 if err := raw.Execute(writer, mapping); err != nil { 132 return []byte{}, fmt.Errorf("replacing placeholders error: %v", err) 133 } 134 if err := writer.Flush(); err != nil { 135 return []byte{}, fmt.Errorf("flush error: %v", err) 136 } 137 return b.Bytes(), nil 138 } 139 140 // TemplateToObject creates object from file specified by the given path 141 // or uses cached object if available. Template's placeholders are replaced based 142 // on provided mapping. 143 func (tp *TemplateProvider) TemplateToObject(path string, mapping map[string]interface{}) (*unstructured.Unstructured, error) { 144 b, err := tp.getMappedTemplate(path, mapping) 145 if err != nil { 146 return nil, err 147 } 148 return convertToObject(b) 149 } 150 151 // TemplateToConfig creates test config from file specified by the given path. 152 // Template's placeholders are replaced based on provided mapping. 153 func (tp *TemplateProvider) TemplateToConfig(path string, mapping map[string]interface{}) (*api.Config, error) { 154 c := &api.Config{} 155 if err := tp.TemplateInto(path, mapping, c); err != nil { 156 return nil, err 157 } 158 return c, nil 159 } 160 161 // TemplateInto decodes template specified by the given path into given structure. 162 func (tp *TemplateProvider) TemplateInto(path string, mapping map[string]interface{}, obj interface{}) error { 163 b, err := tp.getMappedTemplate(path, mapping) 164 if err != nil { 165 return err 166 } 167 return decodeInto(b, obj) 168 } 169 170 // LoadTestSuite creates test suite config from file specified by the given path. 171 func LoadTestSuite(path string) (api.TestSuite, error) { 172 bin, err := ioutil.ReadFile(path) 173 if err != nil { 174 return nil, fmt.Errorf("test suite reading error: %v", err) 175 } 176 var testSuite api.TestSuite 177 if err = decodeInto(bin, &testSuite); err != nil { 178 return nil, err 179 } 180 if err = validateTestSuite(testSuite); err != nil { 181 return nil, err 182 } 183 return testSuite, nil 184 } 185 186 func validateTestSuite(suite api.TestSuite) error { 187 for _, scenario := range suite { 188 // Scenario identifiers cannot contain underscores. This is because underscores 189 // are used as separators in artifact filenames. 190 if strings.Contains(scenario.Identifier, "_") { 191 return fmt.Errorf("scenario identifiers cannot contain underscores: %q", 192 scenario.Identifier) 193 } 194 } 195 return nil 196 } 197 198 func updateMappingFromFile(mapping map[string]interface{}, path string) error { 199 bin, err := ioutil.ReadFile(path) 200 if err != nil { 201 return fmt.Errorf("test overrides reading error: %v", err) 202 } 203 tmpMapping := make(map[string]interface{}) 204 if err = decodeInto(bin, &tmpMapping); err != nil { 205 return fmt.Errorf("test overrides unmarshalling error: %v", err) 206 } 207 // Merge tmpMapping into mapping. 208 for k, v := range tmpMapping { 209 mapping[k] = v 210 } 211 return nil 212 } 213 214 // LoadTestOverrides returns mapping from file specified by the given paths. 215 // Test specific overrides in testOverridePath will supersede any global overrides. 216 func LoadTestOverrides(paths []string, testOverridePaths []string) (map[string]interface{}, error) { 217 var err error 218 mapping := make(map[string]interface{}) 219 for _, path := range paths { 220 err = updateMappingFromFile(mapping, path) 221 if err != nil { 222 return nil, err 223 } 224 } 225 for _, testOverridePath := range testOverridePaths { 226 err = updateMappingFromFile(mapping, testOverridePath) 227 if err != nil { 228 return nil, err 229 } 230 } 231 return mapping, nil 232 } 233 234 // GetMapping returns template variable mapping for the given ClusterLoaderConfig. 235 func GetMapping(clusterLoaderConfig *ClusterLoaderConfig, testOverridePaths []string) (map[string]interface{}, *errors.ErrorList) { 236 mapping, err := LoadTestOverrides(clusterLoaderConfig.OverridePaths, testOverridePaths) 237 if err != nil { 238 return nil, errors.NewErrorList(fmt.Errorf("mapping creation error: %v", err)) 239 } 240 mapping["Nodes"] = clusterLoaderConfig.ClusterConfig.Nodes 241 envMapping, err := LoadCL2Envs() 242 if err != nil { 243 return nil, errors.NewErrorList(goerrors.Errorf("mapping creation error: %v", err)) 244 } 245 err = MergeMappings(mapping, envMapping) 246 if err != nil { 247 return nil, errors.NewErrorList(goerrors.Errorf("mapping merging error: %v", err)) 248 } 249 return mapping, nil 250 } 251 252 // LoadCL2Envs returns mapping from the envs starting with CL2_ prefix. 253 func LoadCL2Envs() (map[string]interface{}, error) { 254 mapping := make(map[string]interface{}) 255 for _, keyValue := range os.Environ() { 256 if !strings.HasPrefix(keyValue, "CL2_") { 257 continue 258 } 259 split := strings.SplitN(keyValue, "=", 2) 260 if len(split) != 2 { 261 return nil, goerrors.Errorf("unparsable string in os.Eviron(): %v", keyValue) 262 } 263 key, value := split[0], split[1] 264 mapping[key] = unpackStringValue(value) 265 } 266 return mapping, nil 267 } 268 269 func unpackStringValue(str string) interface{} { 270 if v, err := strconv.ParseInt(str, 10, 64); err == nil { 271 return v 272 } 273 if v, err := strconv.ParseFloat(str, 64); err == nil { 274 return v 275 } 276 if v, err := strconv.ParseBool(str); err == nil { 277 return v 278 } 279 return str 280 } 281 282 // MergeMappings modifies map b to contain all new key=value pairs from b. 283 // It will return error in case of conflict, i.e. if exists key k for which a[k] != b[k] 284 func MergeMappings(a, b map[string]interface{}) error { 285 for k, bv := range b { 286 av, ok := a[k] 287 if !ok { 288 a[k] = bv 289 continue 290 } 291 if !reflect.DeepEqual(av, bv) { 292 return goerrors.Errorf("merge conflict for key '%v': old value=%v (%T), new value=%v (%T)", k, av, av, bv, bv) 293 } 294 } 295 return nil 296 }