diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..4ba0f06 --- /dev/null +++ b/test/README.md @@ -0,0 +1,18 @@ +# Tests + +Philosophy is to not re-invent the wheel while allowing users to quickly test repository specific tests. + +Example invocation from top-level of repository: + + docker build -t openvpn . + tests/run.sh openvpn + +More details: https://github.com/docker-library/official-images/tree/master/test + +## Continuous Integration + +The set of scripts defined by `config.sh` are run every time a pull request or push to the repository is made. + +## Maintenance + +Periodically these scripts may need to be synchronized with their upsteam source. Would be nice to be able to just use them from upstream if it such a feature is added later to avoid having to copy them in place. diff --git a/test/config.sh b/test/config.sh new file mode 100644 index 0000000..6c4e5d8 --- /dev/null +++ b/test/config.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +testAlias+=( + [kylemanna/openvpn]='openvpn' +) + +imageTests+=( + [openvpn]=' + paranoid + ' +) diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 0000000..47655fe --- /dev/null +++ b/test/run.sh @@ -0,0 +1,202 @@ +#!/bin/bash +set -e + +dir="$(dirname "$(readlink -f "$BASH_SOURCE")")" + +self="$(basename "$0")" + +usage() { + cat <&2 && false; })" +eval set -- "$opts" + +declare -A argTests=() +declare -a configs=() +dryRun= +while true; do + flag=$1 + shift + case "$flag" in + --dry-run) dryRun=1 ;; + --help|-h|'-?') usage && exit 0 ;; + --test|-t) argTests["$1"]=1 && shift ;; + --config|-c) configs+=("$(readlink -f "$1")") && shift ;; + --) break ;; + *) + { + echo "error: unknown flag: $flag" + usage + } >&2 + exit 1 + ;; + esac +done + +if [ $# -eq 0 ]; then + usage >&2 + exit 1 +fi + +# declare configuration variables +declare -a globalTests=() +declare -A testAlias=() +declare -A imageTests=() +declare -A globalExcludeTests=() +declare -A explicitTests=() + +# if there are no user-specified configs, use the default config +if [ ${#configs} -eq 0 ]; then + configs+=("$dir/config.sh") +fi + +# load the configs +declare -A testPaths=() +for conf in "${configs[@]}"; do + . "$conf" + + # Determine the full path to any newly-declared tests + confDir="$(dirname "$conf")" + + for testName in ${globalTests[@]} ${imageTests[@]}; do + [ "${testPaths[$testName]}" ] && continue + + if [ -d "$confDir/tests/$testName" ]; then + # Test directory found relative to the conf file + testPaths[$testName]="$confDir/tests/$testName" + elif [ -d "$dir/tests/$testName" ]; then + # Test directory found in the main tests/ directory + testPaths[$testName]="$dir/tests/$testName" + fi + done +done + +didFail= +for dockerImage in "$@"; do + echo "testing $dockerImage" + + if ! docker inspect "$dockerImage" &> /dev/null; then + echo $'\timage does not exist!' + didFail=1 + continue + fi + + repo="${dockerImage%:*}" + tagVar="${dockerImage#*:}" + #version="${tagVar%-*}" + variant="${tagVar##*-}" + + testRepo=$repo + [ -z "${testAlias[$repo]}" ] || testRepo="${testAlias[$repo]}" + + explicitVariant= + if [ \ + "${explicitTests[:$variant]}" \ + -o "${explicitTests[$repo:$variant]}" \ + -o "${explicitTests[$testRepo:$variant]}" \ + ]; then + explicitVariant=1 + fi + + testCandidates=() + if [ -z "$explicitVariant" ]; then + testCandidates+=( "${globalTests[@]}" ) + fi + testCandidates+=( + ${imageTests[:$variant]} + ) + if [ -z "$explicitVariant" ]; then + testCandidates+=( + ${imageTests[$testRepo]} + ) + fi + testCandidates+=( + ${imageTests[$testRepo:$variant]} + ) + if [ "$testRepo" != "$repo" ]; then + if [ -z "$explicitVariant" ]; then + testCandidates+=( + ${imageTests[$repo]} + ) + fi + testCandidates+=( + ${imageTests[$repo:$variant]} + ) + fi + + tests=() + for t in "${testCandidates[@]}"; do + if [ ${#argTests[@]} -gt 0 -a -z "${argTests[$t]}" ]; then + # skipping due to -t + continue + fi + + if [ \ + ! -z "${globalExcludeTests[${testRepo}_$t]}" \ + -o ! -z "${globalExcludeTests[${testRepo}:${variant}_$t]}" \ + -o ! -z "${globalExcludeTests[:${variant}_$t]}" \ + -o ! -z "${globalExcludeTests[${repo}_$t]}" \ + -o ! -z "${globalExcludeTests[${repo}:${variant}_$t]}" \ + -o ! -z "${globalExcludeTests[:${variant}_$t]}" \ + ]; then + # skipping due to exclude + continue + fi + + tests+=( "$t" ) + done + + currentTest=0 + totalTest="${#tests[@]}" + for t in "${tests[@]}"; do + (( currentTest+=1 )) + echo -ne "\t'$t' [$currentTest/$totalTest]..." + + # run test against dockerImage here + # find the script for the test + scriptDir="${testPaths[$t]}" + if [ -d "$scriptDir" ]; then + script="$scriptDir/run.sh" + if [ -x "$script" -a ! -d "$script" ]; then + # TODO dryRun logic + if output="$("$script" $dockerImage)"; then + if [ -f "$scriptDir/expected-std-out.txt" ] && ! d="$(echo "$output" | diff -u "$scriptDir/expected-std-out.txt" - 2>/dev/null)"; then + echo 'failed; unexpected output:' + echo "$d" + didFail=1 + else + echo 'passed' + fi + else + echo 'failed' + didFail=1 + fi + else + echo "skipping" + echo >&2 "error: $script missing, not executable or is a directory" + didFail=1 + continue + fi + else + echo "skipping" + echo >&2 "error: unable to locate test '$t'" + didFail=1 + continue + fi + done +done + +if [ "$didFail" ]; then + exit 1 +fi diff --git a/test/tests/docker-build.sh b/test/tests/docker-build.sh new file mode 100755 index 0000000..1eefd05 --- /dev/null +++ b/test/tests/docker-build.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +# wrapper around "docker build" that creates a temporary directory and copies files into it first so that arbitrary host directories can be copied into containers without bind mounts, but accepts a Dockerfile on stdin + +# usage: ./docker-build.sh some-host-directory some-new-image:some-tag < "$tmp/Dockerfile" + +from="$(awk -F '[ \t]+' 'toupper($1) == "FROM" { print $2; exit }' "$tmp/Dockerfile")" +onbuilds="$(docker inspect -f '{{len .Config.OnBuild}}' "$from")" +if [ "$onbuilds" -gt 0 ]; then + # crap, the image we want to build has some ONBUILD instructions + # those are kind of going to ruin our day + # let's do some hacks to strip those bad boys out in a new fake layer + "$(dirname "$(readlink -f "$BASH_SOURCE")")/remove-onbuild.sh" "$from" "$imageTag" + awk -F '[ \t]+' 'toupper($1) == "FROM" { $2 = "'"$imageTag"'" } { print }' "$tmp/Dockerfile" > "$tmp/Dockerfile.new" + mv "$tmp/Dockerfile.new" "$tmp/Dockerfile" +fi + +cp -RL "$dir" "$tmp/dir" + +docker build -t "$imageTag" "$tmp" > /dev/null diff --git a/test/tests/image-name.sh b/test/tests/image-name.sh new file mode 100755 index 0000000..1842970 --- /dev/null +++ b/test/tests/image-name.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# usage: ./image-name.sh librarytest/something some/image:some-tag +# output: librarytest/something:some-image-some-tag + +base="$1"; shift +tag="$1"; shift + +echo "$base:$(echo "$tag" | sed 's![:/]!-!g')" diff --git a/test/tests/paranoid/container.sh b/test/tests/paranoid/container.sh new file mode 100644 index 0000000..ae0743c --- /dev/null +++ b/test/tests/paranoid/container.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) + +# +# Generate a simple configuration, returns nonzero on error +# +ovpn_genconfig -u udp://$SERV_IP 2>/dev/null + +export EASYRSA_BATCH=1 +export EASYRSA_REQ_CN="Travis-CI Test CA" + +# +# Initialize the certificate PKI state, returns nonzero on error +# +ovpn_initpki nopass 2>/dev/null + +# +# Test back-up +# +ovpn_copy_server_files diff --git a/test/tests/paranoid/run.sh b/test/tests/paranoid/run.sh new file mode 120000 index 0000000..2778ad9 --- /dev/null +++ b/test/tests/paranoid/run.sh @@ -0,0 +1 @@ +../run-bash-in-container.sh \ No newline at end of file diff --git a/test/tests/run-bash-in-container.sh b/test/tests/run-bash-in-container.sh new file mode 100755 index 0000000..ace3dfc --- /dev/null +++ b/test/tests/run-bash-in-container.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +testDir="$(readlink -f "$(dirname "$BASH_SOURCE")")" +runDir="$(dirname "$(readlink -f "$BASH_SOURCE")")" + +source "$runDir/run-in-container.sh" "$testDir" "$1" bash ./container.sh diff --git a/test/tests/run-in-container.sh b/test/tests/run-in-container.sh new file mode 100755 index 0000000..b937d4b --- /dev/null +++ b/test/tests/run-in-container.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +# NOT INTENDED TO BE USED AS A TEST "run.sh" DIRECTLY +# SEE OTHER "run-*-in-container.sh" SCRIPTS FOR USAGE + +testDir="$1" +shift + +image="$1" +shift +entrypoint="$1" +shift + +# do some fancy footwork so that if testDir is /a/b/c, we mount /a/b and use c as the working directory (so relative symlinks work one level up) +thisDir="$(dirname "$(readlink -f "$BASH_SOURCE")")" +testDir="$(readlink -f "$testDir")" +testBase="$(basename "$testDir")" +hostMount="$(dirname "$testDir")" +containerMount="/tmp/test-dir" +workdir="$containerMount/$testBase" +# TODO should we be doing something fancy with $BASH_SOURCE instead so we can be arbitrarily deep and mount the top level always? + +newImage="$("$thisDir/image-name.sh" librarytest/run-in-container "$image--$testBase")" +"$thisDir/docker-build.sh" "$hostMount" "$newImage" <