#!/bin/bash
#
# Test the Redis image.
#
# IMAGE_NAME specifies the name of the candidate image used for testing.
# The image has to be available before this script is executed.
#

set -o errexit
set -o nounset
shopt -s nullglob

[ "${DEBUG:-0}" -eq 1 ] && set -x

IMAGE_NAME=${IMAGE_NAME-centos/redis-32-centos7-candidate}
test_exit=1

CIDFILE_DIR=$(mktemp --suffix=redis_test_cidfiles -d)

function cleanup() {
  local cidfile
  for cidfile in $CIDFILE_DIR/* ; do
    local CONTAINER
    CONTAINER=$(cat $cidfile)

    echo "Stopping and removing container $CONTAINER..."
    docker stop $CONTAINER >/dev/null
    local exit_status
    exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER)
    if [ "$exit_status" != "0" ]; then
      echo "Inspecting container $CONTAINER"
      docker inspect $CONTAINER
      echo "Dumping logs for $CONTAINER"
      docker logs $CONTAINER
    fi
    docker rm -v $CONTAINER >/dev/null
    rm $cidfile
    echo "Done."
  done
  rmdir $CIDFILE_DIR

  # Report whole test result
  if [ "$test_exit" -eq 0 ] ; then
    echo "Test succeeded."
  else
    echo "Test failed."
  fi
  exit $test_exit
}
trap cleanup EXIT SIGINT

function get_cid() {
  local id="$1" ; shift || return 1
  echo $(cat "$CIDFILE_DIR/$id")
}

function get_container_ip() {
  local id="$1" ; shift
  docker inspect --format='{{.NetworkSettings.IPAddress}}' $(get_cid "$id")
}

function connection_works() {
  local container_ip="$1"; shift
  local password="$1"; shift
  if [ "$(redis_cmd "$container_ip" "$password" ping)" == "PONG" ] ; then
    return 0
  fi
  return 1
}

function redis_cmd() {
  local container_ip="$1"; shift
  local password="$1"; shift
  # if empty password is given, then no password will be specified
  docker run --rm "$IMAGE_NAME" redis-cli -h "$container_ip" ${password:+-a "$password"} "$@"
}

function test_connection() {
  local name=$1 ; shift
  local password=$1 ; shift
  local ip
  ip=$(get_container_ip $name)
  echo "  Testing Redis connection to $ip (password='${password:-}')..."
  local max_attempts=10
  local sleep_time=2
  local i
  for i in $(seq $max_attempts); do
    echo "    Trying to connect..."
    if connection_works "$ip" "$password" ; then
      echo "  Success!"
      echo
      return 0
    fi
    sleep $sleep_time
  done
  echo "  Giving up: Failed to connect. Logs:"
  docker logs $(get_cid $name)
  return 1
}

function test_redis() {
  local container_ip="$1"
  local password="$2"

  echo "  Testing Redis (password='${password:-}')"
  redis_cmd "$container_ip" "$password" set a 1 >/dev/null
  redis_cmd "$container_ip" "$password" set b 2 >/dev/null
  test "$(redis_cmd "$container_ip" "$password" get b)" == '2'
  echo "  Success!"
  echo
}

function create_container() {
  local name=$1 ; shift
  cidfile="$CIDFILE_DIR/$name"
  # create container with a cidfile in a directory for cleanup
  local container_id
  [ "${DEBUG:-0}" -eq 1 ] && echo "DEBUG: docker run ${DOCKER_ARGS:-} --cidfile $cidfile -d \"$@\" $IMAGE_NAME ${CONTAINER_ARGS:-}" >&2
  container_id="$(docker run ${DOCKER_ARGS:-} --cidfile $cidfile -d "$@" $IMAGE_NAME ${CONTAINER_ARGS:-})"
  [ "${DEBUG:-0}" -eq 1 ] && echo "Created container $container_id"
  return 0
}

function run_change_password_test() {
  local tmpdir=$(mktemp -d)
  mkdir "${tmpdir}/data" && chmod -R a+rwx "${tmpdir}"

  # Create Redis container with persistent volume and set the initial password
  create_container "testpass1" -e REDIS_PASSWORD=foo \
    -v ${tmpdir}:/var/lib/redis/data:Z
  test_connection testpass1 foo
  docker stop $(get_cid testpass1) >/dev/null

  # Create second container with changed password
  create_container "testpass2" -e REDIS_PASSWORD=bar \
    -v ${tmpdir}:/var/lib/redis/data:Z
  test_connection testpass2 bar

  # The old password should not work anymore
  container_ip="$(get_container_ip testpass2)"
  if connection_works "$container_ip" foo ; then
    return 1
  fi
}

function assert_login_access() {
  local container_ip=$1; shift
  local PASS=$1 ; shift
  local success=$1 ; shift

  if connection_works "$container_ip" "$PASS" ; then
    if $success ; then
      echo "    Connection ($PASS) access granted as expected"
      return
    fi
  else
    if ! $success ; then
      echo "    Connection ($PASS) access denied as expected"
      return
    fi
  fi
  echo "    Connection ($PASS) login assertion failed"
  exit 1
}

function assert_local_access() {
  local id="$1" ; shift
  docker exec $(get_cid "$id") bash -c 'redis-cli ping'
}

# Make sure the invocation of docker run fails.
function assert_container_creation_fails() {

  # Time the docker run command. It should fail. If it doesn't fail,
  # redis will keep running so we kill it with SIGKILL to make sure
  # timeout returns a non-zero value.
  local ret=0
  timeout -s 9 --preserve-status 60s docker run --rm "$@" $IMAGE_NAME >/dev/null || ret=$?

  # Timeout will exit with a high number.
  if [ $ret -gt 10 ]; then
    return 1
  fi
}

function try_image_invalid_combinations() {
  assert_container_creation_fails -e REDIS_PASSWORD="pass with space" "$@"
}

function run_container_creation_tests() {
  echo "  Testing image entrypoint usage"
  try_image_invalid_combinations
  echo "  Success!"
  echo
}

test_scl_usage() {
  local name="$1"
  local run_cmd="$2"
  local expected="$3"

  echo "  Testing the image SCL enable"
  local out
  out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}")
  if ! echo "${out}" | grep -q "${expected}"; then
    echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'"
    return 1
  fi
  out=$(docker exec $(get_cid $name) /bin/bash -c "${run_cmd}" 2>&1)
  if ! echo "${out}" | grep -q "${expected}"; then
    echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'"
    return 1
  fi
  out=$(docker exec $(get_cid $name) /bin/sh -ic "${run_cmd}" 2>&1)
  if ! echo "${out}" | grep -q "${expected}"; then
    echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'"
    return 1
  fi
}

run_doc_test() {
  local tmpdir=$(mktemp -d)
  local f
  echo "  Testing documentation in the container image"
  # Extract the help files from the container
  for f in /usr/share/container-scripts/redis/README.md help.1 ; do
    docker run --rm ${IMAGE_NAME} /bin/bash -c "cat /${f}" >${tmpdir}/$(basename ${f})
    # Check whether the files include some important information
    for term in 6379 REDIS_PASSWORD volume ; do
      if ! cat ${tmpdir}/$(basename ${f}) | grep -q -e "${term}" ; then
        echo "ERROR: File /${f} does not include '${term}'."
        return 1
      fi
    done
  done
  # Check whether the files use the correct format
  if ! file ${tmpdir}/help.1 | grep -q roff ; then
    echo "ERROR: /help.1 is not in troff or groff format"
    return 1
  fi
  echo "  Success!"
  echo
}

function run_tests() {
  local name=$1 ; shift
  envs=${PASS:+"-e REDIS_PASSWORD=$PASS"}
  PASS=${PASS:-}
  create_container $name $envs
  test_connection "$name" "$PASS"
  echo "  Testing scl usage"
  test_scl_usage $name 'redis-server --version' '3.2.'
  echo "  Testing login accesses"
  local container_ip
  container_ip=$(get_container_ip $name)
  assert_login_access "$container_ip" "$PASS" true
  if [ -n "$PASS" ] ; then
    assert_login_access "$container_ip" "${PASS}_foo" false
  fi
  assert_local_access "$name"
  echo "  Success!"
  echo
  test_redis "$container_ip" "$PASS"
}

# Tests.

# Test whether documentation is part of the image
run_container_creation_tests

# Normal tests without password
run_tests no_pass
# Normal tests with password
PASS=pass run_tests no_root
# Test with arbitrary uid for the container without password
DOCKER_ARGS="-u 12345" run_tests no_pass_altuid
# Test with arbitrary uid for the container with password
DOCKER_ARGS="-u 12345" PASS=pass run_tests no_root_altuid

# Test the password change
run_change_password_test

# Test whether documentation is part of the image
run_doc_test

test_exit=0
