From 45170e979e96e74ffd9b667b673ab38fbc95b0ee Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 10 Dec 2018 11:41:31 +0100 Subject: [PATCH 01/49] Adapt mta build test test JenkinsShellCallRule a lot of code can be replaced by configuring the JenkinsShellCallRule accordingly. At the time when this test was provided there was not JenkinsShellCallRule ... --- test/groovy/MtaBuildTest.groovy | 88 ++++----------------------------- 1 file changed, 9 insertions(+), 79 deletions(-) diff --git a/test/groovy/MtaBuildTest.groovy b/test/groovy/MtaBuildTest.groovy index 05b238846..51c3f0c61 100644 --- a/test/groovy/MtaBuildTest.groovy +++ b/test/groovy/MtaBuildTest.groovy @@ -41,7 +41,14 @@ public class MtaBuildTest extends BasePiperTest { void init() { helper.registerAllowedMethod('fileExists', [String], { s -> false }) - helper.registerAllowedMethod('sh', [Map], { Map m -> getVersionWithoutEnvVars(m) }) + + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*\\$MTA_JAR_LOCATION.*', '') + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*\\$JAVA_HOME.*', '') + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*which java.*', 0) + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*java -version.*', '''openjdk version \"1.8.0_121\" + OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-1~bpo8+1-b13) + OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)''') + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*mta\\.jar -v.*', '1.0.6') binding.setVariable('PATH', '/usr/bin') } @@ -125,7 +132,7 @@ public class MtaBuildTest extends BasePiperTest { @Test void mtaJarLocationFromEnvironmentTest() { - helper.registerAllowedMethod('sh', [Map], { Map m -> getVersionWithEnvVars(m) }) + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*\\$MTA_JAR_LOCATION.*', '/env/mta/mta.jar') jsr.step.mtaBuild(script: nullScript, buildTarget: 'NEO') @@ -289,81 +296,4 @@ public class MtaBuildTest extends BasePiperTest { ''' } - private getVersionWithEnvVars(Map m) { - - if(m.script.contains('java -version')) { - return '''openjdk version \"1.8.0_121\" - OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-1~bpo8+1-b13) - OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)''' - } else if(m.script.contains('mta.jar -v')) { - return '1.0.6' - } else { - return getEnvVars(m) - } - } - - private getVersionWithoutEnvVars(Map m) { - - if(m.script.contains('java -version')) { - return '''openjdk version \"1.8.0_121\" - OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-1~bpo8+1-b13) - OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)''' - } else if(m.script.contains('mta.jar -v')) { - return '1.0.6' - } else { - return getNoEnvVars(m) - } - } - - private getVersionWithoutEnvVarsAndNotInCurrentDir(Map m) { - - if(m.script.contains('java -version')) { - return '''openjdk version \"1.8.0_121\" - OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-1~bpo8+1-b13) - OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)''' - } else if(m.script.contains('mta.jar -v')) { - return '1.0.6' - } else { - return getNoEnvVarsAndNotInCurrentDir(m) - } - } - - private getEnvVars(Map m) { - - if(m.script.contains('JAVA_HOME')) { - return '' - } else if(m.script.contains('MTA_JAR_LOCATION')) { - return '/env/mta/mta.jar' - } else if(m.script.contains('which java')) { - return 0 - } else { - return 0 - } - } - - private getNoEnvVars(Map m) { - - if(m.script.contains('JAVA_HOME')) { - return '' - } else if(m.script.contains('MTA_JAR_LOCATION')) { - return '' - } else if(m.script.contains('which java')) { - return 0 - } else { - return 0 - } - } - - private getNoEnvVarsAndNotInCurrentDir(Map m) { - - if(m.script.contains('JAVA_HOME')) { - return '' - } else if(m.script.contains('MTA_JAR_LOCATION')) { - return '' - } else if(m.script.contains('which java')) { - return 0 - } else { - return 1 - } - } } From 9157ad3ce63c578dfda27ce1846716f9ca40aeeb Mon Sep 17 00:00:00 2001 From: Christoph Szymanski Date: Fri, 14 Dec 2018 18:29:02 +0100 Subject: [PATCH 02/49] Typo in documentation --- documentation/docs/steps/neoDeploy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/steps/neoDeploy.md b/documentation/docs/steps/neoDeploy.md index 252e78be2..d7b7ebf6f 100644 --- a/documentation/docs/steps/neoDeploy.md +++ b/documentation/docs/steps/neoDeploy.md @@ -46,7 +46,7 @@ Note that a version is formed by `major.minor.patch`, and a version is compatibl | `script` | yes | | | | `warAction` | yes | `'deploy'` | `'deploy'`, `'rolling-update'` | -## Parameters when using WAR file deployment method witout .properties file - with parameters (WAR_PARAMS) +## Parameters when using WAR file deployment method without .properties file - with parameters (WAR_PARAMS) | parameter | mandatory | default | possible values | | -------------------|-----------|-------------------------------|-------------------------------------------------| From db9ba38ed12fa3844146e24165e402e5e7667414 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm <2292245+fwilhe@users.noreply.github.com> Date: Mon, 7 Jan 2019 12:54:00 +0100 Subject: [PATCH 03/49] Fail CF Deployment Shell Script Early (#426) Resolves #425 --- vars/cloudFoundryDeploy.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vars/cloudFoundryDeploy.groovy b/vars/cloudFoundryDeploy.groovy index b33d25ba8..81254ecad 100644 --- a/vars/cloudFoundryDeploy.groovy +++ b/vars/cloudFoundryDeploy.groovy @@ -148,6 +148,7 @@ def deployCfNative (config) { sh """#!/bin/bash set +x + set -e export HOME=${config.dockerWorkspace} cf login -u \"${username}\" -p '${password}' -a ${config.cloudFoundry.apiEndpoint} -o \"${config.cloudFoundry.org}\" -s \"${config.cloudFoundry.space}\" cf plugins @@ -200,6 +201,7 @@ def deployMta (config) { sh """#!/bin/bash export HOME=${config.dockerWorkspace} set +x + set -e cf api ${config.cloudFoundry.apiEndpoint} cf login -u ${username} -p '${password}' -a ${config.cloudFoundry.apiEndpoint} -o \"${config.cloudFoundry.org}\" -s \"${config.cloudFoundry.space}\" cf plugins From 724a851bcdd13dea09f39b78272a97982ed37bd9 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Tue, 8 Jan 2019 19:44:28 +0100 Subject: [PATCH 04/49] executeDockerOnKubernetes - specify custom shell (#428) Depending on the Docker image used the default shell will not work in certain cases. This extends the executeDockerOnKubernetes step to be able to use a custom shell according to https://github.com/jenkinsci/kubernetes-plugin#specifying-a-different-shell-command-other-than-binsh --- .../docs/steps/dockerExecuteOnKubernetes.md | 2 ++ test/groovy/DockerExecuteOnKubernetesTest.groovy | 15 +++++++++++++++ vars/dockerExecuteOnKubernetes.groovy | 7 ++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/documentation/docs/steps/dockerExecuteOnKubernetes.md b/documentation/docs/steps/dockerExecuteOnKubernetes.md index 1243b6228..4af1ec011 100644 --- a/documentation/docs/steps/dockerExecuteOnKubernetes.md +++ b/documentation/docs/steps/dockerExecuteOnKubernetes.md @@ -21,6 +21,7 @@ Executes a closure inside a container in a kubernetes pod. Proxy environment var |containerMap|no|`[:]`|| |containerName|no||| |containerPortMappings|no||| +|containerShell|no||| |containerWorkspaces|no||| |dockerEnvVars|no|`[:]`|| |dockerImage|yes||| @@ -37,6 +38,7 @@ Executes a closure inside a container in a kubernetes pod. Proxy environment var * `containerName`: optional configuration in combination with containerMap to define the container where the commands should be executed in * `containerPortMappings`: Map which defines per docker image the port mappings, like `containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]]` +* `containerShell` allows to specify the shell to be executed for container with containerName * `containerWorkspaces` specifies workspace (=home directory of user) per container. If not provided `dockerWorkspace` will be used. If empty, home directory will not be set. * `dockerImage` Name of the docker image that should be used. If empty, Docker is not used. * `dockerEnvVars` Environment variables to set in the container, e.g. [http_proxy:'proxy:8080'] diff --git a/test/groovy/DockerExecuteOnKubernetesTest.groovy b/test/groovy/DockerExecuteOnKubernetesTest.groovy index c55e9a499..fc590d42c 100644 --- a/test/groovy/DockerExecuteOnKubernetesTest.groovy +++ b/test/groovy/DockerExecuteOnKubernetesTest.groovy @@ -49,6 +49,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { def containersList = [] def imageList = [] def containerName = '' + def containerShell = '' def envList = [] def portList = [] def containerCommands = [] @@ -260,9 +261,23 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { assertThat(envList, hasItem(hasItem(allOf(hasEntry('key', 'customEnvKey'), hasEntry ('value','customEnvValue'))))) } + @Test + void testDockerExecuteOnKubernetesWithCustomShell() { + jsr.step.dockerExecuteOnKubernetes( + script: nullScript, + juStabUtils: utils, + dockerImage: 'maven:3.5-jdk-8-alpine', + containerShell: '/busybox/sh' + ) { + //nothing to exeute + } + assertThat(containerShell, is('/busybox/sh')) + } + private container(options, body) { containerName = options.name + containerShell = options.shell body() } } diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index e4eb2de89..700ad2bfc 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -16,6 +16,7 @@ import hudson.AbortException 'containerMap', //specify multiple images which then form a kubernetes pod, example: containerMap: ['maven:3.5-jdk-8-alpine': 'mavenexecute','selenium/standalone-chrome': 'selenium'] 'containerName', //optional configuration in combination with containerMap to define the container where the commands should be executed in 'containerPortMappings', //map which defines per docker image the port mappings, like containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]] + 'containerShell', // allows to specify the shell to be executed for container with containerName 'containerWorkspaces', //specify workspace (=home directory of user) per container. If not provided dockerWorkspace will be used. If empty, home directory will not be set. 'dockerImage', 'dockerWorkspace', @@ -77,7 +78,11 @@ void executeOnPod(Map config, utils, Closure body) { podTemplate(getOptions(config)) { node(config.uniqueId) { if (config.containerName) { - container(name: config.containerName){ + Map containerParams = [name: config.containerName] + if (config.containerShell) { + containerParams.shell = config.containerShell + } + container(containerParams){ try { utils.unstashAll(config.stashContent) body() From f41adcf2d162acd4825686054023469c04b701f9 Mon Sep 17 00:00:00 2001 From: Christoph Szymanski Date: Wed, 9 Jan 2019 10:41:02 +0100 Subject: [PATCH 05/49] Fixing PULL_REQUEST_TEMPLATE.md (#415) * Update PULL_REQUEST_TEMPLATE.md * Update PULL_REQUEST_TEMPLATE.md * CodeClimate --- .github/PULL_REQUEST_TEMPLATE.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9722797b7..74a197d2e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,4 @@ -**changes:** +# Changes -- -- [ ] add tests -- [ ] add documentation +- [ ] Tests +- [ ] Documentation From caf54b0e68262097881508bd89e263d7bd23761b Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Fri, 11 Jan 2019 08:02:30 +0100 Subject: [PATCH 06/49] docs: add reference to newman page (#430) --- documentation/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 8d081014c..e141fbbfa 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -22,6 +22,7 @@ nav: - mavenExecute: steps/mavenExecute.md - mtaBuild: steps/mtaBuild.md - neoDeploy: steps/neoDeploy.md + - newmanExecute: steps/newmanExecute.md - pipelineExecute: steps/pipelineExecute.md - pipelineRestartSteps: steps/pipelineRestartSteps.md - pipelineStashFiles: steps/pipelineStashFiles.md From 3cb70a2a48c7839790e16092a82f77ba55a5be57 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Fri, 11 Jan 2019 16:40:31 +0100 Subject: [PATCH 07/49] clean influx docs (#431) --- documentation/docs/steps/influxWriteData.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/steps/influxWriteData.md b/documentation/docs/steps/influxWriteData.md index a61a35243..9afb2518c 100644 --- a/documentation/docs/steps/influxWriteData.md +++ b/documentation/docs/steps/influxWriteData.md @@ -153,7 +153,7 @@ Measurements are potentially pre-fixed - see parameter `influxPrefix` above. | sonarqube_data |
  • blocker_issues
  • critical_issues
  • info_issues
  • major_issues
  • minor_issues
  • lines_of_code
  • ...
| Details see [InfluxDB plugin documentation](https://wiki.jenkins.io/display/JENKINS/InfluxDB+Plugin) | | jenkins_custom_data | Piper fills following colums by default:
  • build_result
  • build_result_key
  • build_step (->step in case of error)
  • build_error (->error message in case of error)
| filled by `commonPipelineEnvironment.setInfluxCustomDataProperty()` | | pipeline_data | Examples from the Piper templates:
  • build_duration
  • opa_duration
  • deploy_test_duration
  • deploy_test_duration
  • fortify_duration
  • release_duration
  • ...
| filled by step [`measureDuration`](durationMeasure.md) using parameter `measurementName`| -| step_data | Considered, e.g.:
  • build_quality (Milestone/Release)
  • build_url
  • bats
  • checkmarx
  • fortify
  • gauge
  • nsp
  • opa
  • opensourcedependency
  • ppms
  • jmeter
  • supa
  • snyk
  • sonar
  • sourceclear
  • uiveri5
  • vulas
  • whitesource
  • traceability
  • ...
  • xmakestage
  • xmakepromote
| filled by `commonPipelineEnvironment.setInfluxStepData()` | +| step_data | Considered, e.g.:
  • build_url
  • bats
  • checkmarx
  • fortify
  • gauge
  • nsp
  • snyk
  • sonar
  • ...
| filled by `commonPipelineEnvironment.setInfluxStepData()` | ### Examples for InfluxDB queries which can be used in Grafana From 4064e6ffe1e30e6b8bb65b64462e7fee8ff8d3b0 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Mon, 14 Jan 2019 10:25:47 +0100 Subject: [PATCH 08/49] piperStageWrapper - switch to parameter map for extensions (#424) switch to using a parameter map --- test/groovy/PiperStageWrapperTest.groovy | 22 ++++++++++++---------- test/resources/stages/test.groovy | 9 +++++---- vars/piperStageWrapper.groovy | 4 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/test/groovy/PiperStageWrapperTest.groovy b/test/groovy/PiperStageWrapperTest.groovy index 845a8b0cf..46b020273 100644 --- a/test/groovy/PiperStageWrapperTest.groovy +++ b/test/groovy/PiperStageWrapperTest.groovy @@ -54,7 +54,7 @@ class PiperStageWrapperTest extends BasePiperTest { @Test void testDefault() { - def testInt = 1 + def executed = false jsr.step.piperStageWrapper( script: nullScript, juStabUtils: utils, @@ -62,16 +62,16 @@ class PiperStageWrapperTest extends BasePiperTest { stageName: 'test' ) { - testInt ++ + executed = true } - assertThat(testInt, is(2)) + assertThat(executed, is(true)) assertThat(lockMap.size(), is(2)) assertThat(countNodeUsage, is(1)) } @Test void testNoLocking() { - def testInt = 1 + def executed = false jsr.step.piperStageWrapper( script: nullScript, juStabUtils: utils, @@ -81,9 +81,9 @@ class PiperStageWrapperTest extends BasePiperTest { stageName: 'test' ) { - testInt ++ + executed = true } - assertThat(testInt, is(2)) + assertThat(executed, is(true)) assertThat(lockMap.size(), is(0)) assertThat(countNodeUsage, is(1)) assertThat(nodeLabel, is('testLabel')) @@ -98,21 +98,23 @@ class PiperStageWrapperTest extends BasePiperTest { helper.registerAllowedMethod('load', [String.class], { return helper.loadScript('test/resources/stages/test.groovy') }) + nullScript.commonPipelineEnvironment.gitBranch = 'testBranch' - def testInt = 1 + def executed = false jsr.step.piperStageWrapper( script: nullScript, juStabUtils: utils, ordinal: 10, stageName: 'test' ) { - testInt ++ + executed = true } - assertThat(testInt, is(2)) + assertThat(executed, is(true)) assertThat(jlr.log, containsString('[piperStageWrapper] Running project interceptor \'.pipeline/extensions/test.groovy\' for test.')) assertThat(jlr.log, containsString('Stage Name: test')) - assertThat(jlr.log, containsString('Config:')) + assertThat(jlr.log, containsString('Config: [productiveBranch:master,')) + assertThat(jlr.log, containsString('testBranch')) } } diff --git a/test/resources/stages/test.groovy b/test/resources/stages/test.groovy index 675ab337b..7371e03ee 100644 --- a/test/resources/stages/test.groovy +++ b/test/resources/stages/test.groovy @@ -1,6 +1,7 @@ -void call(body, stageName, config) { - echo "Stage Name: ${stageName}" - echo "Config: ${config}" - body() +void call(Map params) { + echo "Stage Name: ${params.stageName}" + echo "Config: ${params.config}" + params.originalStage() + echo "Branch: ${params.script.commonPipelineEnvironment.gitBranch}" } return this diff --git a/vars/piperStageWrapper.groovy b/vars/piperStageWrapper.groovy index 04488c7c8..abf200841 100644 --- a/vars/piperStageWrapper.groovy +++ b/vars/piperStageWrapper.groovy @@ -81,7 +81,7 @@ private void executeStage(script, originalStage, stageName, config, utils) { echo "[${STEP_NAME}] Found global interceptor '${globalInterceptorFile}' for ${stageName}." // If we call the global interceptor, we will pass on originalStage as parameter body = { - globalInterceptorScript(body, stageName, config) + globalInterceptorScript(script: script, originalStage: body, stageName: stageName, config: config) } } @@ -90,7 +90,7 @@ private void executeStage(script, originalStage, stageName, config, utils) { Script projectInterceptorScript = load(projectInterceptorFile) echo "[${STEP_NAME}] Running project interceptor '${projectInterceptorFile}' for ${stageName}." // If we call the project interceptor, we will pass on body as parameter which contains either originalStage or the repository interceptor - projectInterceptorScript(body, stageName, config) + projectInterceptorScript(script: script, originalStage: body, stageName: stageName, config: config) } else { //TODO: assign projectInterceptorScript to body as done for globalInterceptorScript, currently test framework does not seem to support this case. Further investigations needed. body() From 20a54cf094038d766d618e7cb14c5d4998628f7d Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Mon, 14 Jan 2019 14:43:07 +0100 Subject: [PATCH 09/49] dockerExecute - extend Kubernetes capabilities (#432) allow `dockerExecute` to pass on to dockerExecuteOnKubernetes * containerCommand * containerShell --- documentation/docs/steps/dockerExecute.md | 5 ++++- .../docs/steps/dockerExecuteOnKubernetes.md | 3 ++- .../DockerExecuteOnKubernetesTest.groovy | 13 +++++++++++ test/groovy/DockerExecuteTest.groovy | 22 +++++++++++++++++++ vars/dockerExecute.groovy | 4 ++++ vars/dockerExecuteOnKubernetes.groovy | 4 +++- 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/documentation/docs/steps/dockerExecute.md b/documentation/docs/steps/dockerExecute.md index 64d98a3cb..e06f42609 100644 --- a/documentation/docs/steps/dockerExecute.md +++ b/documentation/docs/steps/dockerExecute.md @@ -7,11 +7,12 @@ The workspace is mounted into the docker image. Proxy environment variables defined on the Jenkins machine are also available in the Docker container. ## Parameters - | parameter | mandatory | default | possible values | | ----------|-----------|---------|-----------------| |script|yes||| +|containerCommand|no||| |containerPortMappings|no||| +|containerShell|no||| |dockerEnvVars|no|`[:]`|| |dockerImage|no|`''`|| |dockerName|no||| @@ -27,7 +28,9 @@ Proxy environment variables defined on the Jenkins machine are also available in |sidecarWorkspace|no||| * `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. +* `containerCommand`: only used in case exeuction environment is Kubernetes, allows to specify start command for container created with dockerImage parameter to overwrite Piper default (`/usr/bin/tail -f /dev/null`). * `containerPortMappings`: Map which defines per docker image the port mappings, like `containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]]` +* `containerShell`: only used in case exeuction environment is Kubernetes, allows to specify the shell to be used for execution of commands * `dockerEnvVars`: Environment variables to set in the container, e.g. [http_proxy:'proxy:8080'] * `dockerImage`: Name of the docker image that should be used. If empty, Docker is not used and the command is executed directly on the Jenkins system. * `dockerName`: Kubernetes case: Name of the container launching `dockerImage`, SideCar: Name of the container in local network diff --git a/documentation/docs/steps/dockerExecuteOnKubernetes.md b/documentation/docs/steps/dockerExecuteOnKubernetes.md index 4af1ec011..98b553386 100644 --- a/documentation/docs/steps/dockerExecuteOnKubernetes.md +++ b/documentation/docs/steps/dockerExecuteOnKubernetes.md @@ -16,6 +16,7 @@ Executes a closure inside a container in a kubernetes pod. Proxy environment var | parameter | mandatory | default | possible values | | ----------|-----------|---------|-----------------| |script|yes||| +|containerCommand|no||| |containerCommands|no||| |containerEnvVars|no||| |containerMap|no|`[:]`|| @@ -31,11 +32,11 @@ Executes a closure inside a container in a kubernetes pod. Proxy environment var |stashIncludes|no|`[workspace:**/*.*]`|| * `script` defines the global script environment of the Jenkins file run. Typically `this` is passed to this parameter. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for storing the measured duration. +* `containerCommand`: allows to specify start command for container created with dockerImage parameter to overwrite Piper default (`/usr/bin/tail -f /dev/null`). * `containerCommands` specifies start command for containers to overwrite Piper default (`/usr/bin/tail -f /dev/null`). If container's defaultstart command should be used provide empty string like: `['selenium/standalone-chrome': '']`. * `containerEnvVars` specifies environment variables per container. If not provided `dockerEnvVars` will be used. * `containerMap` A map of docker image to the name of the container. The pod will be created with all the images from this map and they are labled based on the value field of each map entry. Example: `['maven:3.5-jdk-8-alpine': 'mavenExecute', 'selenium/standalone-chrome': 'selenium', 'famiko/jmeter-base': 'checkJMeter', 's4sdk/docker-cf-cli': 'cloudfoundry']` - * `containerName`: optional configuration in combination with containerMap to define the container where the commands should be executed in * `containerPortMappings`: Map which defines per docker image the port mappings, like `containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]]` * `containerShell` allows to specify the shell to be executed for container with containerName diff --git a/test/groovy/DockerExecuteOnKubernetesTest.groovy b/test/groovy/DockerExecuteOnKubernetesTest.groovy index fc590d42c..4c7f885ac 100644 --- a/test/groovy/DockerExecuteOnKubernetesTest.groovy +++ b/test/groovy/DockerExecuteOnKubernetesTest.groovy @@ -274,6 +274,19 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { assertThat(containerShell, is('/busybox/sh')) } + @Test + void testDockerExecuteOnKubernetesWithCustomContainerCommand() { + jsr.step.dockerExecuteOnKubernetes( + script: nullScript, + juStabUtils: utils, + dockerImage: 'maven:3.5-jdk-8-alpine', + containerCommand: '/busybox/tail -f /dev/null' + ) { + //nothing to exeute + } + assertThat(containerCommands, hasItem('/busybox/tail -f /dev/null')) + } + private container(options, body) { containerName = options.name diff --git a/test/groovy/DockerExecuteTest.groovy b/test/groovy/DockerExecuteTest.groovy index ed47da9e1..ac8fe971b 100644 --- a/test/groovy/DockerExecuteTest.groovy +++ b/test/groovy/DockerExecuteTest.groovy @@ -104,6 +104,28 @@ class DockerExecuteTest extends BasePiperTest { assertTrue(bodyExecuted) } + @Test + void testExecuteInsidePodWithCustomCommandAndShell() throws Exception { + Map kubernetesConfig = [:] + helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], {Map config, Closure body -> + kubernetesConfig = config + return body() + }) + binding.setVariable('env', [ON_K8S: 'true']) + jsr.step.dockerExecute( + script: nullScript, + containerCommand: '/busybox/tail -f /dev/null', + containerShell: '/busybox/sh', + dockerImage: 'maven:3.5-jdk-8-alpine' + ){ + bodyExecuted = true + } + assertTrue(jlr.log.contains('Executing inside a Kubernetes Pod')) + assertThat(kubernetesConfig.containerCommand, is('/busybox/tail -f /dev/null')) + assertThat(kubernetesConfig.containerShell, is('/busybox/sh')) + assertTrue(bodyExecuted) + } + @Test void testExecuteInsideDockerContainer() throws Exception { jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine') { diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index 46e7401bc..484314d67 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -16,6 +16,8 @@ import groovy.transform.Field @Field Set PARAMETER_KEYS = [ 'containerPortMappings', + 'containerCommand', + 'containerShell', 'dockerEnvVars', 'dockerImage', 'dockerName', @@ -57,6 +59,8 @@ void call(Map parameters = [:], body) { if (!config.sidecarImage) { dockerExecuteOnKubernetes( script: script, + containerCommand: config.containerCommand, + containerShell: config.containerShell, dockerImage: config.dockerImage, dockerEnvVars: config.dockerEnvVars, dockerWorkspace: config.dockerWorkspace, diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index 700ad2bfc..8b1b1225e 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -11,7 +11,8 @@ import hudson.AbortException @Field def PLUGIN_ID_KUBERNETES = 'kubernetes' @Field Set GENERAL_CONFIG_KEYS = ['jenkinsKubernetes'] @Field Set PARAMETER_KEYS = [ - 'containerCommands', //specify start command for containers to overwrite Piper default (`/usr/bin/tail -f /dev/null`). If container's defaultstart command should be used provide empty string like: `['selenium/standalone-chrome': '']` + 'containerCommand', // specify start command for container created with dockerImage parameter to overwrite Piper default (`/usr/bin/tail -f /dev/null`). + 'containerCommands', //specify start command for containers to overwrite Piper default (`/usr/bin/tail -f /dev/null`). If container's default start command should be used provide empty string like: `['selenium/standalone-chrome': '']` 'containerEnvVars', //specify environment variables per container. If not provided dockerEnvVars will be used 'containerMap', //specify multiple images which then form a kubernetes pod, example: containerMap: ['maven:3.5-jdk-8-alpine': 'mavenexecute','selenium/standalone-chrome': 'selenium'] 'containerName', //optional configuration in combination with containerMap to define the container where the commands should be executed in @@ -49,6 +50,7 @@ void call(Map parameters = [:], body) { configHelper.withMandatoryProperty('dockerImage') config.containerName = 'container-exec' config.containerMap = ["${config.get('dockerImage')}": config.containerName] + config.containerCommands = config.containerCommand ? ["${config.get('dockerImage')}": config.containerCommand] : null } executeOnPod(config, utils, body) } From db5022a4ffc58496f89364fc0d21e3b98bc95c52 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Tue, 15 Jan 2019 13:32:01 +0100 Subject: [PATCH 10/49] New Scenario Step: SAP UI5 on SAP Cloud Platform (#388) This scenario builds a UI5 app and deploys it to SAP Cloud Platform (neo). --- .codeclimate.yml | 2 +- .../docs/scenarios/ui5-sap-cp/Readme.md | 87 +++++++++++ .../docs/scenarios/ui5-sap-cp/files/.npmrc | 11 ++ .../scenarios/ui5-sap-cp/files/Gruntfile.js | 10 ++ .../docs/scenarios/ui5-sap-cp/files/mta.yaml | 16 +++ .../scenarios/ui5-sap-cp/files/package.json | 10 ++ .../scenarios/ui5-sap-cp/images/pipeline.jpg | Bin 0 -> 18507 bytes documentation/mkdocs.yml | 2 + .../FioriOnCloudPlatformPipelineTest.groovy | 136 ++++++++++++++++++ vars/fioriOnCloudPlatformPipeline.groovy | 50 +++++++ 10 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 documentation/docs/scenarios/ui5-sap-cp/Readme.md create mode 100644 documentation/docs/scenarios/ui5-sap-cp/files/.npmrc create mode 100644 documentation/docs/scenarios/ui5-sap-cp/files/Gruntfile.js create mode 100644 documentation/docs/scenarios/ui5-sap-cp/files/mta.yaml create mode 100644 documentation/docs/scenarios/ui5-sap-cp/files/package.json create mode 100644 documentation/docs/scenarios/ui5-sap-cp/images/pipeline.jpg create mode 100644 test/groovy/FioriOnCloudPlatformPipelineTest.groovy create mode 100644 vars/fioriOnCloudPlatformPipeline.groovy diff --git a/.codeclimate.yml b/.codeclimate.yml index bb0336727..a7d27c56a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -10,7 +10,7 @@ plugins: editorconfig: .editorconfig # https://docs.codeclimate.com/docs/advanced-configuration#section-exclude-patterns exclude_patterns: - - "documentation/docs/images/" + - "documentation/**/images/" - "cfg/id_rsa.enc" fixme: enabled: true diff --git a/documentation/docs/scenarios/ui5-sap-cp/Readme.md b/documentation/docs/scenarios/ui5-sap-cp/Readme.md new file mode 100644 index 000000000..c5ed59514 --- /dev/null +++ b/documentation/docs/scenarios/ui5-sap-cp/Readme.md @@ -0,0 +1,87 @@ +# Create a Pipeline for SAP UI5 or SAP Fiori on SAP Cloud Platform + +Create an application based on SAP UI5 or SAP Fiori and deploy the build result into an SAP Cloud Platform account in the Neo environment. + +This document describes a scenario step, which means that it combines various different steps to create a complete pipeline. + + +## Prerequisites + +* You have installed the Java Runtime Environment 8. +* You have installed Jenkins 2.60.3 or higher. +* You have set up Project “Piper”. See [README](https://github.com/SAP/jenkins-library/blob/master/README.md). +* You have installed the Multi-Target Application (MTA) Archive Builder 1.0.6 or newer. See [SAP Development Tools](https://tools.hana.ondemand.com/#cloud). +* You have installed Node.js including node and npm. See [Node.js](https://nodejs.org/en/download/). +* You have installed the SAP Cloud Platform Neo Environment SDK. See [SAP Development Tools](https://tools.hana.ondemand.com/#cloud). + + +### Project Prerequisites + +This scenario step requires additional files in your project and the execution environment on your Jenkins instance. On the project level, provide and adjust the following template: + +| File Name | Description | +|-----|-----| +| [`.npmrc`](https://github.com/marcusholl/jenkins-library/tree/pr/scenarioUI5SAPCP/documentation/docs/scenarios/ui5-sap-cp/files/.npmrc) | This file contains a reference to the SAP NPM registry (`@sap:registry https://npm.sap.com`), which is required to fetch dependencies to build the application. Place it in the root directory of your project. | +| [`mta.yaml`](https://github.com/marcusholl/jenkins-library/tree/pr/scenarioUI5SAPCP/documentation/docs/scenarios/ui5-sap-cp/files/mta.yaml) | This file controls the behavior of the MTA toolset. Place it in your application root folder and adjust the values in brackets with your data. | +| [`package.json`](https://github.com/marcusholl/jenkins-library/tree/pr/scenarioUI5SAPCP/documentation/docs/scenarios/ui5-sap-cp/files/package.json) | This file lists the required development dependencies for the build. Add the content to your existing `package.json` file. | +| [`Gruntfile.js`](https://github.com/marcusholl/jenkins-library/tree/pr/scenarioUI5SAPCP/documentation/docs/scenarios/ui5-sap-cp/files/Gruntfile.js) | This file controls the grunt build. By default the tasks `clean`, `build`, and `lint` are executed. Place it in the root directory of your project. | + + +## Context + +In this scenario step, we want to show how to build an application based on SAP UI5 or SAP Fiori by using the multi-target application (MTA) concept and how to deploy the build result into an SAP Cloud Platform account in the Neo environment. This document comprises the [mtaBuild](https://sap.github.io/jenkins-library/steps/mtaBuild/) and the [neoDeploy](https://sap.github.io/jenkins-library/steps/neoDeploy/) steps. + +![This pipeline in Jenkins Blue Ocean](images/pipeline.jpg) + +## Example + +### Jenkinsfile + +Following the convention for pipeline definitions, use a `Jenkinsfile` which resides in the root directory of your development sources. + +```groovy +@Library('piper-lib-os') _ + +fioriOnCloudPlatformPipeline script:this +``` + +### Configuration (`.pipeline/config.yml`) + +This is a basic configuration example, which is also located in the sources of the project. + +```yaml +steps: + mtaBuild: + buildTarget: 'NEO' + mtaJarLocation: '/opt/sap/mta.jar' + neoDeploy: + neoCredentialsId: 'NEO_DEPLOY' + neoHome: '/opt/sap/neo-sdk/' + account: 'your-account-id' + host: 'hana.ondemand.com' +``` + +#### Configuration for the MTA Build + +| Parameter | Description | +| -----------------|----------------| +| `buildTarget` | The target platform to which the mtar can be deployed. Possible values are: `CF`, `NEO`, `XSA` | +| `mtaJarLocation` | The location of the multi-target application archive builder jar file, including file name and extension. | + + +#### Configuration for the Deployment to SAP Cloud Platform + +| Parameter | Description | +| -------------------|-------------| +| `account` | The SAP Cloud Platform account to deploy to. | +| `host` | The SAP Cloud Platform host to deploy to. | +| `neoCredentialsId` | The Jenkins credentials that contain the user and password which are used for the deployment on SAP Cloud Platform. | +| `neoHome` | The path to the `neo-java-web-sdk` tool that is used for the deployment. | + + +### Parameters + +For the detailed description of the relevant parameters, see: + +* [mtaBuild](https://sap.github.io/jenkins-library/steps/mtaBuild/) +* [neoDeploy](https://sap.github.io/jenkins-library/steps/neoDeploy/) diff --git a/documentation/docs/scenarios/ui5-sap-cp/files/.npmrc b/documentation/docs/scenarios/ui5-sap-cp/files/.npmrc new file mode 100644 index 000000000..b744be289 --- /dev/null +++ b/documentation/docs/scenarios/ui5-sap-cp/files/.npmrc @@ -0,0 +1,11 @@ +# This file can be ommitted in the project if it is ensured +# that the corresponding configuration is provided on a +# higher level in the npm config (either on user level ~/.npmrc or +# globally). For more details with regards to configuring npm check +# man pages for npm-config/npmrc + +# The public npm registry from where to fetch e.g. Grunt +registry=https://registry.npmjs.org + +# The SAP npm registry from where to fetch SAP specific Grunt modules +@sap:registry=https://npm.sap.com diff --git a/documentation/docs/scenarios/ui5-sap-cp/files/Gruntfile.js b/documentation/docs/scenarios/ui5-sap-cp/files/Gruntfile.js new file mode 100644 index 000000000..2075e5995 --- /dev/null +++ b/documentation/docs/scenarios/ui5-sap-cp/files/Gruntfile.js @@ -0,0 +1,10 @@ +module.exports = function (grunt) { + 'use strict'; + grunt.loadNpmTasks('@sap/grunt-sapui5-bestpractice-build'); + + grunt.registerTask('default', [ + 'lint', + 'clean', + 'build' + ]); +}; diff --git a/documentation/docs/scenarios/ui5-sap-cp/files/mta.yaml b/documentation/docs/scenarios/ui5-sap-cp/files/mta.yaml new file mode 100644 index 000000000..a21553035 --- /dev/null +++ b/documentation/docs/scenarios/ui5-sap-cp/files/mta.yaml @@ -0,0 +1,16 @@ +_schema-version: "2.0.0" +ID: "" +version: + +parameters: + hcp-deployer-version: "1.0.0" + +modules: + - name: "" + type: html5 + path: . + parameters: + version: -${timestamp} + build-parameters: + builder: grunt + build-result: dist diff --git a/documentation/docs/scenarios/ui5-sap-cp/files/package.json b/documentation/docs/scenarios/ui5-sap-cp/files/package.json new file mode 100644 index 000000000..1fd106c7e --- /dev/null +++ b/documentation/docs/scenarios/ui5-sap-cp/files/package.json @@ -0,0 +1,10 @@ +{ + "name": "", + "version": "", + "description": "", + "private": true, + "devDependencies": { + "grunt": "1.0.1", + "@sap/grunt-sapui5-bestpractice-build": "1.3.17" + } +} diff --git a/documentation/docs/scenarios/ui5-sap-cp/images/pipeline.jpg b/documentation/docs/scenarios/ui5-sap-cp/images/pipeline.jpg new file mode 100644 index 0000000000000000000000000000000000000000..09efda5db572c96a94cb0a02524d459a47740e1c GIT binary patch literal 18507 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 literal 0 HcmV?d00001 diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index e141fbbfa..ca1287f92 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -34,6 +34,8 @@ nav: - transportRequestCreate: steps/transportRequestCreate.md - transportRequestRelease: steps/transportRequestRelease.md - transportRequestUploadFile: steps/transportRequestUploadFile.md + - 'Scenarios': + - 'Fiori (MTA) build and deployment to SAP Cloud Platform': scenarios/ui5-sap-cp/Readme.md - 'Required Plugins': jenkins/requiredPlugins.md theme: diff --git a/test/groovy/FioriOnCloudPlatformPipelineTest.groovy b/test/groovy/FioriOnCloudPlatformPipelineTest.groovy new file mode 100644 index 000000000..ac324c9c9 --- /dev/null +++ b/test/groovy/FioriOnCloudPlatformPipelineTest.groovy @@ -0,0 +1,136 @@ +import static org.hamcrest.Matchers.allOf +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.equalTo +import static org.hamcrest.Matchers.hasItem +import static org.hamcrest.Matchers.is +import static org.hamcrest.Matchers.subString +import static org.junit.Assert.assertThat + +import org.hamcrest.Matchers +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain + +import com.sap.piper.JenkinsUtils + +import util.BasePiperTest +import util.JenkinsCredentialsRule +import util.JenkinsReadYamlRule +import util.JenkinsShellCallRule +import util.JenkinsStepRule +import util.Rules + +class FioriOnCloudPlatformPipelineTest extends BasePiperTest { + + /* This scenario builds a fiori app and deploys it into an neo account. + The build is performed using mta, which delegates to grunt. grunt in + turn makes use of the 'sap/grunt-sapui5-bestpractice-build' plugin. + The dependencies are resolved via npm. + + In order to run the scenario the project needs to fullfill these + prerequisites: + + Build tools: + * mta.jar available + * npm installed + + Project configuration: + * sap registry `@sap:registry=https://npm.sap.com` configured in + .npmrc (either in the project or on any other suitable level) + * dependency to `@sap/grunt-sapui5-bestpractice-build` declared in + package.json + * npmTask `@sap/grunt-sapui5-bestpractice-build` loaded inside + Gruntfile.js and configure default tasks (e.g. lint, clean, build) + * mta.yaml + */ + + JenkinsStepRule jsr = new JenkinsStepRule(this) + JenkinsReadYamlRule jryr = new JenkinsReadYamlRule(this) + JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) + + @Rule + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(jryr) + .around(jsr) + .around(jscr) + .around(new JenkinsCredentialsRule(this) + .withCredentials('CI_CREDENTIALS_ID', 'foo', 'terceSpot')) + + @Before + void setup() { + // + // needed since we have dockerExecute inside mtaBuild + JenkinsUtils.metaClass.static.isPluginActive = {def s -> false} + + // + // Things we validate: + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*echo \\$JAVA_HOME.*', '/opt/sap/java') + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*echo \\$MTA_JAR_LOCATION.*', '/opt/sap') + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*echo \\$NEO_HOME.*', '/opt/sap/neo') + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, ".*bin/java -version.*", '1.8.0') // the java version + jscr.setReturnValue(JenkinsShellCallRule.Type.REGEX, ".*bin/java -jar .*mta.jar", '1.36.0') // the mta version + + // + // there is a check for the mta.yaml file and for the deployable test.mtar file + helper.registerAllowedMethod('fileExists', [String],{ + + it -> + + // called inside mtaBuild, this file contains build config + it == 'mta.yaml' || + + // called inside neo deploy, this file gets deployed + it == 'test.mtar' + }) + + // + // the properties below we read out of the yaml file + jryr.registerYaml('mta.yaml', (''' + |ID : "test" + |PATH : "." + |''' as CharSequence).stripMargin()) + + // + // we need the path variable since we extend the path in the mtaBuild step. In order + // to be able to extend the path we have to have some initial value. + binding.setVariable('PATH', '/usr/bin') + + } + + @Test + void straightForwardTest() { + + nullScript + .commonPipelineEnvironment + .configuration = [steps: + [neoDeploy: + [ host: 'hana.example.com', + account: 'myTestAccount', + ] + ] + ] + + jsr.step.fioriOnCloudPlatformPipeline(script: nullScript) + + // + // the mta build call: + assertThat(jscr.shell, hasItem( + allOf( containsString('java -jar /opt/sap/mta.jar'), + containsString('--mtar test.mtar'), + containsString('--build-target=NEO'), + containsString('build')))) + + // + // the deployable is exchanged between the involved steps via this property: + assertThat(nullScript.commonPipelineEnvironment.getMtarFilePath(), is(equalTo('test.mtar'))) + + // + // the neo deploy call: + assertThat(jscr.shell, hasItem('#!/bin/bash "/opt/sap/neo/tools/neo.sh" deploy-mta --source "test.mtar" ' + + '--host \'hana.example.com\' --account \'myTestAccount\' --synchronous ' + + '--user \'foo\' --password \'terceSpot\'')) + } +} diff --git a/vars/fioriOnCloudPlatformPipeline.groovy b/vars/fioriOnCloudPlatformPipeline.groovy new file mode 100644 index 000000000..8003c36b9 --- /dev/null +++ b/vars/fioriOnCloudPlatformPipeline.groovy @@ -0,0 +1,50 @@ +import static com.sap.piper.Prerequisites.checkScript + +import groovy.transform.Field + +@Field def STEP_NAME = getClass().getName() + +@Field def GENERAL_CONFIG_KEYS = [] +@Field def PARAMETER_KEYS = [] +@Field def STEP_CONFIG_KEYS = [] + +/** The Scenario is intended for building and uploading a fiori application. + * + * It needs to be called from a pipeline script (Jenkinsfile) like: + * ``` + * @Library('piper-lib-os') _ + * @Library('your-additional-lib') __ // optional + * + * // parameter 'customDefaults' below is optional + * fioriOnCloudPlatformPipeline(script: this, customDefaults: '') + * ``` + */ +void call(parameters = [:]) { + + checkScript(this, parameters) + + node(parameters.label) { + + // + // Cut and paste lines below in order to create a pipeline from this scenario + // In this case `parameters` needs to be replaced by `script: this`. + + stage('prepare') { + + setupCommonPipelineEnvironment(parameters) + } + + stage('build') { + + mtaBuild(parameters) + } + + stage('deploy') { + + neoDeploy(parameters) + } + + // Cut and paste lines above in order to create a pipeline from this scenario + // + } +} From 4803695185dfe69f7859428c91e7e6063b2099ff Mon Sep 17 00:00:00 2001 From: SarahNoack <44202907+SarahNoack@users.noreply.github.com> Date: Tue, 15 Jan 2019 17:45:26 +0100 Subject: [PATCH 11/49] Documentation: Hybrid Applications with Jenkins and SAP Solution Manager (#410) --- .../docs/scenarios/changeManagement.md | 87 +++++++++++++++++++ documentation/mkdocs.yml | 1 + 2 files changed, 88 insertions(+) create mode 100644 documentation/docs/scenarios/changeManagement.md diff --git a/documentation/docs/scenarios/changeManagement.md b/documentation/docs/scenarios/changeManagement.md new file mode 100644 index 000000000..c9147c0aa --- /dev/null +++ b/documentation/docs/scenarios/changeManagement.md @@ -0,0 +1,87 @@ +# Develop Hybrid Applications with Jenkins and SAP Solution Manager + +Set up an agile development process with Jenkins CI, which automatically feeds changes into SAP Solution Manager. + +## Prerequisites + +* You have installed the Java Runtime Environment 8. +* You have installed Jenkins 2.60.3 or higher. +* You have set up Project “Piper”. See [README](https://github.com/SAP/jenkins-library/blob/master/README.md). +* You have installed SAP Solution Manager 7.2 SP6. See [README](https://github.com/SAP/devops-cm-client/blob/master/README.md). +* You have installed the Multi-Target Application (MTA) Archive Builder 1.0.6 or newer. See [SAP Development Tools](https://tools.hana.ondemand.com/#cloud). +* You have installed Node.js including node and npm. See https://nodejs.org/en/download/. + +## Context + +In many SAP development scenarios, it is vital to synchronize both backend and frontend deliveries. These deliveries are typically an SAP UI5 application and an ABAP backend from which it is served. The SAP UI5 parts are often developed using agile practices and use Continuous Integration pipelines that automatically build, test, and deploy the application. + +In this scenario, we want to show how an agile development process with Jenkins CI can automatically feed changes into SAP Solution Manager. In SAP Solution Manager, all parts of the application stack come together and can be subject to classic change and transport management. + +The basic workflow is as follows: + +1. The pipeline scans the Git commit messages between `origin/master` and `HEAD` for a line like `ChangeDocument : `, and validates that the change is in the correct status `in development`.The template for the commit message looks as follows: + + ``` + + + + + ChangeDocument: + ``` + +2. To communicate with SAP Solution Manager, the pipeline uses credentials that must be stored on Jenkins under the label `CM`. +3. The required transport request is created on the fly. However, the change document can contain more components (for example, UI and backend components). +4. The changes of your development team trigger the Jenkins pipeline. It builds and validates the changes and attaches them to the respective transport request. +5. As soon as the development process is completed, the change document in SAP Solution Manager can be set to status `to be tested` and all components can be transported to the test system. + +## Example + +### Jenkinsfile + +```groovy +@Library('piper-library-os') _ + +node() { + + stage('prepare') { + checkout scm + setupCommonPipelineEnvironment script:this + checkChangeInDevelopment script: this + } + + stage('buildMta') { + mtaBuild script: this + } + + stage('uploadToTransportRequest') { + transportRequestCreate script: this + transportRequestUploadFile script:this + transportRequestRelease script: this + } + +} +``` + +### Configuration (`.pipeline/config.yml`) + +```yaml +#Steps Specific Configuration +general: + changeManagement: + endpoint: 'https:///sap/opu/odata/sap/AI_CRM_GW_CM_CI_SRV' + credentialsId: 'CM' + type: 'SOLMAN' +steps: + mtaBuild: + buildTarget: 'NEO' + transportRequestUploadFile: + applicationId: 'HCP' +``` + +### Parameters + +For the detailed description of the relevant parameters, see: + +* [checkChangeInDevelopment](https://sap.github.io/jenkins-library/steps/checkChangeInDevelopment/) +* [mtaBuild](https://sap.github.io/jenkins-library/steps/mtaBuild/) +* [transportRequestUploadFile](https://sap.github.io/jenkins-library/steps/transportRequestUploadFile/) diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index ca1287f92..5919e5ec8 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -35,6 +35,7 @@ nav: - transportRequestRelease: steps/transportRequestRelease.md - transportRequestUploadFile: steps/transportRequestUploadFile.md - 'Scenarios': + - 'Develop Hybrid Applications with Jenkins and SAP Solution Manager': scenarios/changeManagement.md - 'Fiori (MTA) build and deployment to SAP Cloud Platform': scenarios/ui5-sap-cp/Readme.md - 'Required Plugins': jenkins/requiredPlugins.md From e0da4aedfeb52d50b8fc88216909ca47533c5100 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Wed, 16 Jan 2019 14:17:55 +0100 Subject: [PATCH 12/49] bump version (#433) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 352a56770..6d5d89550 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ 4.0.0 com.sap.cp.jenkins jenkins-library - 0.8 + 0.9 SAP CP Piper Library Shared library containing steps and utilities to set up continuous deployment processes for SAP technologies. From f988c908ce59de2aef3774fa111168f09537811a Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Thu, 17 Jan 2019 09:23:21 +0100 Subject: [PATCH 13/49] Travis: remove reporting to coveralls (#443) * remove coveralls reporting * remove badge * remove plugin dependency --- .travis.yml | 1 - README.md | 1 - pom.xml | 5 ----- 3 files changed, 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index eda937618..9b6f69c00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,6 @@ jobs: after_script: - JACOCO_SOURCE_PATH="src vars test" ./cc-test-reporter format-coverage target/site/jacoco/jacoco.xml --input-type jacoco - ./cc-test-reporter upload-coverage - - mvn -DrepoToken=$COVERALLS_REPO_TOKEN org.eluder.coveralls:coveralls-maven-plugin:report - name: Docs Build if: type = pull_request install: docker pull squidfunk/mkdocs-material:3.0.4 diff --git a/README.md b/README.md index d832e97f2..0c3900cba 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![Build Status](https://travis-ci.org/SAP/jenkins-library.svg?branch=master)](https://travis-ci.org/SAP/jenkins-library) -[![Coverage Status](https://coveralls.io/repos/github/SAP/jenkins-library/badge.svg?branch=master)](https://coveralls.io/github/SAP/jenkins-library?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/0e6a23344616e29b4ed0/maintainability)](https://codeclimate.com/github/SAP/jenkins-library/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/0e6a23344616e29b4ed0/test_coverage)](https://codeclimate.com/github/SAP/jenkins-library/test_coverage) diff --git a/pom.xml b/pom.xml index 6d5d89550..d8307343b 100644 --- a/pom.xml +++ b/pom.xml @@ -236,11 +236,6 @@ - - org.eluder.coveralls - coveralls-maven-plugin - 4.3.0 - From 4e20f61dc4ebc1ce4f5ba5d619e541588a2e7863 Mon Sep 17 00:00:00 2001 From: weloli <45266922+weloli@users.noreply.github.com> Date: Thu, 17 Jan 2019 12:36:13 +0100 Subject: [PATCH 14/49] fix documentation --- documentation/docs/steps/newmanExecute.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/steps/newmanExecute.md b/documentation/docs/steps/newmanExecute.md index 7a2bffe2a..290d8fe2b 100644 --- a/documentation/docs/steps/newmanExecute.md +++ b/documentation/docs/steps/newmanExecute.md @@ -16,11 +16,11 @@ Pipeline step: newmanExecute script: this ``` -This step should be used in combination with `publishTestResults`: +This step should be used in combination with `testsPublishResults`: ```groovy newmanExecute script: this, failOnError: false -publishTestResults script: this, junit: [pattern: '**/newman/TEST-newman.xml'] +testsPublishResults script: this, junit: [pattern: '**/newman/TEST-*.xml'] ``` ## Parameters From 98139bb49825b27ed159cbdd0021cb7f910f7037 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Thu, 17 Jan 2019 15:42:03 +0100 Subject: [PATCH 15/49] influxWriteData - support Influx tags (#420) * influxWriteData - support Influx tags In order to better query data in Influx, tags needs to be written. This change allows filling tag data via the Influx plugin. --- documentation/docs/steps/influxWriteData.md | 32 +++++++--- src/com/sap/piper/ConfigurationHelper.groovy | 7 +++ test/groovy/InfluxWriteDataTest.groovy | 61 +++++++++++++++++--- vars/commonPipelineEnvironment.groovy | 52 ++++++++++++++--- vars/influxWriteData.groovy | 34 +++++++---- 5 files changed, 151 insertions(+), 35 deletions(-) diff --git a/documentation/docs/steps/influxWriteData.md b/documentation/docs/steps/influxWriteData.md index 9afb2518c..39a1805e1 100644 --- a/documentation/docs/steps/influxWriteData.md +++ b/documentation/docs/steps/influxWriteData.md @@ -16,7 +16,7 @@ You basically need three components: It will create following files for you and archive them into your build: * `jenkins_data.json`: This file gives you build-specific information, like e.g. build result, stage where the build failed - * `pipeline_data.json`: This file gives you detailed information about your pipeline, e.g. stage durations, steps executed, ... + * `influx_data.json`: This file gives you detailed information about your pipeline, e.g. stage durations, steps executed, ... ## Prerequisites @@ -83,17 +83,33 @@ influxDBServer=jenkins | parameter | mandatory | default | possible values | | ----------|-----------|---------|-----------------| -| script | yes | | | -| artifactVersion | yes | commonPipelineEnvironment.getArtifactVersion() | | -| influxServer | no | `jenkins` | | -| influxPrefix | no | `null` | | +|script|yes||| +|artifactVersion|no|`commonPipelineEnvironment.getArtifactVersion()`|| +|customData|no|`commonPipelineEnvironment.getInfluxCustomData()`|| +|customDataMap|no|`commonPipelineEnvironment.getInfluxCustomDataMap()`|| +|customDataMapTags|no|`commonPipelineEnvironment.getInfluxCustomDataTags()`|| +|customDataTags|no|`commonPipelineEnvironment.getInfluxCustomDataTags()`|| +|influxPrefix|no||| +|influxServer|no||| +|wrapInNode|no|`false`|| ## Step configuration -The following parameters can also be specified as step parameters using the global configuration file: +We recommend to define values of step parameters via [config.yml file](../configuration.md). -- `influxServer` -- `influxPrefix` +In following sections the configuration is possible: + +| parameter | general | step | stage | +| ----------|-----------|---------|-----------------| +|script|||| +|artifactVersion||X|X| +|customData||X|X| +|customDataMap||X|X| +|customDataMapTags||X|X| +|customDataTags||X|X| +|influxPrefix||X|X| +|influxServer||X|X| +|wrapInNode||X|X| ## Example diff --git a/src/com/sap/piper/ConfigurationHelper.groovy b/src/com/sap/piper/ConfigurationHelper.groovy index e8396ca04..5d153c5c4 100644 --- a/src/com/sap/piper/ConfigurationHelper.groovy +++ b/src/com/sap/piper/ConfigurationHelper.groovy @@ -100,6 +100,13 @@ class ConfigurationHelper implements Serializable { return this } + ConfigurationHelper addIfNull(key, value){ + if (config[key] == null){ + config[key] = value + } + return this + } + @NonCPS // required because we have a closure in the // method body that cannot be CPS transformed Map use(){ diff --git a/test/groovy/InfluxWriteDataTest.groovy b/test/groovy/InfluxWriteDataTest.groovy index 1c89c98ce..48486e180 100644 --- a/test/groovy/InfluxWriteDataTest.groovy +++ b/test/groovy/InfluxWriteDataTest.groovy @@ -10,7 +10,12 @@ import util.JenkinsStepRule import util.JenkinsReadYamlRule import util.Rules +import static org.hamcrest.Matchers.allOf +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.hasKey +import static org.hamcrest.Matchers.hasValue import static org.hamcrest.Matchers.is +import static org.hamcrest.Matchers.isEmptyOrNullString import static org.junit.Assert.assertThat import static org.junit.Assert.assertTrue import static org.junit.Assert.assertEquals @@ -59,15 +64,18 @@ class InfluxWriteDataTest extends BasePiperTest { nullScript.commonPipelineEnvironment.setArtifactVersion('1.2.3') jsr.step.influxWriteData(script: nullScript) - assertTrue(loggingRule.log.contains('Artifact version: 1.2.3')) + assertThat(loggingRule.log, containsString('Artifact version: 1.2.3')) - assertEquals('testInflux', stepMap.selectedTarget) - assertEquals(null, stepMap.customPrefix) - assertEquals([:], stepMap.customData) - assertEquals([pipeline_data: [:], step_data: [:]], stepMap.customDataMap) + assertThat(stepMap.selectedTarget, is('testInflux')) + assertThat(stepMap.customPrefix, isEmptyOrNullString()) - assertTrue(fileMap.containsKey('jenkins_data.json')) - assertTrue(fileMap.containsKey('pipeline_data.json')) + assertThat(stepMap.customData, isEmptyOrNullString()) + assertThat(stepMap.customDataMap, is([pipeline_data: [:], step_data: [:]])) + + assertThat(fileMap, hasKey('jenkins_data.json')) + assertThat(fileMap, hasKey('influx_data.json')) + assertThat(fileMap, hasKey('jenkins_data_tags.json')) + assertThat(fileMap, hasKey('influx_data_tags.json')) assertJobStatusSuccess() } @@ -81,7 +89,7 @@ class InfluxWriteDataTest extends BasePiperTest { assertEquals(0, stepMap.size()) assertTrue(fileMap.containsKey('jenkins_data.json')) - assertTrue(fileMap.containsKey('pipeline_data.json')) + assertTrue(fileMap.containsKey('influx_data.json')) assertJobStatusSuccess() } @@ -116,4 +124,41 @@ class InfluxWriteDataTest extends BasePiperTest { assertThat(nodeCalled, is(true)) } + + @Test + void testInfluxCustomData() { + nullScript.commonPipelineEnvironment.setArtifactVersion('1.2.3') + jsr.step.influxWriteData( + //juStabUtils: utils, + script: nullScript, + influxServer: 'myInstance', + customData: [key1: 'test1'], + customDataTags: [tag1: 'testTag1'], + customDataMap: [test_data: [key1: 'keyValue1']], + customDataMapTags: [test_data: [tag1: 'tagValue1']] + ) + assertThat(stepMap.customData, allOf(hasKey('key1'), hasValue('test1'))) + assertThat(stepMap.customDataTags, allOf(hasKey('tag1'), hasValue('testTag1'))) + assertThat(stepMap.customDataMap, hasKey('test_data')) + assertThat(stepMap.customDataMapTags, hasKey('test_data')) + } + + @Test + void testInfluxCustomDataFromCPE() { + nullScript.commonPipelineEnvironment.reset() + nullScript.commonPipelineEnvironment.setArtifactVersion('1.2.3') + nullScript.commonPipelineEnvironment.setInfluxCustomDataTagsEntry('tag1', 'testTag1') + nullScript.commonPipelineEnvironment.setInfluxCustomDataMapEntry('test_data', 'key1', 'keyValue1') + nullScript.commonPipelineEnvironment.setInfluxCustomDataMapTagsEntry('test_data', 'tag1', 'tagValue1') + jsr.step.influxWriteData( + //juStabUtils: utils, + script: nullScript, + influxServer: 'myInstance' + ) + assertThat(stepMap.customData, isEmptyOrNullString()) + assertThat(stepMap.customDataTags, allOf(hasKey('tag1'), hasValue('testTag1'))) + assertThat(stepMap.customDataMap, hasKey('test_data')) + assertThat(stepMap.customDataMapTags, hasKey('test_data')) + } + } diff --git a/vars/commonPipelineEnvironment.groovy b/vars/commonPipelineEnvironment.groovy index 89e34fd74..16b863b03 100644 --- a/vars/commonPipelineEnvironment.groovy +++ b/vars/commonPipelineEnvironment.groovy @@ -25,8 +25,12 @@ class commonPipelineEnvironment implements Serializable { //each Map in influxCustomDataMap represents a measurement in Influx. Additional measurements can be added as a new Map entry of influxCustomDataMap private Map influxCustomDataMap = [pipeline_data: [:], step_data: [:]] + //each Map in influxCustomDataMapTags represents tags for certain measurement in Influx. Tags are required in Influx for easier querying data + private Map influxCustomDataMapTags = [pipeline_data: [:]] //influxCustomData represents measurement jenkins_custom_data in Influx. Metrics can be written into this map private Map influxCustomData = [:] + //influxCustomDataTags represents tags in Influx. Tags are required in Influx for easier querying data + private Map influxCustomDataTags = [:] String mtarFilePath @@ -49,7 +53,9 @@ class commonPipelineEnvironment implements Serializable { githubRepo = null influxCustomData = [:] + influxCustomDataTags = [:] influxCustomDataMap = [pipeline_data: [:], step_data: [:]] + influxCustomDataMapTags = [pipeline_data: [:]] mtarFilePath = null @@ -76,26 +82,56 @@ class commonPipelineEnvironment implements Serializable { return configProperties[property] } + // goes into measurement jenkins_data + def setInfluxCustomDataEntry(field, value) { + influxCustomData[field] = value + } + // goes into measurement jenkins_data def getInfluxCustomData() { return influxCustomData } + // goes into measurement jenkins_data + def setInfluxCustomDataTagsEntry(tag, value) { + influxCustomDataTags[tag] = value + } + + // goes into measurement jenkins_data + def getInfluxCustomDataTags() { + return influxCustomDataTags + } + + void setInfluxCustomDataMapEntry(measurement, field, value) { + if (!influxCustomDataMap[measurement]) { + influxCustomDataMap[measurement] = [:] + } + influxCustomDataMap[measurement][field] = value + } def getInfluxCustomDataMap() { return influxCustomDataMap } - def setInfluxStepData (dataKey, value) { - influxCustomDataMap.step_data[dataKey] = value + def setInfluxCustomDataMapTagsEntry(measurement, tag, value) { + if (!influxCustomDataMapTags[measurement]) { + influxCustomDataMapTags[measurement] = [:] + } + influxCustomDataMapTags[measurement][tag] = value } - def getInfluxStepData (dataKey) { - return influxCustomDataMap.step_data[dataKey] + def getInfluxCustomDataMapTags() { + return influxCustomDataMapTags } - def setPipelineMeasurement (measurementName, value) { - influxCustomDataMap.pipeline_data[measurementName] = value + def setInfluxStepData(key, value) { + setInfluxCustomDataMapEntry('step_data', key, value) + } + def getInfluxStepData(key) { + return influxCustomDataMap.step_data[key] } - def getPipelineMeasurement (measurementName) { - return influxCustomDataMap.pipeline_data[measurementName] + def setPipelineMeasurement(key, value) { + setInfluxCustomDataMapEntry('pipeline_data', key, value) + } + def getPipelineMeasurement(key) { + return influxCustomDataMap.pipeline_data[key] } } diff --git a/vars/influxWriteData.groovy b/vars/influxWriteData.groovy index 8c3d87b48..f76d5777a 100644 --- a/vars/influxWriteData.groovy +++ b/vars/influxWriteData.groovy @@ -11,14 +11,17 @@ import groovy.transform.Field @Field def STEP_NAME = getClass().getName() @Field Set GENERAL_CONFIG_KEYS = [] -@Field Set STEP_CONFIG_KEYS = [ +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ + 'artifactVersion', + 'customData', + 'customDataTags', + 'customDataMap', + 'customDataMapTags', 'influxServer', 'influxPrefix', 'wrapInNode' -] -@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS.plus([ - 'artifactVersion' ]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS void call(Map parameters = [:]) { handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters, allowBuildFailure: true) { @@ -37,6 +40,10 @@ void call(Map parameters = [:]) { artifactVersion: script.commonPipelineEnvironment.getArtifactVersion() ]) .mixin(parameters, PARAMETER_KEYS) + .addIfNull('customData', script.commonPipelineEnvironment.getInfluxCustomData()) + .addIfNull('customDataTags', script.commonPipelineEnvironment.getInfluxCustomDataTags()) + .addIfNull('customDataMap', script.commonPipelineEnvironment.getInfluxCustomDataMap()) + .addIfNull('customDataMapTags', script.commonPipelineEnvironment.getInfluxCustomDataMapTags()) .use() new Utils().pushToSWA([step: STEP_NAME, @@ -52,8 +59,10 @@ void call(Map parameters = [:]) { Artifact version: ${config.artifactVersion} Influx server: ${config.influxServer} Influx prefix: ${config.influxPrefix} -InfluxDB data: ${script.commonPipelineEnvironment.getInfluxCustomData()} -InfluxDB data map: ${script.commonPipelineEnvironment.getInfluxCustomDataMap()} +InfluxDB data: ${config.customData} +InfluxDB data tags: ${config.customDataTags} +InfluxDB data map: ${config.customDataMap} +InfluxDB data map tags: ${config.customDataMapTags} [${STEP_NAME}]----------------------------------------------------------""" if(config.wrapInNode){ @@ -76,15 +85,18 @@ private void writeToInflux(config, script){ $class: 'InfluxDbPublisher', selectedTarget: config.influxServer, customPrefix: config.influxPrefix, - customData: script.commonPipelineEnvironment.getInfluxCustomData(), - customDataMap: script.commonPipelineEnvironment.getInfluxCustomDataMap() + customData: config.customData.size()>0 ? config.customData : null, + customDataTags: config.customDataTags.size()>0 ? config.customDataTags : null, + customDataMap: config.customDataMap.size()>0 ? config.customDataMap : null, + customDataMapTags: config.customDataMapTags.size()>0 ? config.customDataMapTags : null ]) } //write results into json file for archiving - also benefitial when no InfluxDB is available yet def jsonUtils = new JsonUtils() - writeFile file: 'jenkins_data.json', text: jsonUtils.getPrettyJsonString(script.commonPipelineEnvironment.getInfluxCustomData()) - writeFile file: 'pipeline_data.json', text: jsonUtils.getPrettyJsonString(script.commonPipelineEnvironment.getInfluxCustomDataMap()) + writeFile file: 'jenkins_data.json', text: jsonUtils.getPrettyJsonString(config.customData) + writeFile file: 'influx_data.json', text: jsonUtils.getPrettyJsonString(config.customDataMap) + writeFile file: 'jenkins_data_tags.json', text: jsonUtils.getPrettyJsonString(config.customDataTags) + writeFile file: 'influx_data_tags.json', text: jsonUtils.getPrettyJsonString(config.customDataMapTags) archiveArtifacts artifacts: '*data.json', allowEmptyArchive: true - } From e11478ca00e8c266314d66bfed01e10daf4adba1 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Fri, 18 Jan 2019 08:25:22 +0100 Subject: [PATCH 16/49] cloudFoundryDeploy - add deployment reporting to Influx (#421) Add reporting of operations-related data to Influx (if configured), like: * Version of deployed artifact * Deployment time * Target infrastructure for deployment --- documentation/docs/steps/influxWriteData.md | 2 +- resources/default_pipeline_environment.yml | 2 +- src/com/sap/piper/JenkinsUtils.groovy | 29 +++++ test/groovy/CloudFoundryDeployTest.groovy | 64 ++++++++++- vars/cloudFoundryDeploy.groovy | 114 +++++++++++++------- 5 files changed, 172 insertions(+), 39 deletions(-) diff --git a/documentation/docs/steps/influxWriteData.md b/documentation/docs/steps/influxWriteData.md index 39a1805e1..587860b1a 100644 --- a/documentation/docs/steps/influxWriteData.md +++ b/documentation/docs/steps/influxWriteData.md @@ -90,7 +90,7 @@ influxDBServer=jenkins |customDataMapTags|no|`commonPipelineEnvironment.getInfluxCustomDataTags()`|| |customDataTags|no|`commonPipelineEnvironment.getInfluxCustomDataTags()`|| |influxPrefix|no||| -|influxServer|no||| +|influxServer|no|`''`|| |wrapInNode|no|`false`|| ## Step configuration diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index fe7ee797f..46a7bb412 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -182,7 +182,7 @@ steps: healthExecuteCheck: healthEndpoint: '' influxWriteData: - influxServer: 'jenkins' + influxServer: '' karmaExecuteTests: containerPortMappings: 'node:8-stretch': diff --git a/src/com/sap/piper/JenkinsUtils.groovy b/src/com/sap/piper/JenkinsUtils.groovy index e8c43005b..d240ab9cd 100644 --- a/src/com/sap/piper/JenkinsUtils.groovy +++ b/src/com/sap/piper/JenkinsUtils.groovy @@ -18,3 +18,32 @@ def nodeAvailable() { } return true } + +@NonCPS +def getCurrentBuildInstance() { + return currentBuild +} + +@NonCPS +def getRawBuild() { + return getCurrentBuildInstance().rawBuild +} + +def isJobStartedByTimer() { + return isJobStartedByCause(hudson.triggers.TimerTrigger.TimerTriggerCause.class) +} + +def isJobStartedByUser() { + return isJobStartedByCause(hudson.model.Cause.UserIdCause.class) +} + +@NonCPS +def isJobStartedByCause(Class cause) { + def startedByGivenCause = false + def detectedCause = getRawBuild().getCause(cause) + if (null != detectedCause) { + startedByGivenCause = true + echo "Found build cause ${detectedCause}" + } + return startedByGivenCause +} diff --git a/test/groovy/CloudFoundryDeployTest.groovy b/test/groovy/CloudFoundryDeployTest.groovy index ccfc9b6e7..1867d4803 100644 --- a/test/groovy/CloudFoundryDeployTest.groovy +++ b/test/groovy/CloudFoundryDeployTest.groovy @@ -1,10 +1,10 @@ #!groovy +import com.sap.piper.JenkinsUtils import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException import org.junit.rules.RuleChain -import org.yaml.snakeyaml.Yaml import util.BasePiperTest import util.JenkinsCredentialsRule import util.JenkinsEnvironmentRule @@ -19,6 +19,7 @@ import util.Rules import static org.junit.Assert.assertThat 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.containsString @@ -34,6 +35,14 @@ class CloudFoundryDeployTest extends BasePiperTest { private JenkinsEnvironmentRule jer = new JenkinsEnvironmentRule(this) private JenkinsReadYamlRule jryr = new JenkinsReadYamlRule(this) + private writeInfluxMap = [:] + + class JenkinsUtilsMock extends JenkinsUtils { + def isJobStartedByUser() { + return true + } + } + @Rule public RuleChain rules = Rules .getCommonRules(this) @@ -47,6 +56,13 @@ class CloudFoundryDeployTest extends BasePiperTest { .around(new JenkinsCredentialsRule(this).withCredentials('test_cfCredentialsId', 'test_cf', '********')) .around(jsr) // needs to be activated after jedr, otherwise executeDocker is not mocked + @Before + void init() { + helper.registerAllowedMethod('writeInflux', [Map.class], {m -> + writeInfluxMap = m + }) + } + @Test void testNoTool() throws Exception { nullScript.commonPipelineEnvironment.configuration = [ @@ -69,6 +85,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: '', stageName: 'acceptance', ]) @@ -97,6 +114,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'notAvailable', stageName: 'acceptance' ]) @@ -114,6 +132,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', cfOrg: 'testOrg', cfSpace: 'testSpace', @@ -140,6 +159,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', cfApiEndpoint: 'https://customApi', cfOrg: 'testOrg', @@ -162,6 +182,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', cloudFoundry: [ org: 'testOrg', @@ -192,6 +213,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', cfOrg: 'testOrg', cfSpace: 'testSpace', @@ -218,6 +240,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', cfOrg: 'testOrg', cfSpace: 'testSpace', @@ -234,6 +257,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', deployType: 'blue-green', cfOrg: 'testOrg', @@ -260,6 +284,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', deployType: 'blue-green', keepOldInstance: false, @@ -288,6 +313,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', deployType: 'blue-green', keepOldInstance: true, @@ -315,6 +341,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', deployType: 'standard', keepOldInstance: true, @@ -340,6 +367,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), deployTool: 'cf_native', deployType: 'blue-green', cfOrg: 'testOrg', @@ -355,6 +383,7 @@ class CloudFoundryDeployTest extends BasePiperTest { jsr.step.cloudFoundryDeploy([ script: nullScript, juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), cfOrg: 'testOrg', cfSpace: 'testSpace', cfCredentialsId: 'test_cfCredentialsId', @@ -368,4 +397,37 @@ class CloudFoundryDeployTest extends BasePiperTest { assertThat(jscr.shell, hasItem(containsString('cf deploy target/test.mtar -f'))) assertThat(jscr.shell, hasItem(containsString('cf logout'))) } + + @Test + void testInfluxReporting() { + jryr.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]") + helper.registerAllowedMethod('writeYaml', [Map], { Map parameters -> + generatedFile = parameters.file + data = parameters.data + }) + nullScript.commonPipelineEnvironment.setArtifactVersion('1.2.3') + jsr.step.cloudFoundryDeploy([ + script: nullScript, + juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), + deployTool: 'cf_native', + cfOrg: 'testOrg', + cfSpace: 'testSpace', + cfCredentialsId: 'test_cfCredentialsId', + cfAppName: 'testAppName', + cfManifest: 'test.yml' + ]) + // asserts + assertThat(writeInfluxMap.customDataMap.deployment_data.artifactUrl, is('n/a')) + assertThat(writeInfluxMap.customDataMap.deployment_data.deployTime, containsString(new Date().format( 'MMM dd, yyyy'))) + assertThat(writeInfluxMap.customDataMap.deployment_data.jobTrigger, is('USER')) + + assertThat(writeInfluxMap.customDataMapTags.deployment_data.artifactVersion, is('1.2.3')) + assertThat(writeInfluxMap.customDataMapTags.deployment_data.deployUser, is('test_cf')) + assertThat(writeInfluxMap.customDataMapTags.deployment_data.deployResult, is('SUCCESS')) + assertThat(writeInfluxMap.customDataMapTags.deployment_data.cfApiEndpoint, is('https://api.cf.eu10.hana.ondemand.com')) + assertThat(writeInfluxMap.customDataMapTags.deployment_data.cfOrg, is('testOrg')) + assertThat(writeInfluxMap.customDataMapTags.deployment_data.cfSpace, is('testSpace')) + } + } diff --git a/vars/cloudFoundryDeploy.groovy b/vars/cloudFoundryDeploy.groovy index 81254ecad..a0c8c6684 100644 --- a/vars/cloudFoundryDeploy.groovy +++ b/vars/cloudFoundryDeploy.groovy @@ -1,3 +1,5 @@ +import com.sap.piper.JenkinsUtils + import static com.sap.piper.Prerequisites.checkScript import com.sap.piper.Utils @@ -32,10 +34,8 @@ void call(Map parameters = [:]) { handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) { - def utils = parameters.juStabUtils - if (utils == null) { - utils = new Utils() - } + def utils = parameters.juStabUtils ?: new Utils() + def jenkinsUtils = parameters.jenkinsUtilsStub ?: new JenkinsUtils() def script = checkScript(this, parameters) if (script == null) @@ -63,42 +63,54 @@ void call(Map parameters = [:]) { //make sure that for further execution whole workspace, e.g. also downloaded artifacts are considered config.stashContent = [] - if (config.deployTool == 'mtaDeployPlugin') { - // set default mtar path - config = ConfigurationHelper.newInstance(this, config) - .addIfEmpty('mtaPath', config.mtaPath?:findMtar()) - .use() + boolean deploy = false + boolean deploySuccess = true + try { + if (config.deployTool == 'mtaDeployPlugin') { + deploy = true + // set default mtar path + config = ConfigurationHelper.newInstance(this, config) + .addIfEmpty('mtaPath', config.mtaPath?:findMtar()) + .use() - dockerExecute(script: script, dockerImage: config.dockerImage, dockerWorkspace: config.dockerWorkspace, stashContent: config.stashContent) { - deployMta(config) + dockerExecute(script: script, dockerImage: config.dockerImage, dockerWorkspace: config.dockerWorkspace, stashContent: config.stashContent) { + deployMta(config) + } } - return + + if (config.deployTool == 'cf_native') { + deploy = true + config.smokeTest = '' + + if (config.smokeTestScript == 'blueGreenCheckScript.sh') { + writeFile file: config.smokeTestScript, text: libraryResource(config.smokeTestScript) + } + + config.smokeTest = '--smoke-test $(pwd)/' + config.smokeTestScript + sh "chmod +x ${config.smokeTestScript}" + + echo "[${STEP_NAME}] CF native deployment (${config.deployType}) with cfAppName=${config.cloudFoundry.appName}, cfManifest=${config.cloudFoundry.manifest}, smokeTestScript=${config.smokeTestScript}" + + 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) + } + } + } catch (err) { + deploySuccess = false + throw err + } finally { + if (deploy) { + reportToInflux(script, config, deploySuccess, jenkinsUtils) + } + } - if (config.deployTool == 'cf_native') { - config.smokeTest = '' - - if (config.smokeTestScript == 'blueGreenCheckScript.sh') { - writeFile file: config.smokeTestScript, text: libraryResource(config.smokeTestScript) - } - - config.smokeTest = '--smoke-test $(pwd)/' + config.smokeTestScript - sh "chmod +x ${config.smokeTestScript}" - - echo "[${STEP_NAME}] CF native deployment (${config.deployType}) with cfAppName=${config.cloudFoundry.appName}, cfManifest=${config.cloudFoundry.manifest}, smokeTestScript=${config.smokeTestScript}" - - 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) - } - - return - } } } @@ -224,3 +236,33 @@ Transformed manifest file content: $transformedManifest""" writeYaml file: config.cloudFoundry.manifest, data: manifest } } + + +private void reportToInflux(script, config, deploySuccess, JenkinsUtils jenkinsUtils) { + def deployUser = '' + withCredentials([usernamePassword( + credentialsId: config.cloudFoundry.credentialsId, + passwordVariable: 'password', + usernameVariable: 'username' + )]) { + deployUser = username + } + + def timeFinished = new Date().format( 'MMM dd, yyyy - HH:mm:ss' ) + def triggerCause = jenkinsUtils.isJobStartedByUser()?'USER':(jenkinsUtils.isJobStartedByTimer()?'TIMER': 'OTHER') + + def deploymentData = [deployment_data: [ + artifactUrl: 'n/a', //might be added later on during pipeline run (written to commonPipelineEnvironment) + deployTime: timeFinished, + jobTrigger: triggerCause + ]] + def deploymentDataTags = [deployment_data: [ + artifactVersion: script.commonPipelineEnvironment.getArtifactVersion(), + deployUser: deployUser, + deployResult: deploySuccess?'SUCCESS':'FAILURE', + cfApiEndpoint: config.cloudFoundry.apiEndpoint, + cfOrg: config.cloudFoundry.org, + cfSpace: config.cloudFoundry.space, + ]] + writeInflux script: script, customData: [:], customDataTags: [:], customDataMap: deploymentData, customDataMapTags: deploymentDataTags +} From d140a20bb44e9160c9ff9ec22a28ffc3316de669 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 17 Dec 2018 12:00:01 +0100 Subject: [PATCH 17/49] [fix] script can only be passed via parameters --- createDocu.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/createDocu.groovy b/createDocu.groovy index 2d76e9536..8ed2271a7 100644 --- a/createDocu.groovy +++ b/createDocu.groovy @@ -490,9 +490,9 @@ def handleStep(stepName, prepareDefaultValuesStep, gse) { 'commonPipelineEnvironment for retrieving, for example, configuration parameters.', required: true, - GENERAL_CONFIG: 'false', - STEP_CONFIG: 'false', - PARAMS: 'true' + GENERAL_CONFIG: false, + STEP_CONFIG: false, + PARAMS: true ] // END special handling for 'script' parameter From d9c536e25ad0274664d45beebf26c6dfd56fad85 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 17 Dec 2018 12:11:15 +0100 Subject: [PATCH 18/49] [fix] Required parameter is not labeled as required in case there is a default Otherwise the meaning of required is confusing. --- createDocu.groovy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/createDocu.groovy b/createDocu.groovy index 8ed2271a7..166425439 100644 --- a/createDocu.groovy +++ b/createDocu.groovy @@ -501,9 +501,11 @@ def handleStep(stepName, prepareDefaultValuesStep, gse) { it -> + def defaultValue = Helper.getValue(defaultConfig, it.split('/')) + def parameterProperties = [ - defaultValue: Helper.getValue(defaultConfig, it.split('/')), - required: requiredParameters.contains((it as String)) + defaultValue: defaultValue, + required: requiredParameters.contains((it as String)) && defaultValue == null ] step.parameters.put(it, parameterProperties) From 1ef91ce8a379bbe8c3f6fdf0fe6d73505fa819c3 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 17 Dec 2018 11:52:12 +0100 Subject: [PATCH 19/49] Parameters now generated during build --- .../docs/steps/checkChangeInDevelopment.md | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/documentation/docs/steps/checkChangeInDevelopment.md b/documentation/docs/steps/checkChangeInDevelopment.md index b29b43cbe..7462c175d 100644 --- a/documentation/docs/steps/checkChangeInDevelopment.md +++ b/documentation/docs/steps/checkChangeInDevelopment.md @@ -17,30 +17,22 @@ is set to `false`, no `hudson.AbortException` will be thrown. In this case there ## Parameters -| parameter | mandatory | default | possible values | -| -------------------|-----------|--------------------------------------------------------|--------------------| -| `script` | yes | | | -| `changeDocumentId` | yes | | | -| `changeManagement/changeDocumentLabel` | no | `ChangeDocument\s?:` | regex pattern | -| `changeManagement/credentialsId` | yes | | | -| `changeManagement/endpoint` | yes | | | -| `changeManagement/git/from` | no | `origin/master` | | -| `changeManagement/git/to` | no | `HEAD` | | -| `changeManagement/git/format` | no | `%b` | see `git log --help` | -| `failIfStatusIsNotInDevelopment` | no | `true` | `true`, `false` | - -* `script` - The common script environment of the Jenkinsfile running. Typically the reference to the script calling the pipeline step is provided with the `this` parameter, as in `script: this`. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for retrieving, for example, configuration parameters. -* `changeDocumentId` - The id of the change document to transport. If not provided, it is retrieved from the git commit history. -* `changeManagement/changeDocumentLabel` - A pattern used for identifying lines holding the change document id. -* `changeManagement/credentialsId` - The id of the credentials to connect to the Solution Manager. The credentials needs to be maintained on Jenkins. -* `changeManagement/endpoint` - The address of the Solution Manager. -* `changeManagement/git/from` - The starting point for retrieving the change document id -* `changeManagement/git/to` - The end point for retrieving the change document id -* `changeManagement/git/format` - Specifies what part of the commit is scanned. By default the body of the commit message is scanned. -* `failIfStatusIsNotInDevelopment` - when set to `false` the step will not fail in case the step is not in status 'in development'. +Content here is generated from corresponnding step, see `vars`. ## Step configuration +Content here is generated from corresponnding step, see `vars`. + +## Exceptions + +* `AbortException`: + * If the change id is not provided via parameter and if the change document id cannot be retrieved from the commit history. + * If the change is not in status `in development`. In this case no exception will be thrown when `failIfStatusIsNotInDevelopment` is set to `false`. +* `IllegalArgumentException`: + * If a mandatory property is not provided. + +## Examples + The step is configured using a customer configuration file provided as resource in an custom shared library. @@ -87,17 +79,7 @@ The properties can also be configured on a per-step basis: failIfStatusIsNotInDevelopment: true ``` -The parameters can also be provided when the step is invoked. For examples see below. - -## Exceptions - -* `AbortException`: - * If the change id is not provided via parameter and if the change document id cannot be retrieved from the commit history. - * If the change is not in status `in development`. In this case no exception will be thrown when `failIfStatusIsNotInDevelopment` is set to `false`. -* `IllegalArgumentException`: - * If a mandatory property is not provided. - -## Examples +The parameters can also be provided when the step is invoked: ```groovy // simple case. All mandatory parameters provided via From ab83e9f7b84433b32d98627f0b64cee2f10dd34a Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Thu, 20 Dec 2018 09:06:31 +0100 Subject: [PATCH 20/49] Generate docu during travis build (for checkChangeInDevelopment) --- .travis.yml | 8 +++++++- documentation/mkdocs.yml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b6f69c00..e86ba1dc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,11 @@ jobs: - name: Docs Build if: type = pull_request install: docker pull squidfunk/mkdocs-material:3.0.4 - script: docker run --rm -it -v ${TRAVIS_BUILD_DIR}:/docs -w /docs/documentation squidfunk/mkdocs-material:3.0.4 build --clean --verbose --strict + script: + - | + cp -r documentation/docs documentation/docs-tmp + ./createDocu.sh vars documentation/docs-tmp/steps checkChangeInDevelopment + docker run --rm -it -v ${TRAVIS_BUILD_DIR}:/docs -w /docs/documentation squidfunk/mkdocs-material:3.0.4 build --clean --verbose --strict - stage: Docs name: Deploy @@ -42,6 +46,8 @@ jobs: PRIVATE_KEY="cfg/id_rsa" openssl aes-256-cbc -K $encrypted_12c8071d2874_key -iv $encrypted_12c8071d2874_iv -in cfg/id_rsa.enc -out "${PRIVATE_KEY}" -d chmod a+x gh-pages-deploy.sh + cp -r documentation/docs documentation/docs-tmp + ./createDocu.sh vars documentation/docs-tmp/steps checkChangeInDevelopment script: docker run --rm -it --entrypoint "./gh-pages-deploy.sh" -e "TRAVIS_REPO_SLUG=${TRAVIS_REPO_SLUG}" -v ${TRAVIS_BUILD_DIR}:/docs -w /docs squidfunk/mkdocs-material:3.0.4 # TODO: make use of GHPages deploy provider: https://docs.travis-ci.com/user/deployment/pages/ diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 5919e5ec8..8f695a703 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -59,6 +59,6 @@ markdown_extensions: extra_css: - 'css/extra.css' edit_uri: edit/master/documentation/docs -docs_dir: docs +docs_dir: docs-tmp site_dir: docs-gen repo_url: https://github.com/SAP/jenkins-library From 669d91bcdd38d5fe1c93d321db9ff0055112c753 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Thu, 20 Dec 2018 16:02:20 +0100 Subject: [PATCH 21/49] Provide more than one step via cli elements 2 until end are considered to be step names. the first two elements denotes the folder containing the steps (vars) and the folder containing the docu markdown files. --- createDocu.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/createDocu.groovy b/createDocu.groovy index 166425439..f7636a717 100644 --- a/createDocu.groovy +++ b/createDocu.groovy @@ -351,7 +351,9 @@ stepsDocuDir = stepsDocuDir ?: new File('documentation/docs/steps') if(args.length >= 3) - steps << args[2] + steps = (args as List).drop(2) // the first two entries are stepsDir and docuDir + // the other parts are considered as step names + // assign parameters // From 89370b7a934d43262023240c9b91daf8d612f821 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 9 Jan 2019 10:43:09 +0100 Subject: [PATCH 22/49] better text for step configuration section --- createDocu.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/createDocu.groovy b/createDocu.groovy index f7636a717..88c90b15d 100644 --- a/createDocu.groovy +++ b/createDocu.groovy @@ -65,7 +65,7 @@ class TemplateHelper { def t = '''| |We recommend to define values of step parameters via [config.yml file](../configuration.md). | - |In following sections the configuration is possible:\n\n'''.stripMargin() + |In following sections of the config.yml the configuration is possible:\n\n'''.stripMargin() t += '| parameter | general | step | stage |\n' t += '|-----------|---------|------|-------|\n' @@ -494,7 +494,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse) { GENERAL_CONFIG: false, STEP_CONFIG: false, - PARAMS: true + PARAMS: false ] // END special handling for 'script' parameter From 7ac70e4c51c7f0cd5cb26dc7ba80c72422df1230 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 9 Jan 2019 10:47:21 +0100 Subject: [PATCH 23/49] Streamline naming of parameter sections --- createDocu.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/createDocu.groovy b/createDocu.groovy index 88c90b15d..2ac1d867a 100644 --- a/createDocu.groovy +++ b/createDocu.groovy @@ -72,7 +72,7 @@ class TemplateHelper { parameters.keySet().toSorted().each { def props = parameters.get(it) - t += "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} | ${props.PARAMS ? 'X' : ''} |\n" + t += "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} | ${props.STAGE_CONFIG ? 'X' : ''} |\n" } t @@ -301,7 +301,7 @@ class Helper { params.put('STEP_CONFIG', script.STEP_CONFIG_KEYS ?: []) params.put('GENERAL_CONFIG', script.GENERAL_CONFIG_KEYS ?: [] ) - params.put('PARAMS', script.PARAMETER_KEYS ?: [] ) + params.put('STAGE_CONFIG', script.PARAMETER_KEYS ?: [] ) return params } @@ -494,7 +494,7 @@ def handleStep(stepName, prepareDefaultValuesStep, gse) { GENERAL_CONFIG: false, STEP_CONFIG: false, - PARAMS: false + STAGE_CONFIG: false ] // END special handling for 'script' parameter From 96d51846510c75c26b46761b052123e0c8dbd71b Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 9 Jan 2019 10:55:58 +0100 Subject: [PATCH 24/49] Remove hand crafted step descrption --- documentation/docs/steps/checkChangeInDevelopment.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/documentation/docs/steps/checkChangeInDevelopment.md b/documentation/docs/steps/checkChangeInDevelopment.md index 7462c175d..ecf035b39 100644 --- a/documentation/docs/steps/checkChangeInDevelopment.md +++ b/documentation/docs/steps/checkChangeInDevelopment.md @@ -2,14 +2,7 @@ ## Description -Checks if a Change Document in SAP Solution Manager is in status 'in development'. The change document id is retrieved from the git commit history. The change document id -can also be provided via parameter `changeDocumentId`. Any value provided as parameter has a higher precedence than a value from the commit history. - -By default the git commit messages between `origin/master` and `HEAD` are scanned for a line like `ChangeDocument : `. The commit -range and the pattern can be configured. For details see 'parameters' table. - -In case the change is not in status 'in development' an `hudson.AbortException` is thrown. In case `failIfStatusIsNotInDevelopment` -is set to `false`, no `hudson.AbortException` will be thrown. In this case there is only a message in the log stating the change is not in status 'in development'. +Content here is generated from corresponnding step, see `vars`. ## Prerequisites From c6500a7516ffae5071fe37b81adb60765adb37c3 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 9 Jan 2019 16:30:22 +0100 Subject: [PATCH 25/49] Relocate script files for generting documention --- createDocu.groovy => documentation/bin/createDocu.groovy | 0 createDocu.sh => documentation/bin/createDocu.sh | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) rename createDocu.groovy => documentation/bin/createDocu.groovy (100%) rename createDocu.sh => documentation/bin/createDocu.sh (64%) diff --git a/createDocu.groovy b/documentation/bin/createDocu.groovy similarity index 100% rename from createDocu.groovy rename to documentation/bin/createDocu.groovy diff --git a/createDocu.sh b/documentation/bin/createDocu.sh similarity index 64% rename from createDocu.sh rename to documentation/bin/createDocu.sh index b903a2c62..c36f5d412 100755 --- a/createDocu.sh +++ b/documentation/bin/createDocu.sh @@ -1,6 +1,9 @@ #!/bin/bash +d=$(dirname "$0") +[ ! -z "$d" ] && d="$d/" + export CLASSPATH_FILE='cp.txt' mvn dependency:build-classpath -Dmdep.outputFile=${CLASSPATH_FILE} > /dev/null 2>&1 #~/Library/groovy-2.4.13/bin/groovy -cp src:`cat $CLASSPATH_FILE` createDocu -groovy -cp "src:$(cat $CLASSPATH_FILE)" createDocu "${@}" +groovy -cp "src:$(cat $CLASSPATH_FILE)" "${d}createDocu" "${@}" From daaac272d65b949a25c668ca1343b8ef84f47db3 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 11 Jan 2019 13:38:46 +0100 Subject: [PATCH 26/49] Remove commented coding line --- documentation/bin/createDocu.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/documentation/bin/createDocu.sh b/documentation/bin/createDocu.sh index c36f5d412..b8de44b7c 100755 --- a/documentation/bin/createDocu.sh +++ b/documentation/bin/createDocu.sh @@ -5,5 +5,4 @@ d=$(dirname "$0") export CLASSPATH_FILE='cp.txt' mvn dependency:build-classpath -Dmdep.outputFile=${CLASSPATH_FILE} > /dev/null 2>&1 -#~/Library/groovy-2.4.13/bin/groovy -cp src:`cat $CLASSPATH_FILE` createDocu groovy -cp "src:$(cat $CLASSPATH_FILE)" "${d}createDocu" "${@}" From f81dc7b1477eda83fce0603f573e5ff08488737d Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 11 Jan 2019 14:21:00 +0100 Subject: [PATCH 27/49] docu script classpath contains the built classes folder we do not have only groovy script, but also java files inside src folder. Hence we should rely on the target/classes folder in order to see also the compiled java classes, and not only the groovy scripts. --- documentation/bin/createDocu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/bin/createDocu.sh b/documentation/bin/createDocu.sh index b8de44b7c..5ae3b0ad3 100755 --- a/documentation/bin/createDocu.sh +++ b/documentation/bin/createDocu.sh @@ -4,5 +4,5 @@ d=$(dirname "$0") [ ! -z "$d" ] && d="$d/" export CLASSPATH_FILE='cp.txt' -mvn dependency:build-classpath -Dmdep.outputFile=${CLASSPATH_FILE} > /dev/null 2>&1 -groovy -cp "src:$(cat $CLASSPATH_FILE)" "${d}createDocu" "${@}" +mvn compile dependency:build-classpath -Dmdep.outputFile=${CLASSPATH_FILE} > /dev/null 2>&1 +groovy -cp "target/classes:$(cat $CLASSPATH_FILE)" "${d}createDocu" "${@}" From d681b42c00074d519d31ffe7a20a82d162cf5680 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 11 Jan 2019 14:13:47 +0100 Subject: [PATCH 28/49] Introduce @GenerateDocumentation annotation --- src/com/sap/piper/GenerateDocumentation.java | 11 +++++++++++ vars/checkChangeInDevelopment.groovy | 2 ++ 2 files changed, 13 insertions(+) create mode 100644 src/com/sap/piper/GenerateDocumentation.java diff --git a/src/com/sap/piper/GenerateDocumentation.java b/src/com/sap/piper/GenerateDocumentation.java new file mode 100644 index 000000000..b7f57334d --- /dev/null +++ b/src/com/sap/piper/GenerateDocumentation.java @@ -0,0 +1,11 @@ +package com.sap.piper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface GenerateDocumentation { +} diff --git a/vars/checkChangeInDevelopment.groovy b/vars/checkChangeInDevelopment.groovy index f6521f015..05058f984 100644 --- a/vars/checkChangeInDevelopment.groovy +++ b/vars/checkChangeInDevelopment.groovy @@ -1,5 +1,6 @@ import static com.sap.piper.Prerequisites.checkScript +import com.sap.piper.GenerateDocumentation import com.sap.piper.GitUtils import com.sap.piper.Utils import groovy.transform.Field @@ -37,6 +38,7 @@ import static com.sap.piper.cm.StepHelpers.getBackendTypeAndLogInfoIfCMIntegrati * range and the pattern can be configured. For details see 'parameters' table. * */ +@GenerateDocumentation void call(parameters = [:]) { handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) { From 760137058cdc2aa68eeb901aa491655653f113c0 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 11 Jan 2019 15:18:51 +0100 Subject: [PATCH 29/49] Evaluate @GenerateDocumentation annotation. --- documentation/bin/createDocu.groovy | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index 2ac1d867a..c36aaa0e4 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -1,6 +1,7 @@ import groovy.io.FileType; import org.yaml.snakeyaml.Yaml import org.codehaus.groovy.control.CompilerConfiguration +import com.sap.piper.GenerateDocumentation import com.sap.piper.DefaultValueCache import java.util.regex.Matcher @@ -324,6 +325,25 @@ class Helper { if(p in Map) getValue(p, pPath.tail()) else return p } + + static resolveDocuRelevantSteps(GroovyScriptEngine gse, File stepsDir) { + + def docuRelevantSteps = [] + + stepsDir.traverse(type: FileType.FILES, maxDepth: 0) { + if(it.getName().endsWith('.groovy')) { + def scriptName = (it =~ /vars\/(.*)\.groovy/)[0][1] + def stepScript = gse.createScript("${scriptName}.groovy", new Binding()) + for (def method in stepScript.getClass().getMethods()) { + if(method.getName() == 'call' && method.getAnnotation(GenerateDocumentation) != null) { + docuRelevantSteps << scriptName + break + } + } + } + } + docuRelevantSteps + } } roots = [ @@ -374,18 +394,16 @@ if( !stepsDir.exists() ) { // sanity checks // +def gse = new GroovyScriptEngine( [ stepsDir.getName() ] as String[] , getClass().getClassLoader() ) // // find all the steps we have to document (if no step has been provided from outside) if( ! steps) { - stepsDir.traverse(type: FileType.FILES, maxDepth: 0) - { if(it.getName().endsWith('.groovy')) steps << (it =~ /vars\/(.*)\.groovy/)[0][1] } + steps = Helper.resolveDocuRelevantSteps(gse, stepsDir) } else { System.err << "[INFO] Generating docu only for step ${steps.size > 1 ? 's' : ''} ${steps}.\n" } -def gse = new GroovyScriptEngine( [ stepsDir.getName() ] as String[] , getClass().getClassLoader() ) - def prepareDefaultValuesStep = Helper.getPrepareDefaultValuesStep(gse) boolean exceptionCaught = false From e48cfa7a77ae3e9b8cbb2c5c04263b508736c45a Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 11 Jan 2019 15:23:47 +0100 Subject: [PATCH 30/49] adapt generateDocu script for travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e86ba1dc9..d3cb3ac4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ jobs: script: - | cp -r documentation/docs documentation/docs-tmp - ./createDocu.sh vars documentation/docs-tmp/steps checkChangeInDevelopment + ./createDocu.sh vars documentation/docs-tmp/steps docker run --rm -it -v ${TRAVIS_BUILD_DIR}:/docs -w /docs/documentation squidfunk/mkdocs-material:3.0.4 build --clean --verbose --strict - stage: Docs @@ -47,7 +47,7 @@ jobs: openssl aes-256-cbc -K $encrypted_12c8071d2874_key -iv $encrypted_12c8071d2874_iv -in cfg/id_rsa.enc -out "${PRIVATE_KEY}" -d chmod a+x gh-pages-deploy.sh cp -r documentation/docs documentation/docs-tmp - ./createDocu.sh vars documentation/docs-tmp/steps checkChangeInDevelopment + ./createDocu.sh vars documentation/docs-tmp/steps script: docker run --rm -it --entrypoint "./gh-pages-deploy.sh" -e "TRAVIS_REPO_SLUG=${TRAVIS_REPO_SLUG}" -v ${TRAVIS_BUILD_DIR}:/docs -w /docs squidfunk/mkdocs-material:3.0.4 # TODO: make use of GHPages deploy provider: https://docs.travis-ci.com/user/deployment/pages/ From 6f596275513da22eef3391555f8a2be69ae175cd Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 14 Jan 2019 08:32:34 +0100 Subject: [PATCH 31/49] Create file containing classpath into build dir and not into project root. With this appraoch the root folder get not filled with temporary content. --- documentation/bin/createDocu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/bin/createDocu.sh b/documentation/bin/createDocu.sh index 5ae3b0ad3..3f20bbc34 100755 --- a/documentation/bin/createDocu.sh +++ b/documentation/bin/createDocu.sh @@ -3,6 +3,6 @@ d=$(dirname "$0") [ ! -z "$d" ] && d="$d/" -export CLASSPATH_FILE='cp.txt' +export CLASSPATH_FILE='target/cp.txt' mvn compile dependency:build-classpath -Dmdep.outputFile=${CLASSPATH_FILE} > /dev/null 2>&1 groovy -cp "target/classes:$(cat $CLASSPATH_FILE)" "${d}createDocu" "${@}" From 4f804f8876a2ef0de867b210f17444e4576da3df Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 14 Jan 2019 09:47:23 +0100 Subject: [PATCH 32/49] remove trailing line breaks --- documentation/bin/createDocu.groovy | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/documentation/bin/createDocu.groovy b/documentation/bin/createDocu.groovy index c36aaa0e4..7d4164160 100644 --- a/documentation/bin/createDocu.groovy +++ b/documentation/bin/createDocu.groovy @@ -58,13 +58,12 @@ class TemplateHelper { t += "* `${it}` - ${props.docu ?: ''}\n" } - t + t.trim() } static createStepConfigurationSection(Map parameters) { - def t = '''| - |We recommend to define values of step parameters via [config.yml file](../configuration.md). + def t = '''|We recommend to define values of step parameters via [config.yml file](../configuration.md). | |In following sections of the config.yml the configuration is possible:\n\n'''.stripMargin() @@ -76,7 +75,7 @@ class TemplateHelper { t += "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} | ${props.STAGE_CONFIG ? 'X' : ''} |\n" } - t + t.trim() } } From 1baa1f14cb4eede77a2257520fd9075f714a2562 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 14 Jan 2019 09:48:19 +0100 Subject: [PATCH 33/49] remove trailing line break --- vars/checkChangeInDevelopment.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/vars/checkChangeInDevelopment.groovy b/vars/checkChangeInDevelopment.groovy index 05058f984..a6a3c6c4b 100644 --- a/vars/checkChangeInDevelopment.groovy +++ b/vars/checkChangeInDevelopment.groovy @@ -36,7 +36,6 @@ import static com.sap.piper.cm.StepHelpers.getBackendTypeAndLogInfoIfCMIntegrati * * By default the git commit messages between `origin/master` and `HEAD` are scanned for a line like `ChangeDocument : `. The commit * range and the pattern can be configured. For details see 'parameters' table. - * */ @GenerateDocumentation void call(parameters = [:]) { From c14567bf0672850f70870d0a707f6a968e74b983 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 18 Jan 2019 09:03:31 +0100 Subject: [PATCH 34/49] adjust location of createDocu script --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d3cb3ac4c..e0ab22dfd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ jobs: script: - | cp -r documentation/docs documentation/docs-tmp - ./createDocu.sh vars documentation/docs-tmp/steps + documentation/bin/createDocu.sh vars documentation/docs-tmp/steps docker run --rm -it -v ${TRAVIS_BUILD_DIR}:/docs -w /docs/documentation squidfunk/mkdocs-material:3.0.4 build --clean --verbose --strict - stage: Docs @@ -47,7 +47,7 @@ jobs: openssl aes-256-cbc -K $encrypted_12c8071d2874_key -iv $encrypted_12c8071d2874_iv -in cfg/id_rsa.enc -out "${PRIVATE_KEY}" -d chmod a+x gh-pages-deploy.sh cp -r documentation/docs documentation/docs-tmp - ./createDocu.sh vars documentation/docs-tmp/steps + documentation/bin/createDocu.sh vars documentation/docs-tmp/steps script: docker run --rm -it --entrypoint "./gh-pages-deploy.sh" -e "TRAVIS_REPO_SLUG=${TRAVIS_REPO_SLUG}" -v ${TRAVIS_BUILD_DIR}:/docs -w /docs squidfunk/mkdocs-material:3.0.4 # TODO: make use of GHPages deploy provider: https://docs.travis-ci.com/user/deployment/pages/ From 1f96a34f55a1b55943f5816b83d1c8560cccb1cd Mon Sep 17 00:00:00 2001 From: SarahNoack <44202907+SarahNoack@users.noreply.github.com> Date: Fri, 18 Jan 2019 09:23:00 +0100 Subject: [PATCH 35/49] Update changeManagement.md (#437) --- documentation/docs/scenarios/changeManagement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/scenarios/changeManagement.md b/documentation/docs/scenarios/changeManagement.md index c9147c0aa..32b7f0cef 100644 --- a/documentation/docs/scenarios/changeManagement.md +++ b/documentation/docs/scenarios/changeManagement.md @@ -9,7 +9,7 @@ Set up an agile development process with Jenkins CI, which automatically feeds c * You have set up Project “Piper”. See [README](https://github.com/SAP/jenkins-library/blob/master/README.md). * You have installed SAP Solution Manager 7.2 SP6. See [README](https://github.com/SAP/devops-cm-client/blob/master/README.md). * You have installed the Multi-Target Application (MTA) Archive Builder 1.0.6 or newer. See [SAP Development Tools](https://tools.hana.ondemand.com/#cloud). -* You have installed Node.js including node and npm. See https://nodejs.org/en/download/. +* You have installed Node.js including node and npm. See [Node.js](https://nodejs.org/en/download/). ## Context From c7564a6a99b00a39bce13f5b0abf8e348b87f499 Mon Sep 17 00:00:00 2001 From: SarahNoack <44202907+SarahNoack@users.noreply.github.com> Date: Fri, 18 Jan 2019 09:29:50 +0100 Subject: [PATCH 36/49] Doc: Changed Heading for UI5 Scenario (#438) --- documentation/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 8f695a703..9af21a86b 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -36,7 +36,7 @@ nav: - transportRequestUploadFile: steps/transportRequestUploadFile.md - 'Scenarios': - 'Develop Hybrid Applications with Jenkins and SAP Solution Manager': scenarios/changeManagement.md - - 'Fiori (MTA) build and deployment to SAP Cloud Platform': scenarios/ui5-sap-cp/Readme.md + - 'Create a Pipeline for SAP UI5 or SAP Fiori on SAP Cloud Platform': scenarios/ui5-sap-cp/Readme.md - 'Required Plugins': jenkins/requiredPlugins.md theme: From 49317ea852e9a789556e6d79450602b4dd1249e4 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 18 Jan 2019 09:34:03 +0100 Subject: [PATCH 37/49] Dock Introduce google group as communication channel (#435) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0c3900cba..9a2538181 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ project][piper-library-issues]. Feel free to open new issues for feature requests, bugs or general feedback on the [GitHub issues page of this project][piper-library-issues]. +Register to our [google group][google-group] in order to get updates or for asking questions. # Contributing Read and understand our [contribution guidelines][piper-library-contribution] @@ -123,3 +124,4 @@ otherwise in the [LICENSE file][piper-library-license] [jenkins-doc-libraries]: https://jenkins.io/doc/book/pipeline/shared-libraries [jenkins-doc-steps]: https://jenkins.io/doc/pipeline/steps [jenkins-plugin-sharedlibs]: https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Shared+Groovy+Libraries+Plugin +[google-group]: https://groups.google.com/forum/#!forum/project-piper From 303122c899e5308e5cdb04970440e0e9982c85a2 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 18 Jan 2019 09:36:16 +0100 Subject: [PATCH 38/49] Remove unneeded imports for ConfigurationMerger, ConfigurationLoader --- vars/checkChangeInDevelopment.groovy | 1 - vars/influxWriteData.groovy | 2 -- 2 files changed, 3 deletions(-) diff --git a/vars/checkChangeInDevelopment.groovy b/vars/checkChangeInDevelopment.groovy index a6a3c6c4b..97b49c308 100644 --- a/vars/checkChangeInDevelopment.groovy +++ b/vars/checkChangeInDevelopment.groovy @@ -7,7 +7,6 @@ import groovy.transform.Field import hudson.AbortException import com.sap.piper.ConfigurationHelper -import com.sap.piper.ConfigurationMerger import com.sap.piper.cm.BackendType import com.sap.piper.cm.ChangeManagement import com.sap.piper.cm.ChangeManagementException diff --git a/vars/influxWriteData.groovy b/vars/influxWriteData.groovy index f76d5777a..39ef9e1b5 100644 --- a/vars/influxWriteData.groovy +++ b/vars/influxWriteData.groovy @@ -1,8 +1,6 @@ import static com.sap.piper.Prerequisites.checkScript import com.sap.piper.ConfigurationHelper -import com.sap.piper.ConfigurationLoader -import com.sap.piper.ConfigurationMerger import com.sap.piper.JsonUtils import com.sap.piper.Utils From f6647d9ce559b751c13dfceb182df874bb4cefc4 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Fri, 18 Jan 2019 13:14:39 +0100 Subject: [PATCH 39/49] newmanExecuteTests: use user-specific .npm-global directory (#447) * use user-specific .npm-global directory * Update newmanExecute.groovy * fix typo * Update NewmanExecuteTest.groovy * Update NewmanExecuteTest.groovy --- test/groovy/NewmanExecuteTest.groovy | 27 ++++++++++++++++++++++----- vars/newmanExecute.groovy | 4 ++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/test/groovy/NewmanExecuteTest.groovy b/test/groovy/NewmanExecuteTest.groovy index b4a9dc37f..2a4f9c0d4 100644 --- a/test/groovy/NewmanExecuteTest.groovy +++ b/test/groovy/NewmanExecuteTest.groovy @@ -6,6 +6,8 @@ import org.junit.rules.RuleChain import static org.hamcrest.Matchers.hasItem import static org.hamcrest.Matchers.is import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.endsWith +import static org.hamcrest.Matchers.startsWith import static org.junit.Assert.assertThat @@ -68,13 +70,28 @@ class NewmanExecuteTest extends BasePiperTest { newmanGlobals: 'testGlobals' ) // asserts - assertThat(jscr.shell, hasItem('npm install newman newman-reporter-html --global --quiet')) - assertThat(jscr.shell, hasItem('newman run \'testCollection\' --environment \'testEnvironment\' --globals \'testGlobals\' --reporters junit,html --reporter-junit-export \'target/newman/TEST-testCollection.xml\' --reporter-html-export \'target/newman/TEST-testCollection.html\'')) + assertThat(jscr.shell, hasItem(endsWith('npm install newman newman-reporter-html --global --quiet'))) + assertThat(jscr.shell, hasItem(endsWith('newman run \'testCollection\' --environment \'testEnvironment\' --globals \'testGlobals\' --reporters junit,html --reporter-junit-export \'target/newman/TEST-testCollection.xml\' --reporter-html-export \'target/newman/TEST-testCollection.html\''))) assertThat(jedr.dockerParams.dockerImage, is('node:8-stretch')) assertThat(jlr.log, containsString('[newmanExecute] Found files [testCollection]')) assertJobStatusSuccess() } + @Test + void testGlobalInstall() throws Exception { + jsr.step.newmanExecute( + script: nullScript, + juStabUtils: utils, + newmanCollection: 'testCollection', + newmanEnvironment: 'testEnvironment', + newmanGlobals: 'testGlobals' + ) + // asserts + assertThat(jscr.shell, hasItem(startsWith('NPM_CONFIG_PREFIX=~/.npm-global '))) + assertThat(jscr.shell, hasItem(startsWith('PATH=$PATH:~/.npm-global/bin'))) + assertJobStatusSuccess() + } + @Test void testExecuteNewmanWithNoCollection() throws Exception { thrown.expectMessage('[newmanExecute] No collection found with pattern \'notFound.json\'') @@ -103,7 +120,7 @@ class NewmanExecuteTest extends BasePiperTest { // asserts assertThat(jedr.dockerParams.dockerImage, is('testImage')) assertThat(gitMap.url, is('testRepo')) - assertThat(jscr.shell, hasItem('newman run \'testCollection\' --environment \'testEnvironment\' --globals \'testGlobals\' --reporters junit,html --reporter-junit-export \'target/newman/TEST-testCollection.xml\' --reporter-html-export \'target/newman/TEST-testCollection.html\' --suppress-exit-code')) + assertThat(jscr.shell, hasItem(endsWith('newman run \'testCollection\' --environment \'testEnvironment\' --globals \'testGlobals\' --reporters junit,html --reporter-junit-export \'target/newman/TEST-testCollection.xml\' --reporter-html-export \'target/newman/TEST-testCollection.html\' --suppress-exit-code'))) assertJobStatusSuccess() } @@ -115,8 +132,8 @@ class NewmanExecuteTest extends BasePiperTest { newmanRunCommand: 'run ${config.newmanCollection} --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-${config.newmanCollection.toString().replace(File.separatorChar,(char)\'_\').tokenize(\'.\').first()}.xml --reporter-html-export target/newman/TEST-${config.newmanCollection.toString().replace(File.separatorChar,(char)\'_\').tokenize(\'.\').first()}.html' ) // asserts - assertThat(jscr.shell, hasItem('newman run testCollectionsFolder'+File.separatorChar+'A.postman_collection.json --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-testCollectionsFolder_A.xml --reporter-html-export target/newman/TEST-testCollectionsFolder_A.html')) - assertThat(jscr.shell, hasItem('newman run testCollectionsFolder'+File.separatorChar+'B.postman_collection.json --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-testCollectionsFolder_B.xml --reporter-html-export target/newman/TEST-testCollectionsFolder_B.html')) + assertThat(jscr.shell, hasItem(endsWith('newman run testCollectionsFolder'+File.separatorChar+'A.postman_collection.json --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-testCollectionsFolder_A.xml --reporter-html-export target/newman/TEST-testCollectionsFolder_A.html'))) + assertThat(jscr.shell, hasItem(endsWith('newman run testCollectionsFolder'+File.separatorChar+'B.postman_collection.json --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-testCollectionsFolder_B.xml --reporter-html-export target/newman/TEST-testCollectionsFolder_B.html'))) assertJobStatusSuccess() } } diff --git a/vars/newmanExecute.groovy b/vars/newmanExecute.groovy index d45364eca..4fd543507 100644 --- a/vars/newmanExecute.groovy +++ b/vars/newmanExecute.groovy @@ -98,7 +98,7 @@ void call(Map parameters = [:]) { dockerImage: config.dockerImage, stashContent: config.stashContent ) { - sh "${config.newmanInstallCommand}" + sh "NPM_CONFIG_PREFIX=~/.npm-global ${config.newmanInstallCommand}" for(String collection : collectionList){ def collectionDisplayName = collection.toString().replace(File.separatorChar,(char)'_').tokenize('.').first() // resolve templates @@ -109,7 +109,7 @@ void call(Map parameters = [:]) { collectionDisplayName: collectionDisplayName ]).toString() if(!config.failOnError) command += ' --suppress-exit-code' - sh "newman ${command}" + sh "PATH=\$PATH:~/.npm-global/bin newman ${command}" } } } From 423c9f52ba775dee798cfd490f0da9e5c8800743 Mon Sep 17 00:00:00 2001 From: sbmaier <39129097+sbmaier@users.noreply.github.com> Date: Fri, 18 Jan 2019 15:33:36 +0100 Subject: [PATCH 40/49] cloudFoundryDeploy: fix naming of Influx step (#450) * fix naming Influx * fix test for influx name change --- test/groovy/CloudFoundryDeployTest.groovy | 2 +- vars/cloudFoundryDeploy.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/groovy/CloudFoundryDeployTest.groovy b/test/groovy/CloudFoundryDeployTest.groovy index 1867d4803..47f399be5 100644 --- a/test/groovy/CloudFoundryDeployTest.groovy +++ b/test/groovy/CloudFoundryDeployTest.groovy @@ -58,7 +58,7 @@ class CloudFoundryDeployTest extends BasePiperTest { @Before void init() { - helper.registerAllowedMethod('writeInflux', [Map.class], {m -> + helper.registerAllowedMethod('influxWriteData', [Map.class], {m -> writeInfluxMap = m }) } diff --git a/vars/cloudFoundryDeploy.groovy b/vars/cloudFoundryDeploy.groovy index a0c8c6684..480907330 100644 --- a/vars/cloudFoundryDeploy.groovy +++ b/vars/cloudFoundryDeploy.groovy @@ -264,5 +264,5 @@ private void reportToInflux(script, config, deploySuccess, JenkinsUtils jenkinsU cfOrg: config.cloudFoundry.org, cfSpace: config.cloudFoundry.space, ]] - writeInflux script: script, customData: [:], customDataTags: [:], customDataMap: deploymentData, customDataMapTags: deploymentDataTags + influxWriteData script: script, customData: [:], customDataTags: [:], customDataMap: deploymentData, customDataMapTags: deploymentDataTags } From dd8e09c527ece341cdfd3d6784502e7477002b2b Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 18 Jan 2019 14:43:57 +0100 Subject: [PATCH 41/49] Streamline flow control in order to avoid to have to escape at two locations in the code. --- vars/dockerExecute.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index 484314d67..8ba7da835 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -196,8 +196,9 @@ private getDockerOptions(Map dockerEnvVars, Map dockerVolumeBind, def dockerOpti if (dockerOptions) { if (dockerOptions instanceof CharSequence) { - options.add(dockerOptions.toString()) - } else if (dockerOptions instanceof List) { + dockerOptions = [dockerOptions] + } + if (dockerOptions instanceof List) { for (String option : dockerOptions) { options.add "${option}" } From c8c96f11e371e9c2eeab26c34ac6ee3d1d68e2a6 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 18 Jan 2019 13:40:15 +0100 Subject: [PATCH 42/49] Escape blanks when contained in env values for docker --- test/groovy/DockerExecuteTest.groovy | 7 ++++--- vars/dockerExecute.groovy | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/test/groovy/DockerExecuteTest.groovy b/test/groovy/DockerExecuteTest.groovy index ac8fe971b..ae2302595 100644 --- a/test/groovy/DockerExecuteTest.groovy +++ b/test/groovy/DockerExecuteTest.groovy @@ -141,14 +141,14 @@ class DockerExecuteTest extends BasePiperTest { void testExecuteInsideDockerContainerWithParameters() throws Exception { jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', - dockerOptions: '-it', + dockerOptions: '-description=lorem ipsum', dockerVolumeBind: ['my_vol': '/my_vol'], dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { bodyExecuted = true } assertTrue(docker.getParameters().contains('--env https_proxy ')) assertTrue(docker.getParameters().contains('--env http_proxy=http://proxy:8000')) - assertTrue(docker.getParameters().contains('-it')) + assertTrue(docker.getParameters().contains('description=lorem\\ ipsum')) assertTrue(docker.getParameters().contains('--volume my_vol:/my_vol')) assertTrue(bodyExecuted) } @@ -157,13 +157,14 @@ class DockerExecuteTest extends BasePiperTest { void testExecuteInsideDockerContainerWithDockerOptionsList() throws Exception { jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', - dockerOptions: ['-it', '--network=my-network'], + dockerOptions: ['-it', '--network=my-network', 'description=lorem ipsum'], dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { bodyExecuted = true } assertTrue(docker.getParameters().contains('--env http_proxy=http://proxy:8000')) assertTrue(docker.getParameters().contains('-it')) assertTrue(docker.getParameters().contains('--network=my-network')) + assertTrue(docker.getParameters().contains('description=lorem\\ ipsum')) } @Test diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index 8ba7da835..5780f658f 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -200,7 +200,7 @@ private getDockerOptions(Map dockerEnvVars, Map dockerVolumeBind, def dockerOpti } if (dockerOptions instanceof List) { for (String option : dockerOptions) { - options.add "${option}" + options << escapeBlanks(option) } } else { throw new IllegalArgumentException("Unexpected type for dockerOptions. Expected was either a list or a string. Actual type was: '${dockerOptions.getClass()}'") @@ -229,3 +229,22 @@ def getContainerDefined(config) { boolean isKubernetes() { return Boolean.valueOf(env.ON_K8S) } + +/** + * Escapes blanks for values in key/value pairs + * E.g. description=Lorem ipsum is + * changed to description=Lorem\ ipsum. + */ +@NonCPS +def escapeBlanks(def s) { + + def EQ='=' + def parts=s.split(EQ) + + if(parts.length == 2) { + parts[1]=parts[1].replaceAll(' ', '\\\\ ') + s = parts.join(EQ) + } + + return s +} From e452f79368abddc4bcf8f87e48c359fd4875c6e9 Mon Sep 17 00:00:00 2001 From: SarahNoack <44202907+SarahNoack@users.noreply.github.com> Date: Fri, 18 Jan 2019 17:42:32 +0100 Subject: [PATCH 43/49] Doc: Add Image to SolMan Scenario (#453) --- documentation/docs/images/SolMan_Scenario.png | Bin 0 -> 42312 bytes .../docs/scenarios/changeManagement.md | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 documentation/docs/images/SolMan_Scenario.png diff --git a/documentation/docs/images/SolMan_Scenario.png b/documentation/docs/images/SolMan_Scenario.png new file mode 100644 index 0000000000000000000000000000000000000000..5b30662bd0c10d025ef6f3cb4dae4d558c58351c GIT binary patch literal 42312 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

bp2 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 MxOuwPxP+el3n#6a;s5{u literal 0 HcmV?d00001 diff --git a/documentation/docs/scenarios/changeManagement.md b/documentation/docs/scenarios/changeManagement.md index 32b7f0cef..50b49a54e 100644 --- a/documentation/docs/scenarios/changeManagement.md +++ b/documentation/docs/scenarios/changeManagement.md @@ -34,6 +34,9 @@ The basic workflow is as follows: 4. The changes of your development team trigger the Jenkins pipeline. It builds and validates the changes and attaches them to the respective transport request. 5. As soon as the development process is completed, the change document in SAP Solution Manager can be set to status `to be tested` and all components can be transported to the test system. +![Hybrid Application Development Workflow](../images/SolMan_Scenario.png "Hybrid Application Development Workflow") +##### Hybrid Application Development Worflow + ## Example ### Jenkinsfile From 10267ce7d15be5ea3b0252fd574194aff7966e82 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 21 Jan 2019 08:47:34 +0100 Subject: [PATCH 44/49] Analytics: add step parameter keys (#442) * add stepParamKey values * camelCase --- vars/artifactSetVersion.groovy | 12 ++++++++++-- vars/batsExecuteTests.groovy | 7 +++++-- vars/checkChangeInDevelopment.groovy | 7 +++++-- vars/checksPublishResults.groovy | 7 +++++-- vars/cloudFoundryDeploy.groovy | 12 ++++++++++-- vars/gaugeExecuteTests.groovy | 8 +++++++- vars/influxWriteData.groovy | 7 +++++-- vars/mavenExecute.groovy | 7 +++++-- vars/mtaBuild.groovy | 7 +++++-- vars/neoDeploy.groovy | 4 ++++ vars/newmanExecute.groovy | 7 +++++-- vars/pipelineStashFilesAfterBuild.groovy | 7 +++++-- vars/pipelineStashFilesBeforeBuild.groovy | 7 +++++-- vars/piperStageWrapper.groovy | 15 ++++++++++++++- vars/seleniumExecuteTests.groovy | 7 +++++-- vars/setupCommonPipelineEnvironment.groovy | 10 +++++++--- vars/snykExecute.groovy | 7 +++++-- vars/testsPublishResults.groovy | 7 +++++-- vars/transportRequestCreate.groovy | 7 +++++-- vars/transportRequestRelease.groovy | 7 +++++-- vars/transportRequestUploadFile.groovy | 10 +++++++--- 21 files changed, 129 insertions(+), 40 deletions(-) diff --git a/vars/artifactSetVersion.groovy b/vars/artifactSetVersion.groovy index 0c66d816e..ca6c14e14 100644 --- a/vars/artifactSetVersion.groovy +++ b/vars/artifactSetVersion.groovy @@ -61,7 +61,15 @@ void call(Map parameters = [:], Closure body = null) { config = configHelper.addIfEmpty('timestamp', getTimestamp(config.timestampTemplate)) .use() - new Utils().pushToSWA([step: STEP_NAME, stepParam1: config.buildTool, stepParam2: config.artifactType, stepParam3: parameters?.script == null], config) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'buildTool', + stepParam1: config.buildTool, + stepParamKey2: 'artifactType', + stepParam2: config.artifactType, + stepParamKey3: 'scriptMissing', + stepParam3: parameters?.script == null + ], config) def artifactVersioning = ArtifactVersioning.getArtifactVersioning(config.buildTool, script, config) def currentVersion = artifactVersioning.getVersion() @@ -87,7 +95,7 @@ void call(Map parameters = [:], Closure body = null) { :script.commonPipelineEnvironment.getGitSshUrl()) .withMandatoryProperty('gitSshUrl') .use() - + def gitConfig = [] if(config.gitUserEMail) gitConfig.add("-c user.email=\"${config.gitUserEMail}\"") diff --git a/vars/batsExecuteTests.groovy b/vars/batsExecuteTests.groovy index 16a39334d..58c00905c 100644 --- a/vars/batsExecuteTests.groovy +++ b/vars/batsExecuteTests.groovy @@ -43,8 +43,11 @@ void call(Map parameters = [:]) { .use() // report to SWA - utils.pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + utils.pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) script.commonPipelineEnvironment.setInfluxStepData('bats', false) diff --git a/vars/checkChangeInDevelopment.groovy b/vars/checkChangeInDevelopment.groovy index 97b49c308..fdd8540a6 100644 --- a/vars/checkChangeInDevelopment.groovy +++ b/vars/checkChangeInDevelopment.groovy @@ -94,8 +94,11 @@ void call(parameters = [:]) { */ .withMandatoryProperty('changeManagement/endpoint') - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) def changeId = getChangeDocumentId(cm, script, configuration) diff --git a/vars/checksPublishResults.groovy b/vars/checksPublishResults.groovy index 8ec884252..4495cbb1c 100644 --- a/vars/checksPublishResults.groovy +++ b/vars/checksPublishResults.groovy @@ -41,8 +41,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) // JAVA report('PmdPublisher', configuration.pmd, configuration.archive) diff --git a/vars/cloudFoundryDeploy.groovy b/vars/cloudFoundryDeploy.groovy index 480907330..5238b7b51 100644 --- a/vars/cloudFoundryDeploy.groovy +++ b/vars/cloudFoundryDeploy.groovy @@ -54,7 +54,15 @@ void call(Map parameters = [:]) { .withMandatoryProperty('cloudFoundry/credentialsId') .use() - utils.pushToSWA([step: STEP_NAME, stepParam1: config.deployTool, stepParam2: config.deployType, stepParam3: parameters?.script == null], config) + utils.pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'deployTool', + stepParam1: config.deployTool, + stepParamKey2: 'deployType', + stepParam2: config.deployType, + stepParamKey3: 'scriptMissing', + stepParam3: parameters?.script == null + ], config) echo "[${STEP_NAME}] General parameters: deployTool=${config.deployTool}, deployType=${config.deployType}, cfApiEndpoint=${config.cloudFoundry.apiEndpoint}, cfOrg=${config.cloudFoundry.org}, cfSpace=${config.cloudFoundry.space}, cfCredentialsId=${config.cloudFoundry.credentialsId}, deployUser=${config.deployUser}" @@ -159,7 +167,7 @@ def deployCfNative (config) { } sh """#!/bin/bash - set +x + set +x set -e export HOME=${config.dockerWorkspace} cf login -u \"${username}\" -p '${password}' -a ${config.cloudFoundry.apiEndpoint} -o \"${config.cloudFoundry.org}\" -s \"${config.cloudFoundry.space}\" diff --git a/vars/gaugeExecuteTests.groovy b/vars/gaugeExecuteTests.groovy index e2664ccd5..40c2beb70 100644 --- a/vars/gaugeExecuteTests.groovy +++ b/vars/gaugeExecuteTests.groovy @@ -47,7 +47,13 @@ void call(Map parameters = [:]) { .dependingOn('buildTool').mixin('testOptions') .use() - utils.pushToSWA([step: STEP_NAME, stepParam1: config.buildTool, stepParam2: config.dockerName], config) + utils.pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'buildTool', + stepParam1: config.buildTool, + stepParamKey2: 'dockerName', + stepParam2: config.dockerName + ], config) if(!config.dockerEnvVars.TARGET_SERVER_URL && config.testServerUrl) config.dockerEnvVars.TARGET_SERVER_URL = config.testServerUrl diff --git a/vars/influxWriteData.groovy b/vars/influxWriteData.groovy index 39ef9e1b5..48886830d 100644 --- a/vars/influxWriteData.groovy +++ b/vars/influxWriteData.groovy @@ -44,8 +44,11 @@ void call(Map parameters = [:]) { .addIfNull('customDataMapTags', script.commonPipelineEnvironment.getInfluxCustomDataMapTags()) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) if (!config.artifactVersion) { //this takes care that terminated builds due to milestone-locking do not cause an error diff --git a/vars/mavenExecute.groovy b/vars/mavenExecute.groovy index c91137cc4..1f5125084 100644 --- a/vars/mavenExecute.groovy +++ b/vars/mavenExecute.groovy @@ -37,8 +37,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) String command = "mvn" diff --git a/vars/mtaBuild.groovy b/vars/mtaBuild.groovy index 049b77719..24e26a3cb 100644 --- a/vars/mtaBuild.groovy +++ b/vars/mtaBuild.groovy @@ -36,8 +36,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) dockerExecute(script: script, dockerImage: configuration.dockerImage, dockerOptions: configuration.dockerOptions) { def java = new ToolDescriptor('Java', 'JAVA_HOME', '', '/bin/', 'java', '1.8.0', '-version 2>&1') diff --git a/vars/neoDeploy.groovy b/vars/neoDeploy.groovy index 66d4bf134..e31fb4e50 100644 --- a/vars/neoDeploy.groovy +++ b/vars/neoDeploy.groovy @@ -104,9 +104,13 @@ void call(parameters = [:]) { utils.pushToSWA([ step: STEP_NAME, + stepParamKey1: 'deployMode', stepParam1: configuration.deployMode == 'mta'?'mta':'war', // ['mta', 'warParams', 'warPropertiesFile'] + stepParamKey2: 'warAction', stepParam2: configuration.warAction == 'rolling-update'?'blue-green':'standard', // ['deploy', 'deploy-mta', 'rolling-update'] + stepParamKey3: 'scriptMissing', stepParam3: parameters?.script == null, + stepParamKey4: 'legacyConfig', stepParam4: ! stepCompatibilityConfiguration.isEmpty(), ], configuration) diff --git a/vars/newmanExecute.groovy b/vars/newmanExecute.groovy index 4fd543507..d7291fcd3 100644 --- a/vars/newmanExecute.groovy +++ b/vars/newmanExecute.groovy @@ -79,8 +79,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) config.stashContent = config.testRepository ?[GitUtils.handleTestRepository(this, config)] diff --git a/vars/pipelineStashFilesAfterBuild.groovy b/vars/pipelineStashFilesAfterBuild.groovy index 7ca2a56ad..2bbc1c727 100644 --- a/vars/pipelineStashFilesAfterBuild.groovy +++ b/vars/pipelineStashFilesAfterBuild.groovy @@ -34,8 +34,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) // store files to be checked with checkmarx if (config.runCheckmarx) { diff --git a/vars/pipelineStashFilesBeforeBuild.groovy b/vars/pipelineStashFilesBeforeBuild.groovy index 86657feaf..47de4cd20 100644 --- a/vars/pipelineStashFilesBeforeBuild.groovy +++ b/vars/pipelineStashFilesBeforeBuild.groovy @@ -32,8 +32,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) if (config.runOpaTests){ utils.stash('opa5', config.stashIncludes?.get('opa5')?config.stashIncludes.opa5:'**/*.*', config.stashExcludes?.get('opa5')?config.stashExcludes.opa5:'') diff --git a/vars/piperStageWrapper.groovy b/vars/piperStageWrapper.groovy index abf200841..40ae279b2 100644 --- a/vars/piperStageWrapper.groovy +++ b/vars/piperStageWrapper.groovy @@ -103,6 +103,19 @@ private void executeStage(script, originalStage, stageName, config, utils) { deleteDir() def duration = System.currentTimeMillis() - startTime - utils.pushToSWA([eventType: 'library-os-stage', stageName: stageName, stepParam1: "${script.currentBuild.currentResult}", stepParam2: "${startTime}", stepParam3: "${duration}", stepParam4: "${projectExtensions}", stepParam5: "${globalExtensions}"], config) + utils.pushToSWA([ + eventType: 'library-os-stage', + stageName: stageName, + stepParamKey1: 'buildResult', + stepParam1: "${script.currentBuild.currentResult}", + stepParamKey2: 'stageStartTime', + stepParam2: "${startTime}", + stepParamKey3: 'stageDuration', + stepParam3: "${duration}", + stepParamKey4: 'projectExtension', + stepParam4: "${projectExtensions}", + stepParamKey5: 'globalExtension', + stepParam5: "${globalExtensions}" + ], config) } } diff --git a/vars/seleniumExecuteTests.groovy b/vars/seleniumExecuteTests.groovy index 93823db2d..d1de4f9f6 100644 --- a/vars/seleniumExecuteTests.groovy +++ b/vars/seleniumExecuteTests.groovy @@ -48,8 +48,11 @@ void call(Map parameters = [:], Closure body) { .dependingOn('buildTool').mixin('dockerWorkspace') .use() - utils.pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + utils.pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) dockerExecute( script: script, diff --git a/vars/setupCommonPipelineEnvironment.groovy b/vars/setupCommonPipelineEnvironment.groovy index ea6d1f704..eca36b142 100644 --- a/vars/setupCommonPipelineEnvironment.groovy +++ b/vars/setupCommonPipelineEnvironment.groovy @@ -24,9 +24,13 @@ void call(Map parameters = [:]) { .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) .use() - (parameters.utils ?: new Utils()) - .pushToSWA([step: STEP_NAME, stepParam4: parameters.customDefaults?'true':'false', - stepParam5: Boolean.toString( ! (script?.commonPipelineEnvironment?.getConfigProperties() ?: [:]).isEmpty())], config) + (parameters.utils ?: new Utils()).pushToSWA([ + step: STEP_NAME, + stepParamKey4: 'customDefaults', + stepParam4: parameters.customDefaults?'true':'false', + stepParamKey5: 'legacyConfig', + stepParam5: Boolean.toString( ! (script?.commonPipelineEnvironment?.getConfigProperties() ?: [:]).isEmpty()) + ], config) } } diff --git a/vars/snykExecute.groovy b/vars/snykExecute.groovy index f555f04b2..0da14a0e9 100644 --- a/vars/snykExecute.groovy +++ b/vars/snykExecute.groovy @@ -38,8 +38,11 @@ void call(Map parameters = [:]) { .withMandatoryProperty('snykCredentialsId') .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], config) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) utils.unstashAll(config.stashContent) diff --git a/vars/testsPublishResults.groovy b/vars/testsPublishResults.groovy index 881aa9b05..8c08098a1 100644 --- a/vars/testsPublishResults.groovy +++ b/vars/testsPublishResults.groovy @@ -40,8 +40,11 @@ void call(Map parameters = [:]) { .mixin(parameters, PARAMETER_KEYS) .use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) // UNIT TESTS publishJUnitReport(configuration.get('junit')) diff --git a/vars/transportRequestCreate.groovy b/vars/transportRequestCreate.groovy index 91b27e5bb..1dcb94c4a 100644 --- a/vars/transportRequestCreate.groovy +++ b/vars/transportRequestCreate.groovy @@ -63,8 +63,11 @@ void call(parameters = [:]) { def changeDocumentId = null - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) if(backendType == BackendType.SOLMAN) { diff --git a/vars/transportRequestRelease.groovy b/vars/transportRequestRelease.groovy index 938a3c6f0..334251021 100644 --- a/vars/transportRequestRelease.groovy +++ b/vars/transportRequestRelease.groovy @@ -58,8 +58,11 @@ void call(parameters = [:]) { configuration = configHelper.use() - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], configuration) def changeDocumentId = null def transportRequestId = getTransportRequestId(cm, script, configuration) diff --git a/vars/transportRequestUploadFile.groovy b/vars/transportRequestUploadFile.groovy index 10a6a7d71..ceae10712 100644 --- a/vars/transportRequestUploadFile.groovy +++ b/vars/transportRequestUploadFile.groovy @@ -61,9 +61,13 @@ void call(parameters = [:]) { .withMandatoryProperty('changeManagement/git/format') .withMandatoryProperty('filePath') - new Utils().pushToSWA([step: STEP_NAME, - stepParam1: configuration.changeManagement.type, - stepParam2: parameters?.script == null], configuration) + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'changeManagementType', + stepParam1: configuration.changeManagement.type, + stepParamKey2: 'scriptMissing', + stepParam2: parameters?.script == null + ], configuration) def changeDocumentId = null From 6224d2aece42f47b551114ae2327619b340e036a Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 21 Jan 2019 14:12:05 +0100 Subject: [PATCH 45/49] influxWriteData: catch NPE (#434) * Update influxWriteData.groovy * Update influxWriteData.groovy * Update influxWriteData.groovy --- vars/influxWriteData.groovy | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/vars/influxWriteData.groovy b/vars/influxWriteData.groovy index 48886830d..79a083756 100644 --- a/vars/influxWriteData.groovy +++ b/vars/influxWriteData.groovy @@ -82,15 +82,23 @@ InfluxDB data map tags: ${config.customDataMapTags} private void writeToInflux(config, script){ if (config.influxServer) { - step([ - $class: 'InfluxDbPublisher', - selectedTarget: config.influxServer, - customPrefix: config.influxPrefix, - customData: config.customData.size()>0 ? config.customData : null, - customDataTags: config.customDataTags.size()>0 ? config.customDataTags : null, - customDataMap: config.customDataMap.size()>0 ? config.customDataMap : null, - customDataMapTags: config.customDataMapTags.size()>0 ? config.customDataMapTags : null - ]) + try { + step([ + $class: 'InfluxDbPublisher', + selectedTarget: config.influxServer, + customPrefix: config.influxPrefix, + customData: config.customData.size()>0 ? config.customData : null, + customDataTags: config.customDataTags.size()>0 ? config.customDataTags : null, + customDataMap: config.customDataMap.size()>0 ? config.customDataMap : null, + customDataMapTags: config.customDataMapTags.size()>0 ? config.customDataMapTags : null + ]) + } catch (NullPointerException e){ + if(!e.getMessage()){ + //TODO: catch NPEs as long as https://issues.jenkins-ci.org/browse/JENKINS-55594 is not fixed & released + error "[$STEP_NAME] NullPointerException occured, is the correct target defined?" + } + throw e + } } //write results into json file for archiving - also benefitial when no InfluxDB is available yet From 27c3891685023b3fbd539c8bb6afde603c05840f Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Tue, 22 Jan 2019 16:13:59 +0100 Subject: [PATCH 46/49] deployToCloudFoundry - patch mta blue-green deployment (#457) mta deploy plugin has flag: ` --no-confirm` which is described as _"Do not require confirmation for deleting the previously deployed MTA apps"_ This flag is essentials for performing fully automated blue-green deployments. --- .../docs/steps/cloudFoundryDeploy.md | 2 +- test/groovy/CloudFoundryDeployTest.groovy | 19 +++++++++++++++++++ vars/cloudFoundryDeploy.groovy | 6 +++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/documentation/docs/steps/cloudFoundryDeploy.md b/documentation/docs/steps/cloudFoundryDeploy.md index 3fbb05ad1..1a80cd61e 100644 --- a/documentation/docs/steps/cloudFoundryDeploy.md +++ b/documentation/docs/steps/cloudFoundryDeploy.md @@ -32,7 +32,7 @@ Deployment can be done | keepOldInstance | no | false | true, false | | dockerImage | no | s4sdk/docker-cf-cli | | | dockerWorkspace | no | /home/piper | | -| mtaDeployParameters | | -f | | +| mtaDeployParameters | | for _deployType:standard_ `-f`
for _deployType:blue-green_ `-f --no-confirm` | | | mtaExtensionDescriptor | no | '' | | | mtaPath | no | '' | | | smokeTestScript | no | blueGreenCheckScript.sh (provided by library).
Can be overwritten using config property 'smokeTestScript' | | diff --git a/test/groovy/CloudFoundryDeployTest.groovy b/test/groovy/CloudFoundryDeployTest.groovy index 47f399be5..418c1cd3b 100644 --- a/test/groovy/CloudFoundryDeployTest.groovy +++ b/test/groovy/CloudFoundryDeployTest.groovy @@ -16,6 +16,7 @@ import util.JenkinsWriteFileRule import util.JenkinsReadYamlRule import util.Rules +import static org.hamcrest.Matchers.stringContainsInOrder import static org.junit.Assert.assertThat import static org.hamcrest.Matchers.hasItem @@ -398,6 +399,24 @@ class CloudFoundryDeployTest extends BasePiperTest { assertThat(jscr.shell, hasItem(containsString('cf logout'))) } + @Test + void testMtaBlueGreen() { + + jsr.step.cloudFoundryDeploy([ + script: nullScript, + juStabUtils: utils, + jenkinsUtilsStub: new JenkinsUtilsMock(), + cfOrg: 'testOrg', + cfSpace: 'testSpace', + cfCredentialsId: 'test_cfCredentialsId', + deployTool: 'mtaDeployPlugin', + deployType: 'blue-green', + mtaPath: 'target/test.mtar' + ]) + + assertThat(jscr.shell, hasItem(stringContainsInOrder(["cf login -u test_cf", 'cf bg-deploy', '-f', '--no-confirm']))) + } + @Test void testInfluxReporting() { jryr.registerYaml('test.yml', "applications: [[name: 'manifestAppName']]") diff --git a/vars/cloudFoundryDeploy.groovy b/vars/cloudFoundryDeploy.groovy index 5238b7b51..e1f6ef69f 100644 --- a/vars/cloudFoundryDeploy.groovy +++ b/vars/cloudFoundryDeploy.groovy @@ -209,8 +209,12 @@ def deployMta (config) { if (!config.mtaExtensionDescriptor.isEmpty() && !config.mtaExtensionDescriptor.startsWith('-e ')) config.mtaExtensionDescriptor = "-e ${config.mtaExtensionDescriptor}" def deployCommand = 'deploy' - if (config.deployType == 'blue-green') + if (config.deployType == 'blue-green') { deployCommand = 'bg-deploy' + if (config.mtaDeployParameters.indexOf('--no-confirm') < 0) { + config.mtaDeployParameters += ' --no-confirm' + } + } withCredentials([usernamePassword( credentialsId: config.cloudFoundry.credentialsId, From 8f513fb68bf96e415064fa86c11668ea1fb3b292 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Wed, 23 Jan 2019 12:51:46 +0100 Subject: [PATCH 47/49] add config reporting (#459) --- src/com/sap/piper/ConfigurationHelper.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/sap/piper/ConfigurationHelper.groovy b/src/com/sap/piper/ConfigurationHelper.groovy index 5d153c5c4..edf1f1ab4 100644 --- a/src/com/sap/piper/ConfigurationHelper.groovy +++ b/src/com/sap/piper/ConfigurationHelper.groovy @@ -112,6 +112,7 @@ class ConfigurationHelper implements Serializable { Map use(){ handleValidationFailures() MapUtils.traverse(config, { v -> (v instanceof GString) ? v.toString() : v }) + if(config.verbose) step.echo "[${name}] Configuration: ${config}" return config } From 05e0d958dad54f1accd7810a30a0235bdf0f2ea8 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Wed, 23 Jan 2019 14:19:36 +0100 Subject: [PATCH 48/49] dockerExecute - fix issue when image does not contain which (#458) command `which` requires a dedicated OS package to be installed. In case a Jenkins Master or Jenkins Slave Image does not contain `which`, although `docker` command is available the step took a wrong turn. This removes the check using `which` since checking `docker ps` is sufficient. --- test/groovy/DockerExecuteTest.groovy | 8 ++++---- vars/dockerExecute.groovy | 6 ------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/test/groovy/DockerExecuteTest.groovy b/test/groovy/DockerExecuteTest.groovy index ac8fe971b..f66aa039f 100644 --- a/test/groovy/DockerExecuteTest.groovy +++ b/test/groovy/DockerExecuteTest.groovy @@ -31,7 +31,7 @@ class DockerExecuteTest extends BasePiperTest { .around(jlr) .around(jsr) - int whichDockerReturnValue = 0 + int dockerPsReturnValue = 0 def bodyExecuted def containerName @@ -41,7 +41,7 @@ class DockerExecuteTest extends BasePiperTest { docker = new DockerMock() JenkinsUtils.metaClass.static.isPluginActive = {def s -> new PluginMock(s).isActive()} binding.setVariable('docker', docker) - helper.registerAllowedMethod('sh', [Map.class], {return whichDockerReturnValue}) + helper.registerAllowedMethod('sh', [Map.class], {return dockerPsReturnValue}) } @Test @@ -168,12 +168,12 @@ class DockerExecuteTest extends BasePiperTest { @Test void testDockerNotInstalledResultsInLocalExecution() throws Exception { - whichDockerReturnValue = 1 + dockerPsReturnValue = 1 jsr.step.dockerExecute(script: nullScript, dockerOptions: '-it') { bodyExecuted = true } - assertTrue(jlr.log.contains('No docker environment found')) + assertTrue(jlr.log.contains('Cannot connect to docker daemon')) assertTrue(jlr.log.contains('Running on local environment')) assertTrue(bodyExecuted) assertFalse(docker.isImagePulled()) diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index 484314d67..ab0055ccd 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -106,12 +106,6 @@ void call(Map parameters = [:], body) { executeInsideDocker = false } - def returnCode = sh script: 'which docker > /dev/null', returnStatus: true - if (returnCode != 0) { - echo "[WARNING][${STEP_NAME}] No docker environment found (command 'which docker' did not return with '0'). Configured docker image '${config.dockerImage}' will not be used." - executeInsideDocker = false - } - returnCode = sh script: 'docker ps -q > /dev/null', returnStatus: true if (returnCode != 0) { echo "[WARNING][$STEP_NAME] Cannot connect to docker daemon (command 'docker ps' did not return with '0'). Configured docker image '${config.dockerImage}' will not be used." From 5c4843aeaf041774af40b4ebb9e58ea2a93cd3e0 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Wed, 23 Jan 2019 16:18:58 +0100 Subject: [PATCH 49/49] Put GenerateDocumentation annotation inside groovy file ... since java files are not compiled on Jenkins causing compilation failures since the annotation cannot be found. --- src/com/sap/piper/GenerateDocumentation.groovy | 11 +++++++++++ src/com/sap/piper/GenerateDocumentation.java | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 src/com/sap/piper/GenerateDocumentation.groovy delete mode 100644 src/com/sap/piper/GenerateDocumentation.java diff --git a/src/com/sap/piper/GenerateDocumentation.groovy b/src/com/sap/piper/GenerateDocumentation.groovy new file mode 100644 index 000000000..3db75b003 --- /dev/null +++ b/src/com/sap/piper/GenerateDocumentation.groovy @@ -0,0 +1,11 @@ +package com.sap.piper + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.METHOD, ElementType.TYPE]) +public @interface GenerateDocumentation { +} diff --git a/src/com/sap/piper/GenerateDocumentation.java b/src/com/sap/piper/GenerateDocumentation.java deleted file mode 100644 index b7f57334d..000000000 --- a/src/com/sap/piper/GenerateDocumentation.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sap.piper; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface GenerateDocumentation { -}