#!/usr/bin/env bash

# Hook to check server against list of checks specified in CHECKS file.
#
# The CHECKS file may contain empty lines, comments (lines starting with #),
# settings (NAME=VALUE) and check instructions.
#
# The format of a check instruction is a path, optionally followed by the
# expected content.  For example:
#
#   /                       My Amazing App
#   /stylesheets/index.css  .body
#   /scripts/index.js       $(function()
#   /images/logo.png
#
# To check an application that supports multiple hostnames, use relative URLs
# that include the hostname, for example:
#
#  //admin.example.com     Admin Dashboard
#  //static.example.com/logo.png
#
# You can also specify the protocol to explicitly check HTTPS requests.
#
# The default behavior is to wait for 5 seconds before running the first check,
# and timeout each check to 30 seconds.
#
# By default, checks will be retried 5 times.

# You can change these by setting WAIT, TIMEOUT and ATTEMPTS to different values, for
# example:
#
#   WAIT=30     # Wait 1/2 minute
#   TIMEOUT=60  # Timeout after a minute
#   ATTEMPTS=10  # retry checks 10 times
#
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/checks/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"

trigger-scheduler-docker-local-check-deploy() {
  declare desc="scheduler-docker-local check-deploy plugin trigger"
  declare trigger="check-deploy"
  declare APP="$1" DOKKU_APP_CONTAINER_ID="$2" DOKKU_APP_CONTAINER_TYPE="$3" DOKKU_APP_LISTEN_PORT="$4" DOKKU_APP_LISTEN_IP="$5" CONTAINER_INDEX="$6"

  local DOKKU_SCHEDULER=$(get_app_scheduler "$APP")
  if [[ "$DOKKU_SCHEDULER" != "docker-local" ]]; then
    return
  fi

  if [[ -z "$DOKKU_APP_LISTEN_PORT" ]] && [[ -f "$DOKKU_ROOT/$APP/PORT" ]]; then
    local DOKKU_APP_LISTEN_PORT=$(<"$DOKKU_ROOT/$APP/PORT")
  fi
  if [[ -z "$DOKKU_APP_LISTEN_IP" ]] && [[ -f "$DOKKU_ROOT/$APP/IP" ]]; then
    local DOKKU_APP_LISTEN_IP=$(<"$DOKKU_ROOT/$APP/IP")
  fi
  if [[ -z "$DOKKU_APP_CONTAINER_ID" ]]; then
    local DOKKU_APP_CIDS=($(get_app_container_ids "$APP"))
    local DOKKU_APP_CONTAINER_ID=${DOKKU_APP_CIDS[0]}
  fi

  # source global and in-app envs to get DOKKU_CHECKS_WAIT and any other necessary vars
  eval "$(config_export global)"
  eval "$(config_export app "$APP")"

  if [[ "$(is_app_proctype_checks_skipped "$APP" "$DOKKU_APP_CONTAINER_TYPE")" == "true" ]]; then
    dokku_log_info2_quiet "Zero downtime checks have been skipped ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    exit 0
  fi

  # Wait this many seconds (default 5) for server to start before running checks.
  local WAIT="${DOKKU_CHECKS_WAIT:-5}"
  # Wait this many seconds (default 30) for each response.
  local TIMEOUT="${DOKKU_CHECKS_TIMEOUT:-30}"
  # use this number of retries for checks
  local ATTEMPTS="${DOKKU_CHECKS_ATTEMPTS:-5}"

  local CHECK_DEPLOY_TMP_WORK_DIR=$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")
  local CHECKS_FILENAME=${CHECK_DEPLOY_TMP_WORK_DIR}/CHECKS
  local IMAGE_TAG="$(get_running_image_tag "$APP")"
  local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
  copy_from_image "$IMAGE" "CHECKS" "$CHECKS_FILENAME" 2>/dev/null || true

  checks_check_deploy_cleanup() {
    declare desc="cleans up CHECK_DEPLOY_TMP_WORK_DIR and print container output"
    local id="$1"
    rm -rf "$CHECK_DEPLOY_TMP_WORK_DIR" &>/dev/null || true
    if [[ $id ]]; then
      dokku_log_info2_quiet "Start of $APP container output ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
      dokku_container_log_verbose_quiet "$id"
      dokku_log_info2_quiet "End of $APP container output ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    fi
  }
  trap "checks_check_deploy_cleanup $DOKKU_APP_CONTAINER_ID" RETURN INT TERM EXIT

  # We allow custom check for web instances only
  if [[ ! -s "${CHECKS_FILENAME}" ]] || [[ "$DOKKU_APP_CONTAINER_TYPE" != "web" ]]; then
    rm -rf "$CHECK_DEPLOY_TMP_WORK_DIR" &>/dev/null || true

    # simple default check to see if the container stuck around
    local DOKKU_DEFAULT_CHECKS_WAIT="${DOKKU_DEFAULT_CHECKS_WAIT:-10}"
    dokku_log_verbose "Waiting for $DOKKU_DEFAULT_CHECKS_WAIT seconds ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    sleep "$DOKKU_DEFAULT_CHECKS_WAIT"

    ! (is_container_status "$DOKKU_APP_CONTAINER_ID" "Running") && dokku_log_fail "App container failed to start ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    local container_restarts="$("$DOCKER_BIN" container inspect --format "{{ .RestartCount }}" "$DOKKU_APP_CONTAINER_ID")"
    if [[ $container_restarts -ne 0 ]]; then
      "$DOCKER_BIN" container update --restart=no "$DOKKU_APP_CONTAINER_ID" &>/dev/null || true
      "$DOCKER_BIN" container stop "$DOKKU_APP_CONTAINER_ID" || true
      dokku_log_fail "App container failed to start ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    fi

    trap - EXIT
    dokku_log_verbose "Default container check successful ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)" && exit 0
  fi

  local NEW_CHECKS_FILENAME="${CHECK_DEPLOY_TMP_WORK_DIR}/NEW_CHECKS"
  template_checks() {
    eval "$(config_export app "$APP" --merged)"
    sigil -f "$CHECKS_FILENAME" | cat -s >"$NEW_CHECKS_FILENAME"
  }

  template_checks
  CHECKS_FILENAME="$NEW_CHECKS_FILENAME"

  # Reads name/value pairs, sets the WAIT and TIMEOUT variables
  exec <"$CHECKS_FILENAME"
  local line
  local NAME
  local VALUE
  while read -r line; do
    line=$(strip_inline_comments "$line")
    # Name/value pair
    if [[ "$line" =~ ^.+= ]]; then
      NAME=${line%=*}
      VALUE=${line#*=}
      [[ "$NAME" == "WAIT" ]] && local WAIT=$VALUE
      [[ "$NAME" == "TIMEOUT" ]] && local TIMEOUT=$VALUE
      [[ "$NAME" == "ATTEMPTS" ]] && local ATTEMPTS=$VALUE
    fi
  done

  exec <"$CHECKS_FILENAME"
  local CHECK_URL
  local EXPECTED
  local FAILEDCHECKS=0
  while read -r CHECK_URL EXPECTED; do
    # Ignore empty lines and lines starting with #
    # shellcheck disable=SC1001
    [[ -z "$CHECK_URL" || "$CHECK_URL" =~ ^\# ]] && continue
    # Ignore if it's not a URL in a supported format
    # shellcheck disable=SC1001
    ! [[ "$CHECK_URL" =~ ^(http(s)?:)?\/.* ]] && continue

    if [[ "$CHECK_URL" =~ ^https?: ]]; then
      local URL_PROTOCOL=${CHECK_URL%:*}
      local CHECK_URL=${CHECK_URL#*:}
    else
      local URL_PROTOCOL="http"
    fi

    if [[ "$CHECK_URL" =~ ^//.+ ]]; then
      # To test a URL with specific host name, we still make request to localhost,
      # but we set Host header to $SEND_HOST.
      #
      # The pattern is
      #   //SEND_HOST/PATHNAME
      local UNPREFIXED=${CHECK_URL#//}
      local URL_HOSTNAME=${UNPREFIXED%%/*}
      local URL_PATHNAME=${UNPREFIXED#$URL_HOSTNAME}

      local HEADERS="-H Host:$URL_HOSTNAME"
    else
      local URL_HOSTNAME=localhost
      local URL_PATHNAME=$CHECK_URL
    fi

    # -q           Do not use .curlrc (must come first)
    # --compressed Test compression handled correctly
    # --fail       Fail on server errors (4xx, 5xx)
    # --location   Follow redirects
    # --noproxy    Do not use http_proxy env variable
    local CURL_OPTIONS="-q --compressed --fail --location --noproxy $DOKKU_APP_LISTEN_IP --max-time $TIMEOUT"

    # Set X-Forwarded-Proto header if TLS is enabled.
    local SSL="$DOKKU_ROOT/$APP/tls"
    if [[ -e "$SSL/server.crt" && -e "$SSL/server.key" ]]; then
      local CURL_OPTIONS+=" -H X-Forwarded-Proto:https"
    fi

    # LOG_URL is used in verbose output below
    local LOG_URL="$URL_PROTOCOL://$URL_HOSTNAME$URL_PATHNAME"
    # CURL_ARGS includes the behinds the scenes options for hitting the container service directly
    local CURL_ARGS="$CURL_OPTIONS $URL_PROTOCOL://$DOKKU_APP_LISTEN_IP:$DOKKU_APP_LISTEN_PORT$URL_PATHNAME $HEADERS"

    dokku_log_verbose "CHECKS expected result: $LOG_URL => \"$EXPECTED\" ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"

    local ATTEMPT=0
    local SUCCESS=0
    until [[ $SUCCESS == 1 || $ATTEMPT -ge $ATTEMPTS ]]; do
      local ATTEMPT=$((ATTEMPT + 1))
      dokku_log_verbose "Attempt $ATTEMPT/$ATTEMPTS. Waiting for $WAIT seconds ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
      sleep "$WAIT"

      # Capture HTTP response or CURL error message
      # shellcheck disable=SC2086
      if OUTPUT=$(curl -# $CURL_ARGS 2>&1); then
        # OUTPUT contains the HTTP response
        # shellcheck disable=SC2076
        if [[ "$OUTPUT" =~ "$EXPECTED" ]]; then
          SUCCESS=1
          break
        fi
        dokku_log_warn "$LOG_URL: expected to but did not find: \"$EXPECTED\" ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
      else
        # Failed to connect/no response, OUTPUT contains error message
        dokku_log_warn "$OUTPUT"
      fi

      dokku_log_warn "Check attempt $ATTEMPT/$ATTEMPTS failed ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    done

    if [[ $SUCCESS -ne 1 ]]; then
      FAILEDCHECKS=$((FAILEDCHECKS + 1))
    fi
  done

  if [[ $FAILEDCHECKS -gt 0 ]]; then
    dokku_log_fail "Could not start due to $FAILEDCHECKS failed checks ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
    exit 1
  fi

  trap - EXIT
  dokku_log_verbose "All checks successful ($DOKKU_APP_CONTAINER_TYPE.$CONTAINER_INDEX)"
}

trigger-scheduler-docker-local-check-deploy "$@"
