github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/yaml/yamlUtil.go (about)

     1  package yaml
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"github.com/SAP/jenkins-library/pkg/log"
     7  	"github.com/SAP/jenkins-library/pkg/piperutils"
     8  	"gopkg.in/yaml.v2"
     9  	"io"
    10  	"os"
    11  	"reflect"
    12  	"regexp"
    13  	"strings"
    14  )
    15  
    16  type fUtils interface {
    17  	FileRead(name string) ([]byte, error)
    18  	FileWrite(name string, data []byte, mode os.FileMode) error
    19  }
    20  
    21  var _fileUtils fUtils
    22  
    23  var _stat = os.Stat
    24  var _traverse = traverse
    25  
    26  // Substitute ...
    27  func Substitute(ymlFile string, replacements map[string]interface{}, replacementsFiles []string) (bool, error) {
    28  
    29  	if _fileUtils == nil {
    30  		_fileUtils = piperutils.Files{}
    31  	}
    32  
    33  	bIn, err := _fileUtils.FileRead(ymlFile)
    34  	if err != nil {
    35  		return false, err
    36  	}
    37  
    38  	inDecoder := yaml.NewDecoder(bytes.NewReader(bIn))
    39  
    40  	buf := new(bytes.Buffer)
    41  	outEncoder := yaml.NewEncoder(buf)
    42  
    43  	var updated bool
    44  
    45  	mergedReplacements, err := getReplacements(replacements, replacementsFiles)
    46  	if err != nil {
    47  		return false, err
    48  	}
    49  
    50  	for {
    51  
    52  		mIn := make(map[string]interface{})
    53  
    54  		decodeErr := inDecoder.Decode(&mIn)
    55  
    56  		if decodeErr != nil {
    57  			if decodeErr == io.EOF {
    58  				break
    59  			}
    60  			return false, decodeErr
    61  		}
    62  
    63  		if err != nil {
    64  			return false, err
    65  		}
    66  
    67  		out, _updated, err := _traverse(mIn, mergedReplacements)
    68  		if err != nil {
    69  			return false, err
    70  		}
    71  
    72  		updated = _updated || updated
    73  
    74  		err = outEncoder.Encode(out)
    75  	}
    76  
    77  	if updated {
    78  
    79  		fInfo, err := _stat(ymlFile)
    80  		if err != nil {
    81  			return false, err
    82  		}
    83  
    84  		err = _fileUtils.FileWrite(ymlFile, buf.Bytes(), fInfo.Mode())
    85  		if err != nil {
    86  			return false, err
    87  		}
    88  	}
    89  
    90  	return updated, nil
    91  }
    92  
    93  func traverse(node interface{}, replacements map[string]interface{}) (interface{}, bool, error) {
    94  	switch t := node.(type) {
    95  	case string:
    96  		return handleString(t, replacements)
    97  	case bool:
    98  		return t, false, nil
    99  	case int:
   100  		return t, false, nil
   101  	case map[string]interface{}:
   102  		return handleMap(t, replacements)
   103  	case map[interface{}]interface{}:
   104  		m, err := keysToString(t)
   105  		if err != nil {
   106  			return nil, false, err
   107  		}
   108  		return handleMap(m, replacements)
   109  	case []interface{}:
   110  		return handleSlice(t, replacements)
   111  	default:
   112  		return nil, false, fmt.Errorf("Unknown type received: '%v' (%v)", reflect.TypeOf(node), node)
   113  	}
   114  }
   115  
   116  func keysToString(m map[interface{}]interface{}) (map[string]interface{}, error) {
   117  	result := map[string]interface{}{}
   118  	for key, val := range m {
   119  		if k, ok := key.(string); ok {
   120  			result[k] = val
   121  		} else {
   122  			return nil, fmt.Errorf("Cannot downcast'%v' to string. Type: %v)", reflect.TypeOf(key), key)
   123  		}
   124  	}
   125  	return result, nil
   126  }
   127  
   128  func handleString(value string, replacements map[string]interface{}) (interface{}, bool, error) {
   129  
   130  	trimmed := strings.TrimSpace(value)
   131  	re := regexp.MustCompile(`\(\(.*?\)\)`)
   132  	matches := re.FindAllSubmatch([]byte(trimmed), -1)
   133  	fullMatch := isFullMatch(trimmed, matches)
   134  	if fullMatch {
   135  		log.Entry().Infof("FullMatchFound: %v", value)
   136  		parameterName := getParameterName(matches[0][0])
   137  		parameterValue := getParameterValue(parameterName, replacements)
   138  		if parameterValue == nil {
   139  			return nil, false, fmt.Errorf("No value available for parameters '%s', replacements: %v", parameterName, replacements)
   140  		}
   141  		log.Entry().Infof("FullMatchFound: '%s', replacing with '%v'", parameterName, parameterValue)
   142  		return parameterValue, true, nil
   143  	}
   144  	// we have to scan for multiple variables
   145  	// we return always a string
   146  	updated := false
   147  	for i, match := range matches {
   148  		parameterName := getParameterName(match[0])
   149  		log.Entry().Infof("XPartial match found: (%d) %v, %v", i, parameterName, value)
   150  		parameterValue := getParameterValue(parameterName, replacements)
   151  		if parameterValue == nil {
   152  			return nil, false, fmt.Errorf("No value available for parameter '%s', replacements: %v", parameterName, replacements)
   153  		}
   154  
   155  		var conversion string
   156  		switch t := parameterValue.(type) {
   157  		case string:
   158  			conversion = "%s"
   159  		case bool:
   160  			conversion = "%t"
   161  		case int:
   162  			conversion = "%d"
   163  		case float64:
   164  			conversion = "%g" // exponent as need, only required digits
   165  		default:
   166  			return nil, false, fmt.Errorf("Unsupported datatype found during travseral of yaml file: '%v', type: '%v'", parameterValue, reflect.TypeOf(t))
   167  		}
   168  		valueAsString := fmt.Sprintf(conversion, parameterValue)
   169  		log.Entry().Infof("Value as String: %v: '%v'", parameterName, valueAsString)
   170  		value = strings.Replace(value, "(("+parameterName+"))", valueAsString, -1)
   171  		updated = true
   172  		log.Entry().Infof("PartialMatchFound (%d): '%v', replaced with : '%s'", i, parameterName, valueAsString)
   173  	}
   174  
   175  	return value, updated, nil
   176  }
   177  
   178  func getParameterName(b []byte) string {
   179  	pName := string(b)
   180  	log.Entry().Infof("ParameterName is: '%s'", pName)
   181  	return strings.Replace(strings.Replace(string(b), "((", "", 1), "))", "", 1)
   182  }
   183  
   184  func getParameterValue(name string, replacements map[string]interface{}) interface{} {
   185  
   186  	r := replacements[name]
   187  	log.Entry().Infof("Value '%v' resolved for parameter '%s'", r, name)
   188  	return r
   189  }
   190  
   191  func isFullMatch(value string, matches [][][]byte) bool {
   192  	return strings.HasPrefix(value, "((") && strings.HasSuffix(value, "))") && len(matches) == 1 && len(matches[0]) == 1
   193  }
   194  
   195  func handleSlice(t []interface{}, replacements map[string]interface{}) ([]interface{}, bool, error) {
   196  	tNode := make([]interface{}, 0)
   197  	updated := false
   198  	for _, e := range t {
   199  		if val, _updated, err := traverse(e, replacements); err == nil {
   200  			updated = updated || _updated
   201  			tNode = append(tNode, val)
   202  
   203  		} else {
   204  			return nil, false, err
   205  		}
   206  	}
   207  	return tNode, updated, nil
   208  }
   209  
   210  func handleMap(t map[string]interface{}, replacements map[string]interface{}) (map[string]interface{}, bool, error) {
   211  	tNode := make(map[string]interface{})
   212  	updated := false
   213  	for key, value := range t {
   214  		if val, _updated, err := traverse(value, replacements); err == nil {
   215  			updated = updated || _updated
   216  			tNode[key] = val
   217  		} else {
   218  			return nil, false, err
   219  		}
   220  	}
   221  	return tNode, updated, nil
   222  }
   223  
   224  func getReplacements(replacements map[string]interface{}, replacementsFiles []string) (map[string]interface{}, error) {
   225  
   226  	mReplacements := make(map[string]interface{})
   227  
   228  	for _, replacementsFile := range replacementsFiles {
   229  		bReplacements, err := _fileUtils.FileRead(replacementsFile)
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  
   234  		replacementsDecoder := yaml.NewDecoder(bytes.NewReader(bReplacements))
   235  
   236  		for {
   237  			decodeErr := replacementsDecoder.Decode(&mReplacements)
   238  
   239  			if decodeErr != nil {
   240  				if decodeErr == io.EOF {
   241  					break
   242  				}
   243  				return nil, decodeErr
   244  			}
   245  		}
   246  	}
   247  
   248  	// the parameters from the map has a higher precedence,
   249  	// hence we merge after resolving parameters from the files
   250  	for k, v := range replacements {
   251  		mReplacements[k] = v
   252  	}
   253  	return mReplacements, nil
   254  }