github.com/xgoffin/jenkins-library@v1.154.0/vars/piperStageWrapper.groovy (about)

     1  import com.cloudbees.groovy.cps.NonCPS
     2  import com.sap.piper.Utils
     3  import com.sap.piper.ConfigurationHelper
     4  import com.sap.piper.ConfigurationLoader
     5  import com.sap.piper.DebugReport
     6  import com.sap.piper.k8s.ContainerMap
     7  import groovy.transform.Field
     8  
     9  import static com.sap.piper.Prerequisites.checkScript
    10  
    11  @Field String STEP_NAME = 'piperStageWrapper'
    12  
    13  void call(Map parameters = [:], body) {
    14  
    15      final script = checkScript(this, parameters) ?: this
    16      def utils = parameters.juStabUtils ?: new Utils()
    17  
    18      def stageName = parameters.stageName ?: env.STAGE_NAME
    19  
    20      // load default & individual configuration
    21      Map config = ConfigurationHelper.newInstance(this)
    22          .loadStepDefaults()
    23          .mixin(ConfigurationLoader.defaultStageConfiguration(script, stageName))
    24          .mixinGeneralConfig(script.commonPipelineEnvironment)
    25          .mixinStageConfig(script.commonPipelineEnvironment, stageName)
    26          .mixin(parameters)
    27          .addIfEmpty('stageName', stageName)
    28          .addIfEmpty('lockingResourceGroup', script.commonPipelineEnvironment.projectName)
    29          .dependingOn('stageName').mixin('ordinal')
    30          .use()
    31  
    32      stageLocking(config) {
    33          def containerMap = ContainerMap.instance.getMap().get(stageName) ?: [:]
    34          List environment = []
    35          if (stageName && stageName != env.STAGE_NAME) {
    36              // Avoid two sources of truth with regards to stageName.
    37              // env.STAGE_NAME is filled from stage('Display name') {, it only serves the purpose of
    38              // easily getting to the stage name from steps.
    39              environment.add("STAGE_NAME=${stageName}")
    40          }
    41          if (config.sidecarImage) {
    42              echo "sidecarImage configured for stage '${stageName}': '${config.sidecarImage}'"
    43              environment.add("SIDECAR_IMAGE=${config.sidecarImage}")
    44          }
    45          if (Boolean.valueOf(env.ON_K8S) && (containerMap.size() > 0 || config.runStageInPod)) {
    46              environment.add("POD_NAME=${stageName}")
    47              withEnv(environment) {
    48                  dockerExecuteOnKubernetes(script: script, containerMap: containerMap, stageName: stageName) {
    49                      executeStage(script, body, stageName, config, utils, parameters.telemetryDisabled)
    50                  }
    51              }
    52          } else {
    53              withEnvWrapper(environment) {
    54                  node(config.nodeLabel) {
    55                      executeStage(script, body, stageName, config, utils, parameters.telemetryDisabled)
    56                  }
    57              }
    58          }
    59      }
    60  }
    61  
    62  private void withEnvWrapper(List environment, Closure body) {
    63      if (environment) {
    64          withEnv(environment) {
    65              body()
    66          }
    67      } else {
    68          body()
    69      }
    70  }
    71  
    72  private void stageLocking(Map config, Closure body) {
    73      if (config.stageLocking) {
    74          String resource = config.lockingResourceGroup?:env.JOB_NAME
    75          if(config.lockingResource){
    76              resource += "/${config.lockingResource}"
    77          }
    78          else if(config.ordinal){
    79              resource += "/${config.ordinal}"
    80          }
    81          lock(resource: resource, inversePrecedence: true) {
    82              if(config.ordinal) {
    83                  milestone config.ordinal
    84              }
    85              body()
    86          }
    87      } else {
    88          body()
    89      }
    90  }
    91  
    92  private void executeStage(script, originalStage, stageName, config, utils, telemetryDisabled = false) {
    93      boolean projectExtensions
    94      boolean globalExtensions
    95      def startTime = System.currentTimeMillis()
    96  
    97      try {
    98          // Add general stage stashes to config.stashContent
    99          config.stashContent = utils.unstashStageFiles(script, stageName, config.stashContent)
   100  
   101          /* Defining the sources where to look for a project extension and a repository extension.
   102          * Files need to be named like the executed stage to be recognized.
   103          */
   104          def projectInterceptorFile = "${config.projectExtensionsDirectory}${stageName}.groovy"
   105          def globalInterceptorFile = "${config.globalExtensionsDirectory}${stageName}.groovy"
   106          projectExtensions = fileExists(projectInterceptorFile)
   107          globalExtensions = fileExists(globalInterceptorFile)
   108          // Pre-defining the real originalStage in body variable, might be overwritten later if extensions exist
   109          def body = originalStage
   110  
   111          // First, check if a global extension exists via a dedicated repository
   112          if (globalExtensions && allowExtensions(script)) {
   113              echo "[${STEP_NAME}] Found global interceptor '${globalInterceptorFile}' for ${stageName}."
   114              // If we call the global interceptor, we will pass on originalStage as parameter
   115              DebugReport.instance.globalExtensions.put(stageName, "Overwrites")
   116              Closure modifiedOriginalStage = {
   117                  DebugReport.instance.globalExtensions.put(stageName, "Extends")
   118                  originalStage()
   119              }
   120  
   121              body = {
   122                  callInterceptor(script, globalInterceptorFile, modifiedOriginalStage, stageName, config)
   123              }
   124          }
   125  
   126          // Second, check if a project extension (within the same repository) exists
   127          if (projectExtensions && allowExtensions(script)) {
   128              echo "[${STEP_NAME}] Running project interceptor '${projectInterceptorFile}' for ${stageName}."
   129              // If we call the project interceptor, we will pass on body as parameter which contains either originalStage or the repository interceptor
   130              if (projectExtensions && globalExtensions) {
   131                  DebugReport.instance.globalExtensions.put(stageName, "Unknown (Overwritten by local extension)")
   132              }
   133              DebugReport.instance.localExtensions.put(stageName, "Overwrites")
   134              Closure modifiedOriginalBody = {
   135                  DebugReport.instance.localExtensions.put(stageName, "Extends")
   136                  if (projectExtensions && globalExtensions) {
   137                      DebugReport.instance.globalExtensions.put(stageName, "Overwrites")
   138                  }
   139                  body.call()
   140              }
   141  
   142              callInterceptor(script, projectInterceptorFile, modifiedOriginalBody, stageName, config)
   143  
   144          } else {
   145              // NOTE: It may appear more elegant to re-assign 'body' more than once and then call 'body()' after the
   146              // if-block. This could lead to infinite loops however, as any change to the local variable 'body' will
   147              // become visible in all of the closures at the time they run. I.e. 'body' inside any of the closures will
   148              // reflect the last assignment and not its value at the time of constructing the closure!
   149              body()
   150          }
   151  
   152      } finally {
   153          //Perform stashing of selected files in workspace
   154          utils.stashStageFiles(script, stageName)
   155  
   156          // In general telemetry reporting is disabled by the config settings. This flag is used to disable the reporting when the config is not yet read (e.g. init stage).
   157          if (!telemetryDisabled) {
   158              def duration = System.currentTimeMillis() - startTime
   159              utils.pushToSWA([
   160                  eventType       : 'library-os-stage',
   161                  stageName       : stageName,
   162                  stepParamKey1   : 'buildResult',
   163                  stepParam1      : "${script.currentBuild.currentResult}",
   164                  buildResult     : "${script.currentBuild.currentResult}",
   165                  stepParamKey2   : 'stageStartTime',
   166                  stepParam2      : "${startTime}",
   167                  stageStartTime  : "${startTime}",
   168                  stepParamKey3   : 'stageDuration',
   169                  stepParam3      : "${duration}",
   170                  stageDuration   : "${duration}",
   171                  stepParamKey4   : 'projectExtension',
   172                  stepParam4      : "${projectExtensions}",
   173                  projectExtension: "${projectExtensions}",
   174                  stepParamKey5   : 'globalExtension',
   175                  stepParam5      : "${globalExtensions}",
   176                  globalExtension : "${globalExtensions}"
   177              ], config)
   178          }
   179      }
   180  }
   181  
   182  private void callInterceptor(Script script, String extensionFileName, Closure originalStage, String stageName, Map configuration) {
   183      try {
   184          Script interceptor = load(extensionFileName)
   185          if (isOldInterceptorInterfaceUsed(interceptor)) {
   186              echo("[Warning] The interface to implement extensions has changed. " +
   187                  "The extension $extensionFileName has to implement a method named 'call' with exactly one parameter of type Map. " +
   188                  "This map will have the properties script, originalStage, stageName, config. " +
   189                  "For example: def call(Map parameters) { ... }")
   190              interceptor.call(originalStage, stageName, configuration, configuration)
   191          } else {
   192              validateInterceptor(interceptor, extensionFileName)
   193              interceptor.call([
   194                  script       : script,
   195                  originalStage: originalStage,
   196                  stageName    : stageName,
   197                  config       : configuration
   198              ])
   199          }
   200      } catch (Throwable error) {
   201          if (!DebugReport.instance.failedBuild.step) {
   202              DebugReport.instance.storeStepFailure("${stageName}(extended)", error, true)
   203          }
   204          throw error
   205      }
   206  }
   207  
   208  @NonCPS
   209  private boolean isInterceptorValid(Script interceptor) {
   210      MetaMethod method = interceptor.metaClass.pickMethod("call", [Map.class] as Class[])
   211      return method != null
   212  }
   213  
   214  private void validateInterceptor(Script interceptor, String extensionFileName) {
   215      if (!isInterceptorValid(interceptor)) {
   216          error("The extension $extensionFileName has to implement a method named 'call' with exactly one parameter of type Map. " +
   217              "This map will have the properties script, originalStage, stageName, config. " +
   218              "For example: def call(Map parameters) { ... }")
   219      }
   220  }
   221  
   222  @NonCPS
   223  private boolean isOldInterceptorInterfaceUsed(Script interceptor) {
   224      MetaMethod method = interceptor.metaClass.pickMethod("call", [Closure.class, String.class, Map.class, Map.class] as Class[])
   225      return method != null
   226  }
   227  
   228  private static boolean allowExtensions(Script script) {
   229      return script.env.PIPER_DISABLE_EXTENSIONS == null || Boolean.valueOf(script.env.PIPER_DISABLE_EXTENSIONS) == false
   230  }