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:
parent
4f57738888
commit
aefe9243e0
@ -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
|
||||
|
@ -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'))
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user