import static com.sap.piper.Prerequisites.checkScript import com.sap.piper.ConfigurationHelper import com.sap.piper.DefaultValueCache import com.sap.piper.JenkinsUtils import com.sap.piper.PiperGoUtils import com.sap.piper.GenerateDocumentation import com.sap.piper.Utils import groovy.transform.Field @Field String METADATA_FILE = 'metadata/xsDeploy.yaml' @Field String PIPER_DEFAULTS = 'default_pipeline_environment.yml' @Field String STEP_NAME = getClass().getName() @Field String METADATA_FOLDER = '.pipeline' // metadata file contains already the "metadata" folder level, hence we end up in a folder ".pipeline/metadata" @Field String ADDITIONAL_CONFIGS_FOLDER='.pipeline/additionalConfigs' enum DeployMode { DEPLOY, BG_DEPLOY, NONE String toString() { name().toLowerCase(Locale.ENGLISH).replaceAll('_', '-') } } enum Action { RESUME, ABORT, RETRY, NONE String toString() { name().toLowerCase(Locale.ENGLISH) } } void call(Map parameters = [:]) { handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) { final script = checkScript(this, parameters) ?: null if(! script) { error "Reference to surrounding pipeline script not provided (script: this)." } def utils = parameters.juStabUtils ?: new Utils() def piperGoUtils = parameters.piperGoUtils ?: new PiperGoUtils(utils) // // The parameters map in provided from outside. That map might be used elsewhere in the pipeline // hence we should not modify it here. So we create a new map based on the parameters map. parameters = [:] << parameters // hard to predict how these parameters looks like in its serialized form. Anyhow it is better // not to have these parameters forwarded somehow to the go layer. parameters.remove('juStabUtils') parameters.remove('piperGoUtils') parameters.remove('script') piperGoUtils.unstashPiperBin() // // Printing the piper-go version. Should not be done here, but somewhere during materializing // the piper binary. As long as we don't have it elsewhere we should keep it here. def piperGoVersion = sh(returnStdout: true, script: "./piper version") echo "PiperGoVersion: ${piperGoVersion}" // // since there is no valid config provided (... null) telemetry is disabled (same for other go releated steps at the moment). utils.pushToSWA([ step: STEP_NAME, ], null) String configFiles = prepareConfigurations([PIPER_DEFAULTS].plus(script.commonPipelineEnvironment.getCustomDefaults()), ADDITIONAL_CONFIGS_FOLDER) writeFile(file: "${METADATA_FOLDER}/${METADATA_FILE}", text: libraryResource(METADATA_FILE)) withEnv([ "PIPER_parametersJSON=${groovy.json.JsonOutput.toJson(parameters)}", ]) { // // context config gives us e.g. the docker image name. --> How does this work for customer maintained images? // There is a name provided in the metadata file. But we do not provide a docker image for that. // The user has to build that for her/his own. How do we expect to configure this? String projectConfigScript = "./piper getConfig --stepMetadata '${METADATA_FOLDER}/${METADATA_FILE}' --defaultConfig ${configFiles}" String contextConfigScript = projectConfigScript + " --contextConfig" Map projectConfig = readJSON (text: sh(returnStdout: true, script: projectConfigScript)) Map contextConfig = readJSON (text: sh(returnStdout: true, script: contextConfigScript)) Map options = getOptions(parameters, projectConfig, contextConfig, script.commonPipelineEnvironment) Action action = options.action DeployMode mode = options.mode if(parameters.verbose) { echo "[INFO] ContextConfig: ${contextConfig}" echo "[INFO] ProjectConfig: ${projectConfig}" } def mtarFilePath = script.commonPipelineEnvironment.mtarFilePath def operationId = parameters.operationId if(! operationId && mode == DeployMode.BG_DEPLOY && action != Action.NONE) { operationId = script.commonPipelineEnvironment.xsDeploymentId if (! operationId) { throw new IllegalArgumentException('No operationId provided. Was there a deployment before?') } } def xsDeployStdout lock(getLockIdentifier(projectConfig)) { withCredentials([usernamePassword( credentialsId: contextConfig.credentialsId, passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME')]) { dockerExecute([script: this].plus([dockerImage: options.dockerImage, dockerPullImage: options.dockerPullImage])) { xsDeployStdout = sh returnStdout: true, script: """#!/bin/bash ./piper xsDeploy --defaultConfig ${configFiles} --user \${USERNAME} --password \${PASSWORD} ${mtarFilePath ? '--mtaPath ' + mtarFilePath : ''} ${operationId ? '--operationId ' + operationId : ''} """ } } } if(mode == DeployMode.BG_DEPLOY && action == Action.NONE) { script.commonPipelineEnvironment.xsDeploymentId = readJSON(text: xsDeployStdout).operationId if (!script.commonPipelineEnvironment.xsDeploymentId) { error "No Operation id returned from xs deploy step. This is required for mode '${mode}' and action '${action}'." } echo "[INFO] OperationId for subsequent resume or abort: '${script.commonPipelineEnvironment.xsDeploymentId}'." } } } } String getLockIdentifier(Map config) { "$STEP_NAME:${config.apiUrl}:${config.org}:${config.space}" } /* * The returned string can be used directly in the command line for retrieving the configuration via go */ String prepareConfigurations(List configs, String configCacheFolder) { for(def customDefault : configs) { writeFile(file: "${ADDITIONAL_CONFIGS_FOLDER}/${customDefault}", text: libraryResource(customDefault)) } joinAndQuote(configs.reverse(), configCacheFolder) } /* * prefix is supposed to be provided without trailing slash */ String joinAndQuote(List l, String prefix = '') { _l = [] if(prefix == null) { prefix = '' } if(prefix.endsWith('/') || prefix.endsWith('\\')) throw new IllegalArgumentException("Provide prefix (${prefix}) without trailing slash") for(def e : l) { def _e = '' if(prefix.length() > 0) { _e += prefix _e += '/' } _e += e _l << '"' + _e + '"' } _l.join(' ') } /* ugly backward compatibility handling retrieves docker options from project config or from landscape config layer(s) precedence is 1.) parameters via signature 2.) project config (not nested) 3.) project config (nested inside docker node) 4.) context config (if applicable (docker)) */ Map getOptions(Map parameters, Map projectConfig, Map contextConfig, def cpe) { Set configKeys = ['docker', 'mode', 'action', 'dockerImage', 'dockerPullImage'] Map config = ConfigurationHelper.newInstance(this) .loadStepDefaults() .mixinGeneralConfig(cpe, configKeys) .mixinStepConfig(cpe, configKeys) .mixinStageConfig(cpe, env.STAGE_NAME, configKeys) .mixin(parameters, configKeys) .use() def dockerImage = config.dockerImage ?: (projectConfig.dockerImage ?: (config.docker?.dockerImage ?: contextConfig.dockerImage)) def dockerPullImage = config.dockerPullImage ?: (projectConfig.dockerPullImage ?: (config.docker?.dockerPullImage ?: contextConfig.dockerPullImage)) def mode = config.mode ?: projectConfig.mode def action = config.action ?: projectConfig.action [dockerImage: dockerImage, dockerPullImage: dockerPullImage, mode: mode, action: action] }