#!/usr/bin/env bash

# LabCA: a private Certificate Authority for internal lab usage
# (c) 2018-2023 Arjan Hakkesteegt
#
# Install with this command from a Linux machine (only tested with Debian and Ubuntu):
# curl -sSL https://raw.githubusercontent.com/hakwerk/labca/master/install | bash

set -eEo pipefail
trap 'err_report $? $LINENO' INT TERM ERR

err_report() {
    msg_fatal "return code $1 on line $2 in $(basename $0)" $1
}

#
# Variables / Constants
#
installMode=${installMode:-normal}
baseDir=${baseDir:-/home/labca}
logDir=${logDir:-"$baseDir/logs"}
runId="`date +%y%m%d-%H%M%S`"
installLog="$logDir/install-${runId}.log"
logTimeFormat="+%Y-%m-%d %T.%3N"
cloneDir=${cloneDir:-"$baseDir/labca"}
adminDir=${adminDir:-"$baseDir/admin"}
boulderDir=${boulderDir:-"$baseDir/boulder"}
boulderLabCADir=${boulderLabCADir:-"${boulderDir}_labca"}
dockerComposeVersion="v2.5.0"

labcaUrl="https://github.com/hakwerk/labca/"
boulderUrl="https://github.com/letsencrypt/boulder/"
boulderTag="release-2023-07-11"

# Feature flags
flag_skip_redis=true

#
# Color configuration
#
COL_NC='\e[0m' # No Color
COL_LIGHT_GREEN='\e[1;32m'
COL_LIGHT_RED='\e[1;31m'
TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]"
CROSS="[${COL_LIGHT_RED}✗${COL_NC}]"
INFO="[.]"
DONE="${COL_LIGHT_GREEN} done!${COL_NC}"
OVER="\\r\\033[K"

# Dummy implementation in case utils.sh is not available (install via curl method)
wait_down() {
    sleep 1
}
wait_up() {
    sleep 5
}
count() {
    echo 0
}

dn=$(dirname $0)
source "$dn/utils.sh" &>/dev/null || true

cmdlineFqdn=""
cmdlineBranch=""
fullCmdline=""
keepLocal=0
alphaTest=0

#
# Helper functions for informing the user and logging to file
#
msg_info() {
    local msg="$1"
    echo -ne "  ${INFO} ${msg}..."
    echo "[`date "${logTimeFormat}"`] [INFO ] ${msg}..." >> $installLog
}

msg_ok() {
    local msg="$1"
    echo -e "${OVER}  ${TICK} ${msg}"
    echo "[`date "${logTimeFormat}"`] [OK   ] ${msg}" >> $installLog
}

msg_err() {
    local msg="$1"
    echo -e "${OVER}  ${CROSS} ${msg}"
    echo "[`date "${logTimeFormat}"`] [ERROR] ${msg}" >> $installLog
}

msg_fatal() {
    local msg="$1"
    echo -e "\\n  ${COL_LIGHT_RED}Error: ${msg}${COL_NC}\\n"
    echo "[`date "${logTimeFormat}"`] [FATAL] ${msg}" >> $installLog
    exit ${2:-1}
}

#
# Log to /tmp first in case the labca user doesn't exist yet
#
start_temporary_log() {
    backupLog=$installLog
    installLog="/tmp/labca-install-$$.log"
    touch "$installLog"
}

end_temporary_log() {
    mv "$installLog" "$backupLog"
    installLog=$backupLog
    chown labca:labca "$installLog"
}

# Must run as root
check_root() {
    if [ "$EUID" -eq 0 ]; then
        msg_ok "Running as root"
    else
        msg_err "Not running as root"

        local msg="Run using sudo"
        if command -v sudo &> /dev/null; then
            msg_ok "$msg"
            exec curl -sSL https://raw.githubusercontent.com/hakwerk/labca/master/install | sudo bash "$@"
            exit $?
        else
            msg_err "$msg"
            echo -e "  ${COL_LIGHT_RED}Script should be run as the root user${COL_NC}"
        fi
    fi
}

# Create dedicated user
labca_user() {
    adduser --gecos "LabCA,,," --disabled-login labca &>>$installLog && msg_ok "Created user 'labca'" || msg_ok "User 'labca' already exists"
    [ -d "$logDir" ] || mkdir "$logDir"
    chown -R labca:labca "$logDir"

    cd ~labca
    local gig=$(sudo -u labca -H git config --global core.excludesfile 2>/dev/null)
    if [ -z "$gig" ]; then
        sudo -u labca -H git config --global core.excludesfile /home/labca/.gitignore_global >/dev/null 2>&1 || msg_info "WARNING: could not set core.excludesfile"
        gig=$(sudo -u labca -H git config --global core.excludesfile 2>/dev/null)
    fi
    gig=${gig/\~/\/home\/labca}
    gig=${gig:-/home/labca/.gitignore_global}

    [ -e "$gig" ] || sudo -u labca -H touch $gig
    sudo -u labca -H grep config_labca "$gig" >/dev/null 2>&1 || sudo -u labca -H echo "config_labca/" >> "$gig"
}

#
# Get the latest code from the git repository
#
clone_repo() {
    local dir="$1"
    local url="$2"
    local branch="$3"

    local msg="Clone $url to $dir"
    msg_info "$msg"
    sudo -u labca -H git clone -q "$url" "$dir" &>>$installLog && msg_ok "$msg" || msg_fatal "Could not clone git repository"

    if [ "$branch" != "" ]; then
        cd "$dir"
        sudo -u labca -H git checkout $branch &>>$installLog
        cd - >/dev/null
    fi
}

pull_repo() {
    local dir="$1"
    local branch="$2"

    cd "$dir" &>>$installLog || msg_fatal "Could not switch to directory '$dir'"

    local msg="Update git repository in $dir"
    msg_info "$msg"
    sudo -u labca -H git stash --all --quiet &>>$installLog || true
    sudo -u labca -H git clean --quiet --force -d &>>$installLog || true
    sudo -u labca -H git pull --quiet &>>$installLog && msg_ok "$msg" || (
        if [ "$dir" == "$GOPATH/src/github.com/letsencrypt/boulder" ]; then
            sudo -u labca -H git reset --hard $boulderTag &>>$installLog && msg_ok "$msg" || msg_fatal "Could not reset local repository"
            sudo -u labca -H git pull --quiet &>>$installLog && msg_ok "$msg" || msg_fatal "Could not update local repository (after reset)"
        else
            msg_fatal "Could not update local repository"
        fi
    )

    if [ "$branch" != "" ]; then
        cd "$dir"
        sudo -u labca -H git checkout $branch &>>$installLog
        cd - >/dev/null
    fi
}

clone_or_pull() {
    local dir="$1"
    local url="$2"
    local branch="$3"

    local parentdir=$(dirname "$dir")
    local dirbase=$(basename "$dir")

    local found=0
    for sd in $(git config --global --get-all safe.directory); do
        if [ "$sd" == "$dir" ]; then
            found=1
        fi
    done
    if [ $found -eq 0 ]; then
        git config --global --add safe.directory $dir
    fi

    if [ -d "$dir" ]; then
        local rc=0
        cd "$dir"
        sudo -u labca -H git status --short &> /dev/null || rc=$?
        if [ $rc -gt 0 ]; then
            cd "$parentdir"
            mv "$dirbase" "${dirbase}_${runId}" && msg_ok "Backup existing non-git directory '$dir'"
            clone_repo "$dir" "$url" "$branch"
        else
            pull_repo "$dir" "$branch"
        fi
    else
        clone_repo "$dir" "$url" "$branch"
    fi
}

# Checkout the latest release tag
checkout_release() {
    local branch="$1"
    if [ "$branch" == "" ] || [ "$branch" == "master" ] || [ "$branch" == "main" ]; then
        cd "$cloneDir"
        if [ "$curChecksum" == "" ]; then
            curChecksum=$(md5sum $cloneDir/install 2>/dev/null | cut -d' ' -f1)
        fi
        TAG=$(sudo -u labca -H git describe --tags $(sudo -u labca -H git rev-list --tags --max-count=1))
        sudo -u labca -H git reset --hard $TAG &>>$installLog
    fi
}

# Restart the script if it was updated itself
restart_if_updated() {
    local gitRev=$(cd $cloneDir && sudo -u labca -H git describe --always --tags)
    echo "=== version $gitRev ($curChecksum) ===" >>$installLog

    if [ "$curChecksum" != "" ]; then
        local newChecksum=$(md5sum $cloneDir/install 2>/dev/null | cut -d' ' -f1)
        if [ "$curChecksum" != "$newChecksum" ]; then
            msg_info "Restarting updated version of install script"
            echo
            exec $cloneDir/install $fullCmdline
            exit $?
        fi
    fi
}

# Utility method to prompt the user for a config variable and export it
prompt_and_export() {
    local varName="$1"
    local varDefault="$2"
    local promptMsg="$3"
    local answer

    read -p "$promptMsg [$varDefault] " answer </dev/tty
    if [ "$answer" ]; then
        export $varName="$answer"
    else
        export $varName="$varDefault"
    fi
}

# Parse the command line options, if any
parse_cmdline() {
    fullCmdline="$@"
    local parsed=$(getopt --options=n:,b:,k,t --longoptions=name:,fqdn:,branch:,keep,test --name "$0" -- "$@" 2>>$installLog) || msg_fatal "Could not process commandline parameters"
    eval set -- "$parsed"
    while true; do
        case "$1" in
            -n|--name|--fqdn)
                cmdlineFqdn="$2"
                shift 2
                msg_ok "option: using FQDN name '$cmdlineFqdn'"
                ;;
            -b|--branch)
                cmdlineBranch="$2"
                shift 2
                msg_ok "option: using branch '$cmdlineBranch'"
                ;;
            -k|--keep)
                keepLocal=1
                shift 1
                msg_ok "option: keeping local version as is"
                ;;
            -t|--test)
                alphaTest=1
                shift 1
                msg_ok "option: INCLUDING ALPHA TEST STEPS"
                ;;
            --)
                shift
                break
                ;;
            *)
                msg_fatal "Should not have reached this"
                ;;
        esac
    done
}

# Utility method to check if value looks like a host + domain
has_domain() {
    local dom="$1"

    if [[ "$dom" =~ ^\..*$ ]]; then
        false
    elif [[ "$dom" =~ ^.*\.$ ]]; then
        false
    elif [[ "$dom" =~ ^.*\..*$ ]]; then
        true
    else
        false
    fi
}

# Determine the remote address of this machine from (in order): commandline parameter,
# existing configuration or full hostname.
get_fqdn() {
    local cfgFile="$adminDir/data/config.json"
    local cfgFqdn=$(grep fqdn $cfgFile 2>/dev/null | grep -v LABCA_FQDN | cut -d ":" -f 2- | tr -d " \"," || echo "")
    LABCA_FQDN=${cfgFqdn:-$(hostname -f)}

    while [ "$cfgFqdn" == "" ]; do
        if [ "$cmdlineFqdn" != "" ]; then
            export LABCA_FQDN="$cmdlineFqdn"
        else
            prompt_and_export LABCA_FQDN "$LABCA_FQDN" "FQDN (Fully Qualified Domain Name) for this PKI host (users will use this in their browsers and clients)?"
        fi

        if has_domain $LABCA_FQDN; then
            cfgFqdn="ok"
        else
            msg_err "FQDN must include a hostname AND a domain!"
            cmdlineFqdn=""
        fi
    done

    if ! has_domain $LABCA_FQDN; then
        msg_fatal "FQDN must include a hostname AND a domain!"
    fi

    msg_ok "Determine web address"
}

# Utility method to replace all instances of given variables in a file
replace_all() {
    local filename="$1"
    local var

    for var in ${@:2}; do
        sed -i -e "s|$var|${!var}|g" $filename
    done
}

# Copy and configure the admin tree from the local repository
copy_admin() {
    local rc=0

    local msg="Setup admin application"
    msg_info "$msg"

    [ -d "$adminDir" ] || mkdir "$adminDir"
    cd "$adminDir"
    git config --global --add safe.directory "$adminDir"
    git status --short &> /dev/null || rc=$?
    if [ $rc -gt 0 ]; then
        git init &>>$installLog
    fi
    git add --all &>/dev/null || true
    git commit --all --quiet -m "LabCA before update $runId" &>>$installLog && { msg_ok "Commit existing modifications of $adminDir"; msg_info "$msg"; } || true

    cp -rp $cloneDir/gui/* "./" &>>$installLog || msg_fatal "Cannot copy the admin files to $adminDir"

    msg_ok "$msg"
    msg="Configure the admin application"
    msg_info "$msg"

    [ -e "$adminDir/data/config.json" ] || echo -e "{\n  \"config\": {\n    \"complete\": false\n  },\n  \"labca\": {\n    \"fqdn\": \"$LABCA_FQDN\"\n  },\n  \"version\": \"\"\n}" > "$adminDir/data/config.json"
    replace_all $adminDir/data/openssl.cnf LABCA_FQDN
    replace_all $adminDir/data/issuer/openssl.cnf LABCA_FQDN

    cd "$cloneDir"
    sudo -u labca -H git config --global --add safe.directory "$cloneDir"
    version=$(sudo -u labca -H git describe --always --tags HEAD 2>/dev/null)
    cd "$adminDir"
    grep \"version\" data/config.json &>/dev/null || sed -i -e 's/^}$/,\n  "version": ""\n}/' data/config.json
    sed -i -e "s/\"version\": \".*\"/\"version\": \"$version\"/" data/config.json
    [ ! -e bin/labca-gui ] || mv bin/labca-gui bin/labca-gui_prev

    chown -R labca:labca $baseDir
    chown root:root "$cloneDir/cron_d"

    [ -e /etc/cron.d/labca ] && rm /etc/cron.d/labca || true
    [ -e /etc/logrotate.d/labca ] && rm /etc/logrotate.d/labca || true

    git add --all &>/dev/null || true
    git commit --all --quiet -m "LabCA after update $runId" &>>$installLog || true

    msg_ok "$msg"
}

# Update any outdated packages
update_upgrade() {
    msg_info "Making sure all software is up-to-date"
    apt update &>>$installLog
    apt upgrade -y &>>$installLog
    apt autoremove -y &>>$installLog
    msg_ok "Software is up-to-date"
}

#
# Install extra packages that we rely upon
#
install_pkg() {
    local package="$1"
    msg_info "Install package '$package'"
    err=""
    apt install -y "$package" &>>$installLog || err="err"
    if [ "$err" == "err" ]; then
        if [ "$package" == "python" ]; then
            package="python3"
            apt install -y "$package" &>>$installLog || msg_fatal "Could not install package '$package'"
        else
            msg_fatal "Could not install package '$package'"
        fi
    fi
    msg_ok "Package '$package' is installed"
}

install_extra() {
    local packages=(apt-transport-https ca-certificates curl gnupg net-tools tzdata ucspi-tcp zip python)
    for package in "${packages[@]}"; do
        install_pkg "$package"
    done

    distrib=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
    install -m 0755 -d /etc/apt/keyrings
    [ ! -e /etc/apt/keyrings/docker.gpg ] || mv /etc/apt/keyrings/docker.gpg /etc/apt/keyrings/docker.gpg_PREV
    curl -fsSL https://download.docker.com/linux/${distrib}/gpg 2>>$installLog | gpg --dearmor -o /etc/apt/keyrings/docker.gpg &>>$installLog || msg_fatal "Could not download docker repository key"
    chmod a+r /etc/apt/keyrings/docker.gpg
    echo \
      "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${distrib} \
      "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
      tee /etc/apt/sources.list.d/docker.list > /dev/null &>>$installLog
    apt-get update &>>$installLog
    local packages=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
    for package in "${packages[@]}"; do
        install_pkg "$package"
    done

    # Make sure the labca user has docker permissions
    usermod -aG docker labca
}

# Configure the static web pages (for end users)
static_web() {
    local rc=0

    local msg="Static web pages"
    msg_info "$msg"
    if [ -d /etc/nginx ] && [ ! -d /etc/nginx.backup ]; then
        # Migrate cert from host nginx to dockerized nginx
        [ -d /home/labca/nginx_data/ssl ] || mkdir -p /home/labca/nginx_data/ssl
        [ -d /etc/nginx/ssl/ ] && mv /etc/nginx/ssl/* /home/labca/nginx_data/ssl/ || true
        mv /etc/nginx /etc/nginx.backup
    fi

    [ -d /home/labca/nginx_data/conf.d ] || mkdir -p /home/labca/nginx_data/conf.d
    [ -d /home/labca/nginx_data/ssl ] || mkdir -p /home/labca/nginx_data/ssl
    cp $cloneDir/nginx.conf /home/labca/nginx_data/conf.d/labca.conf
    cp $cloneDir/proxy.inc /home/labca/nginx_data/conf.d/proxy.inc
    if [ -f "$boulderLabCADir/setup_complete" ]; then
        perl -i -p0e 's/\n    # BEGIN temporary redirect\n    location = \/ \{\n        return 302 \/admin\/;\n    }\n    # END temporary redirect\n//igs' /home/labca/nginx_data/conf.d/labca.conf
    fi

    [ -d /home/labca/nginx_data/static ] || mkdir /home/labca/nginx_data/static
    cd /home/labca/nginx_data/static
    git config --global --add safe.directory /home/labca/nginx_data/static
    git status --short &> /dev/null || rc=$?
    if [ $rc -gt 0 ]; then
        git init &>>$installLog
    fi
    git add --all &>/dev/null || true
    git commit --all --quiet -m "LabCA before update $runId" &>>$installLog && { msg_ok "Commit existing modifications of $adminDir"; msg_info "$msg"; } || true

    mkdir -p .well-known/acme-challenge
    find .well-known/acme-challenge/ -type f -mtime +10 -exec rm {} \;  # Clean up files older than 10 days
    mkdir -p crl
    [ -e cert ] || ln -s certs cert
    cp -rp $cloneDir/gui/static/* .
    [ -e $adminDir/data/root-ca.crl ] && cp $adminDir/data/root-ca.crl crl/ || true
    [ -e $adminDir/data/root-ca.pem ] && cp $adminDir/data/root-ca.pem certs/ || true
    [ -e $adminDir/data/root-ca.der ] && cp $adminDir/data/root-ca.der certs/ || true
    [ -e $adminDir/data/issuer/ca-int.pem ] && cp $adminDir/data/issuer/ca-int.pem certs/ || true
    [ -e $adminDir/data/issuer/ca-int.der ] && cp $adminDir/data/issuer/ca-int.der certs/ || true

    local have_config=$(grep restarted $adminDir/data/config.json | grep true)
    if [ "$have_config" != "" ]; then
        $adminDir/apply-nginx
    fi

    git add --all &>/dev/null || true
    git commit --all --quiet -m "LabCA after update $runId" &>>$installLog || true

    msg_ok "$msg"
}

# Create a temporary self-signed certificate if there is no certificate yet
selfsigned_cert() {
    if [ -e /home/labca/nginx_data/ssl/labca_cert.pem ]; then
        msg_ok "Certificate is present"
    else
        local msg="Create self-signed certificate"
        msg_info "$msg"
        mkdir -p /home/labca/nginx_data/ssl
        cd /home/labca/nginx_data/ssl
        openssl req -x509 -nodes -sha256 -newkey rsa:2048 -keyout labca_key.pem -out labca_cert.pem -days 7 \
            -subj "/O=LabCA/CN=$LABCA_FQDN" -reqexts SAN -extensions SAN \
            -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nbasicConstraints=CA:FALSE\nnsCertType=server\nsubjectAltName=DNS:$LABCA_FQDN")) &>>$installLog
        msg_ok "$msg"
    fi
}

# Clone or update the boulder code (Let's Encrypt (tm) implementation of ACME protocol)
get_boulder() {
    export GOPATH="/home/labca/gopath"
    sudo -u labca -H mkdir -p "$GOPATH/src/github.com/letsencrypt"
    [ -h "$boulderDir" ] || sudo -u labca -H ln -s "$GOPATH/src/github.com/letsencrypt/boulder" "$boulderDir"

    if [ -e "$boulderDir" ]; then
        cd "$boulderDir"
        sudo -u labca -H git checkout -- docker-compose.yml errors/errors.go policy/pa.go
        sudo -u labca -H git reset --hard &>>$installLog
    fi

    clone_or_pull "$GOPATH/src/github.com/letsencrypt/boulder" "$boulderUrl"

    cd "$boulderDir"
    sudo -u labca -H git reset --hard $boulderTag &>>$installLog
    if [ -e "sa/_db-next/migrations/20190221140139_AddAuthz2.sql" ]; then
        sudo -u labca -H cp sa/_db-next/migrations/20190221140139_AddAuthz2.sql sa/_db/migrations/
    fi
    if [ -e "sa/_db-next/migrations/20190524120239_AddAuthz2ExpiresIndex.sql" ]; then
        sudo -u labca -H cp sa/_db-next/migrations/20190524120239_AddAuthz2ExpiresIndex.sql sa/_db/migrations/
    fi
    msg_ok "Boulder checkout '$boulderTag'"
}

# Configure boulder based on their test subdirectory
config_boulder() {
    local msg="Setup boulder configuration folder"
    msg_info "$msg"

    [ -d "$boulderLabCADir" ] || mkdir -p "$boulderLabCADir"
    cd "$boulderLabCADir"
    local rc=0
    git config --global --add safe.directory "$boulderLabCADir"
    git status --short &> /dev/null || rc=$?
    if [ $rc -gt 0 ]; then
        git init &>>$installLog
    fi
    [ -d ".backup" ] || mkdir -p ".backup"

    git add --all &>/dev/null || true
    [ "$installMode" == "normal" ] && git commit --all --quiet -m "LabCA before update $runId" &>>$installLog && { msg_ok "Commit existing modifications of $boulderLabCADir"; msg_info "$msg"; } || true

    [ ! -e "$boulderLabCADir/secrets/smtp_password" ] || mv "$boulderLabCADir/secrets/smtp_password" "$boulderLabCADir/secrets/smtp_password_PRESERVE"
    cp -r "$boulderDir/test" -T "$boulderLabCADir" &>>$installLog
    [ ! -e "$boulderLabCADir/secrets/smtp_password_PRESERVE" ] || mv "$boulderLabCADir/secrets/smtp_password_PRESERVE" "$boulderLabCADir/secrets/smtp_password"
    [ "$installMode" == "normal" ] && chown -R labca:labca "$boulderLabCADir" || /bin/true

    rm -rf authz-filler challtestsrv gsb-test-srv

    msg_ok "$msg"
    msg="Configure the boulder application"
    msg_info "$msg"

    cd "$boulderDir"
    if [ "$installMode" == "normal" ]; then
        $cloneDir/patch.sh "sudo -u labca -H" &>>$installLog
        sed -i -e "s/LABCA_FQDN: .*/LABCA_FQDN: $LABCA_FQDN/" docker-compose.yml
    else
        $cloneDir/patch.sh &>>$installLog
    fi

    git config --global --add safe.directory /home/labca/boulder_labca

    cp docker-compose.yml "$boulderLabCADir/.backup/"
    cp cmd/shell.go "$boulderLabCADir/.backup/"
    cp core/interfaces.go "$boulderLabCADir/.backup/"
    cp policy/pa.go "$boulderLabCADir/.backup/"
    cp ra/ra.go "$boulderLabCADir/.backup/"
    cp reloader/reloader.go "$boulderLabCADir/.backup/"
    cp mail/mailer.go "$boulderLabCADir/.backup/"
    cp cmd/expiration-mailer/main.go "$boulderLabCADir/.backup/"
    cp cmd/notify-mailer/main.go "$boulderLabCADir/.backup/"
    cp cmd/contact-auditor/main.go "$boulderLabCADir/.backup/"
    cp cmd/bad-key-revoker/main.go "$boulderLabCADir/.backup/"
    cp cmd/cert-checker/main.go "$boulderLabCADir/.backup/"
    cp cmd/log-validator/main.go "$boulderLabCADir/.backup/"
    cp cmd/boulder/main.go "$boulderLabCADir/.backup/"
    cp ratelimit/rate-limits.go "$boulderLabCADir/.backup/"
    cp errors/errors.go "$boulderLabCADir/.backup/"
    cp log/log.go "$boulderLabCADir/.backup/"
    cp sa/db/boulder_sa/20230419000000_CombinedSchema.sql "$boulderLabCADir/.backup/"
    cp Makefile "$boulderLabCADir/.backup/"

    if [ "$installMode" == "normal" ]; then
        $cloneDir/patch-cfg.sh "sudo -u labca -H" "$boulderLabCADir" &>>$installLog
    else
        $cloneDir/patch-cfg.sh " " "$boulderLabCADir" &>>$installLog
    fi

    mkdir -p $baseDir/backup
    if [ ! -z "$(docker ps | grep boulder-bmysql-1)" ]; then
        docker exec boulder-bmysql-1 mysqldump --databases boulder_sa_integration >$baseDir/backup/dbdata-old-${runId}.sql
        docker exec boulder-bmysql-1 mysql boulder_sa_integration -e 'delete from gorp_migrations;'
        msg_ok "Saved pre-update database to dbdata-old-${runId}.sql"
        msg_info "$msg"
    fi
    if [ ! -z "$(docker ps | grep labca-bmysql-1)" ]; then
        docker exec labca-bmysql-1 mysqldump --databases boulder_sa_integration >$baseDir/backup/dbdata-${runId}.sql
        docker exec labca-bmysql-1 mysql boulder_sa_integration -e 'delete from gorp_migrations;'
        msg_ok "Saved pre-update database to dbdata-${runId}.sql"
        msg_info "$msg"
    fi
    # housekeeping
    for file in `ls -1t $baseDir/backup/dbdata-*.sql 2>&1 | tail -n +3 || true`; do
        rm $file
    done

    if [ "$installMode" == "normal" ]; then
        cd "$boulderLabCADir"
        sed -i -e "s|https://boulder.service.consul:4431/terms/v7|https://$LABCA_FQDN/terms/v1|" config/wfe2.json
        sed -i -e "s|boulder.service.consul:4000|$LABCA_FQDN|g" config/wfe2.json
        sed -i -e "s|http://127.0.0.1:4002/|http://$LABCA_FQDN/ocsp/|g" config/ca-a.json
        sed -i -e "s|http://127.0.0.1:4002/|http://$LABCA_FQDN/ocsp/|g" config/ca-b.json
        sed -i -e "s|http://example.com/cps|http://$LABCA_FQDN/cps/|g" config/ca-a.json
        sed -i -e "s|http://example.com/cps|http://$LABCA_FQDN/cps/|g" config/ca-b.json
        sed -i -e "s|http://example.com/crl|http://$LABCA_FQDN/crl/|g" config/ca-a.json
        sed -i -e "s|http://example.com/crl|http://$LABCA_FQDN/crl/|g" config/ca-b.json
        sed -i -e "s|boulder.service.consul:4000|$LABCA_FQDN|g" config/va.json
        sed -i -e "s|boulder.service.consul:4001|$LABCA_FQDN|g" config/va.json
        sed -i -e "s|boulder.service.consul:4000|$LABCA_FQDN|g" config/va-remote-a.json
        sed -i -e "s|boulder.service.consul:4001|$LABCA_FQDN|g" config/va-remote-a.json
        sed -i -e "s|boulder.service.consul:4000|$LABCA_FQDN|g" config/va-remote-b.json
        sed -i -e "s|boulder.service.consul:4001|$LABCA_FQDN|g" config/va-remote-b.json
        cd "$boulderDir"
    fi

    ([ -e mock-vendor.go ] && rm mock-vendor.go) || /bin/true
    ([ -e test-tools.go ] && rm test-tools.go) || /bin/true

    if [ "$installMode" == "normal" ]; then
        local have_config=$(grep restarted $adminDir/data/config.json 2>/dev/null | grep true)
        if [ "$have_config" != "" ]; then
            cd "$boulderLabCADir"
            $adminDir/apply-boulder &>>$installLog
            cd "$boulderDir"
        else
            chown -R labca:labca "$boulderLabCADir" || /bin/true
        fi
    fi

    git add --all &>/dev/null || true
    [ "$installMode" == "normal" ] && git commit --all --quiet -m "LabCA after update $runId" &>>$installLog || true

    msg_ok "$msg"
}

# Cleanup any now obsolete files
cleanup() {
    local msg="Cleaning up obsolete files"
    msg_info "$msg"

    if [ -d /var/www/html ]; then
        rm -f /var/www/html/css/skeleton.css
        rm -f /var/www/html/css/skeleton-tabs.css
        rm -f /var/www/html/css/normalize.css
        rm -f /var/www/html/css/font.css
        rm -f /var/www/html/img/favicon.ico
        rm -f /var/www/html/js/jquery-3.3.1.min.js
        rm -f /var/www/html/js/skeleton-tabs.js
    fi
    rm -f $adminDir/templates/cert.tmpl
    rm -f $adminDir/templates/error.tmpl
    rm -f $adminDir/templates/final.tmpl
    rm -f $adminDir/templates/footer.tmpl
    rm -f $adminDir/templates/header.tmpl
    rm -f $adminDir/templates/index.tmpl
    rm -f $adminDir/templates/login.tmpl
    rm -f $adminDir/templates/polling.tmpl
    rm -f $adminDir/templates/register.tmpl
    rm -f $adminDir/templates/setup.tmpl
    rm -f $adminDir/templates/wrapup.tmpl

    # Remove host nginx if installed, as we are now using the docker container
    systemctl stop nginx &>>$installLog || true
    systemctl disable nginx &>>$installLog || true
    apt remove -y nginx &>>$installLog

    msg_ok "$msg"
}

# Startup all the components
startup() {
    local msg="Restart docker containers and service"

    cd "$boulderDir"
    let num=$(docker ps -a | grep " boulder-" | wc -l)
    if [ $num -eq 0 ]; then
        perl -i -p0e "s/(version:.*\n).*\n?(services:\n)/\1name: labca\n\2/" docker-compose.yml
    fi
    cnt=$(docker compose ps | wc -l)
    if [ "$cnt" -le "2" ]; then
        msg="Download docker images and build containers"
    fi
    msg_info "$msg (this will take a while!!)"

    docker compose pull -q &>>$installLog
    cnt=$(count $PS_CONTROL || echo "0")
    docker compose stop boulder bmysql labca nginx bconsul gui &>>$installLog || true
    for ct in boulder_bhsm_1 boulder_bredis_1 boulder_bredis_2 boulder_bredis_3 boulder_bredis_4 boulder_bredis_5 boulder_bredis_6; do
        [ -z "$(docker ps | grep $ct)" ] || docker stop $ct &>>$installLog
    done
    if [ $num -ne 0 ]; then
        docker compose stop control &>>$installLog || true
    fi
    wait_down $PS_NGINX &>>$installLog || true
    wait_down $PS_MYSQL &>>$installLog || true
    wait_down $PS_CONSUL &>>$installLog || true
    wait_down $PS_LABCA &>>$installLog || true
    wait_down $PS_BOULDER &>>$installLog || true
    if [ $num -ne 0 ]; then
        wait_down $PS_CONTROL &>>$installLog || true
        cnt=0
    fi
    for ct in boulder_bhsm_1 boulder_bredis_1 boulder_bredis_2 boulder_bredis_3 boulder_bredis_4 boulder_bredis_5 boulder_bredis_6; do
        [ -z "$(docker ps -a | grep -e "$ct\$")" ] || docker rm -f $ct &>>$installLog
    done

    local rc=0
    service labca status &> /dev/null || rc=$?
    if [ $rc -eq 0 ]; then
        service labca stop &>>$installLog || true
        update-rc.d labca disable &>>$installLog || true
        [ -e "/etc/init.d/labca" ] && rm /etc/init.d/labca || true
    fi

    [ -d /home/labca/control_logs ] || mkdir -p /home/labca/control_logs

    perl -i -p0e "s/(version:.*\n).*\n?(services:\n)/\1name: labca\n\2/" docker-compose.yml
    docker network rm -f boulder_bluenet boulder_consulnet boulder_rednet &>>$installLog || /bin/true
    docker rm -f boulder-control-1 boulder-boulder-1 boulder-labca-1 boulder-bmysql-1 boulder-bconsul-1 boulder-nginx-1 >/dev/null &>>$installLog

    # Restore MySQL data when moving from boulder-bmysql-1 to labca-bmysql-1
    if [ -z "$(docker volume ls | grep labca_dbdata)" ] && [ ! -z "$(docker volume ls | grep boulder_dbdata)" ]; then
        docker volume create labca_dbdata &>>$installLog
        dimg=$(grep mariadb: docker-compose.yml | sed -e "s/\s*image:\s*//")
        docker run --rm -v boulder_dbdata:/old -v labca_dbdata:/new $dimg bash -c "cp -R /old/* /new/" &>>$installLog
    fi

    COMPOSE_HTTP_TIMEOUT=180 docker compose up -d &>>$installLog

    wait_up $PS_NGINX &>>$installLog || true
    wait_up $PS_MYSQL &>>$installLog || true
    wait_up $PS_CONSUL 2 &>>$installLog || true
    wait_up $PS_LABCA &>>$installLog || true
    wait_up $PS_CONTROL &>>$installLog || true
    docker exec -i labca-bmysql-1 mysql_upgrade &>>$installLog
    [ -f "$boulderLabCADir/setup_complete" ] && wait_up $PS_BOULDER $PS_BOULDER_COUNT &>>$installLog || /bin/true

    if [ $cnt -gt 0 ]; then
        COMPOSE_HTTP_TIMEOUT=180 docker compose restart control &>>$installLog
    fi

    msg_ok "$msg"
}

# If the nginx certificate is self-signed then show extra text
first_time() {
    local certFile="/home/labca/nginx_data/ssl/labca_cert.pem"
    [ -e "$certFile" ] || msg_fatal "The SSL certificate $certFile does not exist"

    local subject=$(openssl x509 -noout -in "$certFile" -subject_hash)
    local issuer=$(openssl x509 -noout -in "$certFile" -issuer_hash)

    if [ "$subject" == "$issuer" ]; then
        echo
        echo ========
        echo
        echo "Congratulations! LabCA is now installed and should be available at https://$LABCA_FQDN"
        echo "Please go there now to finish the setup. Note that a TEMPORARY (7 days) self-signed certificate"
        echo "is used; as part of the setup verification a new certificate will be issued."
        echo
    fi
}

check_dockeronly() {
    set +e
    wd=$(which docker)
    set -e
    if [ "$wd" != "" ]; then
        let num=$(docker volume ls | grep labca_ | grep -v labca_dbdata | wc -l)
        if [ $num -gt 0 ]; then
            scriptname=$(basename $0)
            echo "You can not run the $scriptname script when using dockeronly mode!"
            exit 1
        fi
    fi
}

#
# The actual main function to tie it all together
#
main() {
    local curdir="$PWD"

    echo
    check_dockeronly

    start_temporary_log
    check_root
    install_pkg "git"
    install_pkg "sudo"
    install_pkg "patch"
    labca_user
    end_temporary_log

    this=$0
    [ -e $this ] || this="$curdir/$0"
    curChecksum=$(md5sum $this 2>/dev/null | cut -d' ' -f1)
    [ ! -e "$cloneDir/cron_d" ] || chown labca:labca "$cloneDir/cron_d"

    parse_cmdline "$@"
    if [ $keepLocal -eq 0 ]; then
        clone_or_pull "$cloneDir" "$labcaUrl" "$cmdlineBranch"
        checkout_release "$cmdlineBranch"
        restart_if_updated
    fi

    if [ $alphaTest -eq 1 ]; then
        install_extra
        cd $(dirname $this)
        local msg="TEST: build labca-gui binary"
        msg_info "$msg"
        # this will ultimately NOT be done on the target machine!
        build/build.sh &>>$installLog || msg_fatal "Could not build docker images!"
        msg_ok "$msg"
        msg="TEST build local docker image"
        msg_info "$msg"
        build/tag_and_upload.sh &>>$installLog || msg_fatal "Could not tag (and upload) docker images!"
        msg_ok "$msg"
        msg_ok "That's it for now!"
        exit 0
    fi

    get_fqdn
    copy_admin

    update_upgrade
    install_extra

    static_web
    selfsigned_cert

    get_boulder
    config_boulder

    cleanup
    startup

    echo -e "$DONE"
    echo

    first_time

    cd "$curdir"
}

[ "$installMode" == "normal" ] && main "$@" || /bin/true
