1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00
sap-jenkins-library/pkg/yaml/yamlUtil.go
Marcus Holl 23fe4dcdcd
handle map[interface]interface{} in yaml utils substitute (#1725)
reality teaches us that we need to handle that type
2020-06-29 08:11:05 +02:00

255 lines
6.5 KiB
Go

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
}