This is a simple tutorial to show you how easy it can be to run isolated webtests in docker.
Presumed you want to run your webtests in an isolated environment for the following reasons:
isolation from other test runs (same ports/same directories/...)
automatic cleanup of ...
orphaned processes (to avoid memory leaks of test agent)
cleanup of build files (not filling disk up with temporary files)
repeatability (of course)
Btw. with build files I include the temporary container and images as well. (I just had a bunch of stability tests, which means running tests more than 50 times. I had local memory fuckups twice.)
The examples are taken from https://github.com/lkwg82/de.lgohlke.selenium-webdriver .
Any mentioned files are saved in the current directory otherwise it is specified.
Requirements
docker (I took 1.8)
ubuntu (I took 15.04)
maven (just for this example, I took 3.3.3)
Roadmap
My roadmap in this tutorial:
create a script to run the tests (in and outside of docker)
create a Dockerfile
create a script to run docker with tests
dont run as root, make it secure
bundle some convinient code to live happily with
1. create a script to run the tests
There should be a script wrapping test commands to be used inside and outside of docker. No special docker test commands!
File: run_tests.sh
1
2
3
4
5
6
7
8
#!/bin/bash
# just for resolving all dependencies
mvn install -DskipTests
# actual running tests
timeout --preserve-status --kill-after 7m 6m \
mvn clean verify -P sonar-coverage
Line 7 does timout the test run after specified time. I do have in mind a range of time to timeout the complete test run. To make the download of artifact independent line 4 is kept separate.
2. Creating the Dockerfile
I prefer to have a
Dockerfile to build a fresh container for each run. For my example I choose
lkwg82/mitmproxy-0.11-maven3-jdk8 (contains maven, jdk8, google-chrome and ...).
FROM lkwg82/mitmproxy-0.11-maven3-jdk8
To run my tests I add the current directory while building the container.
FROM lkwg82/mitmproxy-0.11-maven3-jdk8
ADD . /home/build
WORKDIR /home/build
3. create a script to run docker
3.1 get it run
File: run_docker.sh
1
2
3
4
5
6
7
#!/bin/bash
docker build -t test-$$ . | tee docker_build.log
IMAGE_ID = $( tail -n1 docker_build.log| cut -d \ -f3 )
docker run $IMAGE_ID ./run_tests.sh
line 3: build the container and log the output while displaying
line 3: use a tagname which contains the PID to identify when running in parallel
line 4: retrieve the image id
line 5: run the tests in a new container with the retrieved image id
Make some custom adjustments:
File: run_docker.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
DOCKER_USER_TMP = "/tmp/docker_ $USER "
DOCKER_M2 = " $DOCKER_USER_TMP /m2"
DOCKER_WEBDRIVER = " $DOCKER_USER_TMP /webdriver"
args = "--name=webdriver-test- $$ -m 1500M --memory-swap=-1 \
-v /dev/shm:/dev/shm \
-v $DOCKER_M2 :/home/build/.m2/repository \
-v $DOCKER_WEBDRIVER :/home/build/tmp_webdrivers "
rm -rf target/*
docker build -t test . | tee docker_build.log
IMAGE_ID = $( tail -n1 docker_build.log| cut -d \ -f3 )
docker run $args $IMAGE_ID ./run_tests.sh
line 4: reuse the maven repository over many runs
line 5: reuse special temp directory over many runs
line 12: clean target/ before adding to the new image
3.2 let it cleanup after run, dont mess with y disk space
File: run_docker.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
DOCKER_USER_TMP = "/tmp/docker_ $USER "
DOCKER_M2 = " $DOCKER_USER_TMP /m2"
DOCKER_WEBDRIVER = " $DOCKER_USER_TMP /webdriver"
args = "--name=webdriver-test- $$ -m 1500M --memory-swap=-1 \
-v /dev/shm:/dev/shm \
-v $DOCKER_M2 :/home/build/.m2/repository \
-v $DOCKER_WEBDRIVER :/home/build/tmp_webdrivers "
rm -rf target/*
docker build -t test . | tee docker_build.log
IMAGE_ID = $( tail -n1 docker_build.log| cut -d \ -f3 )
CONTAINER_ID = $( docker run -d $args $IMAGE_ID bash -c 'while true; do sleep 10000; done' )
echo -n $IMAGE_ID > docker_IMAGE_ID
echo -n $CONTAINER_ID > docker_CID
function cleanup {
./run_docker_cleanup.sh
}
trap cleanup EXIT INT
docker exec $CONTAINER_ID ./run_tests.sh
line 15: we need a main loop to keep the container running
line 15: keep the $CONTAINER_ID in memory
line 17-18: save ids to be used from outside this script
line 20: create function to call a cleanup script
line 23: register this cleanup function to be called on EXIT and CTRL-C (see traps )
line 25: execute the tests
File: run_docker_cleanup.sh
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
CONTAINER_ID = $( cat docker_CID)
IMAGE_ID = $( cat docker_IMAGE_ID)
# cleanup instances
docker kill $CONTAINER_ID
docker rm -f $CONTAINER_ID
docker rmi $IMAGE_ID
line 7-9: kill and remove the container and remove the specified iamge
4. dont run as root, make it secure
File: run_docker.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash
DOCKER_USER_TMP = "/tmp/docker_ $USER "
DOCKER_M2 = " $DOCKER_USER_TMP /m2"
DOCKER_WEBDRIVER = " $DOCKER_USER_TMP /webdriver"
mkdir -p $DOCKER_M2 # create the directory before
mkdir -p $DOCKER_WEBDRIVER # to have the correct ownership
args = "--name=webdriver-test- $$ -m 1500M --memory-swap=-1 \
-v /dev/shm:/dev/shm \
-v $DOCKER_M2 :/home/build/.m2/repository \
-v $DOCKER_WEBDRIVER :/home/build/tmp_webdrivers "
rm -rf target/*
docker build -t test-$$ . | tee docker_build.log
IMAGE_ID = $( tail -n1 docker_build.log| cut -d \ -f3 )
CONTAINER_ID = $( docker run -d $args $IMAGE_ID bash -c 'while true; do sleep 10000; done' )
echo -n $IMAGE_ID > docker_IMAGE_ID
echo -n $CONTAINER_ID > docker_CID
function cleanup {
./run_docker_cleanup.sh
}
trap cleanup EXIT INT
UID_OUTSIDE = $( id --user )
GID_OUTSIDE = $( id --group )
USER_INSIDE_DOCKER = "build"
docker exec $CONTAINER_ID useradd --uid $UID_OUTSIDE $USER_INSIDE_DOCKER
docker exec $CONTAINER_ID chown -R $USER_INSIDE_DOCKER .
docker exec $CONTAINER_ID su $USER_INSIDE_DOCKER -c './run_tests.sh'
line 7-8: create the shared directories as user which runs the tests
line 32: create an user with the same uid and gid inside the container as you have outside (this fixes the user id mapping problem)
line 33: remember you added the project as root (you dont set any user), so change the ownership to the new user
line 34: run the tests with the new user
5. make it convinient, make the user happy
5.1 Where is my build containing the logs?
File: run_docker_cleanup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
CONTAINER_ID = $( cat docker_CID)
IMAGE_ID = $( cat docker_IMAGE_ID)
rm -rf target
docker cp $CONTAINER_ID :/home/build/target .
# cleanup instances
docker kill $CONTAINER_ID
docker rm -f $CONTAINER_ID
docker rmi $IMAGE_ID
line 6: clear the local target/ directory before filling with copies from container
line 7: copy the build files
5.2 Which software environment I ran the tests with?
File: Dockerfile
1
2
3
4
5
6
FROM lkwg82/mitmproxy-0.11-maven3-jdk8
ADD . /home/build
WORKDIR /home/build
RUN dpkg --list | grep ^ii > installed_software.log
line 5: save the installed package list
the list looks like
ii acl 2.2.52-2 amd64 Access control list utilities
ii adduser 3.113+nmu3 all add and remove users and groups
ii apt 1.0.9.8.1 amd64 commandline package manager
ii base-files 8+deb8u2 amd64 Debian base system miscellaneous files
ii base-passwd 3.5.37 amd64 Debian base system master password and group files
ii bash 4.3-11+b1 amd64 GNU Bourne Again SHell
...
Dont forget to copy the list from the container.
File: run_docker_cleanup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
CONTAINER_ID = $( cat docker_CID)
IMAGE_ID = $( cat docker_IMAGE_ID)
rm -rf target
docker cp $CONTAINER_ID :/home/build/target .
docker cp $CONTAINER_ID :/home/build/installed_software.log target
# cleanup instances
docker kill $CONTAINER_ID
docker rm -f $CONTAINER_ID
docker rmi $IMAGE_ID
line 8: copy the package list
5.3 Do I have any issues in docker with the limits?
If you ever experienced issues with flaky tests, read on!
File: run_docker_cleanup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash
CONTAINER_ID = $( cat docker_CID)
IMAGE_ID = $( cat docker_IMAGE_ID)
rm -rf target
docker cp $CONTAINER_ID :/home/build/target .
docker cp $CONTAINER_ID :/home/build/installed_software.log target
# check for issues (low memory etc.)
if [ $( dmesg -T | grep docker-$CONTAINER_ID | wc -l ) -gt 0 ] ; then
echo "" ;
echo "[ERROR] there were issues with $CONTAINER_ID :" ;
echo "" ;
dmesg -T | grep docker-$CONTAINER_ID
echo "--"
echo "host config looks like (reduced)"
docker inspect --format = "" $CONTAINER_ID | python -m json.tool \
| grep -v ": \"\" " \
| grep -v ": null" \
| grep -vE ": 0|-1" \
| grep -v ": \[\] " \
| grep -v ": {}"
exit 137
fi
# cleanup instances
docker kill $CONTAINER_ID
docker rm -f $CONTAINER_ID
docker rmi $IMAGE_ID
line 10-26: add a check and let the build fail
For more details read
this separate post on this issue.
I appreciate any feedback/corrections and comments.