#!/bin/bash

# debops-update: install or update DebOps playbooks and roles
# Copyright (C) 2014 Maciej Delmanowski <drybjed@gmail.com>
# Part of the DebOps project - http://debops.org/


# This program is free software; you can redistribute
# it and/or modify it under the terms of the
# GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License,
# 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 General Public
# License for more details.
#
# You should have received a copy of the GNU General
# Public License along with this program; if not,
# write to the Free Software Foundation, Inc., 59
# Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# An on-line copy of the GNU General Public License can
# be downloaded from the FSF web page at:
# http://www.gnu.org/copyleft/gpl.html


# This script can be used to install or update installed DebOps playbooks and
# roles to current or specified version. By default it works on the installed
# playbook in users $HOME/.local/share/debops directory, but it can also be
# used on locally installed playbooks and roles in current directory.
#
# Short usage guide:
#
# - 'debops-update' will check if we are in DebOps project directory ('.debops.cfg' exists)
#     * if yes, it will check if 'debops-playbooks/playbooks/site.yml' exists
#       * if yes, update playbooks and roles in $PWD
#     * if no, check if DebOps playbooks are installed in known places, like ~/.local/share/debops
#       * if yes, update playbooks in a place that they are installed at
#       * if no, install DebOps playbooks in ~/.local/share/debops/debops-playbooks
#
# - 'debops-update path/to/dir' will check if specified directory exists
#     * if no, create it
#     * if yes, check if DebOps playbooks are installed at $path/debops-playbooks
#       * if yes, update them
#       * if no, install DebOps playbooks at $path/debops-playbooks


set -e

# ---- Global constants ----

declare -r DEBOPS_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/debops"
declare -r DEBOPS_CONFIG=".debops.cfg"
declare -r DEBOPS_INVENTORY="inventory"
declare -r SCRIPT_NAME="$(basename ${0})"


# ---- Configuration variables ----

# Paths to look through if '.debops.cfg' is found in local directory
DEBOPS_PLAYBOOKS_PATHS=(
  "${PWD}/debops-playbooks"
)

# Paths to look through if local install is not found
DEBOPS_PLAYBOOKS_INSTALL_PATHS=(
  "${DEBOPS_DATA_HOME}/debops-playbooks"
  "/usr/local/share/debops/debops-playbooks"
  "/usr/share/debops/debops-playbooks"
)

# Default installation directory
DEBOPS_DEFAULT_INSTALL_PATH="${DEBOPS_DATA_HOME}/debops-playbooks"

# Default subdirectory where playbooks are stored, relative to the DebOps
# playbooks directory
DEBOPS_PLAYBOOK_DIR="playbooks"

# Default site.yml playbook to look for
DEBOPS_SITE_PLAYBOOK="${DEBOPS_PLAYBOOK_DIR}/site.yml"

# Default uri of DebOps
DEBOPS_GIT_URI="github.com/debops"

# Default git sources for debops-playbooks
DEBOPS_PLAYBOOKS_GIT_URI="https://${DEBOPS_GIT_URI}/debops-playbooks"

# Default slug prefix for roles
DEBOPS_GIT_ROLE_PREFIX="ansible-"

# Ansible Galaxy requirements file to use by default to download or update
DEBOPS_GALAXY_REQUIREMENTS="galaxy/requirements.txt"

# Default Ansible Galaxy user account name
DEBOPS_GALAXY_ACCOUNT="debops"

# Path to install roles, relative to debops-playbooks repository
DEBOPS_ROLES_PATH="${DEBOPS_PLAYBOOK_DIR}/roles"


# ---- Functions ----

# Find specified file or directory in parent dir (if not specified, finds $DEBOPS_CONFIG)
# Source: https://unix.stackexchange.com/questions/13464
find_up () {
  local name=${1:-$DEBOPS_CONFIG}
  local slashes="${PWD//[^\/]/}"
  local directory="${PWD}"

  for (( n=${#slashes}; n>0; --n )) ; do
    test -e "${directory}/${name}" && echo "$(readlink -f ${directory}/${name})" && return
    directory="$directory/.."
  done
}

# Display error message and exit
error_msg () {
  local severity="${2:-Error}"
  local message="${1}"

  echo >&2 "${SCRIPT_NAME}: ${severity}: ${message}"
  set +e
  [[ "${severity}" == "Error" ]] && exit 1
  set -e
}

# Check if required commands exist
require_commands () {
  for name in ${@} ; do
    if ! type ${name} > /dev/null 2>&1 ; then
      error_msg "${name}: command not found"
    fi
  done
}

# Efficiently fetch or clone a role
fetch_or_clone_role() {
  local roles_path="${1}"
  local requirements_path="${2}"

  # Store the contents of the requirements.txt file into an array
  IFS=$"\r\n" GLOBIGNORE="*" :; local requirements=($(<${requirements_path}))

  # Output the number of found roles
  local roles_total="${#requirements[@]}"

  for role in "${!requirements[@]}"; do
    # Parse the requirements.txt file to extract the role name and version
    local role_name="$(awk -F',' '{print $1}' <<< ${requirements[$role]} | sed "s/${DEBOPS_GALAXY_ACCOUNT}.//g")"
    local role_version="$(awk -F',' '{print $2}' <<< ${requirements[$role]})"

    # Make sure to normalize empty versions
    if [ -z ${role_version} ]; then
      role_version="master"
    fi

    local galaxy_name="${DEBOPS_GALAXY_ACCOUNT}.${role_name}"
    local remote_uri="${DEBOPS_GIT_URI}/${DEBOPS_GIT_ROLE_PREFIX}"

    # Check if the remote repository exists by evaluating its return code
    local repo_status="$(git ls-remote git://${remote_uri}${role_name} HEAD &> /dev/null ; echo "$?")"

    # Adjust the role's repository name if it cannot be found with the normal name
    if [ ${repo_status} = "128" ]; then
      role_name="role-${role_name}"
    fi

    # Add 1 to the role index because we want to count from 1, not 0
    role="$((role + 1))"

    # Custom message to show the progress
    local progress_label="'https://${remote_uri}${role_name}' [$role_version] ($role/$roles_total)"

    # Do we want to update or clone the role?
    local role_destination="${roles_path}/${galaxy_name}"

    if [ -d ${role_destination} ]; then
      # Move into the role's directory
      cd ${role_destination}

      echo "Updating ${progress_label}"

      # Parse out the head branch name
      local head_branch_path="$(cat .git/HEAD | cut -d" " -f2)"
      local head_branch="$(basename ${head_branch_path})"

      # Parse out the current sha
      local current_sha="$(cat .git/refs/heads/${head_branch} | awk '{print $1}')"

      # Fetch it silently and store the new sha
      git fetch --quiet
      local fetch_sha="$(cat .git/FETCH_HEAD | awk '{print $1}')"

      # Only merge if the shas are different
      if [ ! ${current_sha} = ${fetch_sha} ]; then
        git merge ${fetch_sha}
        echo
      fi

      # Move back to the initial directory
      cd - > /dev/null
    else
      echo "Installing ${progress_label}"

      # We could half the time by using git:// instead of https:// but then we
      # lose the security benefits of https
      git clone --quiet --branch ${role_version} https://${remote_uri}${role_name} ${role_destination}
    fi
  done
}


# ---- DebOps environment setup ----

# Find DebOps configuration file
debops_config="$(find_up)"

# Find root of the DebOps project dir
[ -n "${debops_config}" ] && debops_root="$(dirname ${debops_config})"

# Source DebOps configuration file
[ -n "${debops_config}" ] && [ -r ${debops_config} ] && source ${debops_config}


# ---- Main script ----

# Check if required commands are available
for name in git ; do
  if ! type ${name} > /dev/null 2>&1 ; then
    echo >&2 "${SCRIPT_NAME}: Error: ${name}: command not found" ; exit 1
  fi
done

# Select default installation directory
debops_install_path="${DEBOPS_DEFAULT_INSTALL_PATH}"

# Check if user specified a directory as a paramter, if yes, use it as
# a project directory and clone DebOps playbooks inside
project="${1}"
if [ -n "${project}" ] ; then

  # If it's a new project, create the directory for it
  if [ ! -e ${project} ] ; then
    echo "Creating project directory in ${project}"
    mkdir -p ${project}
  fi

  # Make sure that playbooks and roles will be installed in project directory if
  # it's specified
  debops_install_path="${project}/debops-playbooks"

  # If playbooks already are installed in specified location, set them as
  # currently used for eventual update
  if [ -f ${project}/debops-playbooks/${DEBOPS_SITE_PLAYBOOK} ] ; then
    debops_playbooks="${project}/debops-playbooks"
  fi

# If there's no project specified, look for playbooks in known locations
else

  # Check if playbooks are installed in local directory
  if [ -z "${debops_playbooks}" ] ;then
    if [ -f ${PWD}/${DEBOPS_CONFIG} ] ; then
      for playbook_path in "${DEBOPS_PLAYBOOKS_PATHS[@]}" ; do
        if [ -f ${playbook_path}/${DEBOPS_SITE_PLAYBOOK} ] ; then
          debops_playbooks="${playbook_path}"
          break
        fi
      done
    fi
  fi

  # If playbooks have not been found in local directory, look for them in known
  # locations
  if [ -z "${debops_playbooks}" ] ; then
    for playbook_path in "${DEBOPS_PLAYBOOKS_INSTALL_PATHS[@]}" ; do
      if [ -f ${playbook_path}/${DEBOPS_SITE_PLAYBOOK} ] ; then
        debops_playbooks="${playbook_path}"
        break
      fi
    done
  fi

fi


# ---- Create or update the playbooks and roles  ----

# Playbooks have not been found, at this point assume playbooks are not
# installed. Install them in user home directory
if [ -z "${debops_playbooks}" ] ; then
  echo "DebOps playbooks have not been found, installing in ${debops_install_path}"
  echo

  # Download main debops-playbooks repository
  git clone --quiet ${DEBOPS_PLAYBOOKS_GIT_URI} ${debops_install_path}

  pushd ${debops_install_path} > /dev/null
  mkdir -p ${DEBOPS_ROLES_PATH}
  fetch_or_clone_role "${DEBOPS_ROLES_PATH}" "${DEBOPS_GALAXY_REQUIREMENTS}"
  popd > /dev/null
else
  echo "DebOps playbooks have been found in ${debops_playbooks}"

  # Go to the playbook directory and get latest updates
  pushd ${debops_playbooks} > /dev/null
  git pull
  echo
  fetch_or_clone_role "${DEBOPS_ROLES_PATH}" "${DEBOPS_GALAXY_REQUIREMENTS}"
  popd > /dev/null
fi
