From 7a961ef38e93e966e6e0f6e211661724650bf4b7 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Thu, 4 Oct 2018 17:06:42 +0200 Subject: [PATCH 1/5] seleniumExecuteTests - add step to run Selenium tests (#318) It comes with an extension to executeDocker and executeDockerOnKubernetes to run sidecar containers. This helps to execute Selenium tests using two Docker images: 1. Execution runtime for tests (e.g. node image) 2. Selenium instance which holds Selenium server + browser * add documentation & some name cleanup * include PR feedback * add step documentation to structure --- documentation/docs/images/k8s_env.png | Bin 0 -> 9653 bytes documentation/docs/steps/dockerExecute.md | 90 ++++++++++-- .../docs/steps/dockerExecuteOnKubernetes.md | 89 +++++++++--- .../docs/steps/seleniumExecuteTests.md | 134 ++++++++++++++++++ documentation/mkdocs.yml | 1 + resources/default_pipeline_environment.yml | 22 +++ .../DockerExecuteOnKubernetesTest.groovy | 77 ++++++++-- test/groovy/DockerExecuteTest.groovy | 86 +++++++++-- test/groovy/SeleniumExecuteTestsTest.groovy | 98 +++++++++++++ vars/dockerExecute.groovy | 84 ++++++++--- vars/dockerExecuteOnKubernetes.groovy | 61 ++++++-- vars/seleniumExecuteTests.groovy | 73 ++++++++++ 12 files changed, 735 insertions(+), 80 deletions(-) create mode 100644 documentation/docs/images/k8s_env.png create mode 100644 documentation/docs/steps/seleniumExecuteTests.md create mode 100644 test/groovy/SeleniumExecuteTestsTest.groovy create mode 100644 vars/seleniumExecuteTests.groovy diff --git a/documentation/docs/images/k8s_env.png b/documentation/docs/images/k8s_env.png new file mode 100644 index 0000000000000000000000000000000000000000..dec98655cf4a1f709c1f0798cd7b529545d24c9f GIT binary patch literal 9653 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 ADgXcg literal 0 HcmV?d00001 diff --git a/documentation/docs/steps/dockerExecute.md b/documentation/docs/steps/dockerExecute.md index cab7997fb..599a706ec 100644 --- a/documentation/docs/steps/dockerExecute.md +++ b/documentation/docs/steps/dockerExecute.md @@ -2,32 +2,73 @@ ## Description -Executes a closure inside a docker container with the specified docker image. +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. ## Parameters -| parameter | mandatory | default | possible values | -| -------------------|-----------|-----------------------------------|----------------------------| -| `script` | no | empty `globalPipelineEnvironment` | | -| `dockerImage` | no | '' | | -| `dockerEnvVars` | no | [:] | | -| `dockerOptions` | no | '' | | -| `dockerVolumeBind` | no | [:] | | +| parameter | mandatory | default | possible values | +| ----------|-----------|---------|-----------------| +|script|yes||| +|containerPortMappings|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. -* `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'] +* `containerPortMappings`: Map which defines per docker image the port mappings, like `containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]]` +* `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`: only relevant for Kubernetes case: Name of the container launching `dockerImage` * `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 ## Kubernetes support -If the Jenkins is setup on a Kubernetes cluster, then you can execute the closure inside a container of a pod by setting an environment variable `ON_K8S` to `true`. However, it will ignore both `dockeOptions` and `dockerVolumeBind` values. +If the Jenkins is setup on a Kubernetes cluster, then you can execute the closure inside a container of a pod by setting an environment variable `ON_K8S` to `true`. However, it will ignore `containerPortMappings`, `dockerOptions` and `dockerVolumeBind` values. ## Step configuration -none + +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| + ## Return value none @@ -38,7 +79,7 @@ none ## Exceptions none -## Example 1: Run closure inside a docker container +## Example 1: Run closure inside a docker container ```groovy dockerExecute(dockerImage: 'maven:3.5-jdk-7'){ @@ -48,7 +89,7 @@ dockerExecute(dockerImage: 'maven:3.5-jdk-7'){ ## Example 2: Run closure inside a container in a kubernetes pod ```sh -# set environment variable +# set environment variable export ON_K8S=true" ``` @@ -58,7 +99,26 @@ dockerExecute(script: this, dockerImage: 'maven:3.5-jdk-7'){ } ``` -In the above example, the `dockerEcecute` step will internally invoke [dockerExecuteOnKubernetes](dockerExecuteOnKubernetes.md) step and execute the closure inside a pod. +In the above example, the `dockerEcecute` step will internally invoke [dockerExecuteOnKubernetes](dockerExecuteOnKubernetes.md) step and execute the closure inside a pod. + +## Example 3: Run closure inside a container which is attached to a sidecar container (as for example used in [seleniumExecuteTests](seleniumExecuteTests.md): + +```groovy +dockerExecute( + script: script, + containerPortMappings: [containerPortMappings:'selenium/standalone-chrome':[containerPort: 4444, hostPort: 4444]], + dockerImage: 'node:8-stretch', + dockerName: 'node', + dockerWorkspace: '/home/node', + sidecarImage: 'selenium/standalone-chrome', + sidecarName: 'selenium', +) { + git url: 'https://github.wdf.sap.corp/XXXXX/WebDriverIOTest.git' + sh '''npm install + node index.js + ''' +} +``` diff --git a/documentation/docs/steps/dockerExecuteOnKubernetes.md b/documentation/docs/steps/dockerExecuteOnKubernetes.md index f30ff55bb..ac4b3babf 100644 --- a/documentation/docs/steps/dockerExecuteOnKubernetes.md +++ b/documentation/docs/steps/dockerExecuteOnKubernetes.md @@ -4,29 +4,64 @@ Executes a closure inside a container in a kubernetes pod. Proxy environment variables defined on the Jenkins machine are also available in the container. -## Prerequisites +## Prerequisites * The Jenkins should be running on kubernetes. -* An environment variable `ON_K8S` should be created on Jenkins and initialized to `true`. - +* An environment variable `ON_K8S` should be created on Jenkins and initialized to `true`. This could for example be done via _Jenkins_ - _Manage Jenkins_ - _Configure System_ - _Global properties_ - _Environment variables_ + +![Jenkins environment variable configuration](../images/k8s_env.png) + ## Parameters -| parameter | mandatory | default | possible values | -| -------------------|-----------|-----------------------------------|----------------------------| -| `script` | no | empty `globalPipelineEnvironment` | | -| `dockerImage` | yes | | | -| `dockerEnvVars` | no | [:] | | -| `dockerWorkspace` | no | '' | | -| `containerMap` | no | [:] | | +| parameter | mandatory | default | possible values | +| ----------|-----------|---------|-----------------| +|script|yes||| +|containerCommands|no||| +|containerEnvVars|no||| +|containerMap|no|`[:]`|| +|containerName|no||| +|containerPortMappings|no||| +|containerWorkspaces|no||| +|dockerEnvVars|no|`[:]`|| +|dockerImage|yes||| +|dockerWorkspace|no|`''`|| +|jenkinsKubernetes|no|`[jnlpAgent:s4sdk/jenkins-agent-k8s:latest]`|| +|stashExcludes|no|`[workspace:nohup.out]`|| +|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. +* `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]]]` +* `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'] * `dockerWorkspace` Docker options to be set when starting the container. It can be a list or a string. -* `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. - Ex `['maven:3.5-jdk-8-alpine': 'mavenExecute', 'famiko/jmeter-base': 'checkJMeter', 's4sdk/docker-cf-cli': 'cloudfoundry']` ## Step configuration -none + +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|||| +|containerCommands||X|X| +|containerEnvVars||X|X| +|containerMap||X|X| +|containerName||X|X| +|containerPortMappings||X|X| +|containerWorkspaces||X|X| +|dockerEnvVars||X|X| +|dockerImage||X|X| +|dockerWorkspace||X|X| +|jenkinsKubernetes|X||| +|stashExcludes||X|X| +|stashIncludes||X|X| ## Return value none @@ -39,13 +74,13 @@ none ## Example 1: Run a closure in a single container pod ```sh -# set environment variable +# set environment variable export ON_K8S=true" ``` ```groovy dockerExecuteOnKubernetes(script: script, dockerImage: 'maven:3.5-jdk-7'){ - sh "mvn clean install" + sh "mvn clean install" } ``` @@ -53,14 +88,14 @@ In the above example, a pod will be created with a docker container of image `ma ## Example 2: Run a closure in a multi-container pod ```sh -# set environment variable +# set environment variable export ON_K8S=true" ``` ```groovy dockerExecuteOnKubernetes(script: script, containerMap: ['maven:3.5-jdk-8-alpine': 'maven', 's4sdk/docker-cf-cli': 'cfcli']){ container('maven'){ - sh "mvn clean install" + sh "mvn clean install" } container('cfcli'){ sh "cf plugins" @@ -68,7 +103,25 @@ dockerExecuteOnKubernetes(script: script, containerMap: ['maven:3.5-jdk-8-alpine } ``` -In the above example, a pod will be created with multiple Docker containers that are passed as a `containerMap`. The containers can be chosen for executing by referring their labels as shown in the example. +In the above example, a pod will be created with multiple Docker containers that are passed as a `containerMap`. The containers can be chosen for executing by referring their labels as shown in the example. +## Example 3: Running a closure in a dedicated container of a multi-container pod +```sh +# set environment variable +export ON_K8S=true" +``` +```groovy +dockerExecuteOnKubernetes( + script: script, + containerCommands: ['selenium/standalone-chrome': ''], + containerMap: ['maven:3.5-jdk-8-alpine': 'maven', 'selenium/standalone-chrome': 'selenium'], + containerName: 'maven', + containerPortMappings: ['selenium/standalone-chrome': [containerPort: 4444, hostPort: 4444]] + containerWorkspaces: ['selenium/standalone-chrome': ''] +){ + echo "Executing inside a Kubernetes Pod inside 'maven' container to run Selenium tests" + sh "mvn clean install" +} +``` diff --git a/documentation/docs/steps/seleniumExecuteTests.md b/documentation/docs/steps/seleniumExecuteTests.md new file mode 100644 index 000000000..9a6d8ecc4 --- /dev/null +++ b/documentation/docs/steps/seleniumExecuteTests.md @@ -0,0 +1,134 @@ +# seleniumExecuteTests + +## Description + +Enables UI test execution with Selenium in a sidecar container. + +The step executes a closure (see example below) connecting to a sidecar container with a Selenium Server. + +When executing in a +* local Docker environment, please make sure to set Selenium host to **`selenium`** in your tests. +* Kubernetes environment, plese make sure to set Seleniums host to **`localhost`** in your tests. + +## Prerequisites + +none + +## Example + +```groovy +seleniumExecuteTests (script: this) { + git url: 'https://github.wdf.sap.corp/xxxxx/WebDriverIOTest.git' + sh '''npm install + node index.js''' +} +``` + +### Example test using WebdriverIO + +Example based on http://webdriver.io/guide/getstarted/modes.html and http://webdriver.io/guide.html + +#### Configuration for Local Docker Environment +``` +var webdriverio = require('webdriverio'); +var options = { + host: 'selenium', + port: 4444, + desiredCapabilities: { + browserName: 'chrome' + } +}; +``` +#### Configuration for Kubernetes Environment +``` +var webdriverio = require('webdriverio'); +var options = { + host: 'localhost', + port: 4444, + desiredCapabilities: { + browserName: 'chrome' + } +}; +``` + +#### Test Code (index.js) + +``` +// ToDo: add configuration from above + +webdriverio + .remote(options) + .init() + .url('http://www.google.com') + .getTitle().then(function(title) { + console.log('Title was: ' + title); + }) + .end() + .catch(function(err) { + console.log(err); + }); +``` + +## Parameters + +| parameter | mandatory | default | possible values | +| ----------|-----------|---------|-----------------| +|script|yes||| +|containerPortMappings|no|`[selenium/standalone-chrome:[[containerPort:4444, hostPort:4444]]]`|| +|dockerImage|no|buildTool=`maven`: `maven:3.5-jdk-7`
buildTool=`npm`: `node:8-stretch`
|| +|dockerName|no|buildTool=`maven`: `maven`
buildTool=`npm`: `npm`
|| +|dockerWorkspace|no|buildTool=`maven`: ``
buildTool=`npm`: `/home/node`
|| +|failOnError|no|`true`|| +|gitBranch|no||| +|gitSshKeyCredentialsId|no|``|| +|sidecarEnvVars|no||| +|sidecarImage|no|`selenium/standalone-chrome`|| +|sidecarName|no|`selenium`|| +|sidecarVolumeBind|no|`[/dev/shm:/dev/shm]`|| +|stashContent|no|
  • `tests`
|| +|testRepository|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. +* `containerPortMappings`, see step [dockerExecute](dockerExecute.md) +* `dockerImage`, see step [dockerExecute](dockerExecute.md) +* `dockerName`, see step [dockerExecute](dockerExecute.md) +* `dockerWorkspace`, see step [dockerExecute](dockerExecute.md) +* `failOnError` specifies if the step should fail in case the execution of the body of this step fails. +* `sidecarEnvVars`, see step [dockerExecute](dockerExecute.md) +* `sidecarImage`, see step [dockerExecute](dockerExecute.md) +* `sidecarName`, see step [dockerExecute](dockerExecute.md) +* `sidecarVolumeBind`, see step [dockerExecute](dockerExecute.md) +* If specific stashes should be considered for the tests, you can pass this via parameter `stashContent` +* In case the test implementation is stored in a different repository than the code itself, you can define the repository containing the tests using parameter `testRepository` and if required `gitBranch` (for a different branch than master) and `gitSshKeyCredentialsId` (for protected repositories). For protected repositories the testRepository needs to contain the ssh git url. + +## 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|X| +|dockerImage|X|X|X| +|dockerName|X|X|X| +|dockerWorkspace|X|X|X| +|failOnError|X|X|X| +|gitBranch|X|X|X| +|gitSshKeyCredentialsId|X|X|X| +|sidecarEnvVars|X|X|X| +|sidecarImage|X|X|X| +|sidecarName|X|X|X| +|sidecarVolumeBind|X|X|X| +|stashContent|X|X|X| +|testRepository|X|X|X| + +## Return value +none + +## Side effects +none + +## Exceptions +none diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 2c4f8520f..55601bfe8 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -19,6 +19,7 @@ nav: - pipelineExecute: steps/pipelineExecute.md - pipelineStashFiles: steps/pipelineStashFiles.md - prepareDefaultValues: steps/prepareDefaultValues.md + - seleniumExecuteTests: steps/seleniumExecuteTests.md - setupCommonPipelineEnvironment: steps/setupCommonPipelineEnvironment.md - toolValidate: steps/toolValidate.md - transportRequestCreate: steps/transportRequestCreate.md diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index f157da12e..7410dacbf 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -192,6 +192,28 @@ steps: pipelineConfigAndTests: '' securityDescriptor: '' tests: '' + seleniumExecuteTests: + buildTool: 'npm' + containerPortMappings: + 'selenium/standalone-chrome': + - containerPort: 4444 + hostPort: 4444 + dockerLinkAlias: 'selenium' + failOnError: true + sidecarImage: 'selenium/standalone-chrome' + sidecarName: 'selenium' + sidecarVolumeBind: + '/dev/shm': '/dev/shm' + stashContent: + - 'tests' + maven: + dockerImage: 'maven:3.5-jdk-7' + dockerName: 'maven' + dockerWorkspace: '' + npm: + dockerImage: 'node:8-stretch' + dockerName: 'npm' + dockerWorkspace: '/home/node' snykExecute: buildDescriptorFile: './package.json' dockerImage: 'node:8-stretch' diff --git a/test/groovy/DockerExecuteOnKubernetesTest.groovy b/test/groovy/DockerExecuteOnKubernetesTest.groovy index 26bfb1f78..a531cb6f8 100644 --- a/test/groovy/DockerExecuteOnKubernetesTest.groovy +++ b/test/groovy/DockerExecuteOnKubernetesTest.groovy @@ -16,6 +16,8 @@ import util.JenkinsStepRule import util.PluginMock import util.Rules +import static org.hamcrest.Matchers.* +import static org.junit.Assert.assertThat import static org.junit.Assert.assertTrue import static org.junit.Assert.assertEquals import static org.junit.Assert.assertFalse @@ -48,6 +50,8 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { def imageList = [] def containerName = '' def envList = [] + def portList = [] + def containerCommands = [] @Before @@ -55,6 +59,8 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { containersList = [] imageList = [] envList = [] + portList = [] + containerCommands = [] bodyExecuted = false JenkinsUtils.metaClass.static.isPluginActive = { def s -> new PluginMock(s).isActive() } helper.registerAllowedMethod('sh', [Map.class], {return whichDockerReturnValue}) @@ -67,6 +73,10 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { containersList.add(option.name) imageList.add(option.image) envList.add(option.envVars) + portList.add(option.ports) + if (option.command) { + containerCommands.add(option.command) + } } body() }) @@ -77,7 +87,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { @Test void testRunOnPodNoContainerMapOnlyDockerImage() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerOptions: '-it', dockerVolumeBind: ['my_vol': '/my_vol'], @@ -90,12 +100,13 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { assertTrue(envList.toString().contains('http://proxy:8000')) assertTrue(envList.toString().contains('/home/piper')) assertTrue(bodyExecuted) + assertThat(containerCommands.size(), is(1)) } @Test void testDockerExecuteOnKubernetesWithCustomContainerMap() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, containerMap: ['maven:3.5-jdk-8-alpine': 'mavenexecute']) { container(name: 'mavenexecute') { bodyExecuted = true @@ -105,12 +116,13 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { assertTrue(containersList.contains('mavenexecute')) assertTrue(imageList.contains('maven:3.5-jdk-8-alpine')) assertTrue(bodyExecuted) + assertThat(containerCommands.size(), is(1)) } @Test void testDockerExecuteOnKubernetesWithCustomJnlpWithContainerMap() throws Exception { nullScript.commonPipelineEnvironment.configuration = ['general': ['jenkinsKubernetes': ['jnlpAgent': 'myJnalpAgent']]] - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, containerMap: ['maven:3.5-jdk-8-alpine': 'mavenexecute']) { container(name: 'mavenexecute') { bodyExecuted = true @@ -127,7 +139,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { @Test void testDockerExecuteOnKubernetesWithCustomJnlpWithDockerImage() throws Exception { nullScript.commonPipelineEnvironment.configuration = ['general': ['jenkinsKubernetes': ['jnlpAgent': 'myJnalpAgent']]] - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine') { bodyExecuted = true } @@ -141,7 +153,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { @Test void testDockerExecuteOnKubernetesWithCustomWorkspace() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, containerMap: ['maven:3.5-jdk-8-alpine': 'mavenexecute'], dockerWorkspace: '/home/piper') { container(name: 'mavenexecute') { @@ -154,7 +166,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { @Test void testDockerExecuteOnKubernetesWithCustomEnv() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, containerMap: ['maven:3.5-jdk-8-alpine': 'mavenexecute'], dockerEnvVars: ['customEnvKey': 'customEnvValue']) { container(name: 'mavenexecute') { @@ -167,7 +179,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { @Test void testDockerExecuteOnKubernetesUpperCaseContainerName() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, containerMap: ['maven:3.5-jdk-8-alpine': 'MAVENEXECUTE'], dockerEnvVars: ['customEnvKey': 'customEnvValue']) { container(name: 'mavenexecute') { @@ -183,7 +195,7 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { @Test void testDockerExecuteOnKubernetesEmptyContainerMapNoDockerImage() throws Exception { exception.expect(IllegalArgumentException.class); - jsr.step.call(script: nullScript, + jsr.step.dockerExecuteOnKubernetes(script: nullScript, containerMap: [:], dockerEnvVars: ['customEnvKey': 'customEnvValue']) { container(name: 'jnlp') { @@ -193,6 +205,55 @@ class DockerExecuteOnKubernetesTest extends BasePiperTest { assertFalse(bodyExecuted) } + @Test + void testSidecarDefault() { + List portMapping = [] + helper.registerAllowedMethod('portMapping', [Map.class], {m -> + portMapping.add(m) + return m + }) + jsr.step.dockerExecuteOnKubernetes( + script: nullScript, + containerCommands: ['selenium/standalone-chrome': ''], + containerEnvVars: [ + 'selenium/standalone-chrome': ['customEnvKey': 'customEnvValue'] + ], + containerMap: [ + 'maven:3.5-jdk-8-alpine': 'mavenexecute', + 'selenium/standalone-chrome': 'selenium' + ], + containerName: 'mavenexecute', + containerPortMappings: [ + 'selenium/standalone-chrome': [[containerPort: 4444, hostPort: 4444]] + ], + containerWorkspaces: [ + 'selenium/standalone-chrome': '' + ], + dockerWorkspace: '/home/piper' + ) { + bodyExecuted = true + } + + assertThat(bodyExecuted, is(true)) + assertThat(containerName, is('mavenexecute')) + + assertThat(containersList, allOf( + hasItem('jnlp'), + hasItem('mavenexecute'), + hasItem('selenium'), + )) + assertThat(imageList, allOf( + hasItem('s4sdk/jenkins-agent-k8s:latest'), + hasItem('maven:3.5-jdk-8-alpine'), + hasItem('selenium/standalone-chrome'), + )) + assertThat(portList, hasItem(hasItem([name: 'selenium0', containerPort: 4444, hostPort: 4444]))) + assertThat(portMapping, hasItem([name: 'selenium0', containerPort: 4444, hostPort: 4444])) + assertThat(containerCommands.size(), is(1)) + assertThat(envList, hasItem(hasItem(allOf(hasEntry('key', 'customEnvKey'), hasEntry ('value','customEnvValue'))))) + } + + private container(options, body) { containerName = options.name body() diff --git a/test/groovy/DockerExecuteTest.groovy b/test/groovy/DockerExecuteTest.groovy index 296f7959c..f2da6ecd7 100644 --- a/test/groovy/DockerExecuteTest.groovy +++ b/test/groovy/DockerExecuteTest.groovy @@ -13,6 +13,8 @@ import util.JenkinsStepRule import util.PluginMock import util.Rules +import static org.hamcrest.Matchers.* +import static org.junit.Assert.assertThat import static org.junit.Assert.assertTrue import static org.junit.Assert.assertEquals import static org.junit.Assert.assertFalse @@ -50,7 +52,7 @@ class DockerExecuteTest extends BasePiperTest { }) binding.setVariable('env', [POD_NAME: 'testpod', ON_K8S: 'true']) ContainerMap.instance.setMap(['testpod': ['maven:3.5-jdk-8-alpine': 'mavenexec']]) - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { bodyExecuted = true @@ -65,7 +67,7 @@ class DockerExecuteTest extends BasePiperTest { helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body -> body() }) binding.setVariable('env', [ON_K8S: 'true']) ContainerMap.instance.setMap(['testpod': ['maven:3.5-jdk-8-alpine': 'mavenexec']]) - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { bodyExecuted = true @@ -79,7 +81,7 @@ class DockerExecuteTest extends BasePiperTest { helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body -> body() }) binding.setVariable('env', [POD_NAME: 'testpod', ON_K8S: 'true']) ContainerMap.instance.setMap([:]) - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { bodyExecuted = true @@ -93,7 +95,7 @@ class DockerExecuteTest extends BasePiperTest { helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { Map config, Closure body -> body() }) binding.setVariable('env', [POD_NAME: 'testpod', ON_K8S: 'true']) ContainerMap.instance.setMap(['testpod':[:]]) - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { bodyExecuted = true @@ -104,7 +106,7 @@ class DockerExecuteTest extends BasePiperTest { @Test void testExecuteInsideDockerContainer() throws Exception { - jsr.step.call(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine') { + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine') { bodyExecuted = true } assertEquals('maven:3.5-jdk-8-alpine', docker.getImageName()) @@ -115,7 +117,7 @@ class DockerExecuteTest extends BasePiperTest { @Test void testExecuteInsideDockerNoScript() throws Exception { - jsr.step.call(dockerImage: 'maven:3.5-jdk-8-alpine') { + jsr.step.dockerExecute(dockerImage: 'maven:3.5-jdk-8-alpine') { bodyExecuted = true } assertEquals('maven:3.5-jdk-8-alpine', docker.getImageName()) @@ -126,7 +128,7 @@ class DockerExecuteTest extends BasePiperTest { @Test void testExecuteInsideDockerContainerWithParameters() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerOptions: '-it', dockerVolumeBind: ['my_vol': '/my_vol'], @@ -142,7 +144,7 @@ class DockerExecuteTest extends BasePiperTest { @Test void testExecuteInsideDockerContainerWithDockerOptionsList() throws Exception { - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerImage: 'maven:3.5-jdk-8-alpine', dockerOptions: ['-it', '--network=my-network'], dockerEnvVars: ['http_proxy': 'http://proxy:8000']) { @@ -156,7 +158,7 @@ class DockerExecuteTest extends BasePiperTest { @Test void testDockerNotInstalledResultsInLocalExecution() throws Exception { whichDockerReturnValue = 1 - jsr.step.call(script: nullScript, + jsr.step.dockerExecute(script: nullScript, dockerOptions: '-it') { bodyExecuted = true } @@ -166,10 +168,70 @@ class DockerExecuteTest extends BasePiperTest { assertFalse(docker.isImagePulled()) } + @Test + void testSidecarDefault(){ + jsr.step.dockerExecute( + script: nullScript, + dockerImage: 'maven:3.5-jdk-8-alpine', + sidecarEnvVars: ['testEnv':'testVal'], + sidecarImage: 'selenium/standalone-chrome', + sidecarVolumeBind: ['/dev/shm':'/dev/shm'], + sidecarName: 'testAlias', + sidecarPorts: ['4444':'4444', '1111':'1111'] + ) { + bodyExecuted = true + } + + assertThat(bodyExecuted, is(true)) + assertThat(docker.imagePullCount, is(2)) + assertThat(docker.sidecarParameters, allOf( + containsString('--env testEnv=testVal'), + containsString('--volume /dev/shm:/dev/shm') + )) + assertThat(docker.parameters, containsString('--link uniqueId:testAlias')) + } + + @Test + void testSidecarKubernetes(){ + boolean dockerExecuteOnKubernetesCalled = false + binding.setVariable('env', [ON_K8S: 'true']) + helper.registerAllowedMethod('dockerExecuteOnKubernetes', [Map.class, Closure.class], { params, body -> + dockerExecuteOnKubernetesCalled = true + assertThat(params.containerCommands['selenium/standalone-chrome'], is('')) + assertThat(params.containerEnvVars, allOf(hasEntry('selenium/standalone-chrome', ['testEnv': 'testVal']),hasEntry('maven:3.5-jdk-8-alpine', null))) + assertThat(params.containerMap, allOf(hasEntry('maven:3.5-jdk-8-alpine', 'maven'), hasEntry('selenium/standalone-chrome', 'selenium'))) + assertThat(params.containerName, is('maven')) + assertThat(params.containerPortMappings['selenium/standalone-chrome'], hasItem(allOf(hasEntry('containerPort', 4444), hasEntry('hostPort', 4444)))) + assertThat(params.containerWorkspaces['maven:3.5-jdk-8-alpine'], is('/home/piper')) + assertThat(params.containerWorkspaces['selenium/standalone-chrome'], is('')) + body() + }) + jsr.step.dockerExecute( + script: nullScript, + containerPortMappings: [ + 'selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]] + ], + dockerImage: 'maven:3.5-jdk-8-alpine', + dockerName: 'maven', + dockerWorkspace: '/home/piper', + sidecarEnvVars: ['testEnv':'testVal'], + sidecarImage: 'selenium/standalone-chrome', + sidecarName: 'selenium', + sidecarVolumeBind: ['/dev/shm':'/dev/shm'], + dockerLinkAlias: 'testAlias', + ) { + bodyExecuted = true + } + assertThat(bodyExecuted, is(true)) + assertThat(dockerExecuteOnKubernetesCalled, is(true)) + } + private class DockerMock { private String imageName private boolean imagePulled = false + private int imagePullCount = 0 private String parameters + private String sidecarParameters DockerMock image(String imageName) { this.imageName = imageName @@ -177,6 +239,7 @@ class DockerExecuteTest extends BasePiperTest { } void pull() { + imagePullCount++ imagePulled = true } @@ -185,6 +248,11 @@ class DockerExecuteTest extends BasePiperTest { body() } + void withRun(String parameters, body) { + this.sidecarParameters = parameters + body([id: 'uniqueId']) + } + String getImageName() { return imageName } diff --git a/test/groovy/SeleniumExecuteTestsTest.groovy b/test/groovy/SeleniumExecuteTestsTest.groovy new file mode 100644 index 000000000..ec9f10e8f --- /dev/null +++ b/test/groovy/SeleniumExecuteTestsTest.groovy @@ -0,0 +1,98 @@ +import hudson.AbortException +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 SeleniumExecuteTestsTest 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(jsr) // needs to be activated after jedr, otherwise executeDocker is not mocked + + boolean bodyExecuted = false + + def gitMap + + @Before + void init() throws Exception { + bodyExecuted = false + helper.registerAllowedMethod('git', [Map.class], {m -> + gitMap = m + }) + } + + @Test + void testExecuteSeleniumDefault() { + jsr.step.seleniumExecuteTests( + script: nullScript, + juStabUtils: utils + ) { + bodyExecuted = true + } + assertThat(bodyExecuted, is(true)) + assertThat(jedr.dockerParams.containerPortMappings, is(['selenium/standalone-chrome': [[containerPort: 4444, hostPort: 4444]]])) + assertThat(jedr.dockerParams.dockerImage, is('node:8-stretch')) + assertThat(jedr.dockerParams.dockerName, is('npm')) + assertThat(jedr.dockerParams.dockerWorkspace, is('/home/node')) + assertThat(jedr.dockerParams.sidecarEnvVars, is(null)) + assertThat(jedr.dockerParams.sidecarImage, is('selenium/standalone-chrome')) + assertThat(jedr.dockerParams.sidecarName, is('selenium')) + assertThat(jedr.dockerParams.sidecarVolumeBind, is(['/dev/shm': '/dev/shm'])) + } + + @Test + void testExecuteSeleniumError() { + thrown.expectMessage('Error occured') + jsr.step.seleniumExecuteTests( + script: nullScript, + juStabUtils: utils + ) { + throw new AbortException('Error occured') + } + } + + @Test + void testExecuteSeleniumIgnoreError() { + jsr.step.seleniumExecuteTests( + script: nullScript, + failOnError: false, + juStabUtils: utils + ) { + bodyExecuted = true + throw new AbortException('Error occured') + } + assertThat(bodyExecuted, is(true)) + } + + @Test + void testExecuteSeleniumCustomRepo() { + jsr.step.seleniumExecuteTests( + script: nullScript, + gitBranch: 'test', + gitSshKeyCredentialsId: 'testCredentials', + juStabUtils: utils, + testRepository: 'git@test/test.git' + ) { + bodyExecuted = true + } + assertThat(bodyExecuted, is(true)) + assertThat(gitMap, hasEntry('branch', 'test')) + assertThat(gitMap, hasEntry('credentialsId', 'testCredentials')) + assertThat(gitMap, hasEntry('url', 'git@test/test.git')) + } +} diff --git a/vars/dockerExecute.groovy b/vars/dockerExecute.groovy index df47c1acb..1343ca7b1 100644 --- a/vars/dockerExecute.groovy +++ b/vars/dockerExecute.groovy @@ -9,11 +9,21 @@ import groovy.transform.Field @Field Set GENERAL_CONFIG_KEYS = ['jenkinsKubernetes'] -@Field Set PARAMETER_KEYS = ['dockerImage', - 'dockerOptions', - 'dockerWorkspace', - 'dockerEnvVars', - 'dockerVolumeBind'] +@Field Set PARAMETER_KEYS = [ + 'containerPortMappings', + 'dockerEnvVars', + 'dockerImage', + 'dockerName', + 'dockerOptions', + 'dockerWorkspace', + 'dockerVolumeBind', + 'sidecarName', + 'sidecarEnvVars', + 'sidecarImage', + 'sidecarOptions', + 'sidecarWorkspace', + 'sidecarVolumeBind' +] @Field Set STEP_CONFIG_KEYS = PARAMETER_KEYS void call(Map parameters = [:], body) { @@ -31,19 +41,48 @@ void call(Map parameters = [:], body) { if (isKubernetes() && config.dockerImage) { if (env.POD_NAME && isContainerDefined(config)) { container(getContainerDefined(config)) { - echo "Executing inside a Kubernetes Container" + echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Container." body() sh "chown -R 1000:1000 ." } } else { - dockerExecuteOnKubernetes( - script: script, - dockerImage: config.dockerImage, - dockerEnvVars: config.dockerEnvVars, - dockerWorkspace: config.dockerWorkspace - ){ - echo "Executing inside a Kubernetes Pod" - body() + if (!config.sidecarImage) { + dockerExecuteOnKubernetes( + script: script, + dockerImage: config.dockerImage, + dockerEnvVars: config.dockerEnvVars, + dockerWorkspace: config.dockerWorkspace + ){ + echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod" + body() + } + } else { + Map paramMap = [ + script: script, + containerCommands: [:], + containerEnvVars: [:], + containerMap: [:], + containerName: config.dockerName, + containerPortMappings: [:], + containerWorkspaces: [:] + ] + paramMap.containerCommands[config.sidecarImage] = '' + + paramMap.containerEnvVars[config.dockerImage] = config.dockerEnvVars + paramMap.containerEnvVars[config.sidecarImage] = config.sidecarEnvVars + + paramMap.containerMap[config.dockerImage] = config.dockerName + paramMap.containerMap[config.sidecarImage] = config.sidecarName + + paramMap.containerPortMappings = config.containerPortMappings + + paramMap.containerWorkspaces[config.dockerImage] = config.dockerWorkspace + paramMap.containerWorkspaces[config.sidecarImage] = '' + + dockerExecuteOnKubernetes(paramMap){ + echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod with sidecar container" + body() + } } } } else { @@ -67,8 +106,21 @@ void call(Map parameters = [:], body) { if (executeInsideDocker && config.dockerImage) { def image = docker.image(config.dockerImage) image.pull() - image.inside(getDockerOptions(config.dockerEnvVars, config.dockerVolumeBind, config.dockerOptions)) { - body() + if (!config.sidecarImage) { + image.inside(getDockerOptions(config.dockerEnvVars, config.dockerVolumeBind, config.dockerOptions)) { + body() + } + } else { + def sidecarImage = docker.image(config.sidecarImage) + sidecarImage.pull() + sidecarImage.withRun(getDockerOptions(config.sidecarEnvVars, config.sidecarVolumeBind, config.sidecarOptions)) { c -> + config.dockerOptions = config.dockerOptions?:[] + config.dockerOptions.add("--link ${c.id}:${config.sidecarName}") + image.inside(getDockerOptions(config.dockerEnvVars, config.dockerVolumeBind, config.dockerOptions)) { + echo "[INFO][${STEP_NAME}] Running with sidecar container." + body() + } + } } } else { echo "[INFO][${STEP_NAME}] Running on local environment." diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index 132e51369..c2ee5bb3b 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -7,10 +7,17 @@ import hudson.AbortException @Field def STEP_NAME = 'dockerExecuteOnKubernetes' @Field def PLUGIN_ID_KUBERNETES = 'kubernetes' @Field Set GENERAL_CONFIG_KEYS = ['jenkinsKubernetes'] -@Field Set PARAMETER_KEYS = ['dockerImage', - 'dockerWorkspace', - 'dockerEnvVars', - 'containerMap'] +@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': '']` + '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 + 'containerPortMappings', //map which defines per docker image the port mappings, like containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]] + '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', + 'dockerEnvVars' +] @Field Set STEP_CONFIG_KEYS = PARAMETER_KEYS.plus(['stashIncludes', 'stashExcludes']) void call(Map parameters = [:], body) { @@ -48,7 +55,14 @@ void executeOnPodWithCustomContainerList(Map parameters, body) { def config = parameters.config podTemplate(getOptions(config)) { node(config.uniqueId) { - body() + //allow execution in dedicated container + if (config.containerName) { + container(name: config.containerName){ + body() + } + } else { + body() + } } } } @@ -119,16 +133,35 @@ private void unstashWorkspace(config, prefix) { } private List getContainerList(config) { - def envVars = getContainerEnvs(config) + result = [] - result.push(containerTemplate(name: 'jnlp', - image: config.jenkinsKubernetes.jnlpAgent)) + result.push(containerTemplate( + name: 'jnlp', + image: config.jenkinsKubernetes.jnlpAgent + )) config.containerMap.each { imageName, containerName -> - result.push(containerTemplate(name: containerName.toLowerCase(), + def templateParameters = [ + name: containerName.toLowerCase(), image: imageName, alwaysPullImage: true, - command: '/usr/bin/tail -f /dev/null', - envVars: envVars)) + envVars: getContainerEnvs(config, imageName) + ] + + if (!config.containerCommands?.get(imageName)?.isEmpty()) { + templateParameters.command = config.containerCommands?.get(imageName)?: '/usr/bin/tail -f /dev/null' + } + + if (config.containerPortMappings?.get(imageName)) { + def ports = [] + def portCounter = 0 + config.containerPortMappings.get(imageName).each {mapping -> + mapping.name = "${containerName}${portCounter}".toString() + ports.add(portMapping(mapping)) + portCounter ++ + } + templateParameters.ports = ports + } + result.push(containerTemplate(templateParameters)) } return result } @@ -139,10 +172,10 @@ private List getContainerList(config) { * (Kubernetes-Plugin only!) * @param config Map with configurations */ -private List getContainerEnvs(config) { +private List getContainerEnvs(config, imageName) { def containerEnv = [] - def dockerEnvVars = config.dockerEnvVars ?: [:] - def dockerWorkspace = config.dockerWorkspace ?: '' + def dockerEnvVars = config.containerEnvVars?.get(imageName) ?: config.dockerEnvVars ?: [:] + def dockerWorkspace = config.containerWorkspaces?.get(imageName) != null ? config.containerWorkspaces?.get(imageName) : config.dockerWorkspace ?: '' if (dockerEnvVars) { for (String k : dockerEnvVars.keySet()) { diff --git a/vars/seleniumExecuteTests.groovy b/vars/seleniumExecuteTests.groovy new file mode 100644 index 000000000..0404839ca --- /dev/null +++ b/vars/seleniumExecuteTests.groovy @@ -0,0 +1,73 @@ +import com.sap.piper.Utils +import com.sap.piper.ConfigurationHelper +import com.sap.piper.Utils +import com.sap.piper.k8s.ContainerMap +import groovy.transform.Field +import groovy.text.SimpleTemplateEngine + +@Field String STEP_NAME = 'seleniumExecuteTests' +@Field Set STEP_CONFIG_KEYS = [ + 'containerPortMappings', //port mappings required for containers. This will only take effect inside a Kubernetes pod, format [[containerPort: 1111, hostPort: 1111]] + 'dockerImage', //Docker image for code execution + 'dockerName', //name of the Docker container. This will only take effect inside a Kubernetes pod. + 'dockerWorkspace', //user home directory for Docker execution. This will only take effect inside a Kubernetes pod. + 'failOnError', + 'gitBranch', //only if testRepository is used: branch of testRepository. Default is master + 'gitSshKeyCredentialsId', //only if testRepository is used: ssh credentials id in case a protected testRepository is used + 'sidecarEnvVars', //envVars to be set in Selenium container if required + 'sidecarImage', //image for Selenium execution which runs as sidecar to dockerImage + 'sidecarName', //name of the Selenium container. If not on Kubernetes pod, this will define the name of the link to the Selenium container and is thus required for accessing the server, example http://selenium:4444 (default) + 'sidecarVolumeBind', //volume bind. This will not take effect in Kubernetes pod. + 'stashContent', //list of stash names which are required to be unstashed before test run + 'testRepository' //if tests are in a separate repository, git url can be defined. For protected repositories the git ssh url is required +] +@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS + +def call(Map parameters = [:], Closure body) { + handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { + def script = parameters?.script ?: [commonPipelineEnvironment: commonPipelineEnvironment] + def utils = parameters?.juStabUtils ?: new Utils() + + // load default & individual configuration + Map config = ConfigurationHelper + .loadStepDefaults(this) + .mixinGeneralConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, STEP_CONFIG_KEYS) + .mixin(parameters, PARAMETER_KEYS) + .dependingOn('buildTool').mixin('dockerImage') + .dependingOn('buildTool').mixin('dockerName') + .dependingOn('buildTool').mixin('dockerWorkspace') + .use() + + utils.pushToSWA([step: STEP_NAME], config) + + dockerExecute( + script: script, + containerPortMappings: config.containerPortMappings, + dockerImage: config.dockerImage, + dockerName: config.dockerName, + dockerWorkspace: config.dockerWorkspace, + sidecarEnvVars: config.sidecarEnvVars, + sidecarImage: config.sidecarImage, + sidecarName: config.sidecarName, + sidecarVolumeBind: config.sidecarVolumeBind + ) { + try { + if (config.testRepository) { + def gitParameters = [url: config.testRepository] + if (config.gitSshKeyCredentialsId) gitParameters.credentialsId = config.gitSshKeyCredentialsId + if (config.gitBranch) gitParameters.branch = config.gitBranch + git gitParameters + } else { + config.stashContent = utils.unstashAll(config.stashContent) + } + body() + } catch (err) { + if (config.failOnError) { + throw err + } + } + } + } +} From d4ca181b1d721ec3d86788d0ad0f35ccad410e3d Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Fri, 5 Oct 2018 08:22:55 +0200 Subject: [PATCH 2/5] ignore bin folder (#326) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b53694dbf..8e150e0a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +bin/ .settings logs reports From f10a8c62466b56e2df285c34d9e8dd9aad64b26d Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Fri, 5 Oct 2018 10:51:01 +0200 Subject: [PATCH 3/5] dockerExecuteOnKubernetes: fix stash overriding (#329) * dockerExecuteOnKubernetes: fix stash overriding * update comment --- vars/dockerExecuteOnKubernetes.groovy | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/vars/dockerExecuteOnKubernetes.groovy b/vars/dockerExecuteOnKubernetes.groovy index c2ee5bb3b..65f475029 100644 --- a/vars/dockerExecuteOnKubernetes.groovy +++ b/vars/dockerExecuteOnKubernetes.groovy @@ -84,9 +84,9 @@ void executeOnPodWithSingleContainer(Map parameters, body) { - The container method - The body * We use nested exception handling in this case. - * In the first 2 cases, the workspace has not been modified. Hence, we can stash existing workspace as container and - * unstash in the finally block. In case of exception thrown by the body, we need to stash the workspace from the container - * in finally block + * In the first 2 cases, the 'container' stash is not created because the inner try/finally is not reached. + * However, the workspace has not been modified and don't need to be restored. + * In case third case, we need to create the 'container' stash to bring the modified content back to the host. */ try { stashWorkspace(config, 'workspace') @@ -98,13 +98,10 @@ void executeOnPodWithSingleContainer(Map parameters, body) { body() } finally { stashWorkspace(config, 'container') - } + } } } } - } catch (e) { - stashWorkspace(config, 'container') - throw e } finally { unstashWorkspace(config, 'container') } From 7965b5a78dd0ae4cdeb199f2dce9b2641d7166d8 Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Fri, 5 Oct 2018 13:56:57 +0200 Subject: [PATCH 4/5] Bump version (#328) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0840960f5..680a8c01c 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ 4.0.0 com.sap.cp.jenkins jenkins-library - 0.6 + 0.7 SAP CP Piper Library Shared library containing steps and utilities to set up continuous deployment processes for SAP technologies. From bf753814e4662c4d013eb3db8b912845ab10beca Mon Sep 17 00:00:00 2001 From: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Date: Fri, 5 Oct 2018 16:10:26 +0200 Subject: [PATCH 5/5] seleniumExecuteTests - Documentation Update (#327) --- documentation/docs/steps/seleniumExecuteTests.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documentation/docs/steps/seleniumExecuteTests.md b/documentation/docs/steps/seleniumExecuteTests.md index 9a6d8ecc4..30d081b92 100644 --- a/documentation/docs/steps/seleniumExecuteTests.md +++ b/documentation/docs/steps/seleniumExecuteTests.md @@ -10,6 +10,9 @@ When executing in a * local Docker environment, please make sure to set Selenium host to **`selenium`** in your tests. * Kubernetes environment, plese make sure to set Seleniums host to **`localhost`** in your tests. +!!! note "Proxy Environments" + If work in an environment containing a proxy, please make sure that `localhost`/`selenium` is added to your proxy exclusion list, e.g. via environment variable `NO_PROXY` & `no_proxy`. You can pass those via parameters `dockerEnvVars` and `sidecarEnvVars` directly to the containers if required. + ## Prerequisites none