From 17e839051186c8c9378e9bd85e2fcadbb851a3ef Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Fri, 12 Oct 2018 16:06:41 +0200 Subject: [PATCH] add step healthExecuteCheck (#339) This step allows to perform a basic health check on an installed application. It verifies that your app has a simple health endpoint available and that there is no error when calling it. --- .../docs/steps/healthExecuteCheck.md | 62 +++++++++++++ documentation/mkdocs.yml | 1 + resources/default_pipeline_environment.yml | 2 + test/groovy/HealthExecuteCheckTest.groovy | 88 +++++++++++++++++++ vars/healthExecuteCheck.groovy | 46 ++++++++++ 5 files changed, 199 insertions(+) create mode 100644 documentation/docs/steps/healthExecuteCheck.md create mode 100644 test/groovy/HealthExecuteCheckTest.groovy create mode 100644 vars/healthExecuteCheck.groovy 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/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/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/vars/healthExecuteCheck.groovy b/vars/healthExecuteCheck.groovy new file mode 100644 index 000000000..13d5ecf06 --- /dev/null +++ b/vars/healthExecuteCheck.groovy @@ -0,0 +1,46 @@ +import com.sap.piper.ConfigurationHelper + +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() + + 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() +}