#!/usr/bin/env bash set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x has_tty() { if [[ "$(/usr/bin/tty || true)" == "not a tty" ]]; then return 1 else return 0 fi } dokku_apps() { local INSTALLED_APPS=$(find "$DOKKU_ROOT" -follow -maxdepth 1 -mindepth 1 -type d ! -name 'tls' ! -name '.*' -printf "%f\n" | sort) || (dokku_log_fail "You haven't deployed any applications yet") [[ $INSTALLED_APPS ]] && echo "$INSTALLED_APPS" } dokku_log_info1() { echo "-----> $*" } dokku_log_info2() { echo "=====> $*" } dokku_log_info1_quiet() { if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo "-----> $*" else return 0 fi } dokku_log_info2_quiet() { if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo "=====> $*" else return 0 fi } dokku_col_log_info1() { printf "%-6s %-18s %-25s %-25s %-25s\n" "----->" "$@" } dokku_col_log_info1_quiet() { if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then printf "%-6s %-18s %-25s %-25s %-25s\n" "----->" "$@" else return 0 fi } dokku_col_log_info2() { printf "%-6s %-18s %-25s %-25s %-25s\n" "=====>" "$@" } dokku_col_log_info2_quiet() { if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then printf "%-6s %-18s %-25s %-25s %-25s\n" "=====>" "$@" else return 0 fi } dokku_col_log_msg() { printf "%-25s %-25s %-25s %-25s\n" "$@" } dokku_col_log_msg_quiet() { if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then printf "%-25s %-25s %-25s %-25s\n" "$@" else return 0 fi } dokku_log_verbose_quiet() { if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo " $*" else return 0 fi } dokku_log_verbose() { echo " $*" } dokku_log_warn() { echo " ! $*" } dokku_log_fail() { echo "$@" 1>&2 exit 1 } dokku_log_event() { logger -t dokku -i -- "$@" } dokku_log_plugn_trigger_call() { local l_hook l_hook="$1" ; shift dokku_log_event "INVOKED: ${l_hook}( $* ) NAME=$NAME FINGERPRINT=$FINGERPRINT" } dokku_container_log_verbose_quiet() { local CID=$1; shift OIFS=$IFS IFS=$'\n' for line in $(docker logs "$CID" 2>&1); do dokku_log_verbose_quiet "$line" done IFS=$OIFS } verify_app_name() { local APP="$1" [[ ! -n "$APP" ]] && dokku_log_fail "(verify_app_name) APP must not be null" if [[ ! "$APP" =~ ^[a-z].* && ! "$APP" =~ ^[0-9].* ]]; then [[ -d "$DOKKU_ROOT/$APP" ]] && rm -rf "$DOKKU_ROOT/$APP" dokku_log_fail "App name must begin with lowercase alphanumeric character" fi [[ ! -d "$DOKKU_ROOT/$APP" ]] && dokku_log_fail "App $APP does not exist" return 0 } verify_image() { local IMAGE="$1" if (docker inspect "$IMAGE" &>/dev/null); then return 0 else return 1 fi } get_app_image_repo() { # central definition of our image repo pattern local APP="$1"; local IMAGE_REPO="dokku/$APP" [[ -z "$APP" ]] && dokku_log_fail "(get_app_image_repo) APP must not be null" echo "$IMAGE_REPO" } get_app_image_name() { # return image identifier for a given app, tag tuple. validate if tag is presented local APP="$1"; local IMAGE_TAG="$2"; local IMAGE_REPO=$(get_app_image_repo "$APP") [[ -z "$APP" ]] && dokku_log_fail "(get_app_image_name) APP must not be null" if [[ -n "$IMAGE_TAG" ]]; then IMAGE="$IMAGE_REPO:$IMAGE_TAG" verify_image "$IMAGE" || dokku_log_fail "app image ($IMAGE) not found" else IMAGE="$IMAGE_REPO:latest" fi echo "$IMAGE" } get_running_image_tag() { # retrieve current image tag for a given app. returns empty string if no deployed containers are found local APP="$1" [[ ! -n "$APP" ]] && dokku_log_fail "(get_running_image_tag) APP must not be null" verify_app_name "$APP" CIDS=( $(get_app_container_ids "$APP") ) RUNNING_IMAGE_TAG=$(docker inspect -f '{{ .Config.Image }}' ${CIDS[0]} 2>/dev/null | awk -F: '{ print $2 }' || echo '') echo "$RUNNING_IMAGE_TAG" } is_image_herokuish_based() { # circleci can't support --rm as they run lxc in lxc [[ ! -f "/home/ubuntu/.circlerc" ]] && local DOCKER_ARGS="--rm" docker run "$DOKKU_GLOBAL_RUN_ARGS" --entrypoint="/bin/sh" $DOCKER_ARGS "$@" -c "test -f /exec" } is_number() { local NUMBER=$1; local NUM_RE='^[0-9]+$' if [[ $NUMBER =~ $NUM_RE ]]; then return 0 else return 1 fi } is_abs_path() { local TEST_PATH=$1 if [[ "$TEST_PATH" = /* ]]; then return 0 else return 1 fi } parse_args() { for arg in "$@"; do case "$arg" in --quiet) export DOKKU_QUIET_OUTPUT=1 ;; --trace) export DOKKU_TRACE=1 ;; --rm-container|--rm) export DOKKU_RM_CONTAINER=1 ;; --force) export DOKKU_APPS_FORCE_DELETE=1 ;; esac done return 0 } copy_from_image() { local IMAGE="$1"; local SRC_FILE="$2"; local DST_DIR="$3" verify_app_name "$APP" if verify_image "$IMAGE"; then if ! is_abs_path "$SRC_FILE"; then local WORKDIR=$(docker inspect -f '{{.Config.WorkingDir}}' "$IMAGE") [[ -z "$WORKDIR" ]] && WORKDIR=/app SRC_FILE="$WORKDIR/$SRC_FILE" fi CID=$(docker run "$DOKKU_GLOBAL_RUN_ARGS" -d "$IMAGE" bash) docker cp "$CID:$SRC_FILE" "$DST_DIR" docker rm -f "$CID" &> /dev/null else return 1 fi } get_app_container_ids() { local APP="$1"; local CONTAINER_TYPE="$2" verify_app_name "$APP" [[ -f $DOKKU_ROOT/$APP/CONTAINER ]] && DOKKU_CIDS+=$(< "$DOKKU_ROOT/$APP/CONTAINER") if [[ -n "$CONTAINER_TYPE" ]]; then CONTAINER_PATTERN="$DOKKU_ROOT/$APP/CONTAINER.$CONTAINER_TYPE.*" if [[ $CONTAINER_TYPE == *.* ]]; then CONTAINER_PATTERN="$DOKKU_ROOT/$APP/CONTAINER.$CONTAINER_TYPE" [[ ! -f $CONTAINER_PATTERN ]] && echo "" && return 0 fi else CONTAINER_PATTERN="$DOKKU_ROOT/$APP/CONTAINER.*" fi shopt -s nullglob for DOKKU_CID_FILE in $CONTAINER_PATTERN; do DOKKU_CIDS+=" " DOKKU_CIDS+=$(< "$DOKKU_CID_FILE") DOKKU_CIDS+=" " done shopt -u nullglob echo "$DOKKU_CIDS" } get_app_running_container_ids() { local APP=$1 verify_app_name "$APP" ! (is_deployed "$APP") && dokku_log_fail "App $APP has not been deployed" local CIDS=$(get_app_container_ids "$APP") for CID in $CIDS; do local APP_CONTAINER_STATUS=$(docker inspect -f '{{.State.Running}}' "$CID" || true) [[ "$APP_CONTAINER_STATUS" == "true" ]] && local APP_RUNNING_CONTAINER_IDS+="$CID " done echo "$APP_RUNNING_CONTAINER_IDS" } get_cmd_from_procfile() { local APP=$1; local PROC_TYPE=$2; local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE" local name; local command; verify_app_name "$APP" if [[ -f $DOKKU_PROCFILE ]]; then while read line || [[ -n "$line" ]]; do if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then continue fi name="${line%%:*}" command="${line#*:[[:space:]]}" [[ "$name" == "$PROC_TYPE" ]] && echo "$command" && break done < "$DOKKU_PROCFILE" fi } is_deployed() { local APP="$1" if [[ -f "$DOKKU_ROOT/$APP/CONTAINER" ]] || [[ $(ls "$DOKKU_ROOT/$APP"/CONTAINER.* &> /dev/null; echo $?) -eq 0 ]]; then return 0 else return 1 fi } is_container_running () { local CID=$1 local CONTAINER_STATUS=$(docker inspect -f '{{.State.Running}}' "$CID" || true) if [[ "$CONTAINER_STATUS" == "true" ]]; then return 0 else return 1 fi } is_container_status () { local CID=$1 local TEMPLATE="{{.State.$2}}" local CONTAINER_STATUS=$(docker inspect -f "$TEMPLATE" "$CID" || true) if [[ "$CONTAINER_STATUS" == "true" ]]; then return 0 else return 1 fi } is_app_running() { local APP="$1" verify_app_name "$APP" local APP_RUNNING_CONTAINER_IDS=$(get_app_running_container_ids "$APP") if [[ -n "$APP_RUNNING_CONTAINER_IDS" ]]; then return 0 else return 1 fi } release_and_deploy() { local APP="$1"; local IMAGE_TAG="$2"; local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") verify_app_name "$APP" if verify_image "$IMAGE"; then if is_image_herokuish_based "$IMAGE"; then IMAGE_SOURCE_TYPE="herokuish" else IMAGE_SOURCE_TYPE="dockerfile" fi DOKKU_APP_SKIP_DEPLOY="$(dokku config:get "$APP" DOKKU_SKIP_DEPLOY || true)" DOKKU_GLOBAL_SKIP_DEPLOY="$(dokku config:get --global DOKKU_SKIP_DEPLOY || true)" DOKKU_SKIP_DEPLOY=${DOKKU_APP_SKIP_DEPLOY:="$DOKKU_GLOBAL_SKIP_DEPLOY"} dokku_log_info1 "Releasing $APP ($IMAGE)..." dokku release "$APP" "$IMAGE_SOURCE_TYPE" "$IMAGE_TAG" if [[ "$DOKKU_SKIP_DEPLOY" != "true" ]]; then dokku_log_info1 "Deploying $APP ($IMAGE)..." dokku deploy "$APP" "$IMAGE_TAG" dokku_log_info2 "Application deployed:" dokku urls "$APP" | sed "s/^/ /" else dokku_log_info1 "Skipping deployment" fi echo fi } docker_cleanup() { # delete all non-running containers # shellcheck disable=SC2046 docker rm $(docker ps -a -f 'status=exited' -q) &> /dev/null || true # delete all dead containers # shellcheck disable=SC2046 docker rm $(docker ps -a -f 'status=dead' -q) &> /dev/null || true # delete unused images # shellcheck disable=SC2046 docker rmi $(docker images -f 'dangling=true' -q) &> /dev/null & } get_available_port() { while true; do local port=$(shuf -i 1025-65535 -n 1) if ! nc -z 0.0.0.0 "$port"; then echo "$port" return 0 else continue fi done } dokku_auth() { export SSH_USER=${SSH_USER:=$USER} export SSH_NAME=${NAME:="default"} if ! plugn trigger user-auth "$SSH_USER" "$SSH_NAME" "$@" ; then return 1 fi return 0 } _ipv4_regex() { echo "([0-9]{1,3}[\.]){3}[0-9]{1,3}" } _ipv6_regex() { local RE_IPV4="$(_ipv4_regex)" local RE_IPV6="([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" # TEST: 1:2:3:4:5:6:7:8 RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,7}:|" # TEST: 1:: 1:2:3:4:5:6:7:: RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" # TEST: 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" # TEST: 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" # TEST: 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" # TEST: 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" # TEST: 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 RE_IPV6="${RE_IPV6}[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" # TEST: 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 RE_IPV6="${RE_IPV6}:((:[0-9a-fA-F]{1,4}){1,7}|:)|" # TEST: ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: RE_IPV6="${RE_IPV6}fe08:(:[0-9a-fA-F]{1,4}){2,2}%[0-9a-zA-Z]{1,}|" # TEST: fe08::7:8%eth0 fe08::7:8%1 (link-local IPv6 addresses with zone index) RE_IPV6="${RE_IPV6}::(ffff(:0{1,4}){0,1}:){0,1}${RE_IPV4}|" # TEST: ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses) RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}:${RE_IPV4}" # TEST: 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 echo "$RE_IPV6" } get_ipv4_regex() { local RE_IPV4="$(_ipv4_regex)" # Ensure the ip address continues to the end of the line # Fixes using a wildcard dns service such as xip.io which allows for *..xip.io echo "${RE_IPV4}\$" } get_ipv6_regex() { local RE_IPV6="$(_ipv6_regex)" # Ensure the ip address continues to the end of the line # Fixes using a wildcard dns service such as xip.io which allows for *..xip.io echo "${RE_IPV6}\$" } get_global_vhost() { local GLOBAL_VHOST_FILE="$DOKKU_ROOT/VHOST" [[ -f "$GLOBAL_VHOST_FILE" ]] && local GLOBAL_VHOST=$(< "$GLOBAL_VHOST_FILE") echo "$GLOBAL_VHOST" } is_global_vhost_enabled() { local desc="returns true if we have a valid global vhost set; otherwise returns false" local GLOBAL_VHOST=$(get_global_vhost) local GLOBAL_VHOST_ENABLED=true local RE_IPV4="$(get_ipv4_regex)" local RE_IPV6="$(get_ipv6_regex)" if [[ -z "$GLOBAL_VHOST" ]] || [[ "$GLOBAL_VHOST" =~ $RE_IPV4 ]] || [[ "$GLOBAL_VHOST" =~ $RE_IPV6 ]]; then local GLOBAL_VHOST_ENABLED=false fi echo $GLOBAL_VHOST_ENABLED } is_app_vhost_enabled() { local desc="returns true or false if vhost support is enabled for a given application" local APP=$1; verify_app_name "$APP" local NO_VHOST=$(config_get "$APP" NO_VHOST) local APP_VHOST_ENABLED=true if [[ "$NO_VHOST" == "1" ]]; then local APP_VHOST_ENABLED=false fi echo $APP_VHOST_ENABLED } disable_app_vhost() { local desc="disable vhost support for given application" local APP=$1; verify_app_name "$APP" local APP_VHOST_FILE="$DOKKU_ROOT/$APP/VHOST" local APP_URLS_FILE="$DOKKU_ROOT/$APP/URLS" local DOKKU_NGINX_PORT=$(config_get "$APP" DOKKU_NGINX_PORT) local DOKKU_NGINX_SSL_PORT=$(config_get "$APP" DOKKU_NGINX_SSL_PORT) if [[ -f "$APP_VHOST_FILE" ]]; then dokku_log_info1 "VHOST support disabled, deleting $APP/VHOST" rm "$APP_VHOST_FILE" fi if [[ -f "$APP_URLS_FILE" ]]; then dokku_log_info1 "VHOST support disabled, deleting $APP/URLS" rm "$APP_URLS_FILE" fi if [[ "$DOKKU_NGINX_PORT" == "80" ]]; then config_unset --no-restart "$APP" DOKKU_NGINX_PORT fi if [[ "$DOKKU_NGINX_SSL_PORT" == "443" ]]; then config_unset --no-restart "$APP" DOKKU_NGINX_SSL_PORT fi [[ "$2" == "--no-restart" ]] && local CONFIG_SET_ARGS=$2 # shellcheck disable=SC2086 config_set $CONFIG_SET_ARGS $APP NO_VHOST=1 } enable_app_vhost() { local desc="enable vhost support for given application" local APP=$1; verify_app_name "$APP" config_unset --no-restart "$APP" DOKKU_NGINX_PORT DOKKU_NGINX_SSL_PORT [[ "$2" == "--no-restart" ]] && local CONFIG_SET_ARGS=$2 # shellcheck disable=SC2086 config_set $CONFIG_SET_ARGS "$APP" NO_VHOST=0 } get_dockerfile_exposed_ports() { local DOCKERFILE_PORTS=$(egrep "^EXPOSE " "$1" | awk '{ print $2 }' | xargs) || true echo "$DOCKERFILE_PORTS" } get_app_raw_tcp_ports() { local APP="$1"; verify_app_name "$APP" local DOCKERFILE_PORTS="$(config_get "$APP" DOKKU_DOCKERFILE_PORTS)" for p in $DOCKERFILE_PORTS; do if [[ ! "$p" =~ .*udp.* ]]; then p=${p//\/tcp} raw_tcp_ports+="$p " fi done raw_tcp_ports="$(echo "$raw_tcp_ports"| xargs)" echo "$raw_tcp_ports" } get_container_ports() { local APP="$1"; verify_app_name "$APP" local APP_CIDS="$(get_app_container_ids "$APP")" local cid for cid in $APP_CIDS; do local container_ports="$(docker port "$cid" | awk '{ print $3 "->" $1}' | awk -F ":" '{ print $2 }')" done echo "$container_ports" } get_app_urls() { local APP="$2"; verify_app_name "$APP" local RAW_TCP_PORTS="$(get_app_raw_tcp_ports "$APP")" local URLS_FILE="$DOKKU_ROOT/$APP/URLS" if [[ -s "$URLS_FILE" ]]; then local app_urls="$(egrep -v "^#" "$URLS_FILE")" if [[ -n "$RAW_TCP_PORTS" ]]; then for url in ${app_urls[*]}; do for p in $RAW_TCP_PORTS; do local port_urls+="$url:$p " done done local port_urls="$(echo "$port_urls"| xargs)" fi local URLS=${port_urls:=$app_urls} case "$1" in url) echo "$URLS" | tr ' ' '\n' | head -n1 ;; urls) echo "$URLS" | tr ' ' '\n' ;; esac else local SCHEME="http"; local SSL="$DOKKU_ROOT/$APP/tls" local DOKKU_NGINX_PORT=$(config_get "$APP" DOKKU_NGINX_PORT || true) local DOKKU_NGINX_SSL_PORT=$(config_get "$APP" DOKKU_NGINX_SSL_PORT || true) if [[ -e "$SSL/server.crt" && -e "$SSL/server.key" ]]; then local SCHEME="https" fi if [[ "$(is_app_proxy_enabled "$APP")" == "false" ]]; then if [[ -n "$RAW_TCP_PORTS" ]]; then local APP_CONTAINER_PORTS="$(get_container_ports "$APP")" local app_port for app_port in $APP_CONTAINER_PORTS; do echo "$(< "$DOKKU_ROOT/HOSTNAME"):$app_port (container)" done else shopt -s nullglob for PORT_FILE in $DOKKU_ROOT/$APP/PORT.*; do echo "$SCHEME://$(< "$DOKKU_ROOT/HOSTNAME"):$(< "$PORT_FILE") (container)" done shopt -u nullglob fi elif [[ -n "$DOKKU_NGINX_PORT" ]] || [[ -n "$DOKKU_NGINX_SSL_PORT" ]]; then if [[ -n "$DOKKU_NGINX_PORT" ]];then echo "http://$(< "$DOKKU_ROOT/HOSTNAME"):$DOKKU_NGINX_PORT ($(get_app_proxy_type "$APP"))" fi if [[ -n "$DOKKU_NGINX_SSL_PORT" ]]; then echo "https://$(< "$DOKKU_ROOT/HOSTNAME"):$DOKKU_NGINX_SSL_PORT ($(get_app_proxy_type "$APP")-ssl)" fi elif [[ -n "$RAW_TCP_PORTS" ]]; then for p in $RAW_TCP_PORTS; do echo "http://$(< "$DOKKU_ROOT/HOSTNAME"):$p" done else echo "$SCHEME://$(< "$DOKKU_ROOT/VHOST")" fi fi } get_json_value() { # return value of provided json key from a json stream on stdin # JSON_NODE should be expresses as either a top-level object that has no children # or in the format of json.node.path local JSON_NODE="$1" local JSON_NODE=${JSON_NODE//\./\"][\"} local JSON_NODE="[\"${JSON_NODE}\"]" cat | python -c 'import json,sys;obj=json.load(sys.stdin);print json.dumps(obj'"${JSON_NODE}"').strip("\"")'; } get_json_keys() { # return space-separated list of json keys from json provided on stdin # JSON_NODE should be expressed as json.node.path and is expected to have children local JSON_NODE="$1" local JSON_NODE=${JSON_NODE//\./\"][\"} local JSON_NODE="[\"${JSON_NODE}\"]" if [[ "$JSON_NODE" == "[\"\"]" ]]; then cat | python -c 'import json,sys;obj=json.load(sys.stdin);print " ".join(obj.keys())'; else cat | python -c 'import json,sys;obj=json.load(sys.stdin);print " ".join(obj'"${JSON_NODE}"'.keys())'; fi }