#!/bin/bash
set -ex -o pipefail

# required settings
NODE_NAME_PREFIX="HA-NODE-" # prefix the node name with the role of the node, e.g. webserver or rails-app-server

NODE_NAME="${NODE_NAME_PREFIX}$(curl --silent --show-error --retry 3 http://169.254.169.254/latest/meta-data/instance-id)" # this uses the EC2 instance ID as the node name
CHEF_SERVER_NAMES=("mainserver backupserver") # enter in your Chef server names
CHEF_SERVER_ENDPOINTS=("mainserver-mxxxxxxxxxxxxx.us-east-1.opsworks-cm.io backupserver-xxxxxxxxx.us-east-1.opsworks-cm.io") # enter in your Chef server endpoints
REGION="us-east-1" # Region of your Chef Server (Choose one of our supported regions - us-east-1, us-east-2, us-west-1, us-west-2, eu-central-1, eu-west-1, ap-northeast-1, ap-southeast-1, ap-southeast-2)
ROOT_CA_URL="https://opsworks-cm-${REGION}-prod-default-assets.s3.amazonaws.com/misc/opsworks-cm-ca-2016-root.pem"
CHEF_CLIENT_VERSION="14.11.21" # latest if empty
CHEF_CLIENT_LOG_LOCATION="/var/log/chef-client.log"
CHEF_CLIENT_OPTS="-L ${CHEF_CLIENT_LOG_LOCATION}"

# optional settings
CHEF_ORGANIZATION="default" # AWS OpsWorks for Chef Server always creates the organization "default"
NODE_ENVIRONMENT="" # E.g. development, staging, onebox, ...
RUN_LIST="" # optional, only when not using Policy
JSON_ATTRIBUTES="" # optional, path to a json file for your node object

# extra optional settings
AWS_CLI_EXTRA_OPTS=()
CFN_SIGNAL=""

# required settings to define your node configuration

# In the example of our Starterkit we are using a policy for your node defined in Policyfile.rb
# To follow our example from the README.md leave RUN_LIST empty

mkdir -p "/etc/chef"
touch "/etc/chef/unassociated_servers.txt" # tracking servers that aren't available while bootstrapping

# when run-list is empty we will use json attributes according to our Policyfile.rb
if [ -z $RUN_LIST ]; then
  (cat <<-JSON
    {
      "name": "${NODE_NAME}",
      "chef_environment": "${NODE_ENVIRONMENT}",
      "policy_name": "opsworks-demo-webserver",
      "policy_group": "opsworks-demo"
    }
JSON
) | sed 's/: ""/: null/g' > /etc/chef/client-attributes.json
fi

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

AWS_CLI_TMP_FOLDER=$(mktemp --directory "/tmp/awscli_XXXX")
CHEF_CA_PATH="/etc/chef/opsworks-cm-ca-2016-root.pem"

prepare_os_packages() {
  local OS=`uname -a`
  if [[ ${OS} = *"Ubuntu"* ]]; then
    apt update && DEBIAN_FRONTEND=noninteractive apt -y upgrade
    apt -y install unzip python python-pip
    # see: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-helper-scripts-reference.html
    pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
    ln -s /root/aws-cfn-bootstrap-latest/init/ubuntu/cfn-hup /etc/init.d/cfn-hup
    mkdir -p /opt/aws
    ln -s /usr/local/bin /opt/aws/bin
  fi
}

install_aws_cli() {
  # see: http://docs.aws.amazon.com/cli/latest/userguide/installing.html#install-bundle-other-os
  pushd "${AWS_CLI_TMP_FOLDER}"
  curl --silent --show-error --retry 3 --location --output "awscli-bundle.zip" "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip"
  unzip "awscli-bundle.zip"
  ./awscli-bundle/install -i "${PWD}"
}

aws_cli() {
  "${AWS_CLI_TMP_FOLDER}/bin/aws" opsworks-cm \
    --region "${REGION}" ${AWS_CLI_EXTRA_OPTS[@]:-} --output text "$@"
}

# Installing a new script to evaluate the accessibility of an OpsWorks for Chef Automate Server
install_opsworks_health_check() {
  curl --silent --show-error --retry 3 --location --output "/etc/chef/opsworks_health_check.rb" "https://opsworkscm-blogposts.s3.eu-central-1.amazonaws.com/health_check.rb"
}

# Extract the client key generation, so it can be reused for multiple servers
generate_client_key() {
  client_key="/etc/chef/client.pem"
  ( umask 077; openssl genrsa -out "${client_key}" 2048 )
}

# Associate node needs a new attribute "server-name", to associate the node to each server in your list
associate_node() {
  aws_cli associate-node \
    --node-name "${NODE_NAME}" \
    --server-name "$1" \
    --engine-attributes \
      "Name=CHEF_AUTOMATE_ORGANIZATION,Value=${CHEF_ORGANIZATION}" \
      "Name=CHEF_AUTOMATE_NODE_PUBLIC_KEY,Value='$(openssl rsa -in "${client_key}" -pubout)'"
}

install_chef_client() {
  # see: https://docs.chef.io/install_omnibus.html
  curl --silent --show-error --retry 3 --location https://omnitruck.chef.io/install.sh | bash -s -- -v "${CHEF_CLIENT_VERSION}"
}

# When you now write the chef client.rb config you will use the opsworks health check script on every single chef client run
# The node will use the first server that is accessible
write_chef_config() {
(cat <<-RUBY
  require_relative "opsworks_health_check"

  #running a health check every time a chef-run is executed
  chef_server_url   "https://#{OpsWorksHealthCheck.active_chef_server_endpoint(%w(${CHEF_SERVER_ENDPOINTS[@]}))}/organizations/${CHEF_ORGANIZATION}"
  node_name         "$NODE_NAME"
  ssl_ca_file       "$CHEF_CA_PATH"
RUBY
) > /etc/chef/client.rb # generic approach, evaluates health for all servers on each chef-client run
}

install_trusted_certs() {
  curl --silent --show-error --retry 3 --location --output "${CHEF_CA_PATH}" ${ROOT_CA_URL}
}

# We also extend the wait_node_associated function to pass through the server-name
wait_node_associated() {
  aws_cli wait node-associated --node-association-status-token "$1" --server-name "$2"
}

track_unassociated_server() {
  echo $1 >> "/etc/chef/unassociated_servers.txt"
  echo "unable to associate to $1"
}

# order of execution of functions
prepare_os_packages
install_aws_cli
install_chef_client
install_opsworks_health_check # installing the new health check script
write_chef_config             # write a chef client.rb that detects the first active server of your list
install_trusted_certs
generate_client_key           # generating one client key for the node this userdata is executed on

# Loop through your servers to associate this node to each of the servers
for server_name in ${CHEF_SERVER_NAMES[@]}
do
  node_association_token=$(associate_node $server_name) || track_unassociated_server $server_name && continue
  wait_node_associated "${node_association_token}" "$server_name" && echo "associated successfully to $server_name"
done

# initial chef-client run to register node

add_node_environment_to_client_opts() {
  if [ ! -z "${NODE_ENVIRONMENT}" ]; then
    CHEF_CLIENT_OPTS+=("-E ${NODE_ENVIRONMENT}");
  fi
}

add_json_attributes_to_client_opts() {
  if [ ! -z "${JSON_ATTRIBUTES}" ]; then
    echo "${JSON_ATTRIBUTES}" > /tmp/chef-attributes.json
    CHEF_CLIENT_OPTS+=("-j /tmp/chef-attributes.json")
  fi
}


# when the run-list is provided we use this for the chef-client run
if [ ! -z "${RUN_LIST}" ]; then
  # use a regular run_list to run chef
  CHEF_CLIENT_OPTS=(-r "${RUN_LIST}")
  add_node_environment_to_client_opts
  add_json_attributes_to_client_opts
else
  # use a node policy following the example of your Starterkit
  CHEF_CLIENT_OPTS=("-j /etc/chef/client-attributes.json")
fi

# initial chef-client run to register node
if [ ! -z "${CHEF_CLIENT_OPTS}" ]; then
  chef-client ${CHEF_CLIENT_OPTS[@]}
fi

touch /tmp/userdata.done
eval ${CFN_SIGNAL}
