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

Merge pull request #1017 from marcusholl/pr/xsDeployGroovyWithGi

xsDeploy.groovy forwarding to go
This commit is contained in:
Marcus Holl 2020-01-15 08:54:24 +01:00 committed by GitHub
commit e5db600cf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 291 additions and 613 deletions

View File

@ -8,16 +8,21 @@ class DefaultValueCache implements Serializable {
private Map defaultValues
private DefaultValueCache(Map defaultValues){
private List customDefaults = []
private DefaultValueCache(Map defaultValues, List customDefaults){
this.defaultValues = defaultValues
if(customDefaults) {
this.customDefaults.addAll(customDefaults)
}
}
static getInstance(){
return instance
}
static createInstance(Map defaultValues){
instance = new DefaultValueCache(defaultValues)
static createInstance(Map defaultValues, List customDefaults = []){
instance = new DefaultValueCache(defaultValues, customDefaults)
}
Map getDefaultValues(){
@ -28,6 +33,12 @@ class DefaultValueCache implements Serializable {
instance = null
}
List getCustomDefaults() {
def result = []
result.addAll(customDefaults)
return result
}
static void prepare(Script steps, Map parameters = [:]) {
if(parameters == null) parameters = [:]
if(!DefaultValueCache.getInstance() || parameters.customDefaults) {
@ -46,7 +57,7 @@ class DefaultValueCache implements Serializable {
MapUtils.pruneNulls(defaultValues),
MapUtils.pruneNulls(configuration))
}
DefaultValueCache.createInstance(defaultValues)
DefaultValueCache.createInstance(defaultValues, customDefaults)
}
}
}

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,106 @@
import static org.hamcrest.Matchers.allOf
import static org.hamcrest.Matchers.contains
import static org.hamcrest.Matchers.containsInAnyOrder
import static org.hamcrest.Matchers.containsString
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 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.is
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.*', '{"dockerImage": "xs", "dockerPullImage": false, "credentialsId":"myCreds"}')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'getConfig.* (?!--contextConfig)', '{"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.* (?!--contextConfig)', '{"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 +110,93 @@ 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())
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) {
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*xsDeploy .*', '{"operationId": "1234"}')
// 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(), containsInAnyOrder(
'.pipeline/additionalConfigs/default_pipeline_environment.yml',
'.pipeline/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')
.hasArgument('--contextConfig'),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasProlog('./piper getConfig --stepMetadata \'.pipeline/metadata/xsDeploy.yaml\''),
new CommandLineMatcher()
.hasProlog('#!/bin/bash ./piper xsDeploy --defaultConfig ".pipeline/additionalConfigs/default_pipeline_environment.yml" --user \\$\\{USERNAME\\} --password \\$\\{PASSWORD\\}'),
not(new CommandLineMatcher()
.hasProlog('#!/bin/bash ./piper xsDeploy')
.hasOption('operationId', '1234'))
)
)
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.* (?!--contextConfig)', '{"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')
.hasOption('operationId', '1234')
)
assertThat(lockRule.getLockResources(), contains('xsDeploy:https://example.org/xs:myOrg:mySpace'))
}
@Test
public void testBlueGreenDeployResumeWithoutDeploymentId() {
@ -367,114 +205,96 @@ 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.* (?!--contextConfig)', '{"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() {
public void testBlueGreenDeployResumeOperationIdViaSignature() {
thrown.expect(AbortException)
thrown.expectMessage(
'For the current configuration an already existing session is required.' +
' But there is no already existing session')
// this happens in case we would like to complete a deployment without having a (successful) deployments before.
existingFiles.remove('.xsconfig')
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'getConfig.* (?!--contextConfig)', '{"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,
operationId: '1357'
)
assertThat(shellRule.shell,
new CommandLineMatcher()
.hasProlog('#!/bin/bash ./piper xsDeploy')
.hasOption('operationId', '1357')
)
}
@Test
public void testBlueGreenDeployResumeFails() {
public void testAdditionalCustomConfigLayers() {
// e.g. we try to resume a deployment which did not succeed or which was already resumed or aborted.
def resources = ['a.yml': '- x: y}', 'b.yml' : '- a: b}']
thrown.expect(AbortException)
thrown.expectMessage('Failed command(s): [xs bg-deploy -a resume].')
helper.registerAllowedMethod('libraryResource', [String], {
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'xs bg-deploy -i .*', 1)
r ->
nullScript.commonPipelineEnvironment.xsDeploymentId = '1234'
def resource = resources[r]
if(resource) return resource
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) {
File res = new File(new File('resources'), r)
if (res.exists()) {
return res.getText()
}
// logout must happen also in case of a failed deployment
assertThat(shellRule.shell,
new CommandLineMatcher()
.hasProlog('')
.hasSnippet('xs logout'))
throw e
throw new RuntimeException("Resource '${r}' not found.")
})
assertThat(nullScript.commonPipelineEnvironment.xsDeploymentId, nullValue())
shellRule.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*xsDeploy .*', '{"operationId": "1234"}')
nullScript.commonPipelineEnvironment = ['reset': {}, 'getCustomDefaults': {['a.yml', 'b.yml']}]
goUtils = new PiperGoUtils(null) {
void unstashPiperBin() {
}
}
}
@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'
piperGoUtils: goUtils
)
// there is no login in case of a resume since we have to use the old session which triggered the deployment.
assertThat(nullScript.commonPipelineEnvironment.xsDeploymentId, is('1234'))
assertThat(writeFileRule.files.keySet(), containsInAnyOrder(
'.pipeline/additionalConfigs/a.yml',
'.pipeline/additionalConfigs/b.yml',
'.pipeline/additionalConfigs/default_pipeline_environment.yml',
'.pipeline/metadata/xsDeploy.yaml',
))
assertThat(shellRule.shell,
allOf(
hasSize(3),
new CommandLineMatcher()
.hasProlog('#!/bin/bash')
.hasSnippet('xs bg-deploy')
.hasOption('i', '1234')
.hasOption('a', 'resume'),
.hasProlog('./piper getConfig')
.hasArgument('--contextConfig')
.hasArgument('--defaultConfig ".pipeline/additionalConfigs/b.yml" ".pipeline/additionalConfigs/a.yml" ".pipeline/additionalConfigs/default_pipeline_environment.yml"'),
new CommandLineMatcher()
.hasProlog("#!/bin/bash")
.hasSnippet('xs logout'),
new CommandLineMatcher()
.hasProlog('')
.hasSnippet('rm \\$\\{XSCONFIG\\}') // delete the session file after logout
.hasProlog('./piper getConfig --stepMetadata \'.pipeline/metadata/xsDeploy.yaml\''),
)
)
assertThat(lockRule.getLockResources(), contains('xsDeploy:https://example.org/xs:myOrg:mySpace'))
}
}

View File

@ -1,5 +1,6 @@
import com.sap.piper.ConfigurationLoader
import com.sap.piper.ConfigurationMerger
import com.sap.piper.DefaultValueCache
import com.sap.piper.analytics.InfluxData
class commonPipelineEnvironment implements Serializable {
@ -143,4 +144,8 @@ class commonPipelineEnvironment implements Serializable {
config = ConfigurationMerger.merge(configuration.get('stages')?.get(stageName) ?: [:], null, config)
return config
}
List getCustomDefaults() {
DefaultValueCache.getInstance().getCustomDefaults()
}
}

View File

@ -1,38 +1,21 @@
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.DefaultValueCache
import com.sap.piper.JenkinsUtils
import com.sap.piper.PiperGoUtils
import com.sap.piper.GenerateDocumentation
import com.sap.piper.Utils
import groovy.transform.Field
import hudson.AbortException
@Field String METADATA_FILE = 'metadata/xsDeploy.yaml'
@Field String PIPER_DEFAULTS = 'default_pipeline_environment.yml'
@Field String STEP_NAME = getClass().getName()
@Field String METADATA_FOLDER = '.pipeline' // metadata file contains already the "metadata" folder level, hence we end up in a folder ".pipeline/metadata"
@Field String ADDITIONAL_CONFIGS_FOLDER='.pipeline/additionalConfigs'
@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,
@ -55,284 +38,142 @@ enum Action {
}
}
/**
* Performs an XS deployment
*
* In case of blue-green deployments the step is called for the deployment in the narrower sense
* and later again for resuming or aborting. In this case both calls needs to be performed from the
* same directory.
*/
@GenerateDocumentation
void call(Map parameters = [:]) {
handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) {
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 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()
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. As long as we don't have it elsewhere we should keep it here.
def piperGoVersion = sh(returnStdout: true, script: "./piper version")
echo "PiperGoVersion: ${piperGoVersion}"
//
// since there is no valid config provided (... null) telemetry is disabled (same for other go releated steps at the moment).
utils.pushToSWA([
step: STEP_NAME,
], config)
], null)
if(action == Action.NONE) {
boolean deployableExists = fileExists file: config.mtaPath
if(! deployableExists)
error "Deployable '${config.mtaPath}' does not exist."
}
String configFiles = prepareConfigurations([PIPER_DEFAULTS].plus(script.commonPipelineEnvironment.getCustomDefaults()), ADDITIONAL_CONFIGS_FOLDER)
if(performLogin) {
login(script, config)
}
writeFile(file: "${METADATA_FOLDER}/${METADATA_FILE}", text: libraryResource(METADATA_FILE))
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?
complete(script, mode, action, config, failures)
String projectConfigScript = "./piper getConfig --stepMetadata '${METADATA_FOLDER}/${METADATA_FILE}' --defaultConfig ${configFiles}"
String contextConfigScript = projectConfigScript + " --contextConfig"
Map projectConfig = readJSON (text: sh(returnStdout: true, script: projectConfigScript))
Map contextConfig = readJSON (text: sh(returnStdout: true, script: contextConfigScript))
} else {
Action action = projectConfig.action
DeployMode mode = projectConfig.mode
deploy(script, mode, config, failures)
}
if(parameters.verbose) {
echo "[INFO] ContextConfig: ${contextConfig}"
echo "[INFO] ProjectConfig: ${projectConfig}"
}
if (performLogout || failures) {
logout(script, config, failures)
} else {
echo "Skipping logout in order to be able to resume or abort later."
}
if(failures) {
error "Failed command(s): ${failures}. Check earlier log for details."
}
}
}
void login(Script script, Map config) {
withCredentials([usernamePassword(
credentialsId: config.credentialsId,
passwordVariable: 'password',
usernameVariable: 'username'
)]) {
def returnCode = executeXSCommand([script: script].plus(config.docker),
[
"xs login -a ${config.apiUrl} -u ${username} -p ${BashUtils.quoteAndEscape(password)} -o ${config.org} -s ${config.space} ${config.loginOpts}",
'RC=$?',
"[ \$RC == 0 ] && cp \"\${HOME}/${config.xsSessionFile}\" .",
'exit $RC'
])
if(returnCode != 0)
error "xs login failed."
}
boolean existsXsSessionFileAfterLogin = fileExists file: config.xsSessionFile
if(! existsXsSessionFileAfterLogin)
error "Session file ${config.xsSessionFile} not found in current working directory after login."
}
void deploy(Script script, DeployMode mode, Map config, def failures) {
def deploymentLog
try {
lock(getLockIdentifier(config)) {
deploymentLog = executeXSCommand([script: script].plus(config.docker),
[
"cp ${config.xsSessionFile} \${HOME}",
"xs ${mode.toString()} '${config.mtaPath}' -f ${config.deployOpts}"
], true)
}
echo "Deploy log: ${deploymentLog}"
} catch(AbortException e) {
echo "deployment failed. Message: ${e.getMessage()}, Log: ${deploymentLog}}"
failures << "xs ${mode.toString()}"
}
if(mode == DeployMode.BG_DEPLOY) {
if(! failures.isEmpty()) {
echo "Retrieval of deploymentId skipped since prior deployment was not successfull."
} else {
for (def logLine : deploymentLog.readLines()) {
def matcher = logLine =~ config.deployIdLogPattern
if(matcher.find()) {
script.commonPipelineEnvironment.xsDeploymentId = matcher[0][1]
echo "DeploymentId: ${script.commonPipelineEnvironment.xsDeploymentId}."
break
def operationId = parameters.operationId
if(! operationId && mode == DeployMode.BG_DEPLOY && action != Action.NONE) {
operationId = script.commonPipelineEnvironment.xsDeploymentId
if (! operationId) {
throw new IllegalArgumentException('No operationId provided. Was there a deployment before?')
}
}
if(script.commonPipelineEnvironment.xsDeploymentId == null) {
failures << "Cannot lookup deploymentId. Search pattern was: '${config.deployIdLogPattern}'."
def xsDeployStdout
lock(getLockIdentifier(projectConfig)) {
withCredentials([usernamePassword(
credentialsId: contextConfig.credentialsId,
passwordVariable: 'PASSWORD',
usernameVariable: 'USERNAME')]) {
dockerExecute([script: this].plus([dockerImage: contextConfig.dockerImage, dockerPullImage: contextConfig.dockerPullImage])) {
xsDeployStdout = sh returnStdout: true, script: """#!/bin/bash
./piper xsDeploy --defaultConfig ${configFiles} --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) {
/*
* The returned string can be used directly in the command line for retrieving the configuration via go
*/
String prepareConfigurations(List configs, String configCacheFolder) {
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."
}
for(def customDefault : configs) {
writeFile(file: "${ADDITIONAL_CONFIGS_FOLDER}/${customDefault}", text: libraryResource(customDefault))
}
r
joinAndQuote(configs.reverse(), configCacheFolder)
}
/*
* prefix is supposed to be provided without trailing slash
*/
String joinAndQuote(List l, String prefix = '') {
_l = []
if(prefix == null) {
prefix = ''
}
if(prefix.endsWith('/') || prefix.endsWith('\\'))
throw new IllegalArgumentException("Provide prefix (${prefix}) without trailing slash")
for(def e : l) {
def _e = ''
if(prefix.length() > 0) {
_e += prefix
_e += '/'
}
_e += e
_l << '"' + _e + '"'
}
_l.join(' ')
}