From aefe9243e034bc297aa5a1819ae9364a1a8a2d49 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 13 Dec 2019 16:05:55 +0100 Subject: [PATCH] xsDeploy with go --- test/groovy/CommonStepsTest.groovy | 3 +- test/groovy/XsDeployTest.groovy | 463 ++++++----------------------- vars/xsDeploy.groovy | 361 ++++++---------------- 3 files changed, 195 insertions(+), 632 deletions(-) diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index d8ebf3a0e..69cce2c05 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -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 diff --git a/test/groovy/XsDeployTest.groovy b/test/groovy/XsDeployTest.groovy index ff5fdf608..a260bdc98 100644 --- a/test/groovy/XsDeployTest.groovy +++ b/test/groovy/XsDeployTest.groovy @@ -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')) - - } - } \ No newline at end of file diff --git a/vars/xsDeploy.groovy b/vars/xsDeploy.groovy index d98033c1b..6cf3db0cd 100644 --- a/vars/xsDeploy.groovy +++ b/vars/xsDeploy.groovy @@ -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 -}