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  }