1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/vars/xsDeploy.groovy
Marcus Holl e54f18e6bc
Introduce xsDeploy step (#749)
Introduce xs deploy
2019-09-11 13:42:38 +02:00

339 lines
11 KiB
Groovy

import com.sap.piper.JenkinsUtils
import static com.sap.piper.Prerequisites.checkScript
import com.sap.piper.BashUtils
import com.sap.piper.ConfigurationHelper
import com.sap.piper.GenerateDocumentation
import com.sap.piper.Utils
import groovy.transform.Field
import hudson.AbortException
@Field String STEP_NAME = getClass().getName()
@Field Set GENERAL_CONFIG_KEYS = STEP_CONFIG_KEYS
@Field Set STEP_CONFIG_KEYS = [
'action',
'apiUrl',
'credentialsId',
'deploymentId',
'deployIdLogPattern',
'deployOpts',
/** A map containing properties forwarded to dockerExecute. For more details see [here][dockerExecute] */
'docker',
'loginOpts',
'mode',
'mtaPath',
'org',
'space',
'xsSessionFile',
]
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
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)
}
}
/**
* Performs an XS deployment
*
* In case of blue-green deployments the step is called for the deployment in the narrower sense
* and later again for resuming or aborting. In this case both calls needs to be performed from the
* same directory.
*/
@GenerateDocumentation
void call(Map parameters = [:]) {
handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) {
def utils = parameters.juStabUtils ?: new Utils()
final script = checkScript(this, parameters) ?: this
ConfigurationHelper configHelper = ConfigurationHelper.newInstance(this)
.loadStepDefaults()
.mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS)
.mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
.mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS)
.addIfEmpty('mtaPath', script.commonPipelineEnvironment.getMtarFilePath())
.addIfEmpty('deploymentId', script.commonPipelineEnvironment.xsDeploymentId)
.mixin(parameters, PARAMETER_KEYS)
Map config = configHelper.use()
DeployMode mode = config.mode
if(mode == DeployMode.NONE) {
echo "Deployment skipped intentionally. Deploy mode '${mode.toString()}'."
return
}
Action action = config.action
if(mode == DeployMode.DEPLOY && action != Action.NONE) {
error "Cannot perform action '${action.toString()}' in mode '${mode.toString()}'. Only action '${Action.NONE.toString()}' is allowed."
}
boolean performLogin = ((mode == DeployMode.DEPLOY) || (mode == DeployMode.BG_DEPLOY && !(action in [Action.RESUME, Action.ABORT])))
boolean performLogout = ((mode == DeployMode.DEPLOY) || (mode == DeployMode.BG_DEPLOY && action != Action.NONE))
boolean sessionExists = fileExists file: config.xsSessionFile
if( (! performLogin) && (! sessionExists) ) {
error 'For the current configuration an already existing session is required. But there is no already existing session.'
}
configHelper
.collectValidationFailures()
/**
* Used for finalizing the blue-green deployment.
* @possibleValues RESUME, ABORT, RETRY
*/
.withMandatoryProperty('action')
/** The file name of the file representing the sesssion after `xs login`. Should not be changed normally. */
.withMandatoryProperty('xsSessionFile')
/** Regex pattern for retrieving the ID of the deployment. */
.withMandatoryProperty('deployIdLogPattern')
/**
* Controls if there is a standard deployment or a blue green deployment
* @possibleValues DEPLOY, BG_DEPLOY
*/
.withMandatoryProperty('mode')
/** The endpoint */
.withMandatoryProperty('apiUrl')
/** The organization */
.withMandatoryProperty('org')
/** The space */
.withMandatoryProperty('space')
/** Additional options appended to the login command. Only needed for sophisticated cases.
* When provided it is the duty of the provider to ensure proper quoting / escaping.
*/
.withMandatoryProperty('loginOpts')
/** Additional options appended to the deploy command. Only needed for sophisticated cases.
* When provided it is the duty of the provider to ensure proper quoting / escaping.
*/
.withMandatoryProperty('deployOpts')
/** The credentialsId */
.withMandatoryProperty('credentialsId')
/** The path to the deployable. If not provided explicitly it is retrieved from the common pipeline environment
* (Parameter `mtarFilePath`).
*/
.withMandatoryProperty('mtaPath', null, {action == Action.NONE})
.withMandatoryProperty('deploymentId',
'No deployment id provided, neither via parameters nor via common pipeline environment. Was there a deployment before?',
{action in [Action.RESUME, Action.ABORT, Action.RETRY]})
.use()
utils.pushToSWA([
step: STEP_NAME,
], config)
if(action == Action.NONE) {
boolean deployableExists = fileExists file: config.mtaPath
if(! deployableExists)
error "Deployable '${config.mtaPath}' does not exist."
}
if(performLogin) {
login(script, config)
}
def failures = []
if(action in [Action.RESUME, Action.ABORT, Action.RETRY]) {
complete(script, mode, action, config, failures)
} else {
deploy(script, mode, config, failures)
}
if (performLogout || failures) {
logout(script, config, failures)
} else {
echo "Skipping logout in order to be able to resume or abort later."
}
if(failures) {
error "Failed command(s): ${failures}. Check earlier log for details."
}
}
}
void login(Script script, Map config) {
withCredentials([usernamePassword(
credentialsId: config.credentialsId,
passwordVariable: 'password',
usernameVariable: 'username'
)]) {
def returnCode = executeXSCommand([script: script].plus(config.docker),
[
"xs login -a ${config.apiUrl} -u ${username} -p ${BashUtils.quoteAndEscape(password)} -o ${config.org} -s ${config.space} ${config.loginOpts}",
'RC=$?',
"[ \$RC == 0 ] && cp \"\${HOME}/${config.xsSessionFile}\" .",
'exit $RC'
])
if(returnCode != 0)
error "xs login failed."
}
boolean existsXsSessionFileAfterLogin = fileExists file: config.xsSessionFile
if(! existsXsSessionFileAfterLogin)
error "Session file ${config.xsSessionFile} not found in current working directory after login."
}
void deploy(Script script, DeployMode mode, Map config, def failures) {
def deploymentLog
try {
lock(getLockIdentifier(config)) {
deploymentLog = executeXSCommand([script: script].plus(config.docker),
[
"cp ${config.xsSessionFile} \${HOME}",
"xs ${mode.toString()} '${config.mtaPath}' -f ${config.deployOpts}"
], true)
}
echo "Deploy log: ${deploymentLog}"
} catch(AbortException e) {
echo "deployment failed. Message: ${e.getMessage()}, Log: ${deploymentLog}}"
failures << "xs ${mode.toString()}"
}
if(mode == DeployMode.BG_DEPLOY) {
if(! failures.isEmpty()) {
echo "Retrieval of deploymentId skipped since prior deployment was not successfull."
} else {
for (def logLine : deploymentLog.readLines()) {
def matcher = logLine =~ config.deployIdLogPattern
if(matcher.find()) {
script.commonPipelineEnvironment.xsDeploymentId = matcher[0][1]
echo "DeploymentId: ${script.commonPipelineEnvironment.xsDeploymentId}."
break
}
}
if(script.commonPipelineEnvironment.xsDeploymentId == null) {
failures << "Cannot lookup deploymentId. Search pattern was: '${config.deployIdLogPattern}'."
}
}
}
}
void complete(Script script, DeployMode mode, Action action, Map config, def failures) {
if(mode != DeployMode.BG_DEPLOY)
error "Action '${action.toString()}' can only be performed for mode '${DeployMode.BG_DEPLOY.toString()}'. Current mode is: '${mode.toString()}'."
def returnCode = 1
lock(getLockIdentifier(config)) {
returnCode = executeXSCommand([script: script].plus(config.docker),
[
"cp ${config.xsSessionFile} \${HOME}",
"xs ${mode.toString()} -i ${config.deploymentId} -a ${action.toString()}"
])
}
if(returnCode != 0) {
echo "${mode.toString()} with action '${action.toString()}' failed with return code ${returnCode}."
failures << "xs ${mode.toString()} -a ${action.toString()}"
}
}
void logout(Script script, Map config, def failures) {
def returnCode = executeXSCommand([script: script].plus(config.docker),
[
"cp ${config.xsSessionFile} \${HOME}",
'xs logout'
])
if(returnCode != 0) {
failures << 'xs logout'
}
sh "XSCONFIG=${config.xsSessionFile}; [ -f \${XSCONFIG} ] && rm \${XSCONFIG}"
}
String getLockIdentifier(Map config) {
"$STEP_NAME:${config.apiUrl}:${config.org}:${config.space}"
}
def executeXSCommand(Map dockerOptions, List commands, boolean returnStdout = false) {
def r
dockerExecute(dockerOptions) {
// in case there are credentials contained in the commands we assume
// the call is properly wrapped by withCredentials(./.)
echo "Executing: '${commands}'."
List prelude = [
'#!/bin/bash'
]
List script = (prelude + commands)
params = [
script: script.join('\n')
]
if(returnStdout) {
params << [ returnStdout: true ]
} else {
params << [ returnStatus: true ]
}
r = sh params
if( (! returnStdout ) && r != 0) {
try {
echo "xs logs:"
sh 'LOG_FOLDER=${HOME}/.xs_logs; [ -d ${LOG_FOLDER} ] && cat ${LOG_FOLDER}/*'
} catch(Exception e) {
echo "Cannot provide xs logs: ${e.getMessage()}."
}
echo "Executing of commands '${commands}' failed. Check earlier logs for details."
}
}
r
}