1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

xsDeploy with go

This commit is contained in:
Marcus Holl 2019-12-13 16:05:55 +01:00
parent 4f57738888
commit aefe9243e0
3 changed files with 195 additions and 632 deletions

View File

@ -115,7 +115,8 @@ public class CommonStepsTest extends BasePiperTest{
'handlePipelineStepErrors', // special step (infrastructure)
'piperStageWrapper', //intended to be called from within stages
'buildSetResult',
'githubPublishRelease' //implementing new golang pattern without fields
'githubPublishRelease', //implementing new golang pattern without fields
'xsDeploy', //implementing new golang pattern without fields
]
@Test

View File

@ -1,252 +1,105 @@
import static org.junit.Assert.assertThat
import org.hamcrest.Matchers
import static org.hamcrest.Matchers.allOf
import static org.hamcrest.Matchers.contains
import static org.hamcrest.Matchers.containsString
import static org.hamcrest.Matchers.hasSize
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.is
import static org.hamcrest.Matchers.not
import static org.hamcrest.Matchers.nullValue
import static org.junit.Assert.assertThat
import org.hamcrest.Matchers
import org.hamcrest.core.IsNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import com.sap.piper.PiperGoUtils
import hudson.AbortException
import util.BasePiperTest
import util.CommandLineMatcher
import util.JenkinsCredentialsRule
import util.JenkinsDockerExecuteRule
import util.JenkinsFileExistsRule
import util.JenkinsLockRule
import util.JenkinsLoggingRule
import util.JenkinsReadJsonRule
import util.JenkinsReadYamlRule
import util.JenkinsShellCallRule
import util.JenkinsStepRule
import util.JenkinsWriteFileRule
import util.Rules
import com.sap.piper.JenkinsUtils
import hudson.AbortException
class XsDeployTest extends BasePiperTest {
private ExpectedException thrown = ExpectedException.none()
private List existingFiles = [
'.xsconfig',
'myApp.mta'
]
private JenkinsStepRule stepRule = new JenkinsStepRule(this)
private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this)
private JenkinsLockRule lockRule = new JenkinsLockRule(this)
private JenkinsLoggingRule logRule = new JenkinsLoggingRule(this)
private JenkinsDockerExecuteRule dockerRule = new JenkinsDockerExecuteRule(this)
private JenkinsWriteFileRule writeFileRule = new JenkinsWriteFileRule(this)
List env
@Rule
public RuleChain ruleChain = Rules.getCommonRules(this)
.around(new JenkinsReadYamlRule(this))
.around(new JenkinsReadJsonRule(this))
.around(stepRule)
.around(new JenkinsDockerExecuteRule(this))
.around(dockerRule)
.around(writeFileRule)
.around(new JenkinsCredentialsRule(this)
.withCredentials('myCreds', 'cred_xs', 'topSecret'))
.around(new JenkinsFileExistsRule(this, existingFiles))
.around(lockRule)
.around(shellRule)
.around(logRule)
.around(thrown)
@Test
public void testSanityChecks() {
thrown.expect(IllegalArgumentException)
thrown.expectMessage(
allOf(
containsString('ERROR - NO VALUE AVAILABLE FOR:'),
containsString('apiUrl'),
containsString('org'),
containsString('space'),
containsString('mtaPath')))
stepRule.step.xsDeploy(script: nullScript)
private PiperGoUtils goUtils = new PiperGoUtils(null) {
void unstashPiperBin() {
}
}
@Test
public void testLoginFailed() {
thrown.expect(AbortException)
thrown.expectMessage('xs login failed')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '#!/bin/bash xs login .*', 1)
try {
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mtaPath: 'myApp.mta'
)
} catch(AbortException e ) {
assertThat(shellRule.shell,
allOf(
// first item: the login attempt
// second item: we try to provide the logs
hasSize(2),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs login'),
new CommandLineMatcher()
.hasProlog('LOG_FOLDER')
.hasSnippet('cat \\$\\{LOG_FOLDER\\}/\\*')
)
)
throw e
}
@Before
public void init() {
helper.registerAllowedMethod('withEnv', [List, Closure], {l, c -> env = l; c()})
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*getConfig --contextConfig --stepMetadata.*', '{"dockerImage": "xs", "credentialsId":"myCreds"}')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*getConfig --stepMetadata.*', '{"mode": "BG_DEPLOY", "action": "NONE", "apiUrl": "https://example.org/xs", "org": "myOrg", "space": "mySpace"}')
nullScript.commonPipelineEnvironment.xsDeploymentId = null
}
@Test
public void testDeployFailed() {
thrown.expect(AbortException)
thrown.expectMessage('Failed command(s): [xs deploy]. Check earlier log for details.')
thrown.expectMessage('script returned exit code 1')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '#!/bin/bash.*xs deploy .*', {throw new AbortException()})
try {
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mtaPath: 'myApp.mta'
)
} catch(AbortException e ) {
assertThat(shellRule.shell,
allOf(
hasSize(4),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs login'),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs deploy'),
new CommandLineMatcher()
.hasProlog('#!/bin/bash')
.hasSnippet('xs logout'), // logout must be present in case deployment failed.
new CommandLineMatcher()
.hasProlog('')
.hasSnippet('rm \\$\\{XSCONFIG\\}') // remove the session file after logout
)
)
throw e
}
}
@Test
public void testNothingHappensWhenModeIsNone() {
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*xsDeploy .*', { throw new AbortException('script returned exit code 1')})
stepRule.step.xsDeploy(
script: nullScript,
mode: 'NONE'
piperGoUtils: goUtils,
)
assertThat(logRule.log, containsString('Deployment skipped intentionally.'))
assertThat(shellRule.shell, hasSize(0))
}
@Test
public void testDeploymentFailsWhenDeployableIsNotPresent() {
thrown.expect(AbortException)
thrown.expectMessage('Deployable \'myApp.mta\' does not exist.')
existingFiles.remove('myApp.mta')
try {
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mtaPath: 'myApp.mta'
)
} catch(AbortException e) {
// no shell operation happened in this case.
assertThat(shellRule.shell.size(), is(0))
throw e
}
}
@Test
public void testDeployStraighForward() {
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
deployOpts: '-t 60',
mtaPath: 'myApp.mta'
)
assertThat(shellRule.shell,
allOf(
new CommandLineMatcher()
.hasProlog("#!/bin/bash xs login")
.hasSnippet('xs login')
.hasOption('a', 'https://example.org/xs')
.hasOption('u', 'cred_xs')
.hasSingleQuotedOption('p', 'topSecret')
.hasOption('o', 'myOrg')
.hasOption('s', 'mySpace'),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs deploy')
.hasOption('t', '60')
.hasArgument('\'myApp.mta\''),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs logout')
)
)
assertThat(lockRule.getLockResources(), contains('xsDeploy:https://example.org/xs:myOrg:mySpace'))
}
@Test
public void testInvalidDeploymentModeProviced() {
public void testInvalidDeploymentModeProvided() {
thrown.expect(IllegalArgumentException)
thrown.expectMessage('No enum constant')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*getConfig --stepMetadata.*', '{"mode": "DOES_NOT_EXIST", "action": "NONE", "apiUrl": "https://example.org/xs", "org": "myOrg", "space": "mySpace"}')
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
deployOpts: '-t 60',
mtaPath: 'myApp.mta',
mode: 'DOES_NOT_EXIST'
piperGoUtils: goUtils,
)
}
@Test
public void testActionProvidedForStandardDeployment() {
public void testParametersViaSignature() {
thrown.expect(AbortException)
thrown.expectMessage(
'Cannot perform action \'resume\' in mode \'deploy\'. Only action \'none\' is allowed.')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*xsDeploy .*', '{"operationId": "1234"}')
stepRule.step.xsDeploy(
script: nullScript,
@ -256,109 +109,88 @@ class XsDeployTest extends BasePiperTest {
credentialsId: 'myCreds',
deployOpts: '-t 60',
mtaPath: 'myApp.mta',
mode: 'DEPLOY', // this is the default anyway
action: 'RESUME'
mode: 'DEPLOY',
action: 'NONE',
piperGoUtils: goUtils
)
// nota bene: script and piperGoUtils are not contained in the json below.
assertThat(env*.toString(), contains('PIPER_parametersJSON={"apiUrl":"https://example.org/xs","org":"myOrg","space":"mySpace","credentialsId":"myCreds","deployOpts":"-t 60","mtaPath":"myApp.mta","mode":"DEPLOY","action":"NONE"}'))
}
@Test
public void testBlueGreenDeployFailes() {
public void testBlueGreenDeployInit() {
thrown.expect(AbortException)
thrown.expectMessage('Failed command(s): [xs bg-deploy]')
//
// Only difference between bg deploy and standard deploy is in the config.
// The surrounding behavior is the same. Hence there is no dedicated test here
// in the groovy layer for standard deploy
//
logRule.expect('Something went wrong')
boolean unstashCalled
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '#!/bin/bash.*xs bg-deploy .*',
{ throw new AbortException('Something went wrong.') })
assertThat(nullScript.commonPipelineEnvironment.xsDeploymentId, nullValue())
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*xsDeploy .*', '{"operationId": "1234"}')
try {
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mtaPath: 'myApp.mta',
mode: 'BG_DEPLOY'
)
} catch(AbortException e) {
// in case there is a deployment failure we have to logout also for bg-deployments
assertThat(shellRule.shell,
new CommandLineMatcher()
.hasProlog('#!/bin/bash')
.hasSnippet('xs logout')
)
throw e
goUtils = new PiperGoUtils(null) {
void unstashPiperBin() {
unstashCalled = true
}
}
}
@Test
public void testBlueGreenDeployStraighForward() {
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '#!/bin/bash.*xs bg-deploy .*',
((CharSequence)''' |
|
|Uploading 1 files:
|/myFolder/my.mtar
|File upload finished
|
|Detected MTA schema version: "3.1.0"
|Detected deploy target as "myOrg mySpace"
|Detected deployed MTA with ID "my_mta" and version "0.0.1"
|Deployed MTA color: blue
|New MTA color: green
|Detected new MTA version: "0.0.1"
|Deployed MTA version: 0.0.1
|Service "xxx" is not modified and will not be updated
|Creating application "db-green" from MTA module "xx"...
|Uploading application "xx-green"...
|Staging application "xx-green"...
|Application "xx-green" staged
|Executing task "deploy" on application "xx-green"...
|Task execution status: succeeded
|Process has entered validation phase. After testing your new deployment you can resume or abort the process.
|Use "xs bg-deploy -i 1234 -a resume" to resume the process.
|Use "xs bg-deploy -i 1234 -a abort" to abort the process.
|Hint: Use the '--no-confirm' option of the bg-deploy command to skip this phase.
|''').stripMargin())
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
deployOpts: '-t 60',
mtaPath: 'myApp.mta',
mode: 'BG_DEPLOY'
piperGoUtils: goUtils
)
assertThat(unstashCalled, equalTo(true))
assertThat(nullScript.commonPipelineEnvironment.xsDeploymentId, is('1234'))
assertThat(writeFileRule.files.keySet(), contains('metadata/xsDeploy.yaml'))
assertThat(dockerRule.dockerParams.dockerImage, equalTo('xs'))
assertThat(dockerRule.dockerParams.dockerPullImage, equalTo(false))
assertThat(shellRule.shell,
allOf(
new CommandLineMatcher()
.hasProlog("#!/bin/bash xs login")
.hasOption('a', 'https://example.org/xs')
.hasOption('u', 'cred_xs')
.hasSingleQuotedOption('p', 'topSecret')
.hasOption('o', 'myOrg')
.hasOption('s', 'mySpace'),
.hasProlog('./piper version'),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasOption('t', '60')
.hasArgument('\'myApp.mta\''),
.hasProlog('./piper getConfig --contextConfig --stepMetadata \'metadata/xsDeploy.yaml\''),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasProlog('./piper getConfig --stepMetadata \'metadata/xsDeploy.yaml\''),
new CommandLineMatcher()
.hasProlog('#!/bin/bash ./piper xsDeploy --user \\$\\{USERNAME\\} --password \\$\\{PASSWORD\\}'),
not(new CommandLineMatcher()
.hasProlog('#!/bin/bash ./piper xsDeploy --user \\$\\{USERNAME\\} --password \\$\\{PASSWORD\\} --operationId'))
)
)
assertThat(lockRule.getLockResources(), contains('xsDeploy:https://example.org/xs:myOrg:mySpace'))
}
@Test
public void testBlueGreenDeployResume() {
nullScript.commonPipelineEnvironment.xsDeploymentId = '1234'
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*getConfig --stepMetadata.*', '{"mode": "BG_DEPLOY", "action": "RESUME", "apiUrl": "https://example.org/xs", "org": "myOrg", "space": "mySpace"}')
stepRule.step.xsDeploy(
script: nullScript,
piperGoUtils: goUtils
)
assertThat(shellRule.shell,
new CommandLineMatcher()
.hasProlog('#!/bin/bash ./piper xsDeploy --user \\$\\{USERNAME\\} --password \\$\\{PASSWORD\\} --operationId 1234')
)
assertThat(lockRule.getLockResources(), contains('xsDeploy:https://example.org/xs:myOrg:mySpace'))
}
@Test
public void testBlueGreenDeployResumeWithoutDeploymentId() {
@ -367,114 +199,17 @@ class XsDeployTest extends BasePiperTest {
thrown.expect(IllegalArgumentException)
thrown.expectMessage(
allOf(
containsString('No deployment id provided'),
containsString('No operationId provided'),
containsString('Was there a deployment before?')))
nullScript.commonPipelineEnvironment.xsDeploymentId = null // is null anyway, just for clarification
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*getConfig --stepMetadata.*', '{"mode": "BG_DEPLOY", "action": "RESUME", "apiUrl": "https://example.org/xs", "org": "myOrg", "space": "mySpace"}')
assertThat(nullScript.commonPipelineEnvironment.xsDeploymentId, nullValue())
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mode: 'BG_DEPLOY',
action: 'RESUME'
piperGoUtils: goUtils,
failOnError: true,
)
}
@Test
public void testBlueGreenDeployWithoutExistingSession() {
thrown.expect(AbortException)
thrown.expectMessage(
'For the current configuration an already existing session is required.' +
' But there is no already existing session')
existingFiles.remove('.xsconfig')
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mode: 'BG_DEPLOY',
action: 'RESUME'
)
}
@Test
public void testBlueGreenDeployResumeFails() {
// e.g. we try to resume a deployment which did not succeed or which was already resumed or aborted.
thrown.expect(AbortException)
thrown.expectMessage('Failed command(s): [xs bg-deploy -a resume].')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'xs bg-deploy -i .*', 1)
nullScript.commonPipelineEnvironment.xsDeploymentId = '1234'
try {
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mode: 'BG_DEPLOY',
action: 'RESUME'
)
} catch(AbortException e) {
// logout must happen also in case of a failed deployment
assertThat(shellRule.shell,
new CommandLineMatcher()
.hasProlog('')
.hasSnippet('xs logout'))
throw e
}
}
@Test
public void testBlueGreenDeployResume() {
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'xs bg-deploy -i .*', 0)
nullScript.commonPipelineEnvironment.xsDeploymentId = '1234'
stepRule.step.xsDeploy(
script: nullScript,
apiUrl: 'https://example.org/xs',
org: 'myOrg',
space: 'mySpace',
credentialsId: 'myCreds',
mode: 'BG_DEPLOY',
action: 'RESUME'
)
// there is no login in case of a resume since we have to use the old session which triggered the deployment.
assertThat(shellRule.shell,
allOf(
hasSize(3),
new CommandLineMatcher()
.hasProlog('#!/bin/bash')
.hasSnippet('xs bg-deploy')
.hasOption('i', '1234')
.hasOption('a', 'resume'),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs logout'),
new CommandLineMatcher()
.hasProlog('')
.hasSnippet('rm \\$\\{XSCONFIG\\}') // delete the session file after logout
)
)
assertThat(lockRule.getLockResources(), contains('xsDeploy:https://example.org/xs:myOrg:mySpace'))
}
}

View File

@ -1,38 +1,17 @@
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.JenkinsUtils
import com.sap.piper.PiperGoUtils
import com.sap.piper.GenerateDocumentation
import com.sap.piper.Utils
import groovy.transform.Field
import hudson.AbortException
@Field String METADATA_FILE = 'metadata/xsDeploy.yaml'
@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,
@ -67,272 +46,120 @@ 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)
final script = checkScript(this, parameters) ?: this
//
// 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
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)
// hard to predict how these two 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')
Map config = configHelper.use()
//
// For now - since the xsDeploy step is not merged and covered by a release - we stash
// a locally built version of the piper-go binary in the pipeline script (Jenkinsfile) with
// stash name "piper-bin". That stash is used inside method "unstashPiperBin".
piperGoUtils.unstashPiperBin()
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()
//
// Printing the piper-go version. Should not be done here, but somewhere during materializing
// the piper binary.
def piperGoVersion = sh(returnStdout: true, script: "./piper version")
echo "PiperGoVersion: ${piperGoVersion}"
//
// since there is no valid config provided (... null) telemetry is disabled.
utils.pushToSWA([
step: STEP_NAME,
], config)
], null)
if(action == Action.NONE) {
boolean deployableExists = fileExists file: config.mtaPath
if(! deployableExists)
error "Deployable '${config.mtaPath}' does not exist."
}
writeFile(file: METADATA_FILE, text: libraryResource(METADATA_FILE))
if(performLogin) {
login(script, config)
}
def failures = []
withEnv([
"PIPER_parametersJSON=${groovy.json.JsonOutput.toJson(parameters)}",
]) {
if(action in [Action.RESUME, Action.ABORT, Action.RETRY]) {
//
// 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?
Map contextConfig = readJSON (text: sh(returnStdout: true, script: "./piper getConfig --contextConfig --stepMetadata '${METADATA_FILE}'"))
complete(script, mode, action, config, failures)
Map projectConfig = readJSON (text: sh(returnStdout: true, script: "./piper ${parameters.verbose ? '--verbose' :''} getConfig --stepMetadata '${METADATA_FILE}'"))
} else {
if(parameters.verbose) {
echo "[INFO] Context-Config: ${contextConfig}"
echo "[INFO] Project-Config: ${projectConfig}"
}
deploy(script, mode, config, failures)
}
Action action = projectConfig.action
DeployMode mode = projectConfig.mode
if (performLogout || failures) {
logout(script, config, failures)
// That config map here is only used in the groovy layer. Nothing is handed over to go.
Map config = contextConfig <<
[
apiUrl: projectConfig.apiUrl, // required on groovy level for acquire the lock
org: projectConfig.org, // required on groovy level for acquire the lock
space: projectConfig.space, // required on groovy level for acquire the lock
docker: [
dockerImage: contextConfig.dockerImage,
dockerPullImage: false // dockerPullImage apparently not provided by context config.
]
]
} else {
echo "Skipping logout in order to be able to resume or abort later."
}
if(parameters.verbose) {
echo "[INFO] Merged-Config: ${config}"
}
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
def operationId
if(mode == DeployMode.BG_DEPLOY && action != Action.NONE) {
operationId = script.commonPipelineEnvironment.xsDeploymentId
if (! operationId) {
throw new IllegalArgumentException('No operationId provided. Was there a deployment before?')
}
}
if(script.commonPipelineEnvironment.xsDeploymentId == null) {
failures << "Cannot lookup deploymentId. Search pattern was: '${config.deployIdLogPattern}'."
def xsDeployStdout
lock(getLockIdentifier(config)) {
withCredentials([usernamePassword(
credentialsId: config.credentialsId,
passwordVariable: 'PASSWORD',
usernameVariable: 'USERNAME')]) {
dockerExecute([script: this].plus(config.docker)) {
xsDeployStdout = sh returnStdout: true, script: """#!/bin/bash
./piper ${parameters.verbose ? '--verbose' : ''} xsDeploy --user \${USERNAME} --password \${PASSWORD} ${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}'."
}
}
}
}
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
}