#!/usr/bin/env bash
#
# Copyright (C) 2014, 2015 Red Hat <contact@redhat.com>
#
# Author: Loic Dachary <loic@dachary.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Library Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library Public License for more details.
#
function get_image_name() {
    local os_type=$1
    local os_version=$2

    echo ceph-$os_type-$os_version-$USER
}

function setup_container() {
    local os_type=$1
    local os_version=$2
    local dockercmd=$3
    local opts="$4"

    # rm not valid here
    opts=${opts//' --rm'};

    local image=$(get_image_name $os_type $os_version)
    local build=true
    if $dockercmd images $image | grep --quiet "^$image " ; then
        eval touch --date=$($dockercmd inspect $image | jq '.[0].Created') $image
        found=$(find -L test/$os_type-$os_version/* -newer $image)
        rm $image
        if test -n "$found" ; then
            $dockercmd rmi $image
        else
            build=false
        fi
    fi
    if $build ; then
        # 
        # In the dockerfile,
        # replace environment variables %%FOO%% with their content
        #
        rm -fr dockerfile
        cp --dereference --recursive test/$os_type-$os_version dockerfile
        os_version=$os_version user_id=$(id -u) \
            perl -p -e 's/%%(\w+)%%/$ENV{$1}/g' \
            dockerfile/Dockerfile.in > dockerfile/Dockerfile
        $dockercmd $opts build --tag=$image dockerfile
        rm -fr dockerfile
    fi
}

function get_upstream() {
    git rev-parse --show-toplevel
}

function get_downstream() {
    local os_type=$1
    local os_version=$2

    local image=$(get_image_name $os_type $os_version)
    local upstream=$(get_upstream)
    local dir=$(dirname $upstream)
    echo "$dir/$image"
}

function setup_downstream() {
    local os_type=$1
    local os_version=$2
    local ref=$3

    local image=$(get_image_name $os_type $os_version)
    local upstream=$(get_upstream)
    local dir=$(dirname $upstream)
    local downstream=$(get_downstream $os_type $os_version)
    
    (
        cd $dir
        if ! test -d $downstream ; then
            # Inspired by https://github.com/git/git/blob/master/contrib/workdir/git-new-workdir
            mkdir -p $downstream/.git || return 1
            for x in config refs logs/refs objects info hooks packed-refs remotes rr-cache
            do
	        case $x in
	            */*)
		        mkdir -p "$downstream/.git/$x"
		        ;;
	        esac
	        ln -s "$upstream/.git/$x" "$downstream/.git/$x"
            done
            cp "$upstream/.git/HEAD" "$downstream/.git/HEAD"
        fi
        cd $downstream
        git reset --hard $ref || return 1
        git submodule sync --recursive || return 1
        git submodule update --force --init --recursive || return 1
    )
}

function run_in_docker() {
    local os_type=$1
    shift
    local os_version=$1
    shift
    local ref=$1
    shift
    local dockercmd=$1
    shift
    local opts="$1"
    shift
    local script=$1

    setup_downstream $os_type $os_version $ref || return 1
    setup_container $os_type $os_version $dockercmd "$opts" || return 1
    local downstream=$(get_downstream $os_type $os_version)
    local image=$(get_image_name $os_type $os_version)
    local upstream=$(get_upstream)
    local ccache
    mkdir -p $HOME/.ccache
    ccache="--volume $HOME/.ccache:$HOME/.ccache"
    user="--user $USER"
    local cmd="$dockercmd run $opts --name $image --privileged $ccache"
    cmd+=" --volume $downstream:$downstream"
    cmd+=" --volume $upstream:$upstream"
    if test "$dockercmd" = "podman" ; then
        cmd+=" --userns=keep-id"
    fi
    local status=0
    if test "$script" = "SHELL" ; then
	echo Running: $cmd --tty --interactive --workdir $downstream $user $image bash
        $cmd --tty --interactive --workdir $downstream $user $image bash
    else
	echo Running: $cmd --workdir $downstream $user $image "$@"
        if ! $cmd --workdir $downstream $user $image "$@" ; then
            status=1
        fi
    fi
    return $status
}

function remove_all() {
    local os_type=$1
    local os_version=$2
    local dockercmd=$3
    local image=$(get_image_name $os_type $os_version)

    $dockercmd rm $image
    $dockercmd rmi $image
}

function usage() {
    cat <<EOF
Run commands within Ceph sources, in a container. Use podman if available,
docker if not.
$0 [options] command args ...

   [-h|--help]            display usage
   [--verbose]            trace all shell lines

   [--os-type type]       docker image repository (centos, ubuntu, etc.) 
                          (defaults to ubuntu)
   [--os-version version] docker image tag (7 for centos, 16.04 for ubuntu, etc.)
                          (defaults to 16.04)
   [--ref gitref]         git reset --hard gitref before running the command
                          (defaults to git rev-parse HEAD)
   [--all types+versions] list of docker image repositories and tags

   [--shell]              run an interactive shell in the container
   [--remove-all]         remove the container and the image for the specified types+versions
   [--no-rm]               don't remove the container when finished

   [--opts options]       run the container with 'options'

docker-test.sh must be run from a Ceph clone and it will run the
command in a container, using a copy of the clone so that long running
commands such as make check are not disturbed while development
continues. Here is a sample use case including an interactive session
and running a unit test:

   $ grep PRETTY_NAME /etc/os-release
   PRETTY_NAME="Ubuntu 16.04.7 LTS"
   $ test/docker-test.sh --os-type centos --os-version 7 --shell
   HEAD is now at 1caee81 autotools: add --enable-docker
   bash-4.2$ pwd
   /srv/ceph/ceph-centos-7
   bash-4.2$ cat /etc/redhat-release 
   CentOS Linux release 7.6.1810 (Core) 
   bash-4.2$ 
   $ time test/docker-test.sh --os-type centos --os-version 7 unittest_str_map
   HEAD is now at 1caee81 autotools: add --enable-docker
   Running main() from gtest_main.cc
   [==========] Running 2 tests from 1 test case.
   [----------] Global test environment set-up.
   [----------] 2 tests from str_map
   [ RUN      ] str_map.json
   [       OK ] str_map.json (1 ms)
   [ RUN      ] str_map.plaintext
   [       OK ] str_map.plaintext (0 ms)
   [----------] 2 tests from str_map (1 ms total)
   
   [----------] Global test environment tear-down
   [==========] 2 tests from 1 test case ran. (1 ms total)
   [  PASSED  ] 2 tests.
   
   real	0m3.759s
   user	0m0.074s
   sys	0m0.051s

The --all argument is a bash associative array literal listing the
operating system version for each operating system type. For instance

   docker-test.sh --all '([ubuntu]="16.04 17.04" [centos]="7")' 

is strictly equivalent to

   docker-test.sh --os-type ubuntu --os-version 16.04
   docker-test.sh --os-type ubuntu --os-version 17.04
   docker-test.sh --os-type centos --os-version 7

The --os-type and --os-version must be exactly as displayed by docker images:

   $ docker images
   REPOSITORY            TAG                 IMAGE ID          ...
   centos                7                   87e5b6b3ccc1      ...
   ubuntu                16.04               6b4e8a7373fe      ...

The --os-type value can be any string in the REPOSITORY column, the --os-version
can be any string in the TAG column.

The --shell and --remove actions are mutually exclusive.

Run make check in centos 7
docker-test.sh --os-type centos --os-version 7 -- make check

Run make check on a giant
docker-test.sh --ref giant -- make check

Run an interactive shell and set resolv.conf to use 172.17.42.1
docker-test.sh --opts --dns=172.17.42.1 --shell

Run make check on centos 7, ubuntu 16.04 and ubuntu 17.04
docker-test.sh --all '([ubuntu]="16.04 17.04" [centos]="7")' -- make check
EOF
}

function main_docker() {
    local dockercmd="docker"
    if type podman > /dev/null; then
        dockercmd="podman"
    fi

    if ! $dockercmd ps > /dev/null 2>&1 ; then
        echo "docker not available: $0"
        return 0
    fi

    local temp
    temp=$(getopt -o scht:v:o:a:r: --long remove-all,verbose,shell,no-rm,help,os-type:,os-version:,opts:,all:,ref: -n $0 -- "$@") || return 1

    eval set -- "$temp"

    local os_type=ubuntu
    local os_version=16.04
    local all
    local remove=false
    local shell=false
    local opts
    local ref=$(git rev-parse HEAD)
    local no-rm=false

    while true ; do
	case "$1" in
            --remove-all)
                remove=true
                shift
                ;;
            --verbose)
                set -xe
                PS4='${BASH_SOURCE[0]}:$LINENO: ${FUNCNAME[0]}:  '
                shift
                ;;
            -s|--shell)
                shell=true
                shift
                ;;
	    -h|--help)
                usage
                return 0
                ;;
	    -t|--os-type) 
                os_type=$2
                shift 2
                ;;
	    -v|--os-version) 
                os_version=$2
                shift 2
                ;;
	    -o|--opts) 
                opts="$2"
                shift 2
                ;;
	    -a|--all) 
                all="$2"
                shift 2
                ;;
	    -r|--ref) 
                ref="$2"
                shift 2
                ;;
	    --no-rm)
		no-rm=true
		shift
		;;
	    --)
                shift
                break
                ;;
	    *)
                echo "unexpected argument $1"
                return 1
                ;;
	esac
    done

    if test -z "$all" ; then
        all="([$os_type]=\"$os_version\")"
    fi

    declare -A os_type2versions
    eval os_type2versions="$all"

    if ! $no-rm ; then
        opts+=" --rm"
    fi

    for os_type in ${!os_type2versions[@]} ; do
        for os_version in ${os_type2versions[$os_type]} ; do
            if $remove ; then
                remove_all $os_type $os_version $dockercmd || return 1
            elif $shell ; then
                run_in_docker $os_type $os_version $ref $dockercmd "$opts" SHELL || return 1
            else
                run_in_docker $os_type $os_version $ref $dockercmd "$opts" "$@" || return 1
            fi
        done
    done
}
