From def66f4ffa9e614eeb3747599a0be7ea9a108453 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Wed, 30 Jan 2019 12:39:33 +0100 Subject: [PATCH 1/8] extend Analytics (#439) * add extension mechanism for analytics * add sha1 hashing * correct return types * correct registerEventListener method * decrese visibility of createInstance * correct typo * catch exceptions from tests * correct test case * Update Analytics.groovy * rename to Telemetry * rename file * fix typo * add test case for generateSha1 * expose methods to tests * add clear method for tests * change return type * add test for Telemetry class * replace UtilsTests * remove unused imports * make default reporter static * add stage parameters to payload * simplify SHA1 method * remove obsolete method * remove obsolete methods * remove outdated tests --- src/com/sap/piper/Utils.groovy | 73 ++------- src/com/sap/piper/analytics/Telemetry.groovy | 97 ++++++++++++ test/groovy/com/sap/piper/UtilsTest.groovy | 37 +---- .../sap/piper/analytics/TelemetryTest.groovy | 145 ++++++++++++++++++ vars/piperStageWrapper.groovy | 8 +- 5 files changed, 269 insertions(+), 91 deletions(-) create mode 100644 src/com/sap/piper/analytics/Telemetry.groovy create mode 100644 test/groovy/com/sap/piper/analytics/TelemetryTest.groovy diff --git a/src/com/sap/piper/Utils.groovy b/src/com/sap/piper/Utils.groovy index 6e14bba27..47f1e8f49 100644 --- a/src/com/sap/piper/Utils.groovy +++ b/src/com/sap/piper/Utils.groovy @@ -1,7 +1,10 @@ package com.sap.piper import com.cloudbees.groovy.cps.NonCPS -import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException +import com.sap.piper.analytics.Telemetry + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest @NonCPS def getMandatoryParameter(Map map, paramName, defaultValue = null) { @@ -72,67 +75,23 @@ def unstashAll(stashContent) { return unstashedContent } -def generateSha1Inline(input) { - return "`echo -n '${input}' | sha1sum | sed 's/ -//'`" +@NonCPS +def generateSha1(input) { + return MessageDigest + .getInstance("SHA-1") + .digest(input.getBytes(StandardCharsets.UTF_8)) + .encodeHex().toString() } void pushToSWA(Map parameters, Map config) { try { - //allow opt-out via configuration - if (!config?.collectTelemetryData) { - echo "[${parameters.get('step')}] Telemetry Report to SWA disabled!" - return - } + parameters.actionName = parameters.get('actionName') ?: 'Piper Library OS' + parameters.eventType = parameters.get('eventType') ?: 'library-os' + parameters.jobUrlSha1 = generateSha1(env.JOB_URL) + parameters.buildUrlSha1 = generateSha1(env.BUILD_URL) - def swaCustom = [:] - - /* SWA custom parameters: - custom3 = step name (passed as parameter step) - custom4 = job url hashed (calculated) - custom5 = build url hashed (calculated) - custom10 = stage name - custom11 = step related parameter 1 (passed as parameter stepParam1) - custom12 = step related parameter 2 (passed as parameter stepParam2) - custom13 = step related parameter 3 (passed as parameter stepParam3) - custom14 = step related parameter 4 (passed as parameter stepParam4) - custom15 = step related parameter 5 (passed as parameter stepParam5) - */ - - def swaUrl = 'https://webanalytics.cfapps.eu10.hana.ondemand.com/tracker/log' - def action_name = 'Piper Library OS' - def idsite = '827e8025-1e21-ae84-c3a3-3f62b70b0130' - def url = 'https://github.com/SAP/jenkins-library' - def event_type = parameters.get('eventType') ?: 'library-os' - - swaCustom.custom3 = parameters.get('step') - swaCustom.custom4 = generateSha1Inline(env.JOB_URL) - swaCustom.custom5 = generateSha1Inline(env.BUILD_URL) - swaCustom.custom10 = parameters.get('stageName') - swaCustom.custom11 = parameters.get('stepParam1') - swaCustom.custom12 = parameters.get('stepParam2') - swaCustom.custom13 = parameters.get('stepParam3') - swaCustom.custom14 = parameters.get('stepParam4') - swaCustom.custom15 = parameters.get('stepParam5') - - def options = [] - options.push("-G") - options.push("-v \"${swaUrl}\"") - options.push("--data-urlencode \"action_name=${action_name}\"") - options.push("--data-urlencode \"idsite=${idsite}\"") - options.push("--data-urlencode \"url=${url}\"") - options.push("--data-urlencode \"event_type=${event_type}\"") - for(def key : ['custom3', 'custom4', 'custom5', 'custom10', 'custom11', 'custom12', 'custom13', 'custom14', 'custom15']){ - if (swaCustom[key] != null) options.push("--data-urlencode \"${key}=${swaCustom[key]}\"") - } - options.push("--connect-timeout 5") - options.push("--max-time 20") - - sh(returnStatus: true, script: "#!/bin/sh +x\ncurl ${options.join(' ')} > /dev/null 2>&1 || echo '[${parameters.get('step')}] Telemetry Report to SWA failed!'") - - } catch (MissingContextVariableException noNode) { - echo "[${parameters.get('step')}] Telemetry Report to SWA skipped, no node available!" + Telemetry.notify(this, config, parameters) } catch (ignore) { - // some error occured in SWA reporting. This should not break anything though. + // some error occured in telemetry reporting. This should not break anything though. } } - diff --git a/src/com/sap/piper/analytics/Telemetry.groovy b/src/com/sap/piper/analytics/Telemetry.groovy new file mode 100644 index 000000000..1a386d6b9 --- /dev/null +++ b/src/com/sap/piper/analytics/Telemetry.groovy @@ -0,0 +1,97 @@ +package com.sap.piper.analytics + +import com.cloudbees.groovy.cps.NonCPS +import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException + +class Telemetry implements Serializable{ + + protected static Telemetry instance + + protected List listenerList = [] + + protected Telemetry(){} + + @NonCPS + protected static Telemetry getInstance(){ + if(!instance) { + instance = new Telemetry() + + registerListener({ steps, payload -> + piperOsDefaultReporting(steps, payload) + }) + } + return instance + } + + static void registerListener(Closure listener){ + getInstance().listenerList.add(listener) + } + + static notify(Script steps, Map config, Map payload){ + //allow opt-out via configuration + if (!config?.collectTelemetryData) { + steps.echo "[${payload.step}] Telemetry reporting disabled!" + return + } + + getInstance().listenerList.each { listener -> + try { + listener(steps, payload) + } catch (ignore) { + // some error occured in telemetry reporting. This should not break anything though. + steps.echo "[${payload.step}] Telemetry Report with listener failed: ${ignore.getMessage()}" + } + } + } + + protected static void piperOsDefaultReporting(Script steps, Map payload) { + try { + + def swaCustom = [:] + + /* SWA custom parameters: + custom3 = step name (passed as parameter step) + custom4 = job url hashed (calculated) + custom5 = build url hashed (calculated) + custom10 = stage name + custom11 = step related parameter 1 (passed as parameter stepParam1) + custom12 = step related parameter 2 (passed as parameter stepParam2) + custom13 = step related parameter 3 (passed as parameter stepParam3) + custom14 = step related parameter 4 (passed as parameter stepParam4) + custom15 = step related parameter 5 (passed as parameter stepParam5) + */ + + def swaUrl = 'https://webanalytics.cfapps.eu10.hana.ondemand.com/tracker/log' + def idsite = '827e8025-1e21-ae84-c3a3-3f62b70b0130' + def url = 'https://github.com/SAP/jenkins-library' + + swaCustom.custom3 = payload.step + swaCustom.custom4 = payload.jobUrlSha1 + swaCustom.custom5 = payload.buildUrlSha1 + swaCustom.custom10 = payload.stageName + swaCustom.custom11 = payload.stepParam1 + swaCustom.custom12 = payload.stepParam2 + swaCustom.custom13 = payload.stepParam3 + swaCustom.custom14 = payload.stepParam4 + swaCustom.custom15 = payload.stepParam5 + + def options = [] + options.push("-G") + options.push("-v \"${swaUrl}\"") + options.push("--data-urlencode \"action_name=${payload.actionName}\"") + options.push("--data-urlencode \"idsite=${idsite}\"") + options.push("--data-urlencode \"url=${url}\"") + options.push("--data-urlencode \"event_type=${payload.eventType}\"") + for(def key : ['custom3', 'custom4', 'custom5', 'custom10', 'custom11', 'custom12', 'custom13', 'custom14', 'custom15']){ + if (swaCustom[key] != null) options.push("--data-urlencode \"${key}=${swaCustom[key]}\"") + } + options.push("--connect-timeout 5") + options.push("--max-time 20") + + steps.sh(returnStatus: true, script: "#!/bin/sh +x\ncurl ${options.join(' ')} > /dev/null 2>&1 || echo '[${payload.step}] Telemetry Report to SWA failed!'") + + } catch (MissingContextVariableException noNode) { + steps.echo "[${payload.step}] Telemetry Report to SWA skipped, no node available!" + } + } +} diff --git a/test/groovy/com/sap/piper/UtilsTest.groovy b/test/groovy/com/sap/piper/UtilsTest.groovy index 0e6c55b23..f951b010d 100644 --- a/test/groovy/com/sap/piper/UtilsTest.groovy +++ b/test/groovy/com/sap/piper/UtilsTest.groovy @@ -2,6 +2,7 @@ package com.sap.piper import org.junit.Rule import org.junit.Before +import org.junit.Ignore import org.junit.Test import static org.junit.Assert.assertThat import org.junit.rules.ExpectedException @@ -63,38 +64,10 @@ class UtilsTest extends BasePiperTest { } @Test - void testSWAReporting() { - utils.env = [BUILD_URL: 'something', JOB_URL: 'nothing'] - utils.pushToSWA([step: 'anything'], [collectTelemetryData: true]) + void testGenerateSHA1() { + def result = utils.generateSha1('ContinuousDelivery') // asserts - assertThat(shellRule.shell, hasItem(containsString('curl -G -v "https://webanalytics.cfapps.eu10.hana.ondemand.com/tracker/log"'))) - assertThat(shellRule.shell, hasItem(containsString('action_name=Piper Library OS'))) - assertThat(shellRule.shell, hasItem(containsString('custom3=anything'))) - assertThat(shellRule.shell, hasItem(containsString('custom5=`echo -n \'something\' | sha1sum | sed \'s/ -//\'`'))) - } - - @Test - void testDisabledSWAReporting() { - utils.env = [BUILD_URL: 'something', JOB_URL: 'nothing'] - utils.pushToSWA([step: 'anything'], [collectTelemetryData: false]) - // asserts - assertThat(loggingRule.log, containsString('[anything] Telemetry Report to SWA disabled!')) - assertThat(shellRule.shell, not(hasItem(containsString('https://webanalytics.cfapps.eu10.hana.ondemand.com')))) - } - - @Test - void testImplicitlyDisabledSWAReporting() { - utils.env = [BUILD_URL: 'something', JOB_URL: 'nothing'] - utils.pushToSWA([step: 'anything'], null) - // asserts - assertThat(loggingRule.log, containsString('[anything] Telemetry Report to SWA disabled!')) - } - - @Test - void testImplicitlyDisabledSWAReporting2() { - utils.env = [BUILD_URL: 'something', JOB_URL: 'nothing'] - utils.pushToSWA([step: 'anything'], [:]) - // asserts - assertThat(loggingRule.log, containsString('[anything] Telemetry Report to SWA disabled!')) + // generated with "echo -n 'ContinuousDelivery' | sha1sum | sed 's/ -//'" + assertThat(result, is('0dad6c33b6246702132454f604dee80740f399ad')) } } diff --git a/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy b/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy new file mode 100644 index 000000000..2d930930f --- /dev/null +++ b/test/groovy/com/sap/piper/analytics/TelemetryTest.groovy @@ -0,0 +1,145 @@ +package com.sap.piper.analytics + +import org.junit.Rule +import org.junit.Before +import org.junit.Test +import static org.junit.Assert.assertThat +import static org.junit.Assume.assumeThat +import org.junit.rules.ExpectedException +import org.junit.rules.RuleChain + +import static org.hamcrest.Matchers.containsString +import static org.hamcrest.Matchers.hasItem +import static org.hamcrest.Matchers.is +import static org.hamcrest.Matchers.not +import static org.hamcrest.Matchers.empty + +import util.JenkinsLoggingRule +import util.JenkinsShellCallRule +import util.BasePiperTest +import util.Rules + +class TelemetryTest extends BasePiperTest { + private ExpectedException thrown = ExpectedException.none() + private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + private JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(thrown) + .around(jscr) + .around(jlr) + + private parameters + + @Before + void setup() { + Telemetry.instance = null + parameters = [:] + } + + @Test + void testCreateInstance() { + Telemetry.instance = new Telemetry() + // asserts + assertThat(Telemetry.getInstance().listenerList, is(empty())) + } + + @Test + void testGetInstance() { + // asserts + assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) + } + + @Test + void testRegisterListenerAndNotify() { + // prepare + Map notificationPayload = [:] + Telemetry.instance = new Telemetry() + assumeThat(Telemetry.getInstance().listenerList, is(empty())) + + Telemetry.registerListener({ steps, payload -> + notificationPayload = payload + }) + // test + Telemetry.notify(nullScript, [collectTelemetryData: true], [step: 'anyStep', anything: 'something']) + // asserts + assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) + assertThat(notificationPayload, is([step: 'anyStep', anything: 'something'])) + } + + @Test + void testNotifyWithOptOut() { + // prepare + Map notificationPayload = [:] + Telemetry.instance = new Telemetry() + assumeThat(Telemetry.getInstance().listenerList, is(empty())) + Telemetry.registerListener({ steps, payload -> + notificationPayload = payload + }) + // test + Telemetry.notify(nullScript, [collectTelemetryData: false], [step: 'anyStep', anything: 'something']) + // asserts + assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) + assertThat(jlr.log, containsString("[anyStep] Telemetry reporting disabled!")) + assertThat(notificationPayload.keySet(), is(empty())) + } + + @Test + void testNotifyWithOptOutWithEmptyConfig() { + // prepare + Map notificationPayload = [:] + Telemetry.instance = new Telemetry() + assumeThat(Telemetry.getInstance().listenerList, is(empty())) + Telemetry.registerListener({ steps, payload -> + notificationPayload = payload + }) + // test + Telemetry.notify(nullScript, [:], [step: 'anyStep', anything: 'something']) + // asserts + assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) + assertThat(jlr.log, containsString("[anyStep] Telemetry reporting disabled!")) + assertThat(notificationPayload.keySet(), is(empty())) + } + + @Test + void testNotifyWithOptOutWithoutConfig() { + // prepare + Map notificationPayload = [:] + Telemetry.instance = new Telemetry() + assumeThat(Telemetry.getInstance().listenerList, is(empty())) + Telemetry.registerListener({ steps, payload -> + notificationPayload = payload + }) + // test + Telemetry.notify(nullScript, null, [step: 'anyStep', anything: 'something']) + // asserts + assertThat(Telemetry.getInstance().listenerList, is(not(empty()))) + assertThat(jlr.log, containsString("[anyStep] Telemetry reporting disabled!")) + assertThat(notificationPayload.keySet(), is(empty())) + } + + @Test + void testReportingToSWA() { + // prepare + assumeThat(Telemetry.getInstance().listenerList, is(not(empty()))) + // test + Telemetry.notify(nullScript, [collectTelemetryData: true], [ + actionName: 'Piper Library OS', + eventType: 'library-os', + jobUrlSha1: '1234', + buildUrlSha1: 'abcd', + step: 'anyStep', + stepParam1: 'something' + ]) + // asserts + assertThat(jscr.shell, hasItem(containsString('curl -G -v "https://webanalytics.cfapps.eu10.hana.ondemand.com/tracker/log"'))) + assertThat(jscr.shell, hasItem(containsString('--data-urlencode "action_name=Piper Library OS"'))) + assertThat(jscr.shell, hasItem(containsString('--data-urlencode "event_type=library-os"'))) + assertThat(jscr.shell, hasItem(containsString('--data-urlencode "custom3=anyStep"'))) + assertThat(jscr.shell, hasItem(containsString('--data-urlencode "custom4=1234"'))) + assertThat(jscr.shell, hasItem(containsString('--data-urlencode "custom5=abcd"'))) + assertThat(jscr.shell, hasItem(containsString('--data-urlencode "custom11=something"'))) + } +} diff --git a/vars/piperStageWrapper.groovy b/vars/piperStageWrapper.groovy index 40ae279b2..6344f1d72 100644 --- a/vars/piperStageWrapper.groovy +++ b/vars/piperStageWrapper.groovy @@ -54,7 +54,6 @@ private void stageLocking(Map config, Closure body) { } private void executeStage(script, originalStage, stageName, config, utils) { - boolean projectExtensions boolean globalExtensions def startTime = System.currentTimeMillis() @@ -108,14 +107,19 @@ private void executeStage(script, originalStage, stageName, config, utils) { stageName: stageName, stepParamKey1: 'buildResult', stepParam1: "${script.currentBuild.currentResult}", + buildResult: "${script.currentBuild.currentResult}", stepParamKey2: 'stageStartTime', stepParam2: "${startTime}", + stageStartTime: "${startTime}", stepParamKey3: 'stageDuration', stepParam3: "${duration}", + stageDuration: "${duration}", stepParamKey4: 'projectExtension', stepParam4: "${projectExtensions}", + projectExtension: "${projectExtensions}", stepParamKey5: 'globalExtension', - stepParam5: "${globalExtensions}" + stepParam5: "${globalExtensions}", + globalExtension: "${globalExtensions}" ], config) } } From fbb9cbeb3c71b7ed802cc2f7d02b108f7089ae2d Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Thu, 31 Jan 2019 08:49:31 +0100 Subject: [PATCH 2/8] Update dockerExecuteOnKubernetes.groovy (#474) --- vars/dockerExecuteOnKubernetes.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index 8b1b1225e..c5f0fd66f 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -110,8 +110,8 @@ private String stashWorkspace(config, prefix) { sh "chown -R 1000:1000 ." stash( name: stashName, - include: config.stashIncludes.workspace, - exclude: config.stashExcludes.excludes + includes: config.stashIncludes.workspace, + excludes: config.stashExcludes.excludes ) return stashName } catch (AbortException | IOException e) { From bca5b8ccf1fd25fed2ca79cf79f4e01bdbd4d0f1 Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Thu, 31 Jan 2019 09:16:34 +0100 Subject: [PATCH 3/8] Add step uiVeri5ExecuteTests (#469) * add defaults for uiVeri5 step * add step and tests * correct parameter names * add empty docs * add docs annotations * remove not needed parameter * add docs annotations --- .../docs/steps/uiVeri5ExecuteTests.md | 19 ++ resources/default_pipeline_environment.yml | 10 + test/groovy/UiVeri5ExecuteTestsTest.groovy | 178 ++++++++++++++++++ vars/uiVeri5ExecuteTests.groovy | 152 +++++++++++++++ 4 files changed, 359 insertions(+) create mode 100644 documentation/docs/steps/uiVeri5ExecuteTests.md create mode 100644 test/groovy/UiVeri5ExecuteTestsTest.groovy create mode 100644 vars/uiVeri5ExecuteTests.groovy diff --git a/documentation/docs/steps/uiVeri5ExecuteTests.md b/documentation/docs/steps/uiVeri5ExecuteTests.md new file mode 100644 index 000000000..09816bffa --- /dev/null +++ b/documentation/docs/steps/uiVeri5ExecuteTests.md @@ -0,0 +1,19 @@ +# uiVeri5ExecuteTests + +## Description + +Content here is generated from corresponnding step, see `vars`. + +## Prerequisites + +## Parameters + +Content here is generated from corresponnding step, see `vars`. + +## Step configuration + +Content here is generated from corresponnding step, see `vars`. + +## Exceptions + +## Examples diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 13d23cc9a..d114564fe 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -345,6 +345,16 @@ steps: developmentSystemId: null transportRequestUploadFile: transportRequestRelease: + uiVeri5ExecuteTests: + failOnError: false + dockerEnvVars: {} + installCommand: 'npm install @ui5/uiveri5 --global --quiet' + seleniumPort: 4444 + stashContent: + - 'buildDescriptor' + - 'tests' + testOptions: '' + runCommand: "uiveri5 --seleniumAddress='http://${config.seleniumHost}:${config.seleniumPort}/wd/hub'" #defaults for stage wrapper piperStageWrapper: diff --git a/test/groovy/UiVeri5ExecuteTestsTest.groovy b/test/groovy/UiVeri5ExecuteTestsTest.groovy new file mode 100644 index 000000000..217c14c6c --- /dev/null +++ b/test/groovy/UiVeri5ExecuteTestsTest.groovy @@ -0,0 +1,178 @@ +#!groovy +package steps + +import static org.hamcrest.Matchers.* + +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.ExpectedException + +import static org.junit.Assert.assertThat + +import util.BasePiperTest +import util.JenkinsDockerExecuteRule +import util.JenkinsLoggingRule +import util.JenkinsReadYamlRule +import util.JenkinsStepRule +import util.Rules + +class UiVeri5ExecuteTestsTest extends BasePiperTest { + private ExpectedException thrown = ExpectedException.none() + private JenkinsStepRule stepRule = new JenkinsStepRule(this) + private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this) + private JenkinsDockerExecuteRule dockerRule = new JenkinsDockerExecuteRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(thrown) + .around(new JenkinsReadYamlRule(this)) + .around(dockerRule) + .around(loggingRule) + .around(stepRule) + + def gitParams = [:] + def shellCommands = [] + def seleniumMap = [:] + + class MockBuild { + MockBuild(){} + def getRootDir(){ + return new MockPath() + } + class MockPath { + MockPath(){} + // return default + String getAbsolutePath(){ + return 'myPath' + } + } + } + + @Before + void init() { + + binding.setVariable('currentBuild', [ + result: 'SUCCESS', + rawBuild: new MockBuild() + ]) + + helper.registerAllowedMethod("git", [Map.class], { map -> gitParams = map}) + helper.registerAllowedMethod("stash", [String.class], null) + helper.registerAllowedMethod("unstash", [String.class], { s -> return [s]}) + helper.registerAllowedMethod("sh", [String.class], { s -> + if (s.contains('failure')) throw new RuntimeException('Test Error') + shellCommands.add(s.toString()) + }) + helper.registerAllowedMethod("sh", [Map.class], { map -> + return 'available' + }) + helper.registerAllowedMethod('seleniumExecuteTests', [Map.class, Closure.class], { m, body -> + seleniumMap = m + return body() + }) + } + + @Test + void testDefault() throws Exception { + // execute test + stepRule.step.uiVeri5ExecuteTests([ + script: nullScript, + juStabUtils: utils, + ]) + // asserts + assertThat(shellCommands, hasItem(containsString('npm install @ui5/uiveri5 --global --quiet'))) + assertThat(shellCommands, hasItem(containsString('uiveri5 --seleniumAddress=\'http://selenium:4444/wd/hub\''))) + assertThat(seleniumMap.dockerImage, isEmptyOrNullString()) + assertThat(seleniumMap.dockerWorkspace, isEmptyOrNullString()) + } + + @Test + void testDefaultOnK8s() throws Exception { + // prepare + binding.variables.env.ON_K8S = 'true' + // execute test + stepRule.step.uiVeri5ExecuteTests([ + script: nullScript, + juStabUtils: utils, + ]) + // asserts + assertThat(shellCommands, hasItem(containsString('npm install @ui5/uiveri5 --global --quiet'))) + assertThat(shellCommands, hasItem(containsString('uiveri5 --seleniumAddress=\'http://localhost:4444/wd/hub\''))) + } + + @Test + void testWithCustomSidecar() throws Exception { + // execute test + stepRule.step.uiVeri5ExecuteTests([ + script: nullScript, + juStabUtils: utils, + sidecarEnvVars: [myEnv: 'testValue'], + sidecarImage: 'myImage' + ]) + // asserts + assertThat(seleniumMap.sidecarImage, is('myImage')) + assertThat(seleniumMap.sidecarEnvVars.myEnv, is('testValue')) + } + + @Test + void testWithTestRepository() throws Exception { + // execute test + stepRule.step.uiVeri5ExecuteTests([ + script: nullScript, + juStabUtils: utils, + testRepository: 'git@myGitUrl' + ]) + // asserts + assertThat(seleniumMap, hasKey('stashContent')) + assertThat(seleniumMap.stashContent, hasItem(startsWith('testContent-'))) + assertThat(gitParams, hasEntry('url', 'git@myGitUrl')) + assertThat(gitParams, not(hasKey('credentialsId'))) + assertThat(gitParams, not(hasKey('branch'))) + assertJobStatusSuccess() + } + + @Test + void testWithTestRepositoryWithGitBranchAndCredentials() throws Exception { + // execute test + stepRule.step.uiVeri5ExecuteTests([ + script: nullScript, + juStabUtils: utils, + testRepository: 'git@myGitUrl', + gitSshKeyCredentialsId: 'myCredentials', + gitBranch: 'myBranch' + ]) + // asserts + assertThat(gitParams, hasEntry('url', 'git@myGitUrl')) + assertThat(gitParams, hasEntry('credentialsId', 'myCredentials')) + assertThat(gitParams, hasEntry('branch', 'myBranch')) + assertJobStatusSuccess() + } + + @Test + void testWithFailOnError() throws Exception { + thrown.expect(RuntimeException) + thrown.expectMessage('Test Error') + // execute test + stepRule.step.uiVeri5ExecuteTests([ + juStabUtils: utils, + failOnError: true, + testOptions: 'failure', + script: nullScript + ]) + } + + @Test + void testWithoutFailOnError() throws Exception { + // execute test + stepRule.step.uiVeri5ExecuteTests([ + juStabUtils: utils, + testOptions: 'failure', + script: nullScript + ]) + // asserts + assertJobStatusSuccess() + } +} diff --git a/vars/uiVeri5ExecuteTests.groovy b/vars/uiVeri5ExecuteTests.groovy new file mode 100644 index 000000000..68a30a91f --- /dev/null +++ b/vars/uiVeri5ExecuteTests.groovy @@ -0,0 +1,152 @@ +import com.sap.piper.ConfigurationHelper +import com.sap.piper.GenerateDocumentation +import com.sap.piper.GitUtils +import com.sap.piper.Utils + +import groovy.text.SimpleTemplateEngine +import groovy.transform.Field + +import static com.sap.piper.Prerequisites.checkScript + +@Field String STEP_NAME = getClass().getName() + +@Field Set GENERAL_CONFIG_KEYS = [ + /** + * In case a `testRepository` is provided and it is protected, access credentials (as Jenkins credentials) can be provided with `gitSshKeyCredentialsId`. **Note: In case of using a protected repository, `testRepository` should include the ssh link to the repository.** + * @possibleValues Jenkins credentialId + */ + 'gitSshKeyCredentialsId' +] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ + /** + * A map of environment variables to set in the container, e.g. [http_proxy:'proxy:8080']. + */ + 'dockerEnvVars', + /** + * The 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. + */ + 'dockerImage', + /** + * Only relevant for Kubernetes case: Specifies a dedicated user home directory for the container which will be passed as value for environment variable `HOME`. + */ + 'dockerWorkspace', + /** + * With `failOnError` the behavior in case tests fail can be defined. + * @possibleValues `true`, `false` + */ + 'failOnError', + /** + * In case a `testRepository` is provided the branch in this repository can be specified with `gitBranch`. + */ + 'gitBranch', + /** + * The command that is executed to install the test tool. + */ + 'installCommand', + /** + * The command that is executed to start the tests. + */ + 'runCommand', + /** + * The host of the selenium hub, this is set automatically to `localhost` in a Kubernetes environment (determined by the `ON_K8S` environment variable) of to `selenium` in any other case. The value is only needed for the `runCommand`. + */ + 'seleniumHost', + /** + * The port of the selenium hub. The value is only needed for the `runCommand`. + */ + 'seleniumPort', + /** + * A map of environment variables to set in the sidecar container, similar to `dockerEnvVars`. + */ + 'sidecarEnvVars', + /** + * The name of the docker image of the sidecar container. If empty, no sidecar container is started. + */ + 'sidecarImage', + /** + * If specific stashes should be considered for the tests, their names need to be passed via the parameter `stashContent`. + */ + 'stashContent', + /** + * This allows to set specific options for the UIVeri5 execution. Details can be found [in the UIVeri5 documentation](https://github.com/SAP/ui5-uiveri5/blob/master/docs/config/config.md#configuration). + */ + 'testOptions', + /** + * With `testRepository` the tests can be loaded from another reposirory. + */ + 'testRepository' +]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +/** + * With this step [UIVeri5](https://github.com/SAP/ui5-uiveri5) tests can be executed. + * + * UIVeri5 describes following benefits on its GitHub page: + * + * * Automatic synchronization with UI5 app rendering so there is no need to add waits and sleeps to your test. Tests are reliable by design. + * * Tests are written in synchronous manner, no callbacks, no promise chaining so are really simple to write and maintain. + * * Full power of webdriverjs, protractor and jasmine - deferred selectors, custom matchers, custom locators. + * * Control locators (OPA5 declarative matchers) allow locating and interacting with UI5 controls. + * * Does not depend on testability support in applications - works with autorefreshing views, resizing elements, animated transitions. + * * Declarative authentications - authentication flow over OAuth2 providers, etc. + * * Console operation, CI ready, fully configurable, no need for java (comming soon) or IDE. + * * Covers full ui5 browser matrix - Chrome,Firefox,IE,Edge,Safari,iOS,Android. + * * Open-source, modify to suite your specific neeeds. + * + * !!! note "Browser Matrix" + * With this step and the underlying Docker image ([selenium/standalone-chrome](https://github.com/SeleniumHQ/docker-selenium/tree/master/StandaloneChrome)) only Chrome tests are possible. + * + * Testing of further browsers can be done with using a custom Docker image. + */ +@GenerateDocumentation +void call(Map parameters = [:]) { + handlePipelineStepErrors (stepName: STEP_NAME, stepParameters: parameters) { + def script = checkScript(this, parameters) ?: this + def utils = parameters.juStabUtils ?: new Utils() + + // load default & individual configuration + Map config = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .addIfEmpty('seleniumHost', isKubernetes()?'localhost':'selenium') + .use() + + new Utils().pushToSWA([ + step: STEP_NAME, + stepParamKey1: 'scriptMissing', + stepParam1: parameters?.script == null + ], config) + + config.stashContent = config.testRepository ? [GitUtils.handleTestRepository(this, config)] : utils.unstashAll(config.stashContent) + config.installCommand = SimpleTemplateEngine.newInstance().createTemplate(config.installCommand).make([config: config]).toString() + config.runCommand = SimpleTemplateEngine.newInstance().createTemplate(config.runCommand).make([config: config]).toString() + + seleniumExecuteTests( + script: script, + buildTool: 'npm', + dockerEnvVars: config.dockerEnvVars, + dockerImage: config.dockerImage, + dockerName: config.dockerName, + dockerWorkspace: config.dockerWorkspace, + sidecarEnvVars: config.sidecarEnvVars, + sidecarImage: config.sidecarImage, + stashContent: config.stashContent + ) { + try { + sh "NPM_CONFIG_PREFIX=~/.npm-global ${config.installCommand}" + sh "PATH=\$PATH:~/.npm-global/bin ${config.runCommand} ${config.testOptions}" + } catch (err) { + echo "[${STEP_NAME}] Test execution failed" + script.currentBuild.result = 'UNSTABLE' + if (config.failOnError) throw err + } + } + } +} + +boolean isKubernetes() { + return Boolean.valueOf(env.ON_K8S) +} From 3b2e42c74f980ebc74a27ebe0982a1744656200b Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Thu, 31 Jan 2019 09:39:13 +0100 Subject: [PATCH 4/8] Add step containerExecuteStructureTest (#441) * add step containerExecuteStructureTest * include PR-review feedback * documentation --- .../steps/containerExecuteStructureTests.md | 82 ++++++++++ documentation/mkdocs.yml | 1 + resources/default_pipeline_environment.yml | 11 +- .../ContainerExecuteStructureTestsTest.groovy | 150 ++++++++++++++++++ test/groovy/util/BasePiperTestContext.groovy | 1 + vars/containerExecuteStructureTests.groovy | 132 +++++++++++++++ vars/dockerExecuteOnKubernetes.groovy | 10 +- 7 files changed, 383 insertions(+), 4 deletions(-) create mode 100644 documentation/docs/steps/containerExecuteStructureTests.md create mode 100644 test/groovy/ContainerExecuteStructureTestsTest.groovy create mode 100644 vars/containerExecuteStructureTests.groovy diff --git a/documentation/docs/steps/containerExecuteStructureTests.md b/documentation/docs/steps/containerExecuteStructureTests.md new file mode 100644 index 000000000..7d1ec3fde --- /dev/null +++ b/documentation/docs/steps/containerExecuteStructureTests.md @@ -0,0 +1,82 @@ +# containerExecuteStructureTests + +## Description + +In this step [Container Structure Tests](https://github.com/GoogleContainerTools/container-structure-test) are executed. + +This testing framework allows you to execute different test types against a Docker container, for example: + +* Command tests (only if a Docker Deamon is available) +* File existence tests +* File content tests +* Metadata test + +## Prerequisites + +Test configuration is available. + +## Example + +``` +containerExecuteStructureTests( + script: this, + testConfiguration: 'config.yml', + testImage: 'node:latest' +) +``` + +## Parameters + +| parameter | mandatory | default | possible values | +| ----------|-----------|---------|-----------------| +|script|yes||| +|containerCommand|no|``|| +|containerShell|no|``|| +|dockerImage|yes|`ppiper/container-structure-test`|| +|dockerOptions|no|`-u 0 --entrypoint=''`|| +|failOnError|no|`true`|`true`, `false`| +|pullImage|no||`true`, `false`| +|stashContent|no|
  • `tests`
|| +|testConfiguration|no||| +|testDriver|no||| +|testImage|no||| +|testReportFilePath|no|`cst-report.json`|| +|verbose|no||`true`, `false`| + +Details: + +* `script` defines the global script environment of the Jenkinsfile run. Typically `this` is passed to this parameter. This allows the function to access the [`commonPipelineEnvironment`](commonPipelineEnvironment.md) for storing the measured duration. +* `containerCommand`: Only for Kubernetes environments: Command which is executed to keep container alive, defaults to '/usr/bin/tail -f /dev/null' +* containerShell: Only for Kubernetes environments: Shell to be used inside container, defaults to '/bin/sh' +* dockerImage: Docker image for code execution. +* dockerOptions: Options to be passed to Docker image when starting it (only relevant for non-Kubernetes case). +* failOnError: Defines the behavior, in case tests fail. +* pullImage: Only relevant for testDriver 'docker'. +* stashContent: If specific stashes should be considered for the tests, you can pass this via this parameter. +* testConfiguration: Container structure test configuration in yml or json format. You can pass a pattern in order to execute multiple tests. +* testDriver: Container structure test driver to be used for testing, please see [https://github.com/GoogleContainerTools/container-structure-test](https://github.com/GoogleContainerTools/container-structure-test) for details. +* testImage: Image to be tested +* testReportFilePath: Path and name of the test report which will be generated +* verbose: Print more detailed information into the log. + +## Step configuration + +We recommend to define values of step parameters via [config.yml file](../configuration.md). + +In following sections the configuration is possible: + +| parameter | general | step | stage | +| ----------|-----------|---------|-----------------| +|script|||| +|containerCommand||X|X| +|containerShell||X|X| +|dockerImage||X|X| +|dockerOptions||X|X| +|failOnError||X|X| +|pullImage||X|X| +|stashContent||X|X| +|testConfiguration||X|X| +|testDriver||X|X| +|testImage||X|X| +|testReportFilePath||X|X| +|verbose|X|X|X| diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 9af21a86b..b9f555ff5 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -9,6 +9,7 @@ nav: - checksPublishResults: steps/checksPublishResults.md - cloudFoundryDeploy: steps/cloudFoundryDeploy.md - commonPipelineEnvironment: steps/commonPipelineEnvironment.md + - containerExecuteStructureTests: steps/containerExecuteStructureTests.md - dockerExecute: steps/dockerExecute.md - dockerExecuteOnKubernetes: steps/dockerExecuteOnKubernetes.md - durationMeasure: steps/durationMeasure.md diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index d114564fe..f3b7b26ce 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -143,6 +143,15 @@ steps: mtaDeployPlugin: dockerImage: 's4sdk/docker-cf-cli' dockerWorkspace: '/home/piper' + containerExecuteStructureTests: + containerCommand: '/busybox/tail -f /dev/null' + containerShell: '/busybox/sh' + dockerImage: 'ppiper/container-structure-test' + dockerOptions: "-u 0 --entrypoint=''" + failOnError: true + stashContent: + - 'tests' + testReportFilePath: 'cst-report.json' dockerExecute: stashContent: [] dockerExecuteOnKubernetes: @@ -261,7 +270,7 @@ steps: opensourceConfiguration: '**/srcclr.yml, **/vulas-custom.properties, **/.nsprc, **/.retireignore, **/.retireignore.json, **/.snyk' pipelineConfigAndTests: '.pipeline/**' securityDescriptor: '**/xs-security.json' - tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js' + tests: '**/pom.xml, **/*.json, **/*.xml, **/src/**, **/node_modules/**, **/specs/**, **/env/**, **/*.js, **/tests/**' stashExcludes: buildDescriptor: '**/node_modules/**/package.json' deployDescriptor: '' diff --git a/test/groovy/ContainerExecuteStructureTestsTest.groovy b/test/groovy/ContainerExecuteStructureTestsTest.groovy new file mode 100644 index 000000000..f45133c62 --- /dev/null +++ b/test/groovy/ContainerExecuteStructureTestsTest.groovy @@ -0,0 +1,150 @@ +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.rules.RuleChain +import util.* + +import static org.hamcrest.Matchers.* +import static org.junit.Assert.assertThat + +class ContainerExecuteStructureTestsTest extends BasePiperTest { + private ExpectedException thrown = ExpectedException.none() + private JenkinsStepRule jsr = new JenkinsStepRule(this) + private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this) + private JenkinsShellCallRule jscr = new JenkinsShellCallRule(this) + private JenkinsDockerExecuteRule jedr = new JenkinsDockerExecuteRule(this) + + @Rule + public RuleChain rules = Rules + .getCommonRules(this) + .around(new JenkinsReadYamlRule(this)) + .around(thrown) + .around(jedr) + .around(jscr) + .around(jlr) + .around(jsr) // needs to be activated after jedr, otherwise executeDocker is not mocked + + @Before + void init() throws Exception { + helper.registerAllowedMethod('stash', [String.class], null) + helper.registerAllowedMethod("findFiles", [Map.class], { map -> + def files + if(map.glob == 'notFound.json') + files = [] + else if(map.glob == 'cst/*.yml') + files = [ + new File("cst/test1.yml"), + new File("cst/test2.yml") + ] + else + files = [new File(map.glob)] + return files.toArray() + }) + } + + @Test + void testExecuteContainterStructureTestsDefault() throws Exception { + helper.registerAllowedMethod('readFile', [String.class], {s -> + return '{testResult: true}' + }) + jsr.step.containerExecuteStructureTests( + script: nullScript, + juStabUtils: utils, + testConfiguration: 'cst/*.yml', + testImage: 'myRegistry/myImage:myTag' + ) + // asserts + assertThat(jscr.shell, hasItem(allOf( + stringContainsInOrder(['#!/busybox/sh', 'container-structure-test', '--config']), + containsString("--config cst${File.separator}test1.yml"), + containsString("--config cst${File.separator}test2.yml"), + containsString('--driver docker'), + containsString('--image myRegistry/myImage:myTag'), + containsString('--test-report cst-report.json'), + ))) + //currently no default Docker image + assertThat(jedr.dockerParams.dockerImage, is('ppiper/container-structure-test')) + assertThat(jedr.dockerParams.dockerOptions, is("-u 0 --entrypoint=''")) + assertThat(jedr.dockerParams.containerCommand, is('/busybox/tail -f /dev/null')) + assertThat(jedr.dockerParams.containerShell, is('/busybox/sh')) + assertThat(jlr.log, containsString('{testResult: true}')) + assertThat(jscr.shell, hasItem('docker pull myRegistry/myImage:myTag')) + } + + @Test + void testExecuteContainterStructureTestsK8S() throws Exception { + def envDefault = nullScript.env + nullScript.env = [ON_K8S: 'true'] + jsr.step.containerExecuteStructureTests( + script: nullScript, + juStabUtils: utils, + containerCommand: '/busybox/tail -f /dev/null', + containerShell: '/bin/sh', + dockerImage: 'myRegistry:55555/pathTo/myImage:myTag', + testConfiguration: 'cst/*.yml', + testImage: 'myRegistry/myImage:myTag' + ) + nullScript.env = envDefault + // asserts + assertThat(jscr.shell, hasItem(allOf( + stringContainsInOrder(['#!/bin/sh', 'container-structure-test', '--config']), + containsString("--config cst${File.separator}test1.yml"), + containsString("--config cst${File.separator}test2.yml"), + containsString('--driver tar'), + containsString('--image myRegistry/myImage:myTag'), + containsString('--test-report cst-report.json'), + ))) + assertThat(jedr.dockerParams.dockerImage, is('myRegistry:55555/pathTo/myImage:myTag')) + assertThat(jedr.dockerParams.containerCommand, is('/busybox/tail -f /dev/null')) + assertThat(jscr.shell, not(hasItem('docker pull myRegistry/myImage:myTag'))) + } + + @Test + void testExecuteContainterStructureTestsError() throws Exception { + helper.registerAllowedMethod('readFile', [String.class], {s -> + return '{testResult: true}' + }) + helper.registerAllowedMethod('sh', [String.class], {s -> + if (s.startsWith('#!/busybox/sh\ncontainer-structure-test test')) { + throw new GroovyRuntimeException('shell call failed') + } + return null + }) + thrown.expectMessage('shell call failed') + + jsr.step.containerExecuteStructureTests( + script: nullScript, + juStabUtils: utils, + containerCommand: '/busybox/tail -f /dev/null', + containerShell: '/busybox/sh', + testConfiguration: 'cst/*.yml', + testImage: 'myRegistry/myImage:myTag' + ) + } + + @Test + void testExecuteContainterStructureTestsErrorNoFailure() throws Exception { + helper.registerAllowedMethod('readFile', [String.class], {s -> + return '{testResult: true}' + }) + helper.registerAllowedMethod('sh', [String.class], {s -> + if (s.startsWith('#!/busybox/sh\ncontainer-structure-test test')) { + throw new GroovyRuntimeException('shell call failed') + } + return null + }) + + jsr.step.containerExecuteStructureTests( + script: nullScript, + juStabUtils: utils, + containerCommand: '/busybox/tail -f /dev/null', + containerShell: '/busybox/sh', + failOnError: false, + testConfiguration: 'cst/*.yml', + testImage: 'myRegistry/myImage:myTag' + ) + + assertThat(jlr.log, containsString('Test execution failed')) + } +} diff --git a/test/groovy/util/BasePiperTestContext.groovy b/test/groovy/util/BasePiperTestContext.groovy index 16f430aee..f27a840b2 100644 --- a/test/groovy/util/BasePiperTestContext.groovy +++ b/test/groovy/util/BasePiperTestContext.groovy @@ -16,6 +16,7 @@ class BasePiperTestContext { Script nullScript() { def nullScript = InvokerHelper.createScript(null, new Binding()) nullScript.currentBuild = [:] + nullScript.env = [:] LibraryLoadingTestExecutionListener.prepareObjectInterceptors(nullScript) return nullScript } diff --git a/vars/containerExecuteStructureTests.groovy b/vars/containerExecuteStructureTests.groovy new file mode 100644 index 000000000..045ada3fd --- /dev/null +++ b/vars/containerExecuteStructureTests.groovy @@ -0,0 +1,132 @@ +import static com.sap.piper.Prerequisites.checkScript + +import com.sap.piper.ConfigurationHelper +import com.sap.piper.Utils +import groovy.transform.Field + +@Field String STEP_NAME = getClass().getName() + +@Field Set GENERAL_CONFIG_KEYS = [ + /** + * Print more detailed information into the log. + * @possibleValues `true`, `false` + */ + 'verbose' + +] + +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ + /** + * Only for Kubernetes environments: Command which is executed to keep container alive, defaults to '/usr/bin/tail -f /dev/null' + */ + 'containerCommand', + /** + * Only for Kubernetes environments: Shell to be used inside container, defaults to '/bin/sh' + */ + 'containerShell', + /** + * Docker image for code execution. + */ + 'dockerImage', + /** + * Options to be passed to Docker image when starting it (only relevant for non-Kubernetes case). + */ + 'dockerOptions', + /** + * Defines the behavior, in case tests fail. + * @possibleValues `true`, `false` + */ + 'failOnError', + /** + * Only relevant for testDriver 'docker'. + * @possibleValues `true`, `false` + */ + 'pullImage', + /** + * If specific stashes should be considered for the tests, you can pass this via this parameter. + */ + 'stashContent', + /** + * Container structure test configuration in yml or json format. You can pass a pattern in order to execute multiple tests. + */ + 'testConfiguration', + /** + * Container structure test driver to be used for testing, please see https://github.com/GoogleContainerTools/container-structure-test for details. + */ + 'testDriver', + /** + * Image to be tested + */ + 'testImage', + /** + * Path and name of the test report which will be generated + */ + 'testReportFilePath', +]) + +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +void call(Map parameters = [:]) { + handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { + + def script = checkScript(this, parameters) ?: this + + def utils = parameters?.juStabUtils ?: new Utils() + + // load default & individual configuration + Map config = ConfigurationHelper.newInstance(this) + .loadStepDefaults() + .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .addIfEmpty('testDriver', Boolean.valueOf(script.env.ON_K8S) ? 'tar' : 'docker') + .addIfNull('pullImage', !Boolean.valueOf(script.env.ON_K8S)) + .withMandatoryProperty('dockerImage') + .use() + + utils.pushToSWA([step: STEP_NAME], config) + + config.stashContent = utils.unstashAll(config.stashContent) + + List testConfig = findFiles(glob: config.testConfiguration)?.toList() + if (testConfig.isEmpty()) { + error "[${STEP_NAME}] No test description found with pattern '${config.testConfiguration}'" + } else { + echo "[${STEP_NAME}] Found files ${testConfig}" + } + + def testConfigArgs = '' + testConfig.each {conf -> + testConfigArgs += "--config ${conf} " + } + + //workaround for non-working '--pull' option in version 1.7.0 of container-structure-tests, see https://github.com/GoogleContainerTools/container-structure-test/issues/193 + if (config.pullImage) { + if (config.verbose) echo "[${STEP_NAME}] Pulling image since configuration option pullImage is set to '${config.pullImage}'" + sh "docker pull ${config.testImage}" + } + + try { + dockerExecute( + script: script, + containerCommand: config.containerCommand, + containerShell: config.containerShell, + dockerImage: config.dockerImage, + dockerOptions: config.dockerOptions, + stashContent: config.stashContent + ) { + sh """#!${config.containerShell?:'/bin/sh'} +container-structure-test test ${testConfigArgs} --driver ${config.testDriver} --image ${config.testImage} --test-report ${config.testReportFilePath}${config.verbose ? ' --verbosity debug' : ''}""" + } + } catch (err) { + echo "[${STEP_NAME}] Test execution failed" + script.currentBuild.result = 'UNSTABLE' + if (config.failOnError) throw err + } finally { + echo "${readFile(config.testReportFilePath)}" + archiveArtifacts artifacts: config.testReportFilePath, allowEmptyArchive: true + } + + } +} diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index c5f0fd66f..52d29b78f 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -84,12 +84,13 @@ void executeOnPod(Map config, utils, Closure body) { if (config.containerShell) { containerParams.shell = config.containerShell } + echo "ContainerConfig: ${containerParams}" container(containerParams){ try { utils.unstashAll(config.stashContent) body() } finally { - stashWorkspace(config, 'container') + stashWorkspace(config, 'container', true) } } } else { @@ -103,11 +104,14 @@ void executeOnPod(Map config, utils, Closure body) { } } -private String stashWorkspace(config, prefix) { +private String stashWorkspace(config, prefix, boolean chown = false) { def stashName = "${prefix}-${config.uniqueId}" try { // Every dockerImage used in the dockerExecuteOnKubernetes should have user id 1000 - sh "chown -R 1000:1000 ." + if (chown) { + sh """#!${config.containerShell?:'/bin/sh'} +chown -R 1000:1000 .""" + } stash( name: stashName, includes: config.stashIncludes.workspace, From ba2e83c76a6042d13e74330dbbdaf407c568c89a Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 4 Feb 2019 09:03:58 +0100 Subject: [PATCH 5/8] dockerExecuteOnKubernetes: correct parameter keys (#475) * Update dockerExecuteOnKubernetes.groovy * Update dockerExecute.groovy * Update dockerExecuteOnKubernetes.groovy --- vars/dockerExecute.groovy | 11 ++++++----- vars/dockerExecuteOnKubernetes.groovy | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index 46b19e0e2..b8dbc14f7 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -12,9 +12,10 @@ import groovy.transform.Field @Field def STEP_NAME = getClass().getName() @Field def PLUGIN_ID_DOCKER_WORKFLOW = 'docker-workflow' -@Field Set GENERAL_CONFIG_KEYS = ['jenkinsKubernetes'] - -@Field Set PARAMETER_KEYS = [ +@Field Set GENERAL_CONFIG_KEYS = [ + 'jenkinsKubernetes' +] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ 'containerPortMappings', 'containerCommand', 'containerShell', @@ -31,8 +32,8 @@ import groovy.transform.Field 'sidecarWorkspace', 'sidecarVolumeBind', 'stashContent' -] -@Field Set STEP_CONFIG_KEYS = PARAMETER_KEYS +]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS void call(Map parameters = [:], body) { handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index 52d29b78f..8736214e5 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -9,8 +9,10 @@ import hudson.AbortException @Field def STEP_NAME = getClass().getName() @Field def PLUGIN_ID_KUBERNETES = 'kubernetes' -@Field Set GENERAL_CONFIG_KEYS = ['jenkinsKubernetes'] -@Field Set PARAMETER_KEYS = [ +@Field Set GENERAL_CONFIG_KEYS = [ + 'jenkinsKubernetes' +] +@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ '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 @@ -22,9 +24,14 @@ import hudson.AbortException 'dockerImage', 'dockerWorkspace', 'dockerEnvVars', - 'stashContent' -] -@Field Set STEP_CONFIG_KEYS = PARAMETER_KEYS.plus(['stashIncludes', 'stashExcludes']) + 'stashContent', + 'stashExcludes', + 'stashIncludes' +]) +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS.minus([ + 'stashIncludes', + 'stashExcludes' +]) void call(Map parameters = [:], body) { handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { From b7deda19645969c37417c76ce388963f6029c59c Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 4 Feb 2019 10:53:20 +0100 Subject: [PATCH 6/8] dockerExecute: make javadoc comment to 'normal' comment (#481) since it is not intendend to expose the method docu as api doc. --- vars/dockerExecute.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index b8dbc14f7..b74283db6 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -154,7 +154,7 @@ void call(Map parameters = [:], body) { -/** +/* * Returns a string with docker options containing * environment variables (if set). * Possible to extend with further options. @@ -225,7 +225,7 @@ 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. From bd32367c31c53be6123dd20fddbf3404f32872ab Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 4 Feb 2019 14:35:44 +0100 Subject: [PATCH 7/8] dockerExecute: generate docs (#480) * correct key order * add docs annotation and description * describe parameters * remove generated content from doc * fix indent * add further decumentations --- documentation/docs/steps/dockerExecute.md | 62 +------------------- vars/dockerExecute.groovy | 70 ++++++++++++++++++++++- 2 files changed, 70 insertions(+), 62 deletions(-) diff --git a/documentation/docs/steps/dockerExecute.md b/documentation/docs/steps/dockerExecute.md index e06f42609..5b4eaa1e9 100644 --- a/documentation/docs/steps/dockerExecute.md +++ b/documentation/docs/steps/dockerExecute.md @@ -2,47 +2,11 @@ ## Description -Executes a closure inside a docker container with the specified docker image. -The workspace is mounted into the docker image. -Proxy environment variables defined on the Jenkins machine are also available in the Docker container. +Content here is generated from corresponnding step, see `vars`. ## Parameters -| parameter | mandatory | default | possible values | -| ----------|-----------|---------|-----------------| -|script|yes||| -|containerCommand|no||| -|containerPortMappings|no||| -|containerShell|no||| -|dockerEnvVars|no|`[:]`|| -|dockerImage|no|`''`|| -|dockerName|no||| -|dockerOptions|no|`''`|| -|dockerVolumeBind|no|`[:]`|| -|dockerWorkspace|no||| -|jenkinsKubernetes|no|`[jnlpAgent:s4sdk/jenkins-agent-k8s:latest]`|| -|sidecarEnvVars|no||| -|sidecarImage|no||| -|sidecarName|no||| -|sidecarOptions|no||| -|sidecarVolumeBind|no||| -|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 -* `dockerOptions` Docker options to be set when starting the container. It can be a list or a string. -* `dockerVolumeBind` Volumes that should be mounted into the container. -* `dockerWorkspace`: only relevant for Kubernetes case: specifies a dedicated user home directory for the container which will be passed as value for environment variable `HOME` -* `sidecarEnvVars` defines environment variables for the sidecar container, similar to `dockerEnvVars` -* `sidecarImage`: Name of the docker image of the sidecar container. Do not provide this value if no sidecar container is required. -* `sidecarName`: as `dockerName` for the sidecar container -* `sidecarOptions`: as `dockerOptions` for the sidecar container -* `sidecarVolumeBind`: as `dockerVolumeBind` for the sidecar container -* `sidecarWorkspace`: as `dockerWorkspace` for the sidecar container +Content here is generated from corresponnding step, see `vars`. ## Kubernetes support @@ -50,27 +14,7 @@ If the Jenkins is setup on a Kubernetes cluster, then you can execute the closur ## Step configuration -We recommend to define values of step parameters via [config.yml file](../configuration.md). - -In following sections the configuration is possible: - -| parameter | general | step | stage | -| ----------|-----------|---------|-----------------| -|script|||| -|containerPortMappings||X|X| -|dockerEnvVars||X|X| -|dockerImage||X|X| -|dockerName||X|X| -|dockerOptions||X|X| -|dockerVolumeBind||X|X| -|dockerWorkspace||X|X| -|jenkinsKubernetes|X||| -|sidecarEnvVars||X|X| -|sidecarImage||X|X| -|sidecarName||X|X| -|sidecarOptions||X|X| -|sidecarVolumeBind||X|X| -|sidecarWorkspace||X|X| +Content here is generated from corresponnding step, see `vars`. ## Side effects diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index b74283db6..41eca5570 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -3,6 +3,7 @@ import static com.sap.piper.Prerequisites.checkScript import com.cloudbees.groovy.cps.NonCPS import com.sap.piper.ConfigurationHelper +import com.sap.piper.GenerateDocumentation import com.sap.piper.JenkinsUtils import com.sap.piper.Utils import com.sap.piper.k8s.ContainerMap @@ -13,28 +14,91 @@ import groovy.transform.Field @Field def PLUGIN_ID_DOCKER_WORKFLOW = 'docker-workflow' @Field Set GENERAL_CONFIG_KEYS = [ + /** + * + */ 'jenkinsKubernetes' ] @Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ - 'containerPortMappings', + /** + * Kubernetes only: + * Allows to specify start command for container created with dockerImage parameter to overwrite Piper default (`/usr/bin/tail -f /dev/null`). + */ 'containerCommand', + /** + * Map which defines per docker image the port mappings, e.g. `containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]]`. + */ + 'containerPortMappings', + /** + * Kubernetes only: + * Allows to specify the shell to be used for execution of commands. + */ 'containerShell', + /** + * Environment variables to set in the container, e.g. [http_proxy: 'proxy:8080']. + */ 'dockerEnvVars', + /** + * 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. + */ 'dockerImage', + /** + * Kubernetes only: + * Name of the container launching `dockerImage`. + * SideCar only: + * Name of the container in local network. + */ 'dockerName', + /** + * Docker options to be set when starting the container (List or String). + */ 'dockerOptions', - 'dockerWorkspace', + /** + * Volumes that should be mounted into the container. + */ 'dockerVolumeBind', + /** + * Kubernetes only: + * Specifies a dedicated user home directory for the container which will be passed as value for environment variable `HOME`. + */ + 'dockerWorkspace', + /** + * as `dockerEnvVars` for the sidecar container + */ 'sidecarEnvVars', + /** + * as `dockerImage` for the sidecar container + */ 'sidecarImage', + /** + * as `dockerName` for the sidecar container + */ 'sidecarName', + /** + * as `dockerOptions` for the sidecar container + */ 'sidecarOptions', - 'sidecarWorkspace', + /** + * as `dockerVolumeBind` for the sidecar container + */ 'sidecarVolumeBind', + /** + * as `dockerWorkspace` for the sidecar container + */ + 'sidecarWorkspace', + /** + * Specific stashes that should be considered for the step execution. + */ 'stashContent' ]) @Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS +/** + * Executes a closure inside a docker container with the specified docker image. + * The workspace is mounted into the docker image. + * Proxy environment variables defined on the Jenkins machine are also available in the Docker container. + */ +@GenerateDocumentation void call(Map parameters = [:], body) { handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { From dde4e0abefbb11f5c29dcbb7b77d49e94f311ccd Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Tue, 5 Feb 2019 09:07:47 +0100 Subject: [PATCH 8/8] Fix regression introduced with #474 (#483) Fix stashing behavior to include all files in workspace. This was for example an issue for PR-voting in Docker pipeline since `Dockerfile` has been excluded from stashing --- resources/default_pipeline_environment.yml | 2 +- vars/dockerExecuteOnKubernetes.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index f3b7b26ce..ca0ffa493 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -157,7 +157,7 @@ steps: dockerExecuteOnKubernetes: stashContent: [] stashIncludes: - workspace: '**/*.*' + workspace: '**/*' stashExcludes: workspace: 'nohup.out' githubPublishRelease: diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index 8736214e5..8ab146681 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -122,7 +122,7 @@ chown -R 1000:1000 .""" stash( name: stashName, includes: config.stashIncludes.workspace, - excludes: config.stashExcludes.excludes + excludes: config.stashExcludes.workspace ) return stashName } catch (AbortException | IOException e) {