Support to run shell script tests in docker environment

Automate shell script tests to run in docker environment so that we can
reduce the manual effort for running these tests. In this setup there
are two containers, one for running gerrit server and another for
running tests. The gerrit server is hosted on gerrit-01 container and
the tests are run on run_tests container, default user which is used to
run docker tests is created by impersonating the 'admin' account which
is created when gerrit is initialized.

Change-Id: I0b4fdebdf9e27cf5250e45845b562bde58248d0e
diff --git a/.gitignore b/.gitignore
index 6ff7fd3..9d9fffc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
 /.settings/org.eclipse.core.resources.prefs
 /.settings/org.eclipse.jdt.core.prefs
 /events.iml
+/test/docker/gerrit/artifacts
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
new file mode 100755
index 0000000..70112f6
--- /dev/null
+++ b/test/docker/docker-compose.yaml
@@ -0,0 +1,33 @@
+version: '3'
+services:
+
+  gerrit-01:
+    build:
+      context: gerrit
+      args:
+        - GERRIT_WAR
+        - EVENTS_PLUGIN_JAR
+    networks:
+      - gerrit-net
+    volumes:
+      - "./:/docker"
+      - "gerrit-site-etc:/var/gerrit/etc"
+
+  run_tests:
+    build: run_tests
+    networks:
+      - gerrit-net
+    volumes:
+      - "../../:/events:ro"
+      - "gerrit-site-etc:/server-ssh-key:ro"
+    depends_on:
+      - gerrit-01
+    environment:
+      - GERRIT_HOST=gerrit-01
+
+networks:
+  gerrit-net:
+    driver: bridge
+
+volumes:
+  gerrit-site-etc:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
new file mode 100755
index 0000000..9c12ffa
--- /dev/null
+++ b/test/docker/gerrit/Dockerfile
@@ -0,0 +1,30 @@
+FROM openjdk:8
+ARG GERRIT_WAR
+ARG EVENTS_PLUGIN_JAR
+ARG UID=1000
+ARG GID=1000
+
+ENV GERRIT_USER gerrit
+ENV GERRIT_SITE /var/gerrit
+ENV USER_HOME /home/$GERRIT_USERENV USER_HOME /home/$GERRIT_USER
+RUN mkdir -p $GERRIT_SITE/bin $GERRIT_SITE/plugins $GERRIT_SITE/etc $USER_HOME/.ssh
+COPY $GERRIT_WAR $GERRIT_SITE/bin/gerrit.war
+COPY $EVENTS_PLUGIN_JAR $GERRIT_SITE/plugins/events.jar
+
+RUN touch $GERRIT_SITE/etc/gerrit.config && \
+    git config -f $GERRIT_SITE/etc/gerrit.config auth.type DEVELOPMENT_BECOME_ANY_ACCOUNT && \
+    git config -f "$GERRIT_SITE"/etc/secure.config ssh-alias.stream-events "events stream"
+
+EXPOSE 29418 8080
+RUN groupadd -f -g $GID users2 && \
+  useradd -u $UID -g $GID $GERRIT_USER && \
+  chown -R $GERRIT_USER $GERRIT_SITE $USER_HOME
+
+USER $GERRIT_USER
+
+RUN echo "Initializing Gerrit site ..." && \
+    java -jar "$GERRIT_SITE"/bin/gerrit.war init --batch -d "$GERRIT_SITE" && \
+    java -jar "$GERRIT_SITE"/bin/gerrit.war reindex -d "$GERRIT_SITE"
+
+RUN echo "Running Gerrit ..."
+ENTRYPOINT "$GERRIT_SITE/bin/gerrit.sh" run
diff --git a/test/docker/run.sh b/test/docker/run.sh
new file mode 100755
index 0000000..442b874
--- /dev/null
+++ b/test/docker/run.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+
+readlink --canonicalize / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+ARTIFACTS=$MYDIR/gerrit/artifacts
+
+die() { echo -e "\nERROR: $@" ; kill $$ ; exit 1 ; } # error_message
+
+progress() { # message cmd [args]...
+    local message=$1 ; shift
+    echo -n "$message"
+    "$@" &
+    local pid=$!
+    while kill -0 $pid 2> /dev/null ; do
+        echo -n "."
+        sleep 2
+    done
+    echo
+    wait "$pid"
+}
+
+usage() { # [error_message]
+    local prog=$(basename "$0")
+    cat <<EOF
+Usage:
+    "$prog" --gerrit-war|-g <WAR URL or file path>
+            --events-plugin-jar|-e <events plugin JAR URL or file path>
+
+    --help|-h
+    --gerrit-war|-g             gerrit WAR URL or the file path in local workspace
+                                eg: file:///path/to/gerrit.war
+    --events-plugin-jar|-e      events plugin JAR URL or the file path in local workspace
+                                eg: file:///path/to/events.jar
+
+EOF
+
+    [ -n "$1" ] && echo -e "\nERROR: $1" && exit 1
+    exit 0
+}
+
+check_prerequisite() {
+    docker --version > /dev/null || die "docker is not installed"
+    docker-compose --version > /dev/null || die "docker-compose is not installed"
+}
+
+fetch_artifact() { # source_location output_path
+    curl --silent --fail --netrc "$1" --output "$2" --create-dirs || die "unable to fetch $1"
+}
+
+fetch_artifacts() {
+    fetch_artifact "$GERRIT_WAR" "$ARTIFACTS/gerrit.war"
+    fetch_artifact "$EVENTS_PLUGIN_JAR" "$ARTIFACTS/events.jar"
+}
+
+build_images() {
+    local build_args=(--build-arg GERRIT_WAR="/artifacts/gerrit.war" \
+        --build-arg EVENTS_PLUGIN_JAR="/artifacts/events.jar" \
+        --build-arg UID="$(id -u)" --build-arg GID="$(id -g)")
+
+    docker-compose "${COMPOSE_ARGS[@]}" build "${build_args[@]}" --quiet
+    rm -r "$ARTIFACTS"
+}
+
+run_events_plugin_tests() {
+    docker-compose "${COMPOSE_ARGS[@]}" up -d
+    local runtests_container=$(docker ps | grep "$PROJECT_NAME"_run_tests | \
+        awk '{print $1}')
+    docker exec --user=gerrit_admin "$runtests_container" \
+            '/events/test/docker/run_tests/start.sh'
+}
+
+cleanup() {
+    docker-compose "${COMPOSE_ARGS[@]}" down -v --rmi local 2>/dev/null
+}
+
+while (( "$#" )); do
+    case "$1" in
+        --help|-h)                    usage ;;
+        --gerrit-war|-g)              shift ; GERRIT_WAR=$1 ;;
+        --events-plugin-jar|-e)       shift ; EVENTS_PLUGIN_JAR=$1 ;;
+        *)                            usage "invalid argument $1" ;;
+    esac
+    shift
+done
+
+[ -n "$GERRIT_WAR" ] || usage "'--gerrit-war' not set"
+[ -n "$EVENTS_PLUGIN_JAR" ] || usage "'--events-plugin-jar' not set "
+
+PROJECT_NAME="events_$$"
+COMPOSE_YAML="$MYDIR/docker-compose.yaml"
+COMPOSE_ARGS=(--project-name "$PROJECT_NAME" -f "$COMPOSE_YAML")
+check_prerequisite
+progress "fetching artifacts" fetch_artifacts
+progress "Building docker images" build_images
+run_events_plugin_tests ; RESULT=$?
+cleanup
+
+exit "$RESULT"
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
new file mode 100755
index 0000000..a68f7c8
--- /dev/null
+++ b/test/docker/run_tests/Dockerfile
@@ -0,0 +1,24 @@
+FROM alpine:3.11
+
+ARG UID=1000
+ARG GID=1000
+ENV USER gerrit_admin
+ENV USER_HOME /home/$USER
+ENV WORKSPACE $USER_HOME/workspace
+
+RUN apk --update add --no-cache openssh bash git util-linux openssl shadow
+RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
+
+RUN groupadd -f -g $GID users2
+RUN useradd -u $UID -g $GID $USER
+RUN mkdir -p $WORKSPACE $USER_HOME/.ssh
+RUN chown -R $USER $USER_HOME
+
+USER $USER
+
+RUN ssh-keygen -P '' -f "$USER_HOME"/.ssh/id_rsa
+RUN chmod 400 "$USER_HOME"/.ssh/id_rsa
+RUN git config --global user.name "Gerrit Admin"
+RUN git config --global user.email "gerrit_admin@example.com"
+
+ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
new file mode 100755
index 0000000..de065db
--- /dev/null
+++ b/test/docker/run_tests/start.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+PORT=29418
+TEST_PROJECT=test-project
+
+setup_test_project() {
+    echo "Creating a test project ..."
+    ssh -p "$PORT" -x "$GERRIT_HOST" gerrit create-project "${TEST_PROJECT}".git \
+        --owner "Administrators" --submit-type "MERGE_IF_NECESSARY"
+    git clone ssh://"$GERRIT_HOST":"$PORT"/"$TEST_PROJECT" "$WORKSPACE"
+    pushd "$WORKSPACE" > /dev/null
+    git commit -m "Initial commit" --allow-empty
+    git push ssh://"$GERRIT_HOST":"$PORT"/"$TEST_PROJECT" HEAD:refs/heads/master
+    popd > /dev/null
+}
+
+cp -r /events "$USER_HOME"/
+
+cd "$USER_HOME"/events/test
+./docker/run_tests/wait-for-it.sh "$GERRIT_HOST":"$PORT" \
+    -t 60 -- echo "Gerrit is up"
+
+echo "Creating a default user account ..."
+
+cat "$USER_HOME"/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \
+  "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit create-account \
+     --ssh-key - --email "gerrit_admin@localdomain"  --group "Administrators" "gerrit_admin"
+
+setup_test_project
+./test_events_plugin.sh --server "$GERRIT_HOST" --project "$TEST_PROJECT"
diff --git a/test/docker/run_tests/wait-for-it.sh b/test/docker/run_tests/wait-for-it.sh
new file mode 100755
index 0000000..d7b6e3c
--- /dev/null
+++ b/test/docker/run_tests/wait-for-it.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env bash
+# https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh
+#   Use this script to test if a given TCP host/port are available
+
+cmdname=$(basename $0)
+
+echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+    cat << USAGE >&2
+Usage:
+    $cmdname host:port [-s] [-t timeout] [-- command args]
+    -h HOST | --host=HOST       Host or IP under test
+    -p PORT | --port=PORT       TCP port under test
+                                Alternatively, you specify the host and port as host:port
+    -s | --strict               Only execute subcommand if the test succeeds
+    -q | --quiet                Don't output any status messages
+    -t TIMEOUT | --timeout=TIMEOUT
+                                Timeout in seconds, zero for no timeout
+    -- COMMAND ARGS             Execute command with args after the test finishes
+USAGE
+    exit 1
+}
+
+wait_for()
+{
+    if [[ $TIMEOUT -gt 0 ]]; then
+        echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
+    else
+        echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
+    fi
+    start_ts=$(date +%s)
+    while :
+    do
+        (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
+        result=$?
+        if [[ $result -eq 0 ]]; then
+            end_ts=$(date +%s)
+            echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
+            break
+        fi
+        sleep 1
+    done
+    return $result
+}
+
+wait_for_wrapper()
+{
+    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+    if [[ $QUIET -eq 1 ]]; then
+        timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    else
+        timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    fi
+    PID=$!
+    trap "kill -INT -$PID" INT
+    wait $PID
+    RESULT=$?
+    if [[ $RESULT -ne 0 ]]; then
+        echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
+    fi
+    return $RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+    case "$1" in
+        *:* )
+        hostport=(${1//:/ })
+        HOST=${hostport[0]}
+        PORT=${hostport[1]}
+        shift 1
+        ;;
+        --child)
+        CHILD=1
+        shift 1
+        ;;
+        -q | --quiet)
+        QUIET=1
+        shift 1
+        ;;
+        -s | --strict)
+        STRICT=1
+        shift 1
+        ;;
+        -h)
+        HOST="$2"
+        if [[ $HOST == "" ]]; then break; fi
+        shift 2
+        ;;
+        --host=*)
+        HOST="${1#*=}"
+        shift 1
+        ;;
+        -p)
+        PORT="$2"
+        if [[ $PORT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --port=*)
+        PORT="${1#*=}"
+        shift 1
+        ;;
+        -t)
+        TIMEOUT="$2"
+        if [[ $TIMEOUT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --timeout=*)
+        TIMEOUT="${1#*=}"
+        shift 1
+        ;;
+        --)
+        shift
+        CLI="$@"
+        break
+        ;;
+        --help)
+        usage
+        ;;
+        *)
+        echoerr "Unknown argument: $1"
+        usage
+        ;;
+    esac
+done
+
+if [[ "$HOST" == "" || "$PORT" == "" ]]; then
+    echoerr "Error: you need to provide a host and port to test."
+    usage
+fi
+
+TIMEOUT=${TIMEOUT:-15}
+STRICT=${STRICT:-0}
+CHILD=${CHILD:-0}
+QUIET=${QUIET:-0}
+
+if [[ $CHILD -gt 0 ]]; then
+    wait_for
+    RESULT=$?
+    exit $RESULT
+else
+    if [[ $TIMEOUT -gt 0 ]]; then
+        wait_for_wrapper
+        RESULT=$?
+    else
+        wait_for
+        RESULT=$?
+    fi
+fi
+
+if [[ $CLI != "" ]]; then
+    if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
+        echoerr "$cmdname: strict mode, refusing to execute subprocess"
+        exit $RESULT
+    fi
+    exec $CLI
+else
+    exit $RESULT
+fi