# This file does nothing else other than redirects to logfile,
# and defining functions.  This allows for code reuse.
if [ ! "$debug_mode" == "true" ]; then
  # close STDERR and STDOUT
  exec 1<&-
  exec 2<&-

  # open STDOUT
  exec 1>>/var/cfengine/outputs/dc-scripts.log

  # redirect STDERR to STDOUT
  exec 2>&1
fi

error_exit() {
    # Display error message and exit
    echo "${0}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

check_git_installed() {
  git --version >/dev/null 2>&1 || error_exit "git not found on path: '${PATH}'"
}

check_cfbs_installed() {
    cfbs --version >/dev/null 2>&1 || error_exit "cfbs not found on path: '${PATH}'"
}

git_setup_local_mirrored_repo() {
# Contributed by Mike Weilgart

  # Depends on $GIT_URL
  # Sets $local_mirrored_repo
  # Accepts one arg:
  # $1 - absolute path in which to place the mirror.

  # This function sets the variable local_mirrored_repo to a directory path
  # based on the value of GIT_URL, and if that directory doesn't exist,
  # creates it as a mirrored clone of the repo at GIT_URL.  If it does exist,
  # update it with git fetch.
  #
  # This code could be improved if there is an inexpensive way to check that
  # a local bare repository is in fact a *mirrored* repository of a specified
  # GIT_URL, but for now if the local_mirrored_repo is in fact a bare git
  # repo (guaranteed by the success of the "git fetch" command) then we just
  # assume it is a *mirrored* repository.
  #
  # Since the pathname is directly based on GIT_URL, there is no chance
  # of *accidental* name collision.

  # Check first char of the argument to ensure it's an absolute path
  [ "${1:0:1}" = / ] ||
    error_exit "Improper path passed to git_setup_local_mirrored_repo"
  local_mirrored_repo="${1}/$(printf '%s' "${GIT_URL}" | sed 's/[^A-Za-z0-9._-]/_/g')"
  ########### Example value:
  # GIT_URL="git@mygitserver.net:joeblow/my_policy_repo.git"
  # parameter passed in: /opt/cfengine
  # value of local_mirrored_repo that is set:
  # /opt/cfengine/git_mygitserver.net_joeblow_my_policy_repo.git

  if [ -d "${local_mirrored_repo}" ] ; then
    git --git-dir="${local_mirrored_repo}" fetch && return 0

    # If execution arrives here, the local_mirrored_repo exists but is messed up somehow
    # (or there is network trouble).  Easiest is to wipe it and start fresh.
    rm -rf "${local_mirrored_repo}"
  fi
  git clone --mirror "${GIT_URL}" "${local_mirrored_repo}" ||
    error_exit "Failed: git clone --mirror '${GIT_URL}' '${local_mirrored_repo}'"
}

git_checkout_into_temp_dir() {
  # Depends on $local_mirrored_repo
  # Accepts two args:
  # $1 - dir to deploy to
  # $2 - refspec to deploy - a git tagname, branch, or commit hash.
  # $3 - project subdirectory inside the repository to deploy.

  # The '^0' at the end of the refspec
  # populates HEAD with the SHA of the commit
  # rather than potentially using a git branch name.
  # Also see http://stackoverflow.com/a/13293010/5419599
  # and https://github.com/cfengine/core/pull/2465#issuecomment-173656475
  
  # Checkout with optional subdirectory if not empty
  if [ -n "$3" ]; then
    # checkout subdirectory only
    git --git-dir="${local_mirrored_repo}" --work-tree="${temp_stage}" checkout -q -f "${2}^0" -- "$3" ||
      error_exit "Failed to checkout subdirectory '$3' from '$2'"
    # move contents of subdirectory to the temp_stage root
    # include dotfile matching in wildcard expansions to move them as well
    shopt -s dotglob 
    mv "${temp_stage}/${3}"/* "${temp_stage}/"
    # disable dotfile matching
    shopt -u dotglob
    # remove empty subdirectory from the temp_stage
    rmdir "${temp_stage}/${3}"
  else
    # checkout the whole repository
    git --git-dir="${local_mirrored_repo}" --work-tree="${temp_stage}" checkout -q -f "${2}^0" ||
      error_exit "Failed to checkout '$2' from '${local_mirrored_repo}'"
  fi
}

git_deploy_refspec() {
# Contributed by Mike Weilgart

  # Depends on $local_mirrored_repo
  # Accepts two args:
  # $1 - dir to deploy to
  # $2 - refspec to deploy - a git tagname, branch, or commit hash.
  # $3 - project subdirectory inside the repository to deploy.

  # This function
  # 1. creates an empty temp dir,
  # 2. checks out the refspec into the empty temp dir,
  #    (including populating .git/HEAD in the temp dir),
  # 3. if the project subdirectory is set, it checks it out and moves files from it to the root of the temp dir.
  # 4. sets appropriate permissions on the policy set,
  # 5. validates the policy set using cf-promises,
  # 6. moves the temp dir policy set into the given deploy dir,
  #    avoiding triggering policy updates unnecessarily
  #    by comparing the cf_promises_validated flag file.
  #    (See long comment at end of function def.)

  # Ensure absolute pathname is given
  [ "${1:0:1}" = / ] ||
    error_exit "You must specify absolute pathnames in channel_config: '$1'"
  mkdir -p "$(dirname "$1")" || error_exit "Failed to mkdir -p dirname $1"
    # We don't mkdir $1 directly, just its parent dir if that doesn't exist.

  if git_check_is_in_sync "${local_mirrored_repo}" "$1" "$2"; then
    return 0
  fi

  ########################## 1. CREATE EMPTY TEMP DIR
  # Put staging dir right next to deploy dir to ensure it's on same filesystem
  local temp_stage
  temp_stage="$(mktemp -d --tmpdir="$(dirname "$1")" )"
  trap 'rm -rf "$temp_stage"' EXIT

  ########################## 2. CHECKOUT INTO TEMP DIR
  git_checkout_into_temp_dir "$1" "$2" "$3"

  # Grab HEAD so it can be used to populate cf_promises_release_id
  mkdir -p "${temp_stage}/.git"
  cp "${local_mirrored_repo}/HEAD" "${temp_stage}/.git/"

  ########################## 3. SET PERMISSIONS ON POLICY SET
  chown -R root:root "${temp_stage}" || error_exit "Unable to chown '${temp_stage}'"
  find "${temp_stage}" \( -type f -exec chmod 600 {} + \) -o \
                       \( -type d -exec chmod 700 {} + \)

  ########################## 4. VALIDATE POLICY SET
  /var/cfengine/bin/cf-promises -T "${temp_stage}" &&
  /var/cfengine/bin/cf-promises -cf "${temp_stage}/update.cf" ||
  error_exit "Update policy staged in ${temp_stage} could not be validated, aborting."

  ########################## 5. ROLL OUT POLICY SET FROM TEMP DIR TO DEPLOY DIR
  if ! [ -d "$1" ] ; then
    # deploy dir doesn't exist yet
    mv "${temp_stage}" "$1" || error_exit "Failed to mv $temp_stage to $1."
    trap -- EXIT
  else
    if /usr/bin/cmp -s "${temp_stage}/cf_promises_release_id" \
                                 "${1}/cf_promises_release_id" ; then
      # release id is the same in stage and deploy dir
      # so prevent triggering update on hosts by keeping old "validated" flag file
      cp -a "${1}/cf_promises_validated" "${temp_stage}/"
    fi
    local third_dir
    third_dir="$(mktemp -d --tmpdir="$(dirname "$1")" )"
    trap 'rm -rf "$third_dir"' EXIT
    mv "${1}" "${third_dir}"  || error_exit "Can't mv ${1} to ${third_dir}"
      # If the above command fails we will have an extra temp dir left.  Otherwise not.
    mv "${temp_stage}" "${1}"          || error_exit "Can't mv ${temp_stage} to ${1}"
    rm -rf "${third_dir}"
    trap -- EXIT
  fi

  # Note about triggering policy updates:
  #
  # cf_promises_validated gets updated by any run of cf-promises,
  # but hosts use cf_promises_validated as the flag file to see
  # if they need to update everything else (the full policy set.)
  #
  # cf_promises_release_id is the same for a given policy set
  # unless changes have actually been made to the policy, so it
  # can be used to check if we want to trigger an update.
  #
  # In other words, update is triggered by putting the
  # newly created copy of cf_promises_validated into the MASTERDIR
  # and update is avoided either by:
  #
  # 1. Completely skipping the rollout_staged_policy_to_masterdir
  # function, or
  #
  # 2. Copying the MASTERDIR's copy of cf_promises_validated
  # *back* into the STAGING_DIR *before* performing the rollout,
  # so that after the rollout the MASTERDIR's copy of the flag
  # file is the same as it was before the rollout.
  #
  # This function uses the second approach.  --Mike Weilgart
}

git_cfbs_deploy_refspec() {
# copied from git_cfbs_deploy_refspec

  # Depends on $local_mirrored_repo
  # Accepts two args:
  # $1 - dir to deploy to
  # $2 - refspec to deploy - a git tagname, branch, or commit hash.
  # $3 - project subdirectory inside the repository to deploy.

  # This function
  # 1. creates an empty temp dir,
  # 2. checks out the refspec into the empty temp dir,
  #    (including populating .git/HEAD in the temp dir),
  # 3. if the project subdirectory is set, it checks it out and moves files from it to the root of the temp dir.
  # 4. builds policy with cfbs
  # 5. sets appropriate permissions on the policy set,
  # 6. validates the policy set using cf-promises,
  # 7. moves the temp dir policy set into the given deploy dir,
  #    avoiding triggering policy updates unnecessarily
  #    by comparing the cf_promises_validated flag file.
  #    (See long comment at end of function def.)

  # The chipmunk in cfbs output breaks things without this or similar
  # The chipmunk in cfbs output breaks things without this or similar
  if [ -f "/etc/locale.conf" ]; then
    source "/etc/locale.conf"
    export LC_ALL="$LANG" # retrieved from locale.conf
  else
    _LOCALE=$(locale -a | grep -i utf | head -1)
    if [ -n "$_LOCALE" ]; then
      export LC_ALL="$_LOCALE"
    fi
  fi

  # Ensure absolute pathname is given
  [ "${1:0:1}" = / ] ||
    error_exit "You must specify absolute pathnames in channel_config: '$1'"
  mkdir -p "$(dirname "$1")" || error_exit "Failed to mkdir -p dirname $1"
    # We don't mkdir $1 directly, just its parent dir if that doesn't exist.

  if git_check_is_in_sync "${local_mirrored_repo}" "$1" "$2"; then
    return 0
  fi

  ########################## 1. CREATE EMPTY TEMP DIR
  # Put staging dir right next to deploy dir to ensure it's on same filesystem
  local temp_stage
  temp_stage="$(mktemp -d --tmpdir="$(dirname "$1")" )"
  trap 'rm -rf "$temp_stage"' EXIT

  ########################## 2. CHECKOUT INTO TEMP DIR
  git_checkout_into_temp_dir "$1" "$2" "$3"

  ########################## 3. cfbs build
  # Remember what directory we were in when we started.
  _start_wrkdir=$(pwd)
  # Switch to the staging directory and build with cfbs
  cd "${temp_stage}"
  CFBS_GLOBAL_DIR="/opt/cfengine/build/cfbs_global" cfbs build || error_exit "cfbs build failed"
  # Switch back to the original working dir
  cd "${_start_wrkdir}"
  # Grab HEAD so it can be used to populate cf_promises_release_id
  mkdir -p "${temp_stage}/out/masterfiles/.git"
  cp "${local_mirrored_repo}/HEAD" "${temp_stage}/out/masterfiles/.git/"

  ########################## 3. SET PERMISSIONS ON POLICY SET
  chown -R root:root "${temp_stage}" || error_exit "Unable to chown '${temp_stage}'"
  find "${temp_stage}" \( -type f -exec chmod 600 {} + \) -o \
                       \( -type d -exec chmod 700 {} + \)

  ########################## 4. VALIDATE POLICY SET
  /var/cfengine/bin/cf-promises -T "${temp_stage}/out/masterfiles" &&
  /var/cfengine/bin/cf-promises -cf "${temp_stage}/out/masterfiles/update.cf" ||
  error_exit "Update policy staged in ${temp_stage}/out/masterfiles could not be validated, aborting."

  ########################## 5. ROLL OUT POLICY SET FROM TEMP DIR TO DEPLOY DIR
  if ! [ -d "$1" ] ; then
    # deploy dir doesn't exist yet
    mv "${temp_stage}/out/masterfiles" "$1" || error_exit "Failed to mv $temp_stage/out/masterfiles to $1."
    trap -- EXIT
  else
    if /usr/bin/cmp -s "${temp_stage}/out/masterfiles//cf_promises_release_id" \
                                 "${1}/cf_promises_release_id" ; then
      # release id is the same in stage and deploy dir
      # so prevent triggering update on hosts by keeping old "validated" flag file
      cp -a "${1}/cf_promises_validated" "${temp_stage}/out/masterfiles/"
    fi
    local third_dir
    third_dir="$(mktemp -d --tmpdir="$(dirname "$1")" )"
    trap 'rm -rf "$third_dir"' EXIT
    mv "${1}" "${third_dir}"  || error_exit "Can't mv ${1} to ${third_dir}"
      # If the above command fails we will have an extra temp dir left.  Otherwise not.
    mv "${temp_stage}/out/masterfiles" "${1}"          || error_exit "Can't mv ${temp_stage}/out/masterfiles to ${1}"
    cp "${temp_stage}/cfbs.json" "${1}"          || error_exit "Can't cp ${temp_stage}/cfbs.json to ${1}"
    rm -rf "${temp_stage}"
    rm -rf "${third_dir}"
    trap -- EXIT
  fi

  # Note about triggering policy updates:
  #
  # cf_promises_validated gets updated by any run of cf-promises,
  # but hosts use cf_promises_validated as the flag file to see
  # if they need to update everything else (the full policy set.)
  #
  # cf_promises_release_id is the same for a given policy set
  # unless changes have actually been made to the policy, so it
  # can be used to check if we want to trigger an update.
  #
  # In other words, update is triggered by putting the
  # newly created copy of cf_promises_validated into the MASTERDIR
  # and update is avoided either by:
  #
  # 1. Completely skipping the rollout_staged_policy_to_masterdir
  # function, or
  #
  # 2. Copying the MASTERDIR's copy of cf_promises_validated
  # *back* into the STAGING_DIR *before* performing the rollout,
  # so that after the rollout the MASTERDIR's copy of the flag
  # file is the same as it was before the rollout.
  #
  # This function uses the second approach.  --Mike Weilgart
}

######################################################
##           VCS_TYPE-based main functions           #
######################################################
git_check_is_in_sync() {
  # $1 -- git repo mirror
  # $2 -- checked out work-tree
  # $3 -- refspec

  # Get the hash of the refspec in the mirror and compare it to the checked out
  # work-tree (see git_deploy_refspec() for details).
  # ^0 is to make sure we get just the commit hash without any unwanted info
  # (like we would otherwise get for a tag, for example).
  mirror_rev="$(git --git-dir="$1" show --pretty=format:%H -s "${3}^0")"
  work_tree_rev="$(cat "$2/.git/HEAD")"

  test "$mirror_rev" = "$work_tree_rev"
}

git_stage_policy_channels() {
  # $1 -- whether to check only [true/false; optional]
  check="false"
  if [ "$#" -gt 0 ]; then
    check="$1"
    shift
  fi

  # Contributed by Mike Weilgart

  # Depends on ${channel_config[@]} and $dir_to_hold_mirror
  # Calls functions dependent on $GIT_URL
  # (See the example git policy channels params file.)
  #
  # Stages multiple policy channels from a specified GIT_URL,
  # each to the specified path.
  #
  # The paths to stage to as well as the policy sets to stage
  # are both specified in the "channel_config" array in the
  # PARAMS file.
  #
  # The value of MASTERDIR that is assigned in masterfiles-stage.sh
  # is ignored by this function, since there is effectively a separate
  # MASTERDIR for each separate policy channel.
  #
  # Example value for channel_config:
  # (This is a single value, split into three lines for readability.)
  #
  # channel_config=()
  # channel_config+=( "/var/cfengine/masterfiles" "my_git_tag" )
  # channel_config+=( "/var/cfengine/policy_channels/channel_1" "my_git_branch" )

  # Simplest validation check first
  set -- "${channel_config[@]}"
  [ "$#" -gt 1 ] ||
    error_exit "The channel_config array must have at least two elements."

  check_git_installed
  git_setup_local_mirrored_repo "$dir_to_hold_mirror"

  if [ "$check" = "true" ]; then
    while [ "$#" -gt 1 ] ; do
      # At start of every loop, "$1" contains deploy dir and "$2" is refspec.
      if ! git_check_is_in_sync "$dir_to_hold_mirror" "$1" "$2"; then
        return 0  # something to update
      fi
      shift 2
    done
    return 1  # nothing to update
  fi

  while [ "$#" -gt 1 ] ; do
    # At start of every loop, "$1" contains deploy dir and "$2" is refspec.
    git_deploy_refspec "$1" "$2"
    echo "Successfully deployed a policy release of '${2}' from '${GIT_URL}' to '${1}' on $(date)"
    shift 2
  done

  [ "$#" -eq 1 ] && error_exit "Trailing parameter found, please fix params file: '$1'"
}

git_masterstage() {
  # $1 -- whether to check only [true/false; optional]
  # Depends on $GIT_URL, $ROOT, $MASTERDIR, $GIT_REFSPEC, $PROJECT_SUBDIRECTORY
  check_git_installed
  git_setup_local_mirrored_repo "$( dirname "$ROOT" )"
  if [ "x$1" = "xtrue" ]; then
    if git_check_is_in_sync "$( dirname "$ROOT" )" "$MASTERDIR" "$GIT_REFSPEC"; then
      return 1  # in sync => nothing to do
    else
      return 0  # not in sync => update available
    fi
  fi
  git_deploy_refspec "$MASTERDIR" "${GIT_REFSPEC}" "${PROJECT_SUBDIRECTORY}"
  echo "Successfully deployed '${GIT_REFSPEC}' from '${GIT_URL}'$( [[ -n "${PROJECT_SUBDIRECTORY}" ]] && echo " subdirectory: '${PROJECT_SUBDIRECTORY}'") to '${MASTERDIR}' on $(date)"
}

git_cfbs_masterstage() {
    # $1 -- whether to check only [true/false; optional]
    # Depends on $GIT_URL, $ROOT, $MASTERDIR, $GIT_REFSPEC
    check_git_installed
    check_cfbs_installed
    git_setup_local_mirrored_repo "$( dirname "$ROOT" )"
    if [ "x$1" = "xtrue" ]; then
      if git_check_is_in_sync "$( dirname "$ROOT" )" "$MASTERDIR" "$GIT_REFSPEC"; then
        return 1  # in sync => nothing to do
      else
        return 0  # not in sync => update available
      fi
    fi
    git_cfbs_deploy_refspec "$MASTERDIR" "${GIT_REFSPEC}" "${PROJECT_SUBDIRECTORY}"
    echo "Successfully built and deployed '${GIT_REFSPEC}' from '${GIT_URL}'$( [[ -n "${PROJECT_SUBDIRECTORY}" ]] && echo " subdirectory: '${PROJECT_SUBDIRECTORY}'") to '${MASTERDIR}' on $(date) with cfbs"
}

svn_branch() {
# Contributed by John Farrar
    # $1 -- whether to check only [true/false; optional]

    # We probably want a different temporary location for each remote repository
    # so that we can avoid conflicts and potential confusion.
    # Example:
    # ROOT="/opt/cfengine/masterfiles_staging"
    # PARAMS="/var/cfengine/policychannel/production_1.sh"
    # STAGING_DIR=/opt/cfengine/masterfiles/staging/_tmp_var_cfengine_policychannel_production_1_sh

    STAGING_DIR="${ROOT}_tmp$(echo "$PARAMS" | tr [./] _)"

    if ! type "svn" >/dev/null ; then
        error_exit "svn not found on path: ${PATH}"
    fi

    CHECKSUM_FILE="svn_promise_checksums"

    # If we already have a checkout, update it, else make a new checkout.
    if [ -d "${STAGING_DIR}/.svn" ] ; then
        svn update --quiet ${STAGING_DIR}
    else
        rm -rf "${STAGING_DIR}"
        svn checkout --quiet "${SVN_URL}"/"${SVN_BRANCH}"/inputs "${STAGING_DIR}"
    fi

    rm -f "${STAGING_DIR}/cf_promises_release_id"

    if /var/cfengine/bin/cf-promises -T "${STAGING_DIR}"; then
        md5sum `find ${STAGING_DIR} -type f -name \*.cf` >"${STAGING_DIR}/${CHECKSUM_FILE}"
        if /usr/bin/diff -q "${STAGING_DIR}/${CHECKSUM_FILE}" "${MASTERDIR}/${CHECKSUM_FILE}" ; then
            # echo "No release needs to be made, the checksum files are the same"
            touch "${STAGING_DIR}"
            if [ "x$1" = "xtrue" ]; then
              # check-only, update not needed => exit code 1
              return 1
            fi
        else
            if [ "x$1" = "xtrue" ]; then
              # check-only, update needed => exit code 0
              return 0
            fi
            cd "${STAGING_DIR}" && (
                chown -R root:root "${STAGING_DIR}" && \
                rsync -CrltDE -c --delete-after --chmod=u+rwX,go-rwx "${STAGING_DIR}/" "${MASTERDIR}/" && \
                rm -rf ${STAGING_DIR}/.svn && \
                echo "Successfully staged a policy release on $(date)"
            )
        fi
    else
       error_exit "The staged policies in ${STAGING_DIR} could not be validated, aborting."
    fi
}
