#!/bin/bash
# SOURCE: GRUNTWORKS 
# This script can be used to install Consul and its dependencies. This script has been tested with the following
# operating systems:
#
# 1. Ubuntu 16.04
# 1. Ubuntu 18.04
# 1. Amazon Linux 2

set -e

readonly DEFAULT_INSTALL_PATH="/opt/consul"
readonly DEFAULT_CONSUL_USER="consul"
readonly DOWNLOAD_PACKAGE_PATH="/tmp/consul.zip"

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SYSTEM_BIN_DIR="/usr/local/bin"

readonly SCRIPT_NAME="$(basename "$0")"

function print_usage {
  echo
  echo "Usage: install-consul [OPTIONS]"
  echo
  echo "This script can be used to install Consul and its dependencies. This script has been tested with Ubuntu 16.04 and Amazon Linux 2."
  echo
  echo "Options:"
  echo
  echo -e "  --version\t\tThe version of Consul to install. Optional if download-url is provided."
  echo -e "  --download-url\t\tUrl to exact Consul package to be installed. Optional if version is provided."
  echo -e "  --path\t\tThe path where Consul should be installed. Optional. Default: $DEFAULT_INSTALL_PATH."
  echo -e "  --user\t\tThe user who will own the Consul install directories. Optional. Default: $DEFAULT_CONSUL_USER."
  echo -e "  --ca-file-path\t\tPath to a PEM-encoded certificate authority used to encrypt and verify authenticity of client and server connections. Will be installed under <install-path>/tls/ca."
  echo -e "  --cert-file-path\t\tPath to a PEM-encoded certificate, which will be provided to clients or servers to verify the agent's authenticity. Will be installed under <install-path>/tls. Must be provided along with --key-file-path."
  echo -e "  --key-file-path\t\tPath to a PEM-encoded private key, used with the certificate to verify the agent's authenticity. Will be installed under <install-path>/tls. Must be provided along with --cert-file-path"
  echo
  echo "Example:"
  echo
  echo "  install-consul --version 1.2.2"
}

function log {
  local -r level="$1"
  local -r message="$2"
  local -r timestamp=$(date +"%Y-%m-%d %H:%M:%S")
  >&2 echo -e "${timestamp} [${level}] [$SCRIPT_NAME] ${message}"
}

function log_info {
  local -r message="$1"
  log "INFO" "$message"
}

function log_warn {
  local -r message="$1"
  log "WARN" "$message"
}

function log_error {
  local -r message="$1"
  log "ERROR" "$message"
}

function assert_not_empty {
  local -r arg_name="$1"
  local -r arg_value="$2"

  if [[ -z "$arg_value" ]]; then
    log_error "The value for '$arg_name' cannot be empty"
    print_usage
    exit 1
  fi
}

function assert_either_or {
  local -r arg1_name="$1"
  local -r arg1_value="$2"
  local -r arg2_name="$3"
  local -r arg2_value="$4"

  if [[ -z "$arg1_value" && -z "$arg2_value" ]]; then
    log_error "Either the value for '$arg1_name' or '$arg2_name' must be passed, both cannot be empty"
    print_usage
    exit 1
  fi
}

# A retry function that attempts to run a command a number of times and returns the output
function retry {
  local -r cmd="$1"
  local -r description="$2"

  for i in $(seq 1 5); do
    log_info "$description"

    # The boolean operations with the exit status are there to temporarily circumvent the "set -e" at the
    # beginning of this script which exits the script immediatelly for error status while not losing the exit status code
    output=$(eval "$cmd") && exit_status=0 || exit_status=$?
    log_info "$output"
    if [[ $exit_status -eq 0 ]]; then
      echo "$output"
      return
    fi
    log_warn "$description failed. Will sleep for 10 seconds and try again."
    sleep 10
  done;

  log_error "$description failed after 5 attempts."
  exit $exit_status
}

function has_yum {
  [ -n "$(command -v yum)" ]
}

function has_apt_get {
  [ -n "$(command -v apt-get)" ]
}

function install_dependencies {
  log_info "Installing dependencies"

  if $(has_apt_get); then
    sudo apt-get update -y
    sudo apt-get install -y awscli curl unzip jq
  elif $(has_yum); then
    sudo yum update -y
    sudo yum install -y aws curl unzip jq
  else
    log_error "Could not find apt-get or yum. Cannot install dependencies on this OS."
    exit 1
  fi
}

function user_exists {
  local -r username="$1"
  id "$username" >/dev/null 2>&1
}

function create_consul_user {
  local -r username="$1"

  if $(user_exists "$username"); then
    echo "User $username already exists. Will not create again."
  else
    log_info "Creating user named $username"
    sudo useradd "$username"
  fi
}

function create_consul_install_paths {
  local -r path="$1"
  local -r username="$2"

  log_info "Creating install dirs for Consul at $path"
  sudo mkdir -p "$path"
  sudo mkdir -p "$path/bin"
  sudo mkdir -p "$path/config"
  sudo mkdir -p "$path/data"
  sudo mkdir -p "$path/tls/ca"

  log_info "Changing ownership of $path to $username"
  sudo chown -R "$username:$username" "$path"
}

function fetch_binary {
  local -r version="$1"
  local download_url="$2"

  if [[ -z "$download_url" && -n "$version" ]];  then
    download_url="https://releases.hashicorp.com/consul/${version}/consul_${version}_linux_amd64.zip"
  fi

  retry \
    "curl -o '$DOWNLOAD_PACKAGE_PATH' '$download_url' --location --silent --fail --show-error" \
    "Downloading Consul to $DOWNLOAD_PACKAGE_PATH"
}

function install_binary {
  local -r install_path="$1"
  local -r username="$2"

  local -r bin_dir="$install_path/bin"
  local -r consul_dest_path="$bin_dir/consul"
  local -r run_consul_dest_path="$bin_dir/run-consul"

  unzip -d /tmp "$DOWNLOAD_PACKAGE_PATH"

  log_info "Moving Consul binary to $consul_dest_path"
  sudo mv "/tmp/consul" "$consul_dest_path"
  sudo chown "$username:$username" "$consul_dest_path"
  sudo chmod a+x "$consul_dest_path"

  local -r symlink_path="$SYSTEM_BIN_DIR/consul"
  if [[ -f "$symlink_path" ]]; then
    log_info "Symlink $symlink_path already exists. Will not add again."
  else
    log_info "Adding symlink to $consul_dest_path in $symlink_path"
    sudo ln -s "$consul_dest_path" "$symlink_path"
  fi

  log_info "Copying Consul run script to $run_consul_dest_path"
  sudo cp "$SCRIPT_DIR/run-consul" "$run_consul_dest_path"
  sudo chown "$username:$username" "$run_consul_dest_path"
  sudo chmod a+x "$run_consul_dest_path"
}

function install_tls_certificates {
  local -r path="$1"
  local -r user="$2"
  local -r ca_file_path="$3"
  local -r cert_file_path="$4"
  local -r key_file_path="$5"

  local -r consul_tls_certs_path="$path/tls"
  local -r ca_certs_path="$consul_tls_certs_path/ca"

  log_info "Moving TLS certs to $consul_tls_certs_path and $ca_certs_path"

  sudo mkdir -p "$ca_certs_path"
  sudo mv "$ca_file_path" "$ca_certs_path/"
  sudo mv "$cert_file_path" "$consul_tls_certs_path/"
  sudo mv "$key_file_path" "$consul_tls_certs_path/"

  sudo chown -R "$user:$user" "$consul_tls_certs_path/"
  sudo find "$consul_tls_certs_path/" -type f -exec chmod u=r,g=,o= {} \;
}

function install {
  local version=""
  local download_url=""
  local path="$DEFAULT_INSTALL_PATH"
  local user="$DEFAULT_CONSUL_USER"
  local ca_file_path=""
  local cert_file_path=""
  local key_file_path=""

  while [[ $# > 0 ]]; do
    local key="$1"

    case "$key" in
      --version)
        version="$2"
        shift
        ;;
      --download-url)
        download_url="$2"
        shift
        ;;
      --path)
        path="$2"
        shift
        ;;
      --user)
        user="$2"
        shift
        ;;
      --ca-file-path)
        assert_not_empty "$key" "$2"
        ca_file_path="$2"
        shift
        ;;
      --cert-file-path)
        assert_not_empty "$key" "$2"
        cert_file_path="$2"
        shift
        ;;
      --key-file-path)
        assert_not_empty "$key" "$2"
        key_file_path="$2"
        shift
        ;;
      --help)
        print_usage
        exit
        ;;
      *)
        log_error "Unrecognized argument: $key"
        print_usage
        exit 1
        ;;
    esac

    shift
  done

  assert_either_or "--version" "$version" "--download-url" "$download_url"
  assert_not_empty "--path" "$path"
  assert_not_empty "--user" "$user"

  log_info "Starting Consul install"

  install_dependencies
  create_consul_user "$user"
  create_consul_install_paths "$path" "$user"

  fetch_binary "$version" "$download_url"
  install_binary "$path" "$user"

  if [[ -n "$ca_file_path" || -n "$cert_file_path" || -n "$key_file_path" ]]; then
    install_tls_certificates "$path" "$user" "$ca_file_path" "$cert_file_path" "$key_file_path"
  fi

  if command -v consul; then
    log_info "Consul install complete!";
  else
    log_info "Could not find consul command. Aborting.";
    exit 1;
  fi
}

install "$@"
