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

Add Docker deploy support to cloudFoundryDeploy (#977)

Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
Co-authored-by: Daniel Kurzynski <daniel.kurzynski@sap.com>
This commit is contained in:
René Kschamer 2020-02-25 10:20:15 +01:00 committed by GitHub
parent c15f6a03d2
commit d754a669b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 224 additions and 74 deletions

View File

@ -1,33 +1,34 @@
import com.sap.piper.JenkinsUtils
import org.junit.Before
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import util.BasePiperTest
import util.JenkinsCredentialsRule
import util.JenkinsEnvironmentRule
import util.JenkinsDockerExecuteRule
import util.JenkinsEnvironmentRule
import util.JenkinsFileExistsRule
import util.JenkinsLoggingRule
import util.JenkinsReadFileRule
import util.JenkinsReadYamlRule
import util.JenkinsShellCallRule
import util.JenkinsStepRule
import util.JenkinsWriteFileRule
import util.JenkinsReadYamlRule
import util.Rules
import static org.hamcrest.Matchers.stringContainsInOrder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.allOf
import static org.hamcrest.Matchers.containsString
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.hasEntry
import static org.hamcrest.Matchers.hasItem
import static org.hamcrest.Matchers.is
import static org.hamcrest.Matchers.not
import static org.hamcrest.Matchers.hasEntry
import static org.hamcrest.Matchers.allOf
import static org.hamcrest.Matchers.containsString
import static org.hamcrest.Matchers.stringContainsInOrder
import static org.junit.Assert.assertNotNull
import static org.junit.Assert.assertThat
import static org.junit.Assert.assertTrue
class CloudFoundryDeployTest extends BasePiperTest {
@ -42,6 +43,8 @@ class CloudFoundryDeployTest extends BasePiperTest {
private JenkinsEnvironmentRule environmentRule = new JenkinsEnvironmentRule(this)
private JenkinsReadYamlRule readYamlRule = new JenkinsReadYamlRule(this)
private JenkinsFileExistsRule fileExistsRule = new JenkinsFileExistsRule(this, [])
private JenkinsCredentialsRule credentialsRule = new JenkinsCredentialsRule(this)
private writeInfluxMap = [:]
@ -63,13 +66,17 @@ class CloudFoundryDeployTest extends BasePiperTest {
.around(fileExistsRule)
.around(dockerExecuteRule)
.around(environmentRule)
.around(new JenkinsCredentialsRule(this).withCredentials('test_cfCredentialsId', 'test_cf', '********'))
.around(credentialsRule)
.around(stepRule) // needs to be activated after dockerExecuteRule, otherwise executeDocker is not mocked
@Before
void init() {
UUID.metaClass.static.randomUUID = { -> 1}
helper.registerAllowedMethod('influxWriteData', [Map.class], {m ->
// removing additional credentials tests might have added; adding default credentials
credentialsRule.reset()
.withCredentials('test_cfCredentialsId', 'test_cf', '********')
UUID.metaClass.static.randomUUID = { -> 1 }
helper.registerAllowedMethod('influxWriteData', [Map.class], { m ->
writeInfluxMap = m
})
}
@ -140,6 +147,8 @@ class CloudFoundryDeployTest extends BasePiperTest {
assertThat(loggingRule.log, containsString('[cloudFoundryDeploy] WARNING! Found unsupported deployTool. Skipping deployment.'))
}
@Test
void testCfNativeWithAppName() {
readYamlRule.registerYaml('test.yml', "applications: [{name: 'manifestAppName'}]")
@ -219,6 +228,95 @@ class CloudFoundryDeployTest extends BasePiperTest {
assertThat(shellRule.shell, hasItem(containsString("cf logout")))
}
@Test
void testCfNativeWithDockerImage() {
// adding additional credentials for Docker registry authorization
credentialsRule.withCredentials('test_cfDockerCredentialsId', 'test_cf_docker', '********')
readYamlRule.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
generatedFile = parameters.file
data = parameters.data
})
stepRule.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
jenkinsUtilsStub: new JenkinsUtilsMock(),
deployTool: 'cf_native',
deployDockerImage: 'repo/image:tag',
cloudFoundry: [
org: 'testOrg',
space: 'testSpace',
credentialsId: 'test_cfCredentialsId',
appName: 'testAppName'
]
])
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
assertThat(shellRule.shell, hasItem(containsString('cf push testAppName --docker-image repo/image:tag')))
assertThat(shellRule.shell, hasItem(containsString('cf logout')))
}
@Test
void testCfNativeWithDockerImageAndCredentials() {
// adding additional credentials for Docker registry authorization
credentialsRule.withCredentials('test_cfDockerCredentialsId', 'test_cf_docker', '********')
readYamlRule.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
generatedFile = parameters.file
data = parameters.data
})
stepRule.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
jenkinsUtilsStub: new JenkinsUtilsMock(),
deployTool: 'cf_native',
deployDockerImage: 'repo/image:tag',
dockerCredentialsId: 'test_cfDockerCredentialsId',
cloudFoundry: [
org: 'testOrg',
space: 'testSpace',
credentialsId: 'test_cfCredentialsId',
appName: 'testAppName'
]
])
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry(equalTo('CF_DOCKER_PASSWORD'), equalTo("${'********'}")))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
assertThat(shellRule.shell, hasItem(containsString('cf push testAppName --docker-image repo/image:tag --docker-username test_cf_docker')))
assertThat(shellRule.shell, hasItem(containsString('cf logout')))
}
@Test
void testCfNativeWithManifestAndDockerCredentials() {
// Docker image can be done via manifest.yml; if a private Docker registry is used, --docker-username and DOCKER_PASSWORD
// must be set; this is checked by this test
// adding additional credentials for Docker registry authorization
credentialsRule.withCredentials('test_cfDockerCredentialsId', 'test_cf_docker', '********')
readYamlRule.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
generatedFile = parameters.file
data = parameters.data
})
stepRule.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
jenkinsUtilsStub: new JenkinsUtilsMock(),
deployTool: 'cf_native',
dockerCredentialsId: 'test_cfDockerCredentialsId',
cloudFoundry: [
org: 'testOrg',
space: 'testSpace',
credentialsId: 'test_cfCredentialsId',
appName: 'testAppName',
manifest: 'manifest.yml'
]
])
assertThat(dockerExecuteRule.dockerParams.dockerEnvVars, hasEntry(equalTo('CF_DOCKER_PASSWORD'), equalTo("${'********'}")))
assertThat(shellRule.shell, hasItem(containsString('cf login -u "test_cf" -p \'********\' -a https://api.cf.eu10.hana.ondemand.com -o "testOrg" -s "testSpace"')))
assertThat(shellRule.shell, hasItem(containsString("cf push testAppName -f 'manifest.yml' --docker-username test_cf_docker")))
assertThat(shellRule.shell, hasItem(containsString('cf logout')))
}
@Test
void testCfNativeAppNameFromManifest() {
fileExistsRule.registerExistingFile('test.yml')

View File

@ -1,10 +1,10 @@
package util
import com.lesfurets.jenkins.unit.BasePipelineTest
import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException
/**
* By default a user &quot;anonymous&quot; with password &quot;********&quot;
@ -32,6 +32,11 @@ class JenkinsCredentialsRule implements TestRule {
return this
}
JenkinsCredentialsRule reset(){
credentials.clear()
return this
}
@Override
Statement apply(Statement base, Description description) {
return statement(base)
@ -63,44 +68,49 @@ class JenkinsCredentialsRule implements TestRule {
})
testInstance.helper.registerAllowedMethod('withCredentials', [List, Closure], { config, closure ->
// there can be multiple credentials defined for the closure; collecting the necessary binding
// preparations and destructions before executing closure
def preparations = []
def destructions = []
config.each { cred ->
def credsId = cred.credentialsId
def credentialsBindingType = bindingTypes.get(credsId)
def creds = credentials.get(credsId)
def credsId = config[0].credentialsId
def credentialsBindingType = bindingTypes.get(credsId)
def creds = credentials.get(credsId)
def tokenVariable, usernameVariable, passwordVariable, prepare, destruct
if(credentialsBindingType == "usernamePassword") {
passwordVariable = config[0].passwordVariable
usernameVariable = config[0].usernameVariable
prepare = {
binding.setProperty(usernameVariable, creds?.user)
binding.setProperty(passwordVariable, creds?.passwd)
def tokenVariable, usernameVariable, passwordVariable, prepare, destruct
if (credentialsBindingType == "usernamePassword") {
passwordVariable = cred.passwordVariable
usernameVariable = cred.usernameVariable
preparations.add({
binding.setProperty(usernameVariable, creds?.user)
binding.setProperty(passwordVariable, creds?.passwd)
})
destructions.add({
binding.setProperty(usernameVariable, null)
binding.setProperty(passwordVariable, null)
})
} else if (credentialsBindingType == "string") {
tokenVariable = cred.variable
preparations.add({
binding.setProperty(tokenVariable, creds?.token)
})
destructions.add({
binding.setProperty(tokenVariable, null)
})
} else {
throw new RuntimeException("Unknown binding type")
}
destruct = {
binding.setProperty(usernameVariable, null)
binding.setProperty(passwordVariable, null)
}
} else if(credentialsBindingType == "string") {
tokenVariable = config[0].variable
prepare = {
binding.setProperty(tokenVariable, creds?.token)
}
destruct = {
binding.setProperty(tokenVariable, null)
}
} else {
throw new RuntimeException("Unknown binding type")
}
prepare()
preparations.each { it() }
try {
closure()
} finally {
destruct()
destructions.each { it() }
}
})
base.evaluate()
base.evaluate()
}
}
}

View File

@ -1,15 +1,13 @@
import com.sap.piper.BashUtils
import com.sap.piper.CfManifestUtils
import com.sap.piper.ConfigurationHelper
import com.sap.piper.GenerateDocumentation
import com.sap.piper.JenkinsUtils
import com.sap.piper.Utils
import groovy.transform.Field
import static com.sap.piper.Prerequisites.checkScript
import com.sap.piper.GenerateDocumentation
import com.sap.piper.Utils
import com.sap.piper.ConfigurationHelper
import com.sap.piper.CfManifestUtils
import com.sap.piper.BashUtils
import groovy.transform.Field
@Field String STEP_NAME = getClass().getName()
@Field Set GENERAL_CONFIG_KEYS = STEP_CONFIG_KEYS
@ -134,10 +132,24 @@ import groovy.transform.Field
*/
'smokeTestStatusCode',
/**
* Provides more output. May reveal sensitive information.
* @possibleValues true, false
*/
* Provides more output. May reveal sensitive information.
* @possibleValues true, false
*/
'verbose',
/**
* Docker image deployments are supported (via manifest file in general)[https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html#docker].
* If no manifest is used, this parameter defines the image to be deployed. The specified name of the image is
* passed to the `--docker-image` parameter of the cf CLI and must adhere it's naming pattern (e.g. REPO/IMAGE:TAG).
* See (cf CLI documentation)[https://docs.cloudfoundry.org/devguide/deploy-apps/push-docker.html] for details.
*
* Note: The used Docker registry must be visible for the targeted Cloud Foundry instance.
*/
'deployDockerImage',
/**
* If the specified image in `deployDockerImage` is contained in a Docker registry, which requires authorization
* this defines the credentials to be used.
*/
'dockerCredentialsId',
]
@Field Map CONFIG_KEY_COMPATIBILITY = [cloudFoundry: [apiEndpoint: 'cfApiEndpoint', appName:'cfAppName', credentialsId: 'cfCredentialsId', manifest: 'cfManifest', manifestVariablesFiles: 'cfManifestVariablesFiles', manifestVariables: 'cfManifestVariables', org: 'cfOrg', space: 'cfSpace']]
@ -259,7 +271,7 @@ def findMtar(){
error "[${STEP_NAME}] No *.mtar file found!"
}
def deployMta (config) {
def deployMta(config) {
if (config.mtaExtensionDescriptor == null) config.mtaExtensionDescriptor = ''
if (!config.mtaExtensionDescriptor.isEmpty() && !config.mtaExtensionDescriptor.startsWith('-e ')) config.mtaExtensionDescriptor = "-e ${config.mtaExtensionDescriptor}"
@ -301,7 +313,7 @@ private void handleCFNativeDeployment(Map config, script) {
checkAndUpdateDeployTypeForNotSupportedManifest(config)
if (config.deployType == 'blue-green') {
prepareBlueGreenCfNativeDeploy(config,script)
prepareBlueGreenCfNativeDeploy(config, script)
} else {
prepareCfPushCfNativeDeploy(config)
}
@ -309,19 +321,38 @@ private void handleCFNativeDeployment(Map config, script) {
echo "[${STEP_NAME}] CF native deployment (${config.deployType}) with:"
echo "[${STEP_NAME}] - cfAppName=${config.cloudFoundry.appName}"
echo "[${STEP_NAME}] - cfManifest=${config.cloudFoundry.manifest}"
echo "[${STEP_NAME}] - cfManifestVariables=${config.cloudFoundry.manifestVariables?:'none specified'}"
echo "[${STEP_NAME}] - cfManifestVariablesFiles=${config.cloudFoundry.manifestVariablesFiles?:'none specified'}"
echo "[${STEP_NAME}] - cfManifestVariables=${config.cloudFoundry.manifestVariables ?: 'none specified'}"
echo "[${STEP_NAME}] - cfManifestVariablesFiles=${config.cloudFoundry.manifestVariablesFiles ?: 'none specified'}"
echo "[${STEP_NAME}] - cfdeployDockerImage=${config.deployDockerImage ?: 'none specified'}"
echo "[${STEP_NAME}] - cfdockerCredentialsId=${config.dockerCredentialsId ?: 'none specified'}"
echo "[${STEP_NAME}] - smokeTestScript=${config.smokeTestScript}"
checkIfAppNameIsAvailable(config)
dockerExecute(
script: script,
dockerImage: config.dockerImage,
dockerWorkspace: config.dockerWorkspace,
stashContent: config.stashContent,
dockerEnvVars: [CF_HOME: "${config.dockerWorkspace}", CF_PLUGIN_HOME: "${config.dockerWorkspace}", STATUS_CODE: "${config.smokeTestStatusCode}"]
) {
deployCfNative(config)
def dockerCredentials = []
if (config.dockerCredentialsId != null && config.dockerCredentialsId != '') {
dockerCredentials.add(usernamePassword(
credentialsId: config.dockerCredentialsId,
passwordVariable: 'dockerPassword',
usernameVariable: 'dockerUsername'
))
}
withCredentials(dockerCredentials) {
dockerExecute(
script: script,
dockerImage: config.dockerImage,
dockerWorkspace: config.dockerWorkspace,
stashContent: config.stashContent,
dockerEnvVars: [
CF_HOME : "${config.dockerWorkspace}",
CF_PLUGIN_HOME : "${config.dockerWorkspace}",
// if the Docker registry requires authentication the DOCKER_PASSWORD env variable must be set
CF_DOCKER_PASSWORD: "${binding.hasVariable("dockerPassword") ? dockerPassword : ''}",
STATUS_CODE : "${config.smokeTestStatusCode}"
]
) {
deployCfNative(config)
}
}
}
@ -420,21 +451,32 @@ private checkIfAppNameIsAvailable(config) {
}
}
def deployCfNative (config) {
def deployStatement = "cf ${config.deployCommand} ${config.cloudFoundry.appName ?: ''} ${config.deployOptions?:''} -f '${config.cloudFoundry.manifest}' ${config.smokeTest} ${config.cfNativeDeployParameters}"
deploy(null, deployStatement, config, { c -> stopOldAppIfRunning(c) })
def deployCfNative(config) {
// the deployStatement is complex and has lot of options; using a list and findAll allows to put each option
// as a single list element; if a option is not set (= null or '') this removed before every element is joined
// via a single whitespace; results in a single line deploy statement
def deployStatement = [
'cf',
config.deployCommand,
config.cloudFoundry.appName,
config.deployOptions,
config.cloudFoundry.manifest ? "-f '${config.cloudFoundry.manifest}'" : null,
config.deployDockerImage ? "--docker-image ${config.deployDockerImage}" : null,
binding.hasVariable("dockerUsername") ? "--docker-username ${dockerUsername}}" : null,
config.smokeTest,
config.cfNativeDeployParameters
].findAll { s -> s != null && s != '' }.join(" ")
deploy(null, deployStatement, config, { c -> stopOldAppIfRunning(c) })
}
private deploy(def cfApiStatement, def cfDeployStatement, def config, Closure postDeployAction) {
private deploy(String cfApiStatement, String cfDeployStatement, config, Closure postDeployAction) {
withCredentials([usernamePassword(
credentialsId: config.cloudFoundry.credentialsId,
passwordVariable: 'password',
usernameVariable: 'username'
)]) {
def cfTraceFile = 'cf.log'
def deployScript = """#!/bin/bash
set +x
set -e
@ -446,7 +488,7 @@ private deploy(def cfApiStatement, def cfDeployStatement, def config, Closure po
${cfDeployStatement}
"""
if(config.verbose) {
if (config.verbose) {
// Password contained in output below is hidden by withCredentials
echo "[INFO][${STEP_NAME}] Executing command: '${deployScript}'."
}
@ -458,11 +500,11 @@ private deploy(def cfApiStatement, def cfDeployStatement, def config, Closure po
error "[${STEP_NAME}] ERROR: The execution of the deploy command failed, see the log for details."
}
if(config.verbose) {
if (config.verbose) {
handleCfCliLog(cfTraceFile)
}
if(postDeployAction) postDeployAction(config)
if (postDeployAction) postDeployAction(config)
sh "cf logout"
}