#! /bin/sh
#
# Seamless interface to Run ElastiCluster in a Docker image.
# Tries to install Docker if it cannot be found.
#
# Copyright (c) 2018, 2019, 2020 Riccardo Murri <riccardo.murri@gmail.com>
#
# This file is part of ElastiCluster.  It can be distributed and
# modified under the same conditions as ElastiCluster.
#

me="$(basename $0)"


## defaults

docker_image_name='riccardomurri/elasticluster'
docker_image_tag='latest'


## helper functions

# see /usr/include/sysexit.h
EX_OK=0           # successful termination
EX_USAGE=1        # command line usage error
EX_DATAERR=65     # data format error
EX_NOINPUT=66     # cannot open input
EX_NOUSER=67      # addressee unknown
EX_NOHOST=68      # host name unknown
EX_UNAVAILABLE=69 # service unavailable
EX_SOFTWARE=70    # internal software error
EX_OSERR=71       # system error (e.g., can't fork)
EX_OSFILE=72      # critical OS file missing
EX_CANTCREAT=73   # can't create (user) output file
EX_IOERR=74       # input/output error
EX_TEMPFAIL=75    # temp failure; user is invited to retry
EX_PROTOCOL=76    # remote error in protocol
EX_NOPERM=77      # permission denied
EX_CONFIG=78      # configuration error


have_command () {
    command -v "$1" >/dev/null 2>/dev/null
}

if have_command tput; then
    TXT_NORMAL=$(tput sgr0)

    TXT_BOLD=$(tput bold)
    TXT_DIM=$(tput dim)
    TXT_STANDOUT=$(tput smso)

    TXT_BLACK=$(tput setaf 0)
    TXT_BLUE=$(tput setaf 4)
    TXT_CYAN=$(tput setaf 6)
    TXT_GREEN=$(tput setaf 2)
    TXT_MAGENTA=$(tput setaf 5)
    TXT_RED=$(tput setaf 1)
    TXT_WHITE=$(tput setaf 7)
    TXT_YELLOW=$(tput setaf 3)
    TXT_NOCOLOR=$(tput op)
else
    TXT_NORMAL=''

    TXT_BOLD=''
    TXT_DIM=''
    TXT_STANDOUT=''

    TXT_BLACK=''
    TXT_BLUE=''
    TXT_CYAN=''
    TXT_GREEN=''
    TXT_MAGENTA=''
    TXT_RED=''
    TXT_WHITE=''
    TXT_YELLOW=''
    TXT_NOCOLOR=''
fi

die () {
  rc="$1"
  shift
  (
      echo -n "${TXT_RED}${TXT_BOLD}$me: ERROR:${TXT_NOCOLOR} ";
      if [ $# -gt 0 ]; then echo "$@"; else cat; fi
      echo -n "${TXT_NORMAL}"
  ) 1>&2
  exit $rc
}

warn () {
    (
        echo -n "$me: ${TXT_YELLOW}WARNING:${TXT_NOCOLOR} ";
        if [ $# -gt 0 ]; then echo "$@"; else cat; fi
    ) 1>&2
}

require_command () {
  if ! have_command "$1"; then
    die 1 "Could not find required command '$1' in system PATH. Aborting."
  fi
}

do_install_docker () {
    # work in a temporary directory
    require_command mktemp
    tmpdir=$(mktemp -d -t)
    if [ -z "$tmpdir" ]; then
        die $EX_CANTCREAT "Cannot create temporary download directory. Aborting."
    fi
    orig_wd="$PWD"
    trap "cd '$orig_wd'; rm -rf '$tmpdir';" EXIT TERM ABRT INT QUIT
    cd "$tmpdir"
    if have_command wget; then
        wget -nv -O get-docker.sh https://get.docker.com
    elif have_command curl; then
        curl -fsSL get.docker.com -o get-docker.sh
    else
        die $EX_UNAVAILABLE <<__EOF__

Docker installation requires either the 'wget' or the 'curl' commands,
and none is available. Aborting.

Please install either 'wget' or 'curl'.
__EOF__
    fi

    # run Docker installation
    if have_command sudo; then
        sudo /bin/sh get-docker.sh
        if [ $? -ne 0 ]; then
            die $EX_NOPERM "Failed to install Docker. Cannot continue."
        fi
    elif have_command su; then
        su -c "/bin/sh $PWD/get-docker.sh"
        if [ $? -ne 0 ]; then
            die $EX_NOPERM "Failed to install Docker. Cannot continue."
        fi
    else
        die $EX_UNAVAILABLE <<__EOF__

Docker installation requires either the 'sudo' or the 'su' commands,
and none is available. Aborting.

Please ask your system administrator to install Docker.
__EOF__
    fi

    cd "$orig_wd"
}

# misc post-installation tasks
do_setup_docker () {
    # add current user to `docker` group
    require_command usermod
    sudo usermod -aG docker "$USER"

    # ensure Docker daemon is started
    if have_command systemctl; then
        sudo systemctl start docker
    elif have_command service; then
        sudo service docker status \
            || sudo service docker start
    fi
}


## main

if ! have_command docker; then
    cat <<__EOF__
${TXT_BOLD}${TXT_YELLOW}${me} requires the 'docker' command,
which could not be found on \$PATH${TXT_NOCOLOR}${TXT_NORMAL}

I can now try to install Docker; this requires the 'sudo' command with
unrestricted administrative access.  If you do not have administrative
access to this machine, please ask your system administrator to
install Docker.

To know more about Docker, visit http://www.docker.com/

${TXT_BOLD}Press Ctrl+C now to abort, or Enter/Return to try installing Docker.${TXT_NORMAL}
__EOF__
    read _
    do_install_docker
fi

# docker should have been installed by now...
require_command egrep
require_command env
require_command docker
require_command readlink

# check if `env` supports the `--null` flag
if (env -0 >/dev/null 2>&1); then
    env_has_null_termination_opt='y'
    require_command awk
else
    env_has_null_termination_opt='n'
    warn <<__EOF__
Command 'env' does not support null-terminated lines;
$(basename $me) cannot properly sanitize the environment in this case.
If you get errors later on about Docker being unable to process environment
variables, you will need to install GNU coreutils' 'env'.
__EOF__
fi

# set up mount commands for host directories
volumes="-v $HOME/.ssh:/home/.ssh -v $HOME/.elasticluster:/home/.elasticluster"
if [ -n "$SSH_AUTH_SOCK" ]; then
    volumes="${volumes} -v $SSH_AUTH_SOCK:/home/.ssh-agent.sock"
fi
if [ -d "$HOME/.config" ]; then
    volumes="${volumes} -v $HOME/.config:/home/.config"
fi

usage () {
    # show ElastiCLuster's main help
    docker run --rm --tty "${docker_image_name}:${docker_image_tag}" --help

    # add post-scriptum
cat <<EOF

In addition, the following options can be used to control execution of
ElastiCluster in Docker:

  --update
      Update Docker image to the latest version available

  --release TAG
      Use the Docker image with the given TAG (default tag: "latest")

  --latest
      Same as '--update --release latest'

EOF
}

# parse command-line for additional options
argv=''
while [ $# -gt 0 ]; do
    case "$1" in
        --config|-c)
            # canonicalize path to avoid Docker error: "create .:
            # volume name is too short, names should be at least two
            # alphanumeric characters." (e.g., when path is `.`)
            cfgpath=$(readlink -f "$2")
            volumes="${volumes} -v ${cfgpath}:/mnt/config"
            argv="$argv --config /mnt/config"
            shift
            ;;
        --help|-h)
            usage;
            exit 0
            ;;
        --latest)
            pull='yes'
            docker_image_tag='latest'
            ;;
        --release)
            docker_image_tag="$2"
            shift
            ;;
        --update|--pull)
            pull='yes'
            ;;
        *)
            argv="$argv $1"
            ;;
    esac
    shift
done

# put args back for elasticluster python to interpret
set -- $argv

# pull (update) Docker image
elasticluster_docker_image="${docker_image_name}:${docker_image_tag}"
if [ "$pull" = 'yes' ]; then
    docker pull "$elasticluster_docker_image"
fi

# cannot run as root, as mountpoints within the Docker image would be
# in the wrong place
if [ $(id -u) -eq 0 ]; then
    die $EX_SOFTWARE <<__EOF__
Cannot run as 'root' user: please run ElastiCluster with a normal,
unprivileged user.  (But ensure this user has permission to start
and access Docker containers; typically this means being part of
the 'docker' group.)
__EOF__
fi

# cannot run `sftp` in a Docker container
for arg in "$@"; do
    if [ "$arg" = 'sftp' ]; then
        die $EX_SOFTWARE <<__EOF__
ElastiCluster's 'sftp' command cannot be run in a Docker container.
Please see https://github.com/elasticluster/elasticluster/issues/576
for an explanation and possible workarounds.
__EOF__
    fi
done

# ensure mount points exist
for dir in "$HOME/.ssh" "$HOME/.elasticluster"; do
    test -d "$dir" || mkdir -v "$dir"
done

# prepare environment to export to Docker
# (necessary e.g. to preserve OpenStack auth)
envfile=$(mktemp -t elasticluster.XXXXXXXXXXXX.env)
if [ -z "$envfile" ]; then
    die 1 "Cannot create temporary file."
fi
trap "rm -f '$envfile';" EXIT INT QUIT ABRT TERM
if [ "$env_has_null_termination_opt" = 'y' ]; then
    # The following incantation requires some explanation:
    #
    # * line (1) `env` is to override some environment variables with
    #   values that are appropriate *within* the container;
    #
    # * line (2) is for filtering out multi-line env vars,
    #   which `docker run --env-file` cannot currently handle
    #   (see https://stackoverflow.com/a/19156442/459543 and
    #   issue #675 if the `awk` code looks overcomplicated);
    #
    # * line (3) `egrep -v` is for removing shell customization (e.g.,
    #   bash/zsh themes with multiline prompts)
    #
    env -0 HOME="$HOME"  SHELL=/bin/sh SSH_AUTH_SOCK=/home/.ssh-agent.sock \
        | awk 'BEGIN { RS="\0"; FS="="; OFS="="; }; { n=index($0, "="); $2=substr($0, n+1); NF=2; if ($2 !~ /\n/) print; };' \
        | egrep -v '^(BASH_ENV|ENV|PROMPT_COMMAND|PS[1234])=' \
                > "$envfile"
else
    env HOME="$HOME"  SHELL=/bin/sh SSH_AUTH_SOCK=/home/.ssh-agent.sock \
        | egrep -v '^(BASH_ENV|ENV|PROMPT_COMMAND|PS[1234])=' \
                > "$envfile"
fi

# go!
exec docker run --rm --interactive --tty --env-file "$envfile" $volumes $elasticluster_docker_image "$@"
