#!/usr/bin/env bash
# ======================================================================
#
# DEPLOYMENT CLIENT
# Roll out a package of IML CI server on a target system.
#
# ----------------------------------------------------------------------
# 2021-04-19  v0.1  <axel.hahn@iml.unibe.ch>  initial version
# 2021-05-09  v0.2  <axel.hahn@iml.unibe.ch>  chown includes dot files
# 2021-05-14  v0.3  <axel.hahn@iml.unibe.ch>  add params (list, force, help)
# 2021-05-27  v0.4  <axel.hahn@iml.unibe.ch>  FIX first install
# 2021-07-08  v0.5  <axel.hahn@iml.unibe.ch>  added function "runas"
# 2021-11-01  v0.6  <axel.hahn@iml.unibe.ch>  save config diffs
# 2021-11-02  v0.7  <axel.hahn@iml.unibe.ch>  delete logs keping N files
# 2022-11-24  v0.8  <axel.hahn@iml.unibe.ch>  tar -xzf without dot as 2nd param
# 2022-11-25  v0.9  <axel.hahn@iml.unibe.ch>  support custom phase + file per project
# 2023-02-14  v1.0  <axel.hahn@unibe.ch>      set v1.0 (no changes) 
# 2023-12-??  v1.1  <axel.hahn@unibe.ch>      show OK message in profile log
# 2023-12-14  v1.2  <axel.hahn@unibe.ch>      export some vars; abort on errors
# ======================================================================


# ----------------------------------------------------------------------
# CONFIG
# ----------------------------------------------------------------------
cd $( dirname $0 )
_version=1.2

export selfdir; selfdir=$( /bin/pwd )
export profiledir

tmpdir=/var/tmp/imldeployment_packages
logdir=/var/log/imldeployment-client

# keep last N logs per project
typeset -i iKeep=10

wait=0
# wait=1

# export variables that will be set in getfile config or project
export IMLCI_PROJECT=TODO
export IMLCI_PHASE=TODO
export cfgdiff=TODO

# ----------------------------------------------------------------------
# FUNCTIONS
# ----------------------------------------------------------------------


# get a list profiles by searching a config.sh
# no param
function getprofiles(){
    find ${selfdir}/profiles/ -name "config.sh" | rev | cut -f 2 -d "/" | rev
}

# set a profile, load it, verify required parameters
# param  string  name of a subdir in ./profiles/
function setprofile(){
    profile=$1

    # source config for software download - as default.
    . ${selfdir}/bin/getfile.sh.cfg 

    # my install dir
    installdir=

    # fileowner
    appowner=

    profiledir=${selfdir}/profiles/${profile}
    . ${profiledir}/config.sh || exit 11

    echo "[${profiledir}/config.sh] was loaded."
    if [ -z "$installdir" -o -z "${IMLCI_PHASE}" -o -z "${IMLCI_PROJECT}" ]; then
        echo "to be defined in ${profiledir}/config.sh:"
        echo "installdir    = $installdir"
        echo "These variables must be set in bin/getfile.sh.cfg or in [profile]/config.sh:"
        echo "IMLCI_PHASE   = $IMLCI_PHASE"
        echo "IMLCI_PROJECT = $IMLCI_PROJECT"
        exit 12
    fi 
    echo "OK, profile [${profile}] was set."

    local localfile
    if [ -n "$IMLCI_FILE" ]; then
        localfile="${IMLCI_PROJECT}__${IMLCI_FILE}"
    else
        IMLCI_FILE="${IMLCI_PROJECT}.tgz"
        localfile="${IMLCI_FILE}"
    fi

    downloadfile="${tmpdir}/${localfile}"
    downloadtmp="${tmpdir}/${localfile}.tmp"
    cfgdiff="${tmpdir}/${localfile}_cfgdiff.txt"
    test -f "${cfgdiff}" && rm -f "${cfgdiff}"
}

# output a colored infoline with date and given message
# param  string  message text
function header(){
    local COLOR="\033[34m"
    local NO_COLOR="\033[0m"
    echo
    echo -en "${COLOR}"
    echo ______________________________________________________________________
    echo -n ">>>>>>>>>> $(date) "
    test ! -z "$profile" && echo -n "${profile} :: " 
    echo -n "$*"
    echo -en "${NO_COLOR}"
    if [ "$wait" = "1" ]; then
        echo -n " RETURN"; read dummy;
    fi 
    echo
}

# run a command as another posix user (even if it does not have a shell)
# to be used in taskas_*install.sh
#
# example:
# runas www-data "./hooks/ondeploy"
#
# param  string  username
# param  string  command to execute. Needs to be quoted.
# param  string  optional: shell (default: /bin/sh)
function runas(){
    local _user=$1
    local _cmd=$2
    local _shell=$3
    test -z "$_shell" && _shell=/bin/sh

    su $_user -s $_shell -c "$_cmd"
}

# execute a task/ hook - if the given task script exists and has executable
# persmissions; if not it is not an error
# param  string  filename
function run_task(){
    local taskscript=$1
    if [ -x "${taskscript}" ]; then
        echo "INFO: starting script ${taskscript}..."
        . "${taskscript}" || exit 10
        echo "DONE: script ${taskscript}"
    else
        test -f "${taskscript}" && ( echo "SKIP: task script ${taskscript} is not executable." ; ls -l "${taskscript}")
        test -f "${taskscript}" || echo "SKIP: task script ${taskscript} does not exist."
    fi
}


function deploy(){
    local dlparams
    skipmessage="SKIP: no newer download file. You can use parameter -f to force reinstall."

    # ----------------------------------------------------------------------
    header "Set profile [$1]"
    setprofile $1


    # ----------------------------------------------------------------------
    header "Download ${IML} ${IMLCI_PROJECT}.tgz"
    typeset -i local isupdate=$defaultupdate
    
    # getfile.sh reads phase from its cfg file - we need to add it as parameter
    test -n "${IMLCI_PHASE}" && dlparams="$dlparams -e ${IMLCI_PHASE}"

    # set the filename to fetch
    test -n "$IMLCI_FILE" || dlparams="$dlparams -f ${IMLCI_PROJECT}.tgz"
    test -n "$IMLCI_FILE" && dlparams="$dlparams -f $IMLCI_FILE"

    ${selfdir}/bin/getfile.sh ${dlparams} -o ${downloadtmp}
    if [ $? -ne 0 ]; then
        echo Download failed.
        echo Repeating request with debug param -d to get the error...
        
        # added sleep to repeat the request with another hashed secret
        sleep 2
        
        ${selfdir}/bin/getfile.sh -d ${dlparams} -o ${downloadtmp}
        exit 1
    fi


    # ----------------------------------------------------------------------
    header "Detect if download is newer than last download."
    if [ -f ${downloadfile} ]; then
        # ls -l "${downloadfile}" "${downloadtmp}" 
        diff "${downloadfile}" "${downloadtmp}"
        if [ $? -eq 0 ]; then
            echo "INFO: the downloaded file is the same like last download."
            rm -f "${downloadtmp}"
        else
            echo "OK: donwload contains an update."
            isupdate=$isupdate+1
            mv "${downloadtmp}" "${downloadfile}"
        fi
    else
        echo "INFO: last download not available - first install or a forced update."
        isupdate=$isupdate+1
        mv "${downloadtmp}" "${downloadfile}"
    fi
    ls -l "${downloadfile}"


    # ----------------------------------------------------------------------
    header "Switch into install dir ${installdir} ..."
    test -d "${installdir}" || mkdir -p "${installdir}"
    cd ${installdir} || exit 2
    ls -1 * >/dev/null 2>&1
    if [ $? -ne 0 ]; then
        echo "INFO: target directory is empty."
        isupdate=$isupdate+1
    fi


    # ----------------------------------------------------------------------
    header "PRE tasks"
    # what you could do here:
    # - enable maintenance flag
    # - stop service
    # - cleanup directory ... up to remove all current files
    test $isupdate -eq 0 && echo $skipmessage
    test $isupdate -eq 0 || run_task "${profiledir}/tasks_preinstall.sh"


    # ----------------------------------------------------------------------
    header "PRE tasks II - cleanup"
    if [ $isupdate -eq 0 ]; then
        echo $skipmessage
    else
        test "$cleanup_preview" -eq "1" || echo "SKIP: preview of cleanup is disabled."
        test "$cleanup_preview" -eq "1" && "${selfdir}/bin/preinstall_cleanup.sh" "${installdir}" "${downloadfile}"

        test "$cleanup_force" -eq "1"   || echo "SKIP: cleanup files is disabled."
        test "$cleanup_force" -eq "1"   && "${selfdir}/bin/preinstall_cleanup.sh" "${installdir}" "${downloadfile}" "force"
    fi


    # ----------------------------------------------------------------------
    header "Extract ${downloadfile} in $( pwd )"
    test $isupdate -eq 0 && echo $skipmessage
    test $isupdate -eq 0 || tar -xzf "${downloadfile}" || exit 3
    ls -l


    # ----------------------------------------------------------------------
    # header "Remove download archive ${IMLCI_PROJECT}.tgz"
    # echo rm -f  ${IMLCI_PROJECT}.tgz


    # ----------------------------------------------------------------------
    header "Update config files"
    echo "Showing replacements:" ; grep '@replace\[' hooks/templates/*
    run_task "${profiledir}/tasks_config.sh"


    # ----------------------------------------------------------------------
    header "Set file owner [${appowner}]"
    if [ $isupdate -eq 0 ]; then
        echo $skipmessage
    else
        if [ ! -z "${appowner}" ]; then
            # . is ${installdir}
            sudo chown -R $appowner . || exit 5
            ls -la
        else
            echo "SKIP: variable appowner was not set"
        fi
    fi


    # ----------------------------------------------------------------------
    header "POST tasks"
    # what you could do here:
    # - start current deploy scripts
    # - apply database updates
    # - set permissions
    # - start service
    # - remove maintenance flag
    # - send success message as email/ slack/ [another fancy tool]
    test $isupdate -eq 0 && echo $skipmessage
    test $isupdate -eq 0 || run_task "${profiledir}/tasks_postinstall.sh"
    hasfilechange=0
    grep . $cfgdiff && hasfilechange=1
    if [ $hasfilechange -eq 1 ]; then
        echo "INFO: a config file was created or changed."
    else
        echo SKIP: No config file was changed.
    fi
    
    if [ $isupdate -ne 0 -o $hasfilechange -eq 1 ]; then
        run_task "${profiledir}/tasks_postchange.sh"
    fi

    cd $( dirname $0 )

    header "Installer Status"
    echo "OK: ${IMLCI_PROJECT}"

}

# delete old logfiles keeping the last N files
# param  string  name of project
function logdelete(){
    local sProfile=$1

    header "DELETE LOGS ${logdir}/${sProfile}__* ... keep $iKeep"
    # order files by time 
    typeset -i local _iFiles=$( ls -1t ${logdir}/${sProfile}__*.log | wc -l )
    typeset -i local _iStart=$iKeep+1

    if [ $_iFiles -gt $iKeep ]; then
        ls -1t ${logdir}/${sProfile}__*.log | sed -n "${_iStart},${_iFiles}p" | while read mylogfile
        do
            echo -n "deleting "
            ls -l "${mylogfile}" && rm -f "${mylogfile}" 
        done
    else
        echo SKIP: deletion ... less than $iKeep files found 
    fi
}

# ----------------------------------------------------------------------
# MAIN
# ----------------------------------------------------------------------


cd $( dirname $0 )

action="deploy"
typeset -i defaultupdate=0

echo "_______________________________________________________________________________

 IML - DEPLOYMENT CLIENT
 DOCS: https://os-docs.iml.unibe.ch/imldeployment-client/                 _____
_________________________________________________________________________/ v$_version
"


while getopts 'hfl' arg; do
  case ${arg} in
    h) 
        echo "HELP:"
        echo "  Loads one or more profiles profile to deploy an application."
        echo "  If the download file is not newer then it does not extract files and does not"
        echo "  Optionally it cleans up the target directory."
        echo "  Runs pre and post hooks - it updates the config files only and sets the owner."
        echo
        echo "SYNTAX:"
        echo "  $( basename $0 ) [OPTION] [PROFILE(S)]"
        echo
        echo "OPTIONS:"
        echo "  -h | show this help and exit"
        echo "  -f | force full installation even if the download file is not newer"
        echo "  -l | list exiting profile names"
        echo
        echo "PROFILE(S):"
        echo "  Set one or more valid profile names. By default it loops over all profiles."
        echo "  This prameter limits the execution to the given profiles." 
        echo "  Use option -l to get a list of profiles." 
        echo
        exit 0
        ;;
    f)
      echo "FORCE update"
      defaultupdate=1
      shift 1
      ;;
    l)
      echo "LIST of existing profiles:"
      getprofiles
      echo
      exit 0
      ;;
    ?)
      echo "Invalid option: -${OPTARG}."
      exit 2
      ;;
  esac
done


if [ $# -eq 0 ]; then
    header "looping over all profiles"
    getprofiles
    echo
    allprofiles=$( getprofiles )
else
    allprofiles="$*"
fi

test -d "${logdir}" || mkdir -p "${logdir}"
for myprofile in $allprofiles
do
    ( deploy $myprofile; logdelete $myprofile ) 2>&1 | tee ${logdir}/${myprofile}__$(date +%Y-%m-%d__%H%M%S).log
    test ${PIPESTATUS[0]} -eq 0 || exit ${PIPESTATUS[0]}
    profile=
done

rc=$?
profile=
header "All done."
echo exiting with statuscode $rc
exit $rc



# ----------------------------------------------------------------------
header "DONE :-)"


# ----------------------------------------------------------------------