github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/src/com/sap/piper/variablesubstitution/YamlUtils.groovy (about) 1 package com.sap.piper.variablesubstitution 2 3 import hudson.AbortException 4 5 /** 6 * A utility class for Yaml data. 7 * Deals with the substitution of variables within Yaml objects. 8 */ 9 class YamlUtils implements Serializable { 10 11 private final DebugHelper logger 12 private final Script script 13 14 /** 15 * Creates a new utils instance for the given script. 16 * @param script - the script which will be used to call pipeline steps. 17 * @param logger - an optional debug helper to print debug messages. 18 */ 19 YamlUtils(Script script, DebugHelper logger = null) { 20 if(!script) { 21 throw new IllegalArgumentException("[YamlUtils] Script must not be null.") 22 } 23 this.script = script 24 this.logger = logger 25 } 26 27 /** 28 * Substitutes variables references in a given input Yaml object with values that are read from the 29 * passed variables Yaml object. Variables may be of primitive or complex types. 30 * The format of variable references follows [Cloud Foundry standards](https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html#variable-substitution) 31 * 32 * @param inputYaml - the input Yaml data as `Object`. Can be either of type `Map` or `List`. 33 * @param variablesYaml - the variables Yaml data as `Object`. Can be either of type `Map` or `List` and should 34 * contain variables names and values to replace variable references contained in `inputYaml`. 35 * @param context - an `ExecutionContext` that can be used to query whether the script actually replaced any variables. 36 * @return the YAML object graph of substituted data. 37 */ 38 Object substituteVariables(Object inputYaml, Object variablesYaml, ExecutionContext context = null) { 39 if (!inputYaml) { 40 throw new IllegalArgumentException("[YamlUtils] Input Yaml data must not be null or empty.") 41 } 42 43 if (!variablesYaml) { 44 throw new IllegalArgumentException("[YamlUtils] Variables Yaml data must not be null or empty.") 45 } 46 47 return substitute(inputYaml, variablesYaml, context) 48 } 49 50 /** 51 * Recursively substitutes all variables inside the object tree of the manifest YAML. 52 * @param manifestNode - the manifest YAML to replace variables in. 53 * @param variablesData - the variables values. 54 * @param context - an execution context that can be used to query if any variables were replaced. 55 * @return a YAML object graph which has all variables replaced. 56 */ 57 private Object substitute(Object manifestNode, Object variablesData, ExecutionContext context) { 58 Map<String, Object> variableSubstitutes = getVariableSubstitutes(variablesData) 59 60 if (containsVariableReferences(manifestNode)) { 61 62 Object complexResult = null 63 String stringNode = manifestNode as String 64 Map<String, String> referencedVariables = getReferencedVariables(stringNode) 65 referencedVariables.each { referencedVariable -> 66 String referenceToReplace = referencedVariable.getKey() 67 String referenceName = referencedVariable.getValue() 68 Object substitute = variableSubstitutes.get(referenceName) 69 70 if (null == substitute) { 71 logger?.debug("[YamlUtils] WARNING - Found variable reference ${referenceToReplace} in input Yaml but no variable value to replace it with Leaving it unresolved. Check your variables Yaml data and make sure the variable is properly declared.") 72 return manifestNode 73 } 74 75 script.echo "[YamlUtils] Replacing: ${referenceToReplace} with ${substitute}" 76 77 if(isSingleVariableReference(stringNode)) { 78 logger?.debug("[YamlUtils] Node ${stringNode} is SINGLE variable reference. Substitute type is: ${substitute.getClass().getName()}") 79 // if the string node we need to do replacements for is 80 // a reference to a single variable, i.e. should be replaced 81 // entirely with the variable value, we replace the entire node 82 // with the variable's value (which can possibly be a complex type). 83 complexResult = substitute 84 } 85 else { 86 logger?.debug("[YamlUtils] Node ${stringNode} is multi-variable reference or contains additional string constants. Substitute type is: ${substitute.getClass().getName()}") 87 // if the string node we need to do replacements for contains various 88 // variable references or a variable reference and constant string additions 89 // we do a string replacement of the variables inside the node. 90 String regex = "\\(\\(${referenceName}\\)\\)" 91 stringNode = stringNode.replaceAll(regex, substitute as String) 92 } 93 } 94 95 if (context) { 96 context.variablesReplaced = true // remember that variables were found in the YAML file that have been replaced. 97 } 98 99 return (complexResult != null) ? complexResult : stringNode 100 } 101 else if (manifestNode instanceof List) { 102 List<Object> listNode = manifestNode as List<Object> 103 // This copy is only necessary, since Jenkins executes Groovy using 104 // CPS (https://wiki.jenkins.io/display/JENKINS/Pipeline+CPS+method+mismatches) 105 // and has issues with closures in Java 8 lambda expressions. Otherwise we could replace 106 // entries of the list in place (using replaceAll(lambdaExpression)) 107 List<Object> copy = new ArrayList<>() 108 listNode.each { entry -> 109 copy.add(substitute(entry, variableSubstitutes, context)) 110 } 111 return copy 112 } 113 else if(manifestNode instanceof Map) { 114 Map<String, Object> mapNode = manifestNode as Map<String, Object> 115 // This copy is only necessary to avoid immutability errors reported by Jenkins 116 // runtime environment. 117 Map<String, Object> copy = new HashMap<>() 118 mapNode.entrySet().each { entry -> 119 copy.put(entry.getKey(), substitute(entry.getValue(), variableSubstitutes, context)) 120 } 121 return copy 122 } 123 else { 124 logger?.debug("[YamlUtils] Found data type ${manifestNode.getClass().getName()} that needs no substitute. Value: ${manifestNode}") 125 return manifestNode 126 } 127 } 128 129 /** 130 * Turns the parsed variables Yaml data into a 131 * single map. Takes care of multiple YAML sections (separated by ---) if they are found and flattens them into a single 132 * map if necessary. 133 * @param variablesYamlData - the variables data as a Yaml object. 134 * @return the `Map` of variable names mapped to their substitute values. 135 */ 136 private Map<String, Object> getVariableSubstitutes(Object variablesYamlData) { 137 138 if(variablesYamlData instanceof List) { 139 return flattenVariablesFileData(variablesYamlData as List) 140 } 141 else if (variablesYamlData instanceof Map) { 142 return variablesYamlData as Map<String, Object> 143 } 144 else { 145 // should never happen (hopefully...) 146 throw new AbortException("[YamlUtils] Found unsupported data type of variables file after parsing YAML. Expected either List or Map. Got: ${variablesYamlData.getClass().getName()}.") 147 } 148 } 149 150 /** 151 * Flattens a list of Yaml sections (which are deemed to be key-value mappings of variable names and values) 152 * to a single map. In case multiple Yaml sections contain the same key, values will be overridden and the result 153 * will be undefined. 154 * @param variablesYamlData - the `List` of Yaml objects of the different sections. 155 * @return the `Map` of variable substitute mappings. 156 */ 157 private Map<String, Object> flattenVariablesFileData(List<Map<String, Object>> variablesYamlData) { 158 Map<String, Object> substitutes = new HashMap<>() 159 variablesYamlData.each { map -> 160 map.entrySet().each { entry -> 161 substitutes.put(entry.key, entry.value) 162 } 163 } 164 return substitutes 165 } 166 167 /** 168 * Returns true, if the given object node contains variable references. 169 * @param node - the object-typed value to check for variable references. 170 * @return `true`, if this node references at least one variable, `false` otherwise. 171 */ 172 private boolean containsVariableReferences(Object node) { 173 if(!(node instanceof String)) { 174 // variable references can only be contained in 175 // string nodes. 176 return false 177 } 178 String stringNode = node as String 179 return stringNode.contains("((") && stringNode.contains("))") 180 } 181 182 /** 183 * Returns true, if and only if the entire node passed in as a parameter 184 * is a variable reference. Returns false if the node references multiple 185 * variables or if the node embeds the variable reference inside of a constant 186 * string surrounding, e.g. `This-text-has-((numberOfWords))-words`. 187 * @param node - the node to check. 188 * @return `true` if the node is a single variable reference. `false` otherwise. 189 */ 190 private boolean isSingleVariableReference(String node) { 191 // regex matching only if the entire node is a reference. (^ = matches start of word, $ = matches end of word) 192 String regex = '^\\(\\([\\d\\w-]*\\)\\)$' // use single quote not to have to escape $ (interpolation) sign. 193 List<String> matches = node.findAll(regex) 194 return (matches != null && !matches.isEmpty()) 195 } 196 197 /** 198 * Returns a map of variable references (including braces) to plain variable names referenced in the given `String`. 199 * The keys of the map are the variable references, the values are the names of the referenced variables. 200 * @param value - the value to look for variable references in. 201 * @return the `Map` of names of referenced variables. 202 */ 203 private Map<String, String> getReferencedVariables(String value) { 204 Map<String, String> referencesNamesMap = new HashMap<>() 205 List<String> variableReferences = value.findAll("\\(\\([\\d\\w-]*\\)\\)") // find all variables in braces, e.g. ((my-var_05)) 206 207 variableReferences.each { reference -> 208 referencesNamesMap.put(reference, getPlainVariableName(reference)) 209 } 210 211 return referencesNamesMap 212 } 213 214 /** 215 * Expects a variable reference (including braces) as input and returns the plain name 216 * (by stripping braces) of the variable. E.g. input: `((my_var-04))`, output: `my_var-04` 217 * @param variableReference - the variable reference including braces. 218 * @return the plain variable name 219 */ 220 private String getPlainVariableName(String variableReference) { 221 String result = variableReference.replace("((", "") 222 result = result.replace("))", "") 223 return result 224 } 225 }