mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-18 05:18:24 +02:00
Merge branch 'master' into pr/stepReturnVoid
This commit is contained in:
commit
dba20d496c
@ -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'
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
62
documentation/docs/steps/healthExecuteCheck.md
Normal file
62
documentation/docs/steps/healthExecuteCheck.md
Normal file
@ -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|
|
@ -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
|
||||
|
2
pom.xml
2
pom.xml
@ -10,7 +10,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.sap.cp.jenkins</groupId>
|
||||
<artifactId>jenkins-library</artifactId>
|
||||
<version>0.7</version>
|
||||
<version>0.8</version>
|
||||
|
||||
<name>SAP CP Piper Library</name>
|
||||
<description>Shared library containing steps and utilities to set up continuous deployment processes for SAP technologies.</description>
|
||||
|
@ -139,6 +139,8 @@ steps:
|
||||
workspace: '**/*.*'
|
||||
stashExcludes:
|
||||
workspace: 'nohup.out'
|
||||
healthExecuteCheck:
|
||||
healthEndpoint: ''
|
||||
influxWriteData:
|
||||
influxServer: 'jenkins'
|
||||
mavenExecute:
|
||||
|
26
src/com/sap/piper/CfManifestUtils.groovy
Normal file
26
src/com/sap/piper/CfManifestUtils.groovy
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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.')
|
||||
|
||||
|
88
test/groovy/HealthExecuteCheckTest.groovy
Normal file
88
test/groovy/HealthExecuteCheckTest.groovy
Normal file
@ -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"))
|
||||
}
|
||||
|
||||
}
|
30
test/groovy/com/sap/piper/CfManifestUtilsTest.groovy
Normal file
30
test/groovy/com/sap/piper/CfManifestUtilsTest.groovy
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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: [:]]
|
||||
|
48
vars/healthExecuteCheck.groovy
Normal file
48
vars/healthExecuteCheck.groovy
Normal file
@ -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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user