package yaml import ( "bytes" "fmt" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperutils" "gopkg.in/yaml.v2" "io" "os" "reflect" "regexp" "strings" ) type fUtils interface { FileRead(name string) ([]byte, error) FileWrite(name string, data []byte, mode os.FileMode) error } var _fileUtils fUtils var _stat = os.Stat var _traverse = traverse // Substitute ... func Substitute(ymlFile string, replacements map[string]interface{}, replacementsFiles []string) (bool, error) { if _fileUtils == nil { _fileUtils = piperutils.Files{} } bIn, err := _fileUtils.FileRead(ymlFile) if err != nil { return false, err } inDecoder := yaml.NewDecoder(bytes.NewReader(bIn)) buf := new(bytes.Buffer) outEncoder := yaml.NewEncoder(buf) var updated bool mergedReplacements, err := getReplacements(replacements, replacementsFiles) if err != nil { return false, err } for { mIn := make(map[string]interface{}) decodeErr := inDecoder.Decode(&mIn) if decodeErr != nil { if decodeErr == io.EOF { break } return false, decodeErr } if err != nil { return false, err } out, _updated, err := _traverse(mIn, mergedReplacements) if err != nil { return false, err } updated = _updated || updated err = outEncoder.Encode(out) } if updated { fInfo, err := _stat(ymlFile) if err != nil { return false, err } err = _fileUtils.FileWrite(ymlFile, buf.Bytes(), fInfo.Mode()) if err != nil { return false, err } } return updated, nil } func traverse(node interface{}, replacements map[string]interface{}) (interface{}, bool, error) { switch t := node.(type) { case string: return handleString(t, replacements) case bool: return t, false, nil case int: return t, false, nil case map[string]interface{}: return handleMap(t, replacements) case map[interface{}]interface{}: m, err := keysToString(t) if err != nil { return nil, false, err } return handleMap(m, replacements) case []interface{}: return handleSlice(t, replacements) default: return nil, false, fmt.Errorf("Unkown type received: '%v' (%v)", reflect.TypeOf(node), node) } } func keysToString(m map[interface{}]interface{}) (map[string]interface{}, error) { result := map[string]interface{}{} for key, val := range m { if k, ok := key.(string); ok { result[k] = val } else { return nil, fmt.Errorf("Cannot downcast'%v' to string. Type: %v)", reflect.TypeOf(key), key) } } return result, nil } func handleString(value string, replacements map[string]interface{}) (interface{}, bool, error) { trimmed := strings.TrimSpace(value) re := regexp.MustCompile(`\(\(.*?\)\)`) matches := re.FindAllSubmatch([]byte(trimmed), -1) fullMatch := isFullMatch(trimmed, matches) if fullMatch { log.Entry().Infof("FullMatchFound: %v", value) parameterName := getParameterName(matches[0][0]) parameterValue := getParameterValue(parameterName, replacements) if parameterValue == nil { return nil, false, fmt.Errorf("No value available for parameters '%s', replacements: %v", parameterName, replacements) } log.Entry().Infof("FullMatchFound: '%s', replacing with '%v'", parameterName, parameterValue) return parameterValue, true, nil } // we have to scan for multiple variables // we return always a string updated := false for i, match := range matches { parameterName := getParameterName(match[0]) log.Entry().Infof("XPartial match found: (%d) %v, %v", i, parameterName, value) parameterValue := getParameterValue(parameterName, replacements) if parameterValue == nil { return nil, false, fmt.Errorf("No value available for parameter '%s', replacements: %v", parameterName, replacements) } var conversion string switch t := parameterValue.(type) { case string: conversion = "%s" case bool: conversion = "%t" case int: conversion = "%d" case float64: conversion = "%g" // exponent as need, only required digits default: return nil, false, fmt.Errorf("Unsupported datatype found during travseral of yaml file: '%v', type: '%v'", parameterValue, reflect.TypeOf(t)) } valueAsString := fmt.Sprintf(conversion, parameterValue) log.Entry().Infof("Value as String: %v: '%v'", parameterName, valueAsString) value = strings.Replace(value, "(("+parameterName+"))", valueAsString, -1) updated = true log.Entry().Infof("PartialMatchFound (%d): '%v', replaced with : '%s'", i, parameterName, valueAsString) } return value, updated, nil } func getParameterName(b []byte) string { pName := string(b) log.Entry().Infof("ParameterName is: '%s'", pName) return strings.Replace(strings.Replace(string(b), "((", "", 1), "))", "", 1) } func getParameterValue(name string, replacements map[string]interface{}) interface{} { r := replacements[name] log.Entry().Infof("Value '%v' resolved for parameter '%s'", r, name) return r } func isFullMatch(value string, matches [][][]byte) bool { return strings.HasPrefix(value, "((") && strings.HasSuffix(value, "))") && len(matches) == 1 && len(matches[0]) == 1 } func handleSlice(t []interface{}, replacements map[string]interface{}) ([]interface{}, bool, error) { tNode := make([]interface{}, 0) updated := false for _, e := range t { if val, _updated, err := traverse(e, replacements); err == nil { updated = updated || _updated tNode = append(tNode, val) } else { return nil, false, err } } return tNode, updated, nil } func handleMap(t map[string]interface{}, replacements map[string]interface{}) (map[string]interface{}, bool, error) { tNode := make(map[string]interface{}) updated := false for key, value := range t { if val, _updated, err := traverse(value, replacements); err == nil { updated = updated || _updated tNode[key] = val } else { return nil, false, err } } return tNode, updated, nil } func getReplacements(replacements map[string]interface{}, replacementsFiles []string) (map[string]interface{}, error) { mReplacements := make(map[string]interface{}) for _, replacementsFile := range replacementsFiles { bReplacements, err := _fileUtils.FileRead(replacementsFile) if err != nil { return nil, err } replacementsDecoder := yaml.NewDecoder(bytes.NewReader(bReplacements)) for { decodeErr := replacementsDecoder.Decode(&mReplacements) if decodeErr != nil { if decodeErr == io.EOF { break } return nil, decodeErr } } } // the parameters from the map has a higher precedence, // hence we merge after resolving parameters from the files for k, v := range replacements { mReplacements[k] = v } return mReplacements, nil }