#!/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" APP="$1"; DOKKU_APP_CONTAINER_ID="$2"; DOKKU_APP_CONTAINER_TYPE="$3"; DOKKU_APP_LISTEN_PORT="$4"; DOKKU_APP_LISTEN_IP="$5" if [[ -z "$DOKKU_APP_LISTEN_PORT" ]] && [[ -f "$DOKKU_ROOT/$APP/PORT" ]]; then DOKKU_APP_LISTEN_PORT=$(< "$DOKKU_ROOT/$APP/PORT") fi if [[ -z "$DOKKU_APP_LISTEN_IP" ]] && [[ -f "$DOKKU_ROOT/$APP/IP" ]]; then DOKKU_APP_LISTEN_IP=$(< "$DOKKU_ROOT/$APP/IP") fi if [[ -z "$DOKKU_APP_CONTAINER_ID" ]]; then DOKKU_APP_CIDS=( $(get_app_container_ids "$APP") ) 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_checks_enabled "$APP")" == "false" ]];then dokku_log_info2_quiet "Zero downtime checks for app ($APP) have been disabled. moving on..." exit 0 fi # Wait this many seconds (default 5) for server to start before running checks. WAIT="${DOKKU_CHECKS_WAIT:-5}" # Wait this many seconds (default 30) for each response. TIMEOUT="${DOKKU_CHECKS_TIMEOUT:-30}" # use this number of retries for checks ATTEMPTS="${DOKKU_CHECKS_ATTEMPTS:-5}" # try to copy CHECKS from container if not in APP dir & quit gracefully if it doesn't exist # docker cp exits with status 1 when run as non-root user when it tries to chown the file # after successfully copying the file. Thus, we suppress stderr. # ref: https://github.com/dotcloud/docker/issues/3986 TMPDIR=$(mktemp -d /tmp/dokku_CHECKS.XXXXX) docker cp $DOKKU_APP_CONTAINER_ID:/app/CHECKS "$TMPDIR" 2> /dev/null || true FILENAME=${TMPDIR}/CHECKS cleanup() { rm -rf "$TMPDIR" dokku_log_info2_quiet "$APP container output:" dokku_container_log_verbose_quiet $DOKKU_APP_CONTAINER_ID dokku_log_info2_quiet "end $APP container output" } trap cleanup EXIT if [[ ! -s "${TMPDIR}/CHECKS" ]] || [[ "$DOKKU_APP_CONTAINER_TYPE" != "web" ]]; then # We allow custom check for web instances only if [[ "$DOKKU_APP_CONTAINER_TYPE" == "web" ]]; then dokku_log_verbose "For more efficient zero downtime deployments, create a file CHECKS." dokku_log_verbose "See http://dokku.viewdocs.io/dokku/checks-examples.md for examples" dokku_log_verbose "CHECKS file not found in container: Running simple container check..." else dokku_log_verbose "Non web container detected: Running default container check..." fi rm -rf "$TMPDIR" # simple default check to see if the container stuck around DOKKU_DEFAULT_CHECKS_WAIT="${DOKKU_DEFAULT_CHECKS_WAIT:-10}" dokku_log_info1 "Waiting for $DOKKU_DEFAULT_CHECKS_WAIT seconds ..." sleep "$DOKKU_DEFAULT_CHECKS_WAIT" ! (is_container_running $DOKKU_APP_CONTAINER_ID) && dokku_log_fail "App container failed to start!!" container_restarts=$(docker inspect -f "{{ .RestartCount }}" $DOKKU_APP_CONTAINER_ID) if [[ $container_restarts -ne 0 ]]; then docker stop "$DOKKU_APP_CONTAINER_ID" || true dokku_log_fail "App container failed to start!!" fi dokku_log_info1 "Default container check successful!" && exit 0 fi # Reads name/value pairs, sets the WAIT and TIMEOUT variables exec < "$FILENAME" while read -r LINE; do # Name/value pair if [[ "$LINE" =~ ^.+= ]]; then TRIM=${LINE%#*} NAME=${TRIM%=*} VALUE=${TRIM#*=} [[ "$NAME" = "WAIT" ]] && WAIT=$VALUE [[ "$NAME" = "TIMEOUT" ]] && TIMEOUT=$VALUE [[ "$NAME" = "ATTEMPTS" ]] && ATTEMPTS=$VALUE fi done ATTEMPT=0 until [[ $SUCCESS == 1 || $ATTEMPT -ge $ATTEMPTS ]] do FAILEDCHECKS=0 ATTEMPT=$(( ATTEMPT + 1 )) dokku_log_info1 "Attempt $ATTEMPT/$ATTEMPTS Waiting for $WAIT seconds ..." sleep "$WAIT" # -q do not use .curlrc (must come first) # --compressed Test compression handled correctly # --fail Fail on server errors (4xx, 5xx) # --location Follow redirects CURL_OPTIONS="-q --compressed --fail --location --max-time $TIMEOUT" # Set X-Forwarded-Proto header if TLS is enabled. SSL="$DOKKU_ROOT/$APP/tls"; WILDCARD_SSL="$DOKKU_ROOT/tls" if [[ -e "$SSL/server.crt" && -e "$SSL/server.key" ]] || [[ -e "$WILDCARD_SSL/server.crt" && -e "$WILDCARD_SSL/server.key" ]]; then CURL_OPTIONS+=" -H X-Forwarded-Proto:https" fi exec < "$FILENAME" 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 URL_PROTOCOL=${CHECK_URL%:*} CHECK_URL=${CHECK_URL#*:} else 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 UNPREFIXED=${CHECK_URL#//} URL_HOSTNAME=${UNPREFIXED%%/*} URL_PATHNAME=${UNPREFIXED#$URL_HOSTNAME} HEADERS="-H Host:$URL_HOSTNAME" else URL_HOSTNAME=localhost URL_PATHNAME=$CHECK_URL fi # This URL will show up in the messages LOG_URL="$URL_PROTOCOL://$URL_HOSTNAME$URL_PATHNAME" # And how we formulate the CURL request CURL_ARGS="$CURL_OPTIONS $URL_PROTOCOL://$DOKKU_APP_LISTEN_IP:$DOKKU_APP_LISTEN_PORT$URL_PATHNAME $HEADERS" dokku_log_verbose "CHECKS expected result:" dokku_log_verbose "$LOG_URL => \"$EXPECTED\"" [[ $DOKKU_TRACE ]] && dokku_log_verbose "$ curl $CURL_ARGS" # Capture HTTP response or CURL error message # shellcheck disable=SC2086 if OUTPUT=$(curl -# $CURL_ARGS 2>&1); then # OUTPUT contains the HTTP response if [[ ! "$OUTPUT" =~ $EXPECTED ]]; then dokku_log_warn "$LOG_URL: expected to but did not find: \"$EXPECTED\"" FAILEDCHECKS=$(( FAILEDCHECKS + 1 )) fi else # Failed to connect/no response, OUTPUT contains error message dokku_log_warn "$OUTPUT" FAILEDCHECKS=$(( FAILEDCHECKS + 1 )) fi done if [[ $FAILEDCHECKS -gt 0 ]]; then dokku_log_warn "Check attempt $ATTEMPT/$ATTEMPTS failed." SUCCESS=0 else SUCCESS=1 fi done if [[ $FAILEDCHECKS -gt 0 ]]; then dokku_log_fail "Could not start due to $FAILEDCHECKS failed checks." exit 1 fi dokku_log_info1 "All checks successful!"