github.com/jaylevin/jenkins-library@v1.230.4/vars/cfManifestSubstituteVariables.groovy (about)

     1  import com.sap.piper.ConfigurationHelper
     2  import com.sap.piper.GenerateDocumentation
     3  import com.sap.piper.variablesubstitution.ExecutionContext
     4  import com.sap.piper.variablesubstitution.DebugHelper
     5  import com.sap.piper.variablesubstitution.YamlUtils
     6  import groovy.transform.Field
     7  
     8  import static com.sap.piper.Prerequisites.checkScript
     9  
    10  @Field String STEP_NAME = getClass().getName()
    11  @Field Set GENERAL_CONFIG_KEYS = []
    12  @Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS + [
    13      /**
    14       * The `String` path of the Yaml file to replace variables in.
    15       * Defaults to "manifest.yml" if not specified otherwise.
    16       */
    17      'manifestFile',
    18      /**
    19       * The `String` path of the Yaml file to produce as output.
    20       * If not specified this will default to `manifestFile` and overwrite it.
    21       */
    22      'outputManifestFile',
    23      /**
    24       * The `List` of `String` paths of the Yaml files containing the variable values to use as a replacement in the manifest file.
    25       * Defaults to `["manifest-variables.yml"]` if not specified otherwise. The order of the files given in the list is relevant
    26       * in case there are conflicting variable names and values within variable files. In such a case, the values of the last file win.
    27       */
    28      'manifestVariablesFiles',
    29      /**
    30       * A `List` of `Map` entries for key-value pairs used for variable substitution within the file given by `manifestFile`.
    31       * Defaults to an empty list, if not specified otherwise. This can be used to set variables like it is provided
    32       * by `cf push --var key=value`.
    33       *
    34       * The order of the maps of variables given in the list is relevant in case there are conflicting variable names and values
    35       * between maps contained within the list. In case of conflicts, the last specified map in the list will win.
    36       *
    37       * Though each map entry in the list can contain more than one key-value pair for variable substitution, it is recommended
    38       * to stick to one entry per map, and rather declare more maps within the list. The reason is that
    39       * if a map in the list contains more than one key-value entry, and the entries are conflicting, the
    40       * conflict resolution behavior is undefined (since map entries have no sequence).
    41       *
    42       * Note: variables defined via `manifestVariables` always win over conflicting variables defined via any file given
    43       * by `manifestVariablesFiles` - no matter what is declared before. This reproduces the same behavior as can be
    44       * observed when using `cf push --var` in combination with `cf push --vars-file`.
    45       */
    46      'manifestVariables'
    47  ]
    48  
    49  @Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
    50  
    51  /**
    52   * Step to substitute variables in a given YAML file with those specified in one or more variables files given by the
    53   * `manifestVariablesFiles` parameter. This follows the behavior of `cf push --vars-file`, and can be
    54   * used as a pre-deployment step if commands other than `cf push` are used for deployment (e.g. `cf blue-green-deploy`).
    55   *
    56   * The format to reference a variable in the manifest YAML file is to use double parentheses `((` and `))`, e.g. `((variableName))`.
    57   *
    58   * You can declare variable assignments as key value-pairs inside a YAML variables file following the
    59   * [Cloud Foundry standards](https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html#variable-substitution) format.
    60   *
    61   * Optionally, you can also specify a direct list of key-value mappings for variables using the `manifestVariables` parameter.
    62   * Variables given in the `manifestVariables` list will take precedence over those found in variables files. This follows
    63   * the behavior of `cf push --var`, and works in combination with `manifestVariablesFiles`.
    64   *
    65   * The step is activated by the presence of the file specified by the `manifestFile` parameter and all variables files
    66   * specified by the `manifestVariablesFiles` parameter, or if variables are passed in directly via `manifestVariables`.
    67   *
    68   * In case no `manifestVariablesFiles` were explicitly specified, a default named `manifest-variables.yml` will be looked
    69   * for and if present will activate this step also. This is to support convention over configuration.
    70   */
    71  @GenerateDocumentation
    72  void call(Map arguments = [:]) {
    73      handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: arguments) {
    74          def script = checkScript(this, arguments)  ?: this
    75          String stageName = arguments.stageName ?: env.STAGE_NAME
    76  
    77          // load default & individual configuration
    78          Map config = ConfigurationHelper.newInstance(this)
    79              .loadStepDefaults([:], stageName)
    80              .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
    81              .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS)
    82              .mixin(arguments, PARAMETER_KEYS)
    83              .use()
    84  
    85          String defaultManifestFileName = "manifest.yml"
    86          String defaultManifestVariablesFileName = "manifest-variables.yml"
    87  
    88          Boolean manifestVariablesFilesExplicitlySpecified = config.manifestVariablesFiles != null
    89  
    90          String manifestFilePath = config.manifestFile ?: defaultManifestFileName
    91          List<String> manifestVariablesFiles = (config.manifestVariablesFiles != null) ? config.manifestVariablesFiles : [ defaultManifestVariablesFileName ]
    92          List<Map<String, Object>> manifestVariablesList = config.manifestVariables ?: []
    93          String outputFilePath = config.outputManifestFile ?: manifestFilePath
    94  
    95          DebugHelper debugHelper = new DebugHelper(script, config)
    96          YamlUtils yamlUtils = new YamlUtils(script, debugHelper)
    97  
    98          Boolean manifestExists = fileExists manifestFilePath
    99          Boolean manifestVariablesFilesExist = allManifestVariableFilesExist(manifestVariablesFiles)
   100          Boolean manifestVariablesListSpecified = !manifestVariablesList.isEmpty()
   101  
   102          if (!manifestExists) {
   103              echo "[CFManifestSubstituteVariables] Could not find YAML file at ${manifestFilePath}. Skipping variable substitution."
   104              return
   105          }
   106  
   107          if (!manifestVariablesFilesExist && manifestVariablesFilesExplicitlySpecified) {
   108              // If the user explicitly specified a list of variables files, make sure they all exist.
   109              // Otherwise throw an error so the user knows that he / she made a mistake.
   110              error "[CFManifestSubstituteVariables] Could not find all given manifest variable substitution files. Make sure all files given as manifestVariablesFiles exist."
   111          }
   112  
   113          def result
   114          ExecutionContext context = new ExecutionContext()
   115  
   116          if (!manifestVariablesFilesExist && !manifestVariablesFilesExplicitlySpecified) {
   117              // If no variables files exist (not even the default one) we check if at least we have a list of variables.
   118  
   119              if (!manifestVariablesListSpecified) {
   120                  // If we have no variable values to replace references with, we skip substitution.
   121                  echo "[CFManifestSubstituteVariables] Could not find any default manifest variable substitution file at ${defaultManifestVariablesFileName}, and no manifest variables list was specified. Skipping variable substitution."
   122                  return
   123              }
   124  
   125              // If we have a list of variables specified, we can start replacing them...
   126              result = substitute(manifestFilePath, [], manifestVariablesList, yamlUtils, context, debugHelper)
   127          }
   128          else {
   129              // If we have at least one existing variable substitution file, we can start replacing variables...
   130              result = substitute(manifestFilePath, manifestVariablesFiles, manifestVariablesList, yamlUtils, context, debugHelper)
   131          }
   132  
   133          if (!context.variablesReplaced) {
   134              // If no variables have been replaced at all, we skip writing a file.
   135              echo "[CFManifestSubstituteVariables] No variables were found or could be replaced in ${manifestFilePath}. Skipping variable substitution."
   136              return
   137          }
   138  
   139          // writeYaml won't overwrite the file. You need to delete it first.
   140          deleteFile(outputFilePath)
   141  
   142          writeYaml file: outputFilePath, data: result
   143  
   144          echo "[CFManifestSubstituteVariables] Replaced variables in ${manifestFilePath}."
   145          echo "[CFManifestSubstituteVariables] Wrote output file (with variables replaced) at ${outputFilePath}."
   146      }
   147  }
   148  
   149  /*
   150   * Substitutes variables specified in files and as lists in a given manifest file.
   151   * @param manifestFilePath - the path to the manifest file to replace variables in.
   152   * @param manifestVariablesFiles - the paths to variables substitution files.
   153   * @param manifestVariablesList - the list of variables data to replace variables with.
   154   * @param yamlUtils - the `YamlUtils` used for variable substitution.
   155   * @param context - an `ExecutionContext` to examine if any variables have been replaced and should be written.
   156   * @param debugHelper - a debug output helper.
   157   * @return an Object graph of Yaml data with variables substituted (if any were found and could be replaced).
   158   */
   159  private Object substitute(String manifestFilePath, List<String> manifestVariablesFiles, List<Map<String, Object>> manifestVariablesList, YamlUtils yamlUtils, ExecutionContext context, DebugHelper debugHelper) {
   160      Boolean noVariablesReplaced = true
   161  
   162      def manifestData = loadManifestData(manifestFilePath, debugHelper)
   163  
   164      // replace variables from list first.
   165      List<Map<String,Object>> reversedManifestVariablesList = manifestVariablesList.reverse() // to make sure last one wins.
   166  
   167      def result = manifestData
   168      for (Map<String, Object> manifestVariableData : reversedManifestVariablesList) {
   169          def executionContext = new ExecutionContext()
   170          result = yamlUtils.substituteVariables(result, manifestVariableData, executionContext)
   171          noVariablesReplaced = noVariablesReplaced && !executionContext.variablesReplaced // remember if variables were replaced.
   172      }
   173  
   174      // replace remaining variables from files
   175      List<String> reversedManifestVariablesFilesList = manifestVariablesFiles.reverse() // to make sure last one wins.
   176      for (String manifestVariablesFilePath : reversedManifestVariablesFilesList) {
   177          def manifestVariablesFileData = loadManifestVariableFileData(manifestVariablesFilePath, debugHelper)
   178          def executionContext = new ExecutionContext()
   179          result = yamlUtils.substituteVariables(result, manifestVariablesFileData, executionContext)
   180          noVariablesReplaced = noVariablesReplaced && !executionContext.variablesReplaced // remember if variables were replaced.
   181      }
   182  
   183      context.variablesReplaced = !noVariablesReplaced
   184      return result
   185  }
   186  
   187  /*
   188   * Loads the contents of a manifest.yml file by parsing Yaml and returning the
   189   * object graph. May return a `List<Object>`  (in case more YAML segments are in the file)
   190   * or a `Map<String, Object>` in case there is just one segment.
   191   * @param manifestFilePath - the file path of the manifest to parse.
   192   * @param debugHelper - a debug output helper.
   193   * @return the parsed object graph.
   194   */
   195  private Object loadManifestData(String manifestFilePath, DebugHelper debugHelper) {
   196      try {
   197          // may return a List<Object>  (in case more YAML segments are in the file)
   198          // or a Map<String, Object> in case there is just one segment.
   199          def result = readYaml file: manifestFilePath
   200          echo "[CFManifestSubstituteVariables] Loaded manifest at ${manifestFilePath}!"
   201          return result
   202      }
   203      catch(Exception ex) {
   204          debugHelper.debug("Exception: ${ex}")
   205          echo "[CFManifestSubstituteVariables] Could not load manifest file at ${manifestFilePath}. Exception was: ${ex}"
   206          throw ex
   207      }
   208  }
   209  
   210  /*
   211   * Loads the contents of a manifest variables file by parsing Yaml and returning the
   212   * object graph. May return a `List<Object>`  (in case more YAML segments are in the file)
   213   * or a `Map<String, Object>` in case there is just one segment.
   214   * @param variablesFilePath - the path to the variables file to parse.
   215   * @param debugHelper - a debug output helper.
   216   * @return the parsed object graph.
   217   */
   218  private Object loadManifestVariableFileData(String variablesFilePath, DebugHelper debugHelper) {
   219      try {
   220          // may return a List<Object>  (in case more YAML segments are in the file)
   221          // or a Map<String, Object> in case there is just one segment.
   222          def result = readYaml file: variablesFilePath
   223          echo "[CFManifestSubstituteVariables] Loaded variables file at ${variablesFilePath}!"
   224          return result
   225      }
   226      catch(Exception ex) {
   227          debugHelper.debug("Exception: ${ex}")
   228          echo "[CFManifestSubstituteVariables] Could not load manifest variables file at ${variablesFilePath}. Exception was: ${ex}"
   229          throw ex
   230      }
   231  }
   232  
   233  /*
   234   * Checks if all file paths given in the list exist as files.
   235   * @param manifestVariablesFiles - the list of file paths pointing to manifest variables files.
   236   * @return `true`, if all given files exist, `false` otherwise.
   237   */
   238  private boolean allManifestVariableFilesExist(List<String> manifestVariablesFiles) {
   239      for (String filePath : manifestVariablesFiles) {
   240          Boolean fileExists = fileExists filePath
   241          if (!fileExists) {
   242              echo "[CFManifestSubstituteVariables] Did not find manifest variable substitution file at ${filePath}."
   243              return false
   244          }
   245      }
   246      return true
   247  }
   248  
   249  /*
   250   * Removes the given file, if it exists.
   251   * @param filePath - the path to the file to remove.
   252   */
   253  private void deleteFile(String filePath) {
   254  
   255      Boolean fileExists = fileExists file: filePath
   256      if(fileExists) {
   257          Boolean failure = sh script: "rm '${filePath}'", returnStatus: true
   258          if(!failure) {
   259              echo "[CFManifestSubstituteVariables] Successfully deleted file '${filePath}'."
   260          }
   261          else {
   262              error "[CFManifestSubstituteVariables] Could not delete file '${filePath}'. Check file permissions."
   263          }
   264      }
   265  }