#!/bin/sh

set -eu

usage() {
    cat <<EOF
Usage: $BINNAME create|destroy|reset|clobber <name> [<name> ...]
       $BINNAME list|clean
       $BINNAME connect <name>
       $BINNAME help|-h|--help
EOF
    if [ "$#" -eq 2 ]; then
        cat <<EOF

This script manages docker containers for the PVT course. Containers are
exposed via reverse proxy in apache and are accessible over the internet
at https://<name>.pvt.dsv.su.se.

Two containers are managed per <name> (a container group). One is a tomcat to
deploy applications to, the other is a jenkins instance to manage CI. Both are 
always handled together. The tomcat container has no persistent data whatsoever,
while the jenkins container has an associated docker volume for persistence.

The script automatically handles proxy and SSL certificate management as
necessary in addition to the container management functions.


Commands:
  create <name>    Creates a new container group with the given name.
                   If the name exists in the registry, the associated passwords 
                   are used. If it doesn't exist in the registry, new passwords
                   are generated and saved to the registry.
                   If the docker volume to be used with jenkins exists it will 
                   be re-used as is.

  destroy <name>   Destroys the container group with the given name.
                   It will remain in the registry and the jenkins volume saved.

  reset <name>     Resets the container group with the given name.
                   This is done by first destroying and then re-creating them.
                   Jenkins data is unaffected.

  clobber <name>   Fully destroys the container group with the given name,
                   including jenkins volume data.
                   Otherwise acts the same as destroy.

  list             Lists known container groups along with their passwords.

  clean            Purges containers that aren't currently running.
                   This also deletes them from the registry, unlike the
                   'destroy' action. Jenkins volumes not in use are also removed.

  connect <name>   Starts a bash process in the container with the given name
                   and connects the terminal to it.

  help             Displays this help text.

The create, destroy and reset commands can be used with multiple names separated 
by spaces, in which case the relevant action is preformed for all listed names.


Files and directories:
  $BASEDIR/containers.list
                   The registry file that keeps track of containers and manager
                   passwords. The format is "<name>\t<manager>\t<jenkins>".

  $BASEDIR/proxy.conf
                   The template file used to set up the apache vhost for
                   each container.

  $BASEDIR/base/
                   The base dockerfile and supporting files are located here.

  $BASEDIR/target/
                   The dockerfile to create the final image for a given
                   container and supporting files are located here.

EOF
    fi
    exit "$1"
}

registry_read() {
    local name="$1"
    local field="$2"

    case "$field" in
        manager )
            field=2
            ;;
        jenkins )
            field=3
            ;;
        * )
            echo "Error: reading unknown field '$field'" >&2
            exit 2
            ;;
    esac

    local pass=''
    local existing="$(grep "^$name\s" "$REGISTRY")"
    if [ -n "$existing" ]; then
        pass="$(echo "$existing" | cut -f"$field")"
    fi

    echo "$pass"
}

registry_write() {
    local name="$1"
    local field="$2"
    local value="$3"

    local manager_pass=''
    local jenkins_pass=''
    local existing="$(grep "^$name\s" "$REGISTRY")"
    if [ -n "$existing" ]; then
        manager_pass="$(echo "$existing" | cut -f2)"
        jenkins_pass="$(echo "$existing" | cut -f3)"
    fi

    case "$field" in
        manager )
            manager_pass="$value"
            ;;
        jenkins )
            jenkins_pass="$value"
            ;;
        * )
            echo "Error: writing unknown field '$field'" >&2
            exit 2
            ;;
    esac

    grep -v "^$name\s" "$REGISTRY" > "$REGISTRY.new" || true
    printf "$name\t$manager_pass\t$jenkins_pass\n" >> "$REGISTRY.new"
    mv "$REGISTRY.new" "$REGISTRY"
}

docker_clone() {
    local volume="$1"
    local jenkins_pass="$2"

    docker volume create --name "$volume" >/dev/null
    docker run --rm -it \
           -v "jenkins-base":/from -v "$volume":/to \
           alpine ash -c "cd /from; cp -a . /to" >/dev/null

    # Set the passed password in the clone
    local adminconf=$(find "$VOLUMES/$volume/_data/users" -name "config.xml")
    local hash=$(python3 /opt/pvt/hash.py "$jenkins_pass")
    xmlstarlet -q ed -L \
               -u /user/properties/hudson.security.HudsonPrivateSecurityRealm_-Details/passwordHash \
               -v "$hash" \
               "$adminconf"

    # Set the correct URL for jenkins
    xmlstarlet -q ed -L \
               -u /jenkins.model.JenkinsLocationConfiguration/jenkinsUrl \
               -v "https://$FQDN/jenkins" \
               "$VOLUMES/$volume/_data/jenkins.model.JenkinsLocationConfiguration.xml"
}

create_tomcat() {
    local name="$1"
    local tag="pvt:$name"

    # Get password from registry or generate if not found
    local manager_pass="$(registry_read "$name" manager)"
    if [ -z "$manager_pass" ]; then
        manager_pass=$(pwgen 20 1)
        registry_write "$name" manager "$manager_pass"
    fi

    # Build images and start container
    docker build --tag pvt:base base >/dev/null
    docker build --tag "$tag" --build-arg pass="$manager_pass" target
    docker run -tid --restart unless-stopped \
               --memory=2G --memory-swap=2G \
               --name "$name" "$tag" >/dev/null
}

destroy_tomcat() {
    local name="$1"

    docker stop "$name" >/dev/null
    docker rm "$name" >/dev/null
}

create_jenkins() {
    local name="$1"
    local jenkins_name="$name-jenkins"

    # Get password from registry or generate if not found
    local jenkins_pass="$(registry_read "$name" jenkins)"
    if [ -z "$jenkins_pass" ]; then
        jenkins_pass=$(pwgen 20 1)
        registry_write "$name" jenkins "$jenkins_pass"
    fi

    # Init jenkins homedir if it doesn't exist
    if ! docker volume ls | awk '{print $2}' | grep -q "^$jenkins_name$"; then
        docker_clone "$jenkins_name" "$jenkins_pass"
    fi

    # Start group jenkins
    docker run -d --restart unless-stopped \
           -v "$jenkins_name":/var/jenkins_home \
           --env JENKINS_OPTS="--prefix=/jenkins" \
           --memory=1G --memory-swap=1G \
           --name "$jenkins_name" \
           jenkins/jenkins:lts >/dev/null
}

destroy_jenkins() {
    local jenkins="$1-jenkins"
    local purge="$2"

    # Kill jenkins
    docker stop "$jenkins" >/dev/null
    docker rm "$jenkins" >/dev/null

    if [ -n "$purge" ]; then
        docker volume rm "$jenkins" >/dev/null
    fi
}

create_proxy() {
    local name="$1"

    # Determine container IP
    local ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$name")"
    local jip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$name-jenkins")"

    # Set up proxy
    sed -r \
        -e "s/%FQDN%/$FQDN/g" \
        -e "s/%BACKEND%/$ip/g" \
        -e "s/%JENKINS%/$jip/g" \
        proxy.conf > "$SITES"/"$CONFNAME"
    ARELOAD=true
}

destroy_proxy() {
    # Disable proxy
    rm "$SITES"/"$CONFNAME" || true
    ARELOAD=true
}

create() {
    local name="$1"
    shift

    local no_proxy=""
    while [ "$#" -gt 0 ]; do
        case "$1" in
            no-proxy )
                no_proxy="$1"
                shift
                ;;
            * )
                echo "Invalid argument: $1"
                exit 3
                ;;
        esac
    done

    create_tomcat "$name"
    create_jenkins "$name"

    # Update vhosts
    if ! grep -q "^${FQDN}$" "$VHOSTS"; then
        echo "$FQDN" >> "$VHOSTS"
        NEEDCERT=true
    fi

    if [ -z "$no_proxy" ]; then
        create_proxy "$name"
    fi
}

destroy() {
    local name="$1"
    shift
    local purge=""
    local keep_proxy=""
    while [ "$#" -gt 0 ]; do
        case "$1" in
            purge )
                purge="$1"
                shift
                ;;
            keep-proxy )
                keep_proxy="$1"
                shift
                ;;
            * )
                echo "Invalid argument: $1"
                exit 3
                ;;
        esac
    done

    if [ -z "$keep_proxy" ]; then
        destroy_proxy
    fi

    destroy_tomcat "$name"
    destroy_jenkins "$name" "$purge"
}

reset() {
    local name="$1"

    destroy "$name"
    create "$name"
}

resetsoft() {
    local name="$1"

    destroy "$name" keep-proxy
    create "$name" no-proxy
}

clobber() {
    local name="$1"

    destroy "$name" purge
}

connect() {
    local name="$1"
    if [ "$#" -gt 1 ]; then
        shift
        exec docker exec "$name" "$@"
    else
        exec docker exec -ti "$name" bash
    fi
}

list() {
    running="$(docker container ls --format '{{.Names}}')"
    (
        printf "#Name\tManager pass\tJenkins pass\tStatus\n"
        while read name manager jenkins; do
            printf "${name}\t${manager}\t${jenkins}"
            if echo "$running" | grep -q "^${name}$"; then
                printf "\trunning"
            fi
            printf '\n'
        done < "$REGISTRY"
    ) | column -s"$(printf '\t')" -t
}

clean() {
    # List running
    running="$(docker container ls --format '{{.Names}}')"

    # Collect unused names
    prune=""
    while read name junk; do
        if echo "$running" | grep -q "^${name}$"; then
            continue
        fi
        prune="$prune $name"
    done < "$REGISTRY"

    for name in $prune; do
        # Delete from registry
        sed -ri "/^${name}\s/d" "$REGISTRY"

        # Delete from vhosts
        sed -ri "/^${name}.pvt.dsv.su.se$/d" "$VHOSTS"
    done
}

BINNAME="$(basename $0)"
BASEDIR="$(dirname "$(readlink -f "$0")")"
cd "$BASEDIR"

if [ "$#" -lt 1 ]; then
    usage 0
fi

SITES="/etc/apache2/sites-enabled"
VHOSTS="/etc/vhosts"
VOLUMES="/var/lib/docker/volumes"

REGISTRY="containers.list"
NEEDCERT=false
ARELOAD=false

ACTION="$1"
shift

case "$ACTION" in
    create|destroy|reset|resetsoft|clobber )
        while [ "$#" -gt 0 ]; do
            NAME="$1"
            shift
            FQDN="${NAME}.pvt.dsv.su.se"
            CONFNAME="proxy-$NAME.conf"
            $ACTION "$NAME"
        done
        ;;
    connect )
        connect "$@"
        ;;
    list|clean )
        $ACTION
        ;;
    help|-h|--help)
        usage 0 long
        ;;
    * )
        usage 1
        ;;
esac

# Update SSL cert if needed
if [ "$NEEDCERT" = true ]; then
    /adm/scripts/lecert get >/dev/null
    ARELOAD=true
fi

if [ "$ARELOAD" = true ]; then
    service apache2 reload
fi
