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 }