diff --git a/documentation/docs/steps/cloudFoundryDeploy.md b/documentation/docs/steps/cloudFoundryDeploy.md
index f8c5cebf1..cb13fe4b9 100644
--- a/documentation/docs/steps/cloudFoundryDeploy.md
+++ b/documentation/docs/steps/cloudFoundryDeploy.md
@@ -61,6 +61,10 @@ Deployment can be done
- cfOrg
- cfSpace
+!!! note
+ Due to [an incompatible change](https://github.com/cloudfoundry/cli/issues/1445) in the Cloud Foundry CLI, multiple buildpacks are not supported by this step.
+ If your `application` contains a list of `buildpacks` instead a single `buildpack`, this will be automatically re-written by the step when blue-green deployment is used.
+
* `deployTool` defines the tool which should be used for deployment.
* `deployType` defines the type of deployment, either `standard` deployment which results in a system downtime or a zero-downtime `blue-green` deployment.
* `dockerImage` defines the Docker image containing the deployment tools (like cf cli, ...) and `dockerWorkspace` defines the home directory of the default user of the `dockerImage`
@@ -106,7 +110,11 @@ The following parameters can also be specified as step/stage/general parameters
## Example
```groovy
-artifactSetVersion script: this, buildTool: 'maven'
+cloudFoundryDeploy(
+ script: script,
+ deployType: 'blue-green',
+ cloudFoundry: [apiEndpoint: 'https://test.server.com', appName:'cfAppName', credentialsId: 'cfCredentialsId', manifest: 'cfManifest', org: 'cfOrg', space: 'cfSpace'],
+ deployTool: 'cf_native'
+)
```
-
diff --git a/documentation/docs/steps/healthExecuteCheck.md b/documentation/docs/steps/healthExecuteCheck.md
new file mode 100644
index 000000000..ca0cf1c08
--- /dev/null
+++ b/documentation/docs/steps/healthExecuteCheck.md
@@ -0,0 +1,62 @@
+# healthExecuteCheck
+
+## Description
+Calls the health endpoint url of the application.
+
+The intention of the check is to verify that a suitable health endpoint is available. Such a health endpoint is required for operation purposes.
+
+This check is used as a real-life test for your productive health endpoints.
+
+!!! note "Check Depth"
+ Typically, tools performing simple health checks are not too smart. Therefore it is important to choose an endpoint for checking wisely.
+
+ This check therefore only checks if the application/service url returns `HTTP 200`.
+
+ This is in line with health check capabilities of platforms which are used for example in load balancing scenarios. Here you can find an [example for Amazon AWS](http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-healthchecks.html).
+
+
+
+## Prerequisites
+
+Endpoint for health check is configured.
+
+!!! warning
+ The health endpoint needs to be available without authentication!
+
+!!! tip
+ If using Spring Boot framework, ideally the provided `/health` endpoint is used and extended by development. Further information can be found in the [Spring Boot documenation for Endpoints](http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html)
+
+
+## Example
+
+Pipeline step:
+
+```groovy
+healthExecuteCheck testServerUrl: 'https://testserver.com'
+```
+
+## Parameters
+
+| parameter | mandatory | default | possible values |
+| ----------|-----------|---------|-----------------|
+|script|yes|||
+|healthEndpoint|no|``||
+|testServerUrl|no|||
+
+
+Details:
+* `script` defines the global script environment of the Jenkinsfile run. Typically `this` is passed to this parameter. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for storing the measured duration.
+* Health check function is called providing full qualified `testServerUrl` (and optionally with `healthEndpoint` if endpoint is not the standard url) to the health check.
+* In case response of the call is different than `HTTP 200 OK` the **health check fails and the pipeline stops**.
+
+## Step configuration
+
+We recommend to define values of step parameters via [config.yml file](../configuration.md).
+
+In following sections the configuration is possible:
+
+| parameter | general | step | stage |
+| ----------|-----------|---------|-----------------|
+|script||||
+|healthEndpoint|X|X|X|
+|testServerUrl|X|X|X|
diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml
index 55601bfe8..b7747d903 100644
--- a/documentation/mkdocs.yml
+++ b/documentation/mkdocs.yml
@@ -12,6 +12,7 @@ nav:
- dockerExecuteOnKubernetes: steps/dockerExecuteOnKubernetes.md
- durationMeasure: steps/durationMeasure.md
- handlePipelineStepErrors: steps/handlePipelineStepErrors.md
+ - healthExecuteCheck: steps/healthExecuteCheck.md
- influxWriteData: steps/influxWriteData.md
- mavenExecute: steps/mavenExecute.md
- mtaBuild: steps/mtaBuild.md
diff --git a/pom.xml b/pom.xml
index 680a8c01c..c33f91185 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
4.0.0
com.sap.cp.jenkins
jenkins-library
- 0.7
+ 0.8
SAP CP Piper Library
Shared library containing steps and utilities to set up continuous deployment processes for SAP technologies.
diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml
index 7410dacbf..05ac19a78 100644
--- a/resources/default_pipeline_environment.yml
+++ b/resources/default_pipeline_environment.yml
@@ -139,6 +139,8 @@ steps:
workspace: '**/*.*'
stashExcludes:
workspace: 'nohup.out'
+ healthExecuteCheck:
+ healthEndpoint: ''
influxWriteData:
influxServer: 'jenkins'
mavenExecute:
diff --git a/src/com/sap/piper/CfManifestUtils.groovy b/src/com/sap/piper/CfManifestUtils.groovy
new file mode 100644
index 000000000..a65a3b2eb
--- /dev/null
+++ b/src/com/sap/piper/CfManifestUtils.groovy
@@ -0,0 +1,26 @@
+package com.sap.piper
+
+import com.cloudbees.groovy.cps.NonCPS
+
+class CfManifestUtils {
+ @NonCPS
+ static Map transform(Map manifest) {
+ if (manifest.applications[0].buildpacks) {
+ manifest['applications'].each { Map application ->
+ def buildpacks = application['buildpacks']
+ if (buildpacks) {
+ if (buildpacks instanceof List) {
+ if (buildpacks.size > 1) {
+ throw new RuntimeException('More than one Cloud Foundry Buildpack is not supported. Please check your manifest.yaml file.')
+ }
+ application['buildpack'] = buildpacks[0]
+ application.remove('buildpacks')
+ } else {
+ throw new RuntimeException('"buildpacks" in manifest.yaml is not a list. Please check your manifest.yaml file.')
+ }
+ }
+ }
+ }
+ return manifest
+ }
+}
diff --git a/test/groovy/CloudFoundryDeployTest.groovy b/test/groovy/CloudFoundryDeployTest.groovy
index f4e5d7326..a22a76f50 100644
--- a/test/groovy/CloudFoundryDeployTest.groovy
+++ b/test/groovy/CloudFoundryDeployTest.groovy
@@ -105,6 +105,11 @@ class CloudFoundryDeployTest extends BasePiperTest {
@Test
void testCfNativeWithAppName() {
+ jryr.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
+ helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
+ generatedFile = parameters.file
+ data = parameters.data
+ })
jsr.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
@@ -125,6 +130,11 @@ class CloudFoundryDeployTest extends BasePiperTest {
@Test
void testCfNativeWithAppNameCustomApi() {
+ jryr.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
+ helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
+ generatedFile = parameters.file
+ data = parameters.data
+ })
jsr.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
@@ -142,6 +152,11 @@ class CloudFoundryDeployTest extends BasePiperTest {
@Test
void testCfNativeWithAppNameCompatible() {
+ jryr.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
+ helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
+ generatedFile = parameters.file
+ data = parameters.data
+ })
jsr.step.cloudFoundryDeploy([
script: nullScript,
juStabUtils: utils,
@@ -165,7 +180,11 @@ class CloudFoundryDeployTest extends BasePiperTest {
@Test
void testCfNativeAppNameFromManifest() {
helper.registerAllowedMethod('fileExists', [String.class], { s -> return true })
- jryr.registerYaml('test.yml', "[applications: [[name: 'manifestAppName']]]")
+ jryr.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]")
+ helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
+ generatedFile = parameters.file
+ data = parameters.data
+ })
jsr.step.cloudFoundryDeploy([
script: nullScript,
@@ -185,6 +204,10 @@ class CloudFoundryDeployTest extends BasePiperTest {
void testCfNativeWithoutAppName() {
helper.registerAllowedMethod('fileExists', [String.class], { s -> return true })
jryr.registerYaml('test.yml', "applications: [[]]")
+ helper.registerAllowedMethod('writeYaml', [Map], { Map parameters ->
+ generatedFile = parameters.file
+ data = parameters.data
+ })
thrown.expect(hudson.AbortException)
thrown.expectMessage('[cloudFoundryDeploy] ERROR: No appName available in manifest test.yml.')
diff --git a/test/groovy/HealthExecuteCheckTest.groovy b/test/groovy/HealthExecuteCheckTest.groovy
new file mode 100644
index 000000000..6f2bdda4f
--- /dev/null
+++ b/test/groovy/HealthExecuteCheckTest.groovy
@@ -0,0 +1,88 @@
+#!groovy
+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.JenkinsLoggingRule
+import util.JenkinsReadYamlRule
+import util.JenkinsStepRule
+import util.Rules
+
+import static org.hamcrest.Matchers.*
+import static org.junit.Assert.assertThat
+
+class HealthExecuteCheckTest extends BasePiperTest {
+ private JenkinsStepRule jsr = new JenkinsStepRule(this)
+ private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this)
+ private ExpectedException thrown = ExpectedException.none()
+
+ @Rule
+ public RuleChain rules = Rules
+ .getCommonRules(this)
+ .around(new JenkinsReadYamlRule(this))
+ .around(jlr)
+ .around(jsr)
+ .around(thrown)
+
+
+ @Before
+ void init() throws Exception {
+ // register Jenkins commands with mock values
+ def command1 = "curl -so /dev/null -w '%{response_code}' http://testserver"
+ def command2 = "curl -so /dev/null -w '%{response_code}' http://testserver/endpoint"
+ helper.registerAllowedMethod('sh', [Map.class], {map ->
+ return map.script == command1 || map.script == command2 ? "200" : "404"
+ })
+ }
+
+ @Test
+ void testHealthCheckOk() throws Exception {
+ def testUrl = 'http://testserver/endpoint'
+
+ jsr.step.healthExecuteCheck(
+ script: nullScript,
+ testServerUrl: testUrl
+ )
+
+ assertThat(jlr.log, containsString("Health check for ${testUrl} successful"))
+ }
+
+ @Test
+ void testHealthCheck404() throws Exception {
+ def testUrl = 'http://testserver/404'
+
+ thrown.expect(Exception)
+ thrown.expectMessage('Health check failed: 404')
+
+ jsr.step.healthExecuteCheck(
+ script: nullScript,
+ testServerUrl: testUrl
+ )
+ }
+
+
+ @Test
+ void testHealthCheckWithEndPoint() throws Exception {
+ jsr.step.healthExecuteCheck(
+ script: nullScript,
+ testServerUrl: 'http://testserver',
+ healthEndpoint: 'endpoint'
+ )
+
+ assertThat(jlr.log, containsString("Health check for http://testserver/endpoint successful"))
+ }
+
+ @Test
+ void testHealthCheckWithEndPointTrailingSlash() throws Exception {
+ jsr.step.healthExecuteCheck(
+ script: nullScript,
+ testServerUrl: 'http://testserver/',
+ healthEndpoint: 'endpoint'
+ )
+
+ assertThat(jlr.log, containsString("Health check for http://testserver/endpoint successful"))
+ }
+
+}
diff --git a/test/groovy/com/sap/piper/CfManifestUtilsTest.groovy b/test/groovy/com/sap/piper/CfManifestUtilsTest.groovy
new file mode 100644
index 000000000..f2feadc7a
--- /dev/null
+++ b/test/groovy/com/sap/piper/CfManifestUtilsTest.groovy
@@ -0,0 +1,30 @@
+package com.sap.piper
+
+import org.junit.Test
+
+import static org.junit.Assert.*
+
+class CfManifestUtilsTest {
+
+ @Test
+ void testManifestTransform() {
+ Map testFixture = [applications: [[buildpacks: ['sap_java_buildpack']]]]
+ Map expected = [applications: [[buildpack: 'sap_java_buildpack']]]
+ def actual = CfManifestUtils.transform(testFixture)
+ assertEquals(expected, actual)
+ }
+
+ @Test(expected = RuntimeException)
+ void testManifestTransformMultipleBuildpacks() {
+ Map testFixture = [applications: [[buildpacks: ['sap_java_buildpack', 'another_buildpack']]]]
+ CfManifestUtils.transform(testFixture)
+ }
+
+ @Test
+ void testManifestTransformShouldNotChange() {
+ Map testFixture = [applications: [[buildpack: 'sap_java_buildpack']]]
+ Map expected = [applications: [[buildpack: 'sap_java_buildpack']]]
+ def actual = CfManifestUtils.transform(testFixture)
+ assertEquals(expected, actual)
+ }
+}
diff --git a/vars/cloudFoundryDeploy.groovy b/vars/cloudFoundryDeploy.groovy
index 7c5027970..867340e24 100644
--- a/vars/cloudFoundryDeploy.groovy
+++ b/vars/cloudFoundryDeploy.groovy
@@ -1,5 +1,6 @@
import com.sap.piper.Utils
import com.sap.piper.ConfigurationHelper
+import com.sap.piper.CfManifestUtils
import groovy.transform.Field
@@ -112,6 +113,7 @@ def deployCfNative (config) {
def deployCommand = 'push'
if (config.deployType == 'blue-green') {
deployCommand = 'blue-green-deploy'
+ handleLegacyCfManifest(config)
} else {
config.smokeTest = ''
}
@@ -129,7 +131,7 @@ def deployCfNative (config) {
}
sh """#!/bin/bash
- set +x
+ set +x
export HOME=${config.dockerWorkspace}
cf login -u \"${username}\" -p '${password}' -a ${config.cloudFoundry.apiEndpoint} -o \"${config.cloudFoundry.org}\" -s \"${config.cloudFoundry.space}\"
cf plugins
@@ -166,3 +168,18 @@ def deployMta (config) {
sh "cf logout"
}
}
+
+def handleLegacyCfManifest(config) {
+ def manifest = readYaml file: config.cloudFoundry.manifest
+ String originalManifest = manifest.toString()
+ manifest = CfManifestUtils.transform(manifest)
+ String transformedManifest = manifest.toString()
+ if (originalManifest != transformedManifest) {
+ echo """The file ${config.cloudFoundry.manifest} is not compatible with the Cloud Foundry blue-green deployment plugin. Re-writing inline.
+See this issue if you are interested in the background: https://github.com/cloudfoundry/cli/issues/1445.\n
+Original manifest file content: $originalManifest\n
+Transformed manifest file content: $transformedManifest"""
+ sh "rm ${config.cloudFoundry.manifest}"
+ writeYaml file: config.cloudFoundry.manifest, data: manifest
+ }
+}
diff --git a/vars/commonPipelineEnvironment.groovy b/vars/commonPipelineEnvironment.groovy
index b3e4ebfd9..7a78279e7 100644
--- a/vars/commonPipelineEnvironment.groovy
+++ b/vars/commonPipelineEnvironment.groovy
@@ -7,6 +7,12 @@ class commonPipelineEnvironment implements Serializable {
//stores the gitCommitId as well as additional git information for the build during pipeline run
String gitCommitId
String gitSshUrl
+ String gitHttpsUrl
+ String gitBranch
+
+ //GiutHub specific information
+ String githubOrg
+ String githubRepo
//stores properties for a pipeline which build an artifact and then bundles it into a container
private Map appContainerProperties = [:]
@@ -30,6 +36,11 @@ class commonPipelineEnvironment implements Serializable {
gitCommitId = null
gitSshUrl = null
+ gitHttpsUrl = null
+ gitBranch = null
+
+ githubOrg = null
+ githubRepo = null
influxCustomData = [:]
influxCustomDataMap = [pipeline_data: [:], step_data: [:]]
diff --git a/vars/healthExecuteCheck.groovy b/vars/healthExecuteCheck.groovy
new file mode 100644
index 000000000..995da8715
--- /dev/null
+++ b/vars/healthExecuteCheck.groovy
@@ -0,0 +1,48 @@
+import com.sap.piper.ConfigurationHelper
+import com.sap.piper.Utils
+import groovy.transform.Field
+
+@Field String STEP_NAME = 'healthExecuteCheck'
+@Field Set STEP_CONFIG_KEYS = [
+ 'healthEndpoint',
+ 'testServerUrl'
+]
+@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
+
+void call(Map parameters = [:]) {
+ handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) {
+ def script = parameters?.script ?: [commonPipelineEnvironment: commonPipelineEnvironment]
+ // load default & individual configuration
+ Map config = ConfigurationHelper
+ .loadStepDefaults(this)
+ .mixinGeneralConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
+ .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
+ .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS)
+ .mixin(parameters, PARAMETER_KEYS)
+ .withMandatoryProperty('testServerUrl')
+ .use()
+
+ new Utils().pushToSWA([step: STEP_NAME], config)
+
+ def checkUrl = config.testServerUrl
+ if(config.healthEndpoint){
+ if(!checkUrl.endsWith('/'))
+ checkUrl += '/'
+ checkUrl += config.healthEndpoint
+ }
+
+ def statusCode = curl(checkUrl)
+ if (statusCode != '200') {
+ error "Health check failed: ${statusCode}"
+ } else {
+ echo "Health check for ${checkUrl} successful"
+ }
+ }
+}
+
+def curl(url){
+ return sh(
+ returnStdout: true,
+ script: "curl -so /dev/null -w '%{response_code}' ${url}"
+ ).trim()
+}