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  }