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 }