Skip to content

Commit d21111e

Browse files
authored
Merge pull request #2 from cloudogu/feature/extendWithDockerNetwork
Feature/extend with docker network
2 parents 3092363 + 899563e commit d21111e

File tree

5 files changed

+143
-38
lines changed

5 files changed

+143
-38
lines changed

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,38 @@ Even more convenient: You could watch the videos in the browser directly. This i
4545
* Or you start your Jenkins instance with
4646
`-Dhudson.model.DirectoryBrowserSupport.CSP="sandbox; default-src 'none'; img-src 'self'; style-src 'self'; media-src 'self';"`
4747

48+
## Docker Network creation
49+
50+
It is possible (although not necessary) to explicitly work with docker networks. This library supports the automatic creation and removal of a bridge network with a unique name.
51+
52+
### How
53+
54+
`withZalenium` accepts now an optional network name the Zalenium container can attach to the given network. Conveniently a docker network can be created with this pipeline step which provides the dynamically created network name.
55+
56+
```
57+
withDockerNetwork { networkName ->
58+
def yourConfig = [:]
59+
withZalenium(yourConfig, networkName) {}
60+
docker.image("foo/bar:1.2.3").withRun("--network ${network}") {
61+
...
62+
}
63+
64+
```
65+
4866
## Locking
4967

5068
Right now, only one Job can run Zalenium Tests at a time.
5169
This could be improved in the future.
5270

53-
## Why?
71+
### Why?
5472

5573
When multiple jobs executed we faced non-deterministic issues that the zalenium container was gone all of a sudden,
5674
connections were aborted or timed out.
5775

5876
So we implemented a lock before starting zalenium that can only be passed by one job at a time.
5977
It feels like this issue is gone now, but we're not sure if the lock was the proper fix.
6078

61-
## How?
79+
### How?
6280

6381
We use the `lock` step of the [Lockable Resources Plugin](https://wiki.jenkins.io/display/JENKINS/Lockable+Resources+Plugin).
6482

vars/withDockerNetwork.groovy

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Start a temporary docker network so docker containers can interact even without IP address. The created network will
3+
* be removed automatically once the body finishes.
4+
* @param printDebugOutput logs creation and removal of the network if set to true. Can be left out.
5+
* @param inner the body to be executed
6+
*/
7+
void call(printDebugOutput = false, Closure inner) {
8+
def networkName = "net_" + generateJobName()
9+
10+
try {
11+
debugOut(printDebugOutput, "create docker bridge network")
12+
sh "docker network create ${networkName}"
13+
// provide network name to closure
14+
inner.call(networkName)
15+
} finally {
16+
debugOut(printDebugOutput, "remove docker network")
17+
sh "docker network rm ${networkName}"
18+
}
19+
}
20+
21+
void debugOut(boolean printDebugOutput, String logMessage) {
22+
if (printDebugOutput) {
23+
echo "DEBUG: " + logMessage
24+
}
25+
}
26+
27+
String generateJobName() {
28+
return "${JOB_BASE_NAME}_${BUILD_NUMBER}"
29+
}

vars/withDockerNetwork.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Starts a temporary docker network so docker containers can interact even without IP address. The created network will
2+
be removed once the body finishes.
3+
4+
Requires Docker!
5+
6+
(optional) Parameters:
7+
8+
- printDebugOutput - this bool adds network creation and removal output to the Jenkins console log for debugging
9+
purposes.
10+
11+
Exemplary calls:
12+
13+
- withDockerNetwork { networkName ->
14+
// create your container with the networkName along the lines of this:
15+
docker.image("foo/bar:1.2.3").withRun("--network ${networkName}") {
16+
...
17+
}
18+
- debugOutput = true; withDockerNetwork(debugOutput) { networkName ->
19+
// prints the creation and removal of the docker network in your output
20+
// create your container with the networkName along the lines of this:
21+
docker.image("foo/bar:1.2.3").withRun("--network ${networkName}") {
22+
}

vars/withZalenium.groovy

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,78 @@
1-
2-
void call(config = [:], Closure closure) {
3-
4-
def defaultConfig = [seleniumVersion : '3.141.59-p8',
5-
zaleniumVersion : '3.141.59g',
6-
zaleniumVideoDir: "zalenium",
7-
debugZalenium : false]
1+
/**
2+
* Starts Zalenium in a Selenium grid and executes the given body. When the body finishes, the Zalenium container will
3+
* gracefully shutdown and archive all videos generated by the tests.
4+
*
5+
* @param config contains a map of settings that change the Zalenium behavior. Can be a partial map or even left out.
6+
* The defaults are:
7+
* [seleniumVersion : '3.141.59-p8',
8+
* seleniumImage : 'elgalu/selenium',
9+
* zaleniumVersion : '3.141.59g',
10+
* zaleniumImage : 'dosel/zalenium',
11+
* zaleniumVideoDir : 'zalenium',
12+
* sendGoogleAnalytics: false,
13+
* debugZalenium : false]
14+
* @param zaleniumNetwork The Zalenium container will be added to this docker network. This is useful if other containers
15+
* must communicate with Zalenium while being in a docker network. If empty or left out, Zalenium will stay in the
16+
* default network.
17+
* @param closure the body
18+
*/
19+
void call(Map config = [:], String zaleniumNetwork, Closure closure) {
20+
21+
def defaultConfig = [seleniumVersion : '3.141.59-p8',
22+
seleniumImage : 'elgalu/selenium',
23+
zaleniumVersion : '3.141.59g',
24+
zaleniumImage : 'dosel/zalenium',
25+
zaleniumVideoDir : 'zalenium',
26+
debugZalenium : false,
27+
sendGoogleAnalytics: false]
828

929
// Merge default config with the one passed as parameter
1030
config = defaultConfig << config
1131

1232
sh "mkdir -p ${config.zaleniumVideoDir}"
1333

14-
docker.image("elgalu/selenium:${config.seleniumVersion}").pull()
15-
def zaleniumImage = docker.image("dosel/zalenium:${config.zaleniumVersion}")
34+
// explicitly pull the image into the registry. The documentation is not fully clear but it seems that pull()
35+
// will persist the image in the registry better than an docker.image(...).runWith()
36+
docker.image("${config.seleniumImage}:${config.seleniumVersion}").pull()
37+
def zaleniumImage = docker.image("${config.zaleniumImage}:${config.zaleniumVersion}")
1638
zaleniumImage.pull()
1739

18-
uid = findUid()
19-
gid = findGid()
40+
def uid = findUid()
41+
def gid = findGid()
42+
43+
networkParameter = ""
44+
45+
if (zaleniumNetwork != null && !zaleniumNetwork.isEmpty()) {
46+
networkParameter = "--network ${zaleniumNetwork}"
47+
}
2048

2149
lock("zalenium") {
2250
zaleniumImage.withRun(
2351
// Run with Jenkins user, so the files created in the workspace by zalenium can be deleted later
52+
// Otherwise that would be root, and you know how hard it is to get rid of root-owned files.
2453
"-u ${uid}:${gid} -e HOST_UID=${uid} -e HOST_GID=${gid} " +
25-
// Zalenium starts headless browsers in docker containers, so it needs the socket
26-
'-v /var/run/docker.sock:/var/run/docker.sock ' +
27-
"-v ${WORKSPACE}/${config.zaleniumVideoDir}:/home/seluser/videos",
54+
// Zalenium starts headless browsers in each in a docker container, so it needs the Socket
55+
'-v /var/run/docker.sock:/var/run/docker.sock ' +
56+
'--privileged ' +
57+
"${networkParameter} " +
58+
"-v ${WORKSPACE}/${config.zaleniumVideoDir}:/home/seluser/videos",
2859
'start ' +
29-
"${config.debugZalenium ? '--debugEnabled true' : ''}"
60+
"--seleniumImageName ${config.seleniumImage} " +
61+
"${config.debugZalenium ? '--debugEnabled true' : ''} " +
62+
// switch off analytic gathering
63+
"${config.sendGoogleAnalytics ? '--sendAnonymousUsageInfo false' : ''} "
3064
) { zaleniumContainer ->
31-
32-
def zaleniumIp = findContainerIp(zaleniumContainer)
33-
34-
waitForSeleniumToGetReady(zaleniumIp)
35-
// Delete videos from previous builds, if any
36-
// This also works around the bug that zalenium stores files as root (before version 3.141.59f)
37-
// https://github.com/zalando/zalenium/issues/760
38-
// This workaround still leaves a couple of files owned by root in the zaleniumVideoDir
39-
resetZalenium(zaleniumIp)
65+
String zaleniumIp = findContainerIp(zaleniumContainer)
4066

4167
try {
42-
closure(zaleniumIp)
68+
waitForSeleniumToGetReady(zaleniumIp)
69+
// Delete videos from previous builds, if any
70+
// This also works around the bug that zalenium stores files as root (before version 3.141.59f)
71+
// https://github.com/zalando/zalenium/issues/760
72+
// This workaround still leaves a couple of files owned by root in the zaleniumVideoDir
73+
resetZalenium(zaleniumIp)
74+
75+
closure.call(zaleniumContainer, zaleniumIp, uid, gid)
4376
} finally {
4477
// Wait for Selenium sessions to end (i.e. videos to be copied)
4578
// Leaving the withRun() closure leads to "docker rm -f" being called, cancelling copying
@@ -52,23 +85,23 @@ void call(config = [:], Closure closure) {
5285
sh "docker logs ${zaleniumContainer.id} > zalenium-docker.log 2>&1"
5386
}
5487
}
55-
5688
}
5789
}
5890

5991
String findContainerIp(container) {
60-
sh (returnStdout: true,
92+
sh(returnStdout: true,
6193
script: "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${container.id}")
6294
.trim()
6395
}
6496

6597
String findUid() {
66-
sh (returnStdout: true,
98+
sh(returnStdout: true,
6799
script: 'id -u')
68100
.trim()
69101
}
102+
70103
String findGid() {
71-
sh (returnStdout: true,
104+
sh(returnStdout: true,
72105
script: 'id -g')
73106
.trim()
74107
}
@@ -107,4 +140,4 @@ boolean isSeleniumSessionsActive(String host) {
107140
void resetZalenium(String host) {
108141
sh(returnStatus: true,
109142
script: "curl -sSL http://${host}:4444/dashboard/cleanup?action=doReset") == 0
110-
}
143+
}

vars/withZalenium.txt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
Starts a temporary zalenium server, that stores videos of the selenium tests in the workspace.
1+
Starts a temporary Zalenium server that stores videos of the selenium tests in the workspace.
22

33
Requires Docker!
44

55
(optional) Parameters:
66

7-
- seleniumVersion - version of the "elgalu/selenium" docker image
8-
- zaleniumVersion - version of the "dosel/zalenium" docker image
9-
- zaleniumVideoDir - workspace relative path where the videos are stored
7+
- seleniumImage - the full name of the selenium image name including the registry. Defaults to 'elgalu/selenium' from hub.docker.com. The selenium image is used by Zalenium.
8+
- seleniumVersion - version of the selenium docker image
9+
- zaleniumImage - the full name of the zalenium image name including the registry. Defaults to 'dosel/zalenium' from hub.docker.com.
10+
- zaleniumVersion - version of the zalenium docker image
11+
- zaleniumVideoDir - path where the videos are stored, relative to the Jenkins workspace.
1012
- debugZalenium - makes the zalenium container write a lot more logs
13+
- sendGoogleAnalytics - if true this will send analytic data to Google/Zalando
1114

1215
Exemplary calls:
1316

14-
- withZalenium { zaleniumIp ->
17+
- withZalenium { zaleniumContainer, zaleniumIp, userid, groupid ->
1518
// call your selenium tests using maven, yarn, etc.
1619
}
17-
- withZalenium([ seleniumVersion : '3.14.0-p15' ]) { zaleniumIp ->
20+
- withZalenium([ seleniumVersion : '3.14.0-p15' ]) { zaleniumContainer, zaleniumIp, userid, groupid ->
1821
// call your selenium tests using maven, yarn, etc.
1922
}

0 commit comments

Comments
 (0)