#!/usr/bin/env bash set -eo pipefail [[ $DOKKU_TRACE ]] && set -x has_tty() { declare desc="return 0 if we have a tty" if [[ "$(LC_ALL=C /usr/bin/tty || true)" == "not a tty" ]]; then return 1 else return 0 fi } dokku_apps() { declare desc="prints list of all local apps" local INSTALLED_APPS=$(find "$DOKKU_ROOT" -follow -maxdepth 1 -mindepth 1 -type d ! -name '.*' -printf "%f\n" 2>/dev/null | sort) || (dokku_log_fail "You haven't deployed any applications yet") [[ $INSTALLED_APPS ]] && echo "$INSTALLED_APPS" } dokku_version() { if [[ -f "$DOKKU_LIB_ROOT/STABLE_VERSION" ]]; then DOKKU_VERSION=$(cat "${DOKKU_LIB_ROOT}/STABLE_VERSION") elif [[ -f "$DOKKU_LIB_ROOT/VERSION" ]]; then DOKKU_VERSION=$(cat "${DOKKU_LIB_ROOT}/VERSION") else dokku_log_fail "Unable to determine dokku's version" fi echo "dokku version ${DOKKU_VERSION}" } dokku_log_quiet() { declare desc="log quiet formatter" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo "$*" fi } dokku_log_info1() { declare desc="log info1 formatter" echo "-----> $*" } dokku_log_info2() { declare desc="log info2 formatter" echo "=====> $*" } dokku_log_info1_quiet() { declare desc="log info1 formatter (with quiet option)" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo "-----> $*" fi } dokku_log_info2_quiet() { declare desc="log info2 formatter (with quiet option)" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo "=====> $*" fi } dokku_col_log_info1() { declare desc="columnar log info1 formatter" printf "%-6s %-18s %-25s %-25s %-25s\n" "----->" "$@" } dokku_col_log_info1_quiet() { declare desc="columnar log info1 formatter (with quiet option)" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then printf "%-6s %-18s %-25s %-25s %-25s\n" "----->" "$@" fi } dokku_col_log_info2() { declare desc="columnar log info2 formatter" printf "%-6s %-18s %-25s %-25s %-25s\n" "=====>" "$@" } dokku_col_log_info2_quiet() { declare desc="columnar log info2 formatter (with quiet option)" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then printf "%-6s %-18s %-25s %-25s %-25s\n" "=====>" "$@" fi } dokku_col_log_msg() { declare desc="columnar log formatter" printf "%-25s %-25s %-25s %-25s\n" "$@" } dokku_col_log_msg_quiet() { declare desc="columnar log formatter (with quiet option)" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then printf "%-25s %-25s %-25s %-25s\n" "$@" fi } dokku_log_verbose_quiet() { declare desc="log verbose formatter (with quiet option)" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo " $*" fi } dokku_log_verbose() { declare desc="log verbose formatter" echo " $*" } dokku_log_exclaim_quiet() { declare desc="log exclaim formatter" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo " ! $*" fi } dokku_log_exclaim() { declare desc="log exclaim formatter" echo " ! $*" } dokku_log_warn_quiet() { declare desc="log warning formatter" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo " ! $*" 1>&2 fi } dokku_log_warn() { declare desc="log warning formatter" echo " ! $*" 1>&2 } dokku_log_exit_quiet() { declare desc="log exit formatter" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo "$@" 1>&2 fi exit 0 } dokku_log_exit() { declare desc="log exit formatter" echo "$@" 1>&2 exit 0 } dokku_log_fail_quiet() { declare desc="log fail formatter" if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then echo " ! $*" 1>&2 fi exit ${DOKKU_FAIL_EXIT_CODE:=1} } dokku_log_fail() { declare desc="log fail formatter" echo " ! $*" 1>&2 exit ${DOKKU_FAIL_EXIT_CODE:=1} } dokku_log_stderr() { declare desc="log stderr formatter" echo "$@" 1>&2 } dokku_log_event() { declare desc="log dokku events" logger -t dokku-event -i -- "$@" } dokku_log_plugn_trigger_call() { declare desc="log plugn trigger calls" local l_hook="$1" shift dokku_log_event "INVOKED: ${l_hook}( $* ) NAME=$NAME FINGERPRINT=$FINGERPRINT DOKKU_PID=$DOKKU_PID" } dokku_container_log_verbose_quiet() { declare desc="log verbose container output (with quiet option)" local CID=$1 shift OIFS=$IFS IFS=$'\n' local line for line in $("$DOCKER_BIN" container logs "$CID" 2>&1); do dokku_log_verbose_quiet "$line" done IFS=$OIFS } fn-is-valid-app-name() { declare desc="verify that the app name matches naming restrictions" local APP="$1" [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" if [[ "$APP" =~ ^[a-z].* ]] || [[ "$APP" =~ ^[0-9].* ]]; then if [[ ! $APP =~ [A-Z] ]] && [[ ! $APP =~ [:] ]] && [[ ! $APP =~ [_] ]]; then return 0 fi fi return 1 } fn-is-valid-app-name-old() { declare desc="verify that the app name matches the old naming restrictions" local APP="$1" [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" if [[ "$APP" =~ ^[a-z].* ]] || [[ "$APP" =~ ^[0-9].* ]]; then if [[ ! $APP =~ [A-Z] ]] && [[ ! $APP =~ [:] ]]; then return 0 fi fi return 1 } is_valid_app_name() { declare desc="verify that the app name matches naming restrictions" local APP="$1" if fn-is-valid-app-name "$APP"; then return fi dokku_log_fail "App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, colons, or underscores" } is_valid_app_name_old() { declare desc="verify that the app name matches the old naming restrictions" local APP="$1" if fn-is-valid-app-name-old "$APP"; then return fi dokku_log_fail "App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, or colons" } verify_app_name() { declare desc="verify app name format and app existence" declare APP="$1" local VALID_NEW=false local VALID_OLD=false if fn-is-valid-app-name "$APP" 2>/dev/null; then VALID_NEW=true fi if fn-is-valid-app-name-old "$APP" 2>/dev/null; then VALID_OLD=true fi if [[ "$VALID_NEW" == "false" ]] && [[ "$VALID_OLD" == "false" ]]; then dokku_log_fail "App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, colons, or underscores" fi [[ ! -d "$DOKKU_ROOT/$APP" ]] && DOKKU_FAIL_EXIT_CODE=20 dokku_log_fail "App $APP does not exist" return 0 } verify_image() { declare desc="verify image existence" local IMAGE="$1" if "$DOCKER_BIN" image inspect "$IMAGE" &>/dev/null; then return 0 else return 1 fi } get_app_image_repo() { declare desc="central definition of image repo pattern" local APP="$1" local IMAGE_REPO="dokku/$APP" echo "$IMAGE_REPO" } get_deploying_app_image_name() { declare desc="return deploying image identifier for a given app, tag tuple. validate if tag is presented" local APP="$1" local IMAGE_TAG="$2" IMAGE_REPO="$3" local IMAGE_REMOTE_REPOSITORY=$(plugn trigger deployed-app-repository "$APP") local NEW_IMAGE_TAG=$(plugn trigger deployed-app-image-tag "$APP") local NEW_IMAGE_REPO=$(plugn trigger deployed-app-image-repo "$APP") [[ -n "$NEW_IMAGE_REPO" ]] && IMAGE_REPO="$NEW_IMAGE_REPO" [[ -n "$NEW_IMAGE_TAG" ]] && IMAGE_TAG="$NEW_IMAGE_TAG" [[ -z "$IMAGE_REPO" ]] && IMAGE_REPO=$(get_app_image_repo "$APP") [[ -z "$IMAGE_TAG" ]] && IMAGE_TAG="latest" local IMAGE="${IMAGE_REMOTE_REPOSITORY}${IMAGE_REPO}:${IMAGE_TAG}" verify_image "$IMAGE" || dokku_log_fail "App image ($IMAGE) not found" echo "$IMAGE" } get_app_image_name() { declare desc="return image identifier for a given app, tag tuple. validate if tag is presented" local APP="$1" local IMAGE_TAG="$2" IMAGE_REPO="$3" if [[ -z "$IMAGE_REPO" ]]; then IMAGE_REPO=$(get_app_image_repo "$APP") fi if [[ -n "$IMAGE_TAG" ]]; then local IMAGE="$IMAGE_REPO:$IMAGE_TAG" verify_image "$IMAGE" || dokku_log_fail "App image ($IMAGE) not found" else local IMAGE="$IMAGE_REPO:latest" fi echo "$IMAGE" } get_app_scheduler() { declare desc="fetch the scheduler for a given application" declare APP="$1" local DOKKU_APP_SCHEDULER DOKKU_GLOBAL_SCHEDULER DOKKU_SCHEDULER if [[ "$APP" == "--global" ]]; then APP="" fi source "$PLUGIN_AVAILABLE_PATH/config/functions" [[ -n "$APP" ]] && DOKKU_APP_SCHEDULER="$(config_get "$APP" DOKKU_SCHEDULER || true)" DOKKU_GLOBAL_SCHEDULER="$(config_get --global DOKKU_SCHEDULER || true)" DOKKU_SCHEDULER=${DOKKU_APP_SCHEDULER:="$DOKKU_GLOBAL_SCHEDULER"} if [[ -z "$DOKKU_SCHEDULER" ]]; then DOKKU_SCHEDULER="docker-local" fi echo "$DOKKU_SCHEDULER" } get_running_image_tag() { declare desc="retrieve current image tag for a given app. returns empty string if no deployed containers are found" local APP="$1" local CIDS=($(get_app_container_ids "$APP")) local RUNNING_IMAGE_TAG=$("$DOCKER_BIN" container inspect --format '{{ .Config.Image }}' "${CIDS[0]}" 2>/dev/null | awk -F: '{ print $2 }' || echo '') echo "$RUNNING_IMAGE_TAG" } is_image_cnb_based() { declare desc="returns true if app image is based on cnb" declare IMAGE="$1" if [[ -z "$IMAGE" ]]; then return 1 fi CNB_STACK_ID="$("$DOCKER_BIN" image inspect --format '{{ index .Config.Labels "io.buildpacks.stack.id" }}' "$IMAGE")" if [[ -n "$CNB_STACK_ID" ]]; then return 0 fi return 1 } is_image_herokuish_based() { declare desc="returns true if app image is based on herokuish or a buildpack-like env" declare IMAGE="$1" APP="$2" local DOKKU_APP_USER local USER_VALUE if [[ -z "$IMAGE" ]]; then return 1 fi if is_image_cnb_based "$IMAGE"; then return 0 fi if [[ -n "$APP" ]]; then DOKKU_APP_USER=$(config_get "$APP" DOKKU_APP_USER || true) fi DOKKU_APP_USER=${DOKKU_APP_USER:="herokuishuser"} # due to how the build process works, all herokuish images have the Environment variable USER=$DOKKU_APP_USER USER_VALUE="$("$DOCKER_BIN" image inspect --format '{{range .Config.Env}}{{if eq . "USER='"$DOKKU_APP_USER"'"}}{{println .}}{{end}}{{end}}' "$IMAGE")" [[ "$USER_VALUE" == "" ]] && return 1 return 0 } get_docker_version() { CLIENT_VERSION_STRING="$("$DOCKER_BIN" version --format "{{ .Client.Version }}")" echo "$CLIENT_VERSION_STRING" } is_number() { declare desc="returns 0 if input is a number" local NUMBER=$1 local NUM_RE='^[0-9]+$' if [[ $NUMBER =~ $NUM_RE ]]; then return 0 else return 1 fi } is_abs_path() { declare desc="returns 0 if input path is absolute" local TEST_PATH=$1 if [[ "$TEST_PATH" == /* ]]; then return 0 else return 1 fi } parse_args() { declare desc="top-level cli arg parser" local next_index=1 local skip=false local args=("$@") local flags for arg in "$@"; do if [[ "$skip" == "true" ]]; then next_index=$((next_index + 1)) skip=false continue fi case "$arg" in --quiet) export DOKKU_QUIET_OUTPUT=1 ;; --detach) export DOKKU_DETACH_CONTAINER=1 ;; --trace) export DOKKU_TRACE=1 ;; --rm-container | --rm) export DOKKU_RM_CONTAINER=1 ;; --force) export DOKKU_APPS_FORCE_DELETE=1 ;; --app) export DOKKU_APP_NAME=${args[$next_index]} skip=true ;; esac if [[ "$skip" == "true" ]]; then flags="${flags} ${arg}" elif [[ "$arg" == "--app" ]]; then flags="${flags} ${arg}=${args[$next_index]}" elif [[ "$arg" =~ ^--.* ]]; then flags="${flags} ${arg}" fi next_index=$((next_index + 1)) done if [[ -z "$DOKKU_GLOBAL_FLAGS" ]]; then export DOKKU_GLOBAL_FLAGS="$(echo -e "${flags}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" fi return 0 } copy_from_image() { declare desc="copy file from named image to destination" declare IMAGE="$1" SRC_FILE="$2" DST_FILE="$3" local WORK_DIR="" local DOCKER_CREATE_LABEL_ARGS="--label=com.dokku.app-name=$APP" if verify_image "$IMAGE"; then if ! is_abs_path "$SRC_FILE"; then if is_image_cnb_based "$IMAGE"; then WORKDIR="/workspace" elif is_image_herokuish_based "$IMAGE" "$APP"; then WORKDIR="/app" else WORKDIR="$("$DOCKER_BIN" image inspect --format '{{.Config.WorkingDir}}' "$IMAGE")" fi if [[ -n "$WORKDIR" ]]; then SRC_FILE="${WORKDIR}/${SRC_FILE}" fi fi TMP_FILE_COMMAND_OUTPUT=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") trap "rm -rf '$TMP_FILE_COMMAND_OUTPUT' >/dev/null 2>&1 || true" RETURN local CID=$("$DOCKER_BIN" container create "${DOCKER_CREATE_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS "$IMAGE") "$DOCKER_BIN" container cp "$CID:$SRC_FILE" "$TMP_FILE_COMMAND_OUTPUT" 2>/dev/null || true "$DOCKER_BIN" container rm --force "$CID" &>/dev/null plugn trigger scheduler-register-retired "$APP" "$CID" # 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 if [[ ! -s "$TMP_FILE_COMMAND_OUTPUT" ]]; then return 1 fi # workaround for CHECKS file when owner is root. seems to only happen when running inside docker dos2unix -l <"$TMP_FILE_COMMAND_OUTPUT" >"$DST_FILE" # add trailing newline for certain places where file parsing depends on it if [[ "$(tail -c1 "$DST_FILE")" != "" ]]; then echo "" >>"$DST_FILE" fi else return 1 fi } get_app_container_ids() { declare desc="returns list of docker container ids for given app and optional container_type" local APP="$1" local CONTAINER_TYPE="$2" [[ -f $DOKKU_ROOT/$APP/CONTAINER ]] && DOKKU_CIDS+=$(<"$DOKKU_ROOT/$APP/CONTAINER") if [[ -n "$CONTAINER_TYPE" ]]; then local CONTAINER_PATTERN="$DOKKU_ROOT/$APP/CONTAINER.$CONTAINER_TYPE.*" if [[ $CONTAINER_TYPE == *.* ]]; then local CONTAINER_PATTERN="$DOKKU_ROOT/$APP/CONTAINER.$CONTAINER_TYPE" [[ ! -f $CONTAINER_PATTERN ]] && echo "" && return 0 fi else local CONTAINER_PATTERN="$DOKKU_ROOT/$APP/CONTAINER.*" fi shopt -s nullglob local DOKKU_CID_FILE for DOKKU_CID_FILE in $CONTAINER_PATTERN; do local DOKKU_CIDS+=" " local DOKKU_CIDS+=$(<"$DOKKU_CID_FILE") local DOKKU_CIDS+=" " done shopt -u nullglob echo "$DOKKU_CIDS" } get_app_running_container_ids() { declare desc="return list of running docker container ids for given app and optional container_type" local APP="$1" CONTAINER_TYPE="$2" local CIDS ! (is_deployed "$APP") && dokku_log_fail "App $APP has not been deployed" CIDS=$(get_app_container_ids "$APP" "$CONTAINER_TYPE") for CID in $CIDS; do (is_container_status "$CID" "Running") && local APP_RUNNING_CONTAINER_IDS+="$CID " done echo "$APP_RUNNING_CONTAINER_IDS" } get_app_running_container_types() { declare desc="return list of running container types for given app" local APP=$1 CONTAINER_TYPES ! (is_deployed "$APP") && dokku_log_fail "App $APP has not been deployed" CONTAINER_TYPES="$(find "$DOKKU_ROOT/$APP" -maxdepth 1 -name "CONTAINER.*" -print0 2>/dev/null | xargs -0)" if [[ -n "$CONTAINER_TYPES" ]]; then CONTAINER_TYPES="${CONTAINER_TYPES//$DOKKU_ROOT\/$APP\//}" CONTAINER_TYPES="$(tr " " $'\n' <<<"$CONTAINER_TYPES" | awk -F. '{ print $2 }' | sort | uniq | xargs)" fi echo "$CONTAINER_TYPES" } is_deployed() { declare desc="return 0 if given app has a running container" local APP="$1" "$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet is-deployed "$APP" } is_container_running() { declare desc="return 0 if given docker container id is in running state" declare deprecated=true declare CID="$1" local CONTAINER_STATUS dokku_log_warn "Deprecated: common#is_container_status" CONTAINER_STATUS=$("$DOCKER_BIN" container inspect --format '{{.State.Running}}' "$CID" || true) if [[ "$CONTAINER_STATUS" == "true" ]]; then return 0 fi return 1 } is_container_status() { declare desc="return 0 if given docker container id is in given state" local CID=$1 local TEMPLATE="{{.State.$2}}" local CONTAINER_STATUS=$("$DOCKER_BIN" container inspect --format "$TEMPLATE" "$CID" 2>/dev/null || true) if [[ "$CONTAINER_STATUS" == "true" ]]; then return 0 fi return 1 } is_app_running() { declare desc="return 0 if given app has a running container" declare deprecated=true local APP="$1" dokku_log_warn "Deprecated: ps#fn-ps-is-app-running" local APP_RUNNING_CONTAINER_IDS=$(get_app_running_container_ids "$APP" 2>/dev/null) if [[ -n "$APP_RUNNING_CONTAINER_IDS" ]]; then return 0 else return 1 fi } dokku_build() { declare desc="build phase" declare APP="$1" IMAGE_SOURCE_TYPE="$2" SOURCECODE_WORK_DIR="$3" plugn trigger builder-build "$IMAGE_SOURCE_TYPE" "$APP" "$SOURCECODE_WORK_DIR" } dokku_release() { declare desc="release phase" declare APP="$1" IMAGE_SOURCE_TYPE="$2" IMAGE_TAG="$3" local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") if is_image_cnb_based "$IMAGE"; then IMAGE_SOURCE_TYPE="pack" fi plugn trigger builder-release "$IMAGE_SOURCE_TYPE" "$APP" "$IMAGE_TAG" } cmd-deploy() { declare desc="deploy phase" declare APP="$1" IMAGE_TAG="$2" source "$PLUGIN_AVAILABLE_PATH/config/functions" verify_app_name "$APP" local DOKKU_SCHEDULER=$(get_app_scheduler "$APP") plugn trigger scheduler-deploy "$DOKKU_SCHEDULER" "$APP" "$IMAGE_TAG" } release_and_deploy() { declare desc="main function for releasing and deploying an app" source "$PLUGIN_AVAILABLE_PATH/config/functions" local APP="$1" local IMAGE_TAG="$2" local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") local DOKKU_DOCKERFILE_PORTS if verify_image "$IMAGE"; then if is_image_herokuish_based "$IMAGE" "$APP"; then local IMAGE_SOURCE_TYPE="herokuish" else local IMAGE_SOURCE_TYPE="dockerfile" DOKKU_DOCKERFILE_PORTS=$(config_get "$APP" DOKKU_DOCKERFILE_PORTS || true) if [[ -z "$DOKKU_DOCKERFILE_PORTS" ]]; then local DOCKER_IMAGE_PORTS=$(get_exposed_ports_from_image "$IMAGE") [[ -n "$DOCKER_IMAGE_PORTS" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_PORTS="$DOCKER_IMAGE_PORTS" fi fi local DOKKU_APP_SKIP_DEPLOY="$(config_get "$APP" DOKKU_SKIP_DEPLOY || true)" local DOKKU_GLOBAL_SKIP_DEPLOY="$(config_get --global DOKKU_SKIP_DEPLOY || true)" local DOKKU_SKIP_DEPLOY=${DOKKU_APP_SKIP_DEPLOY:="$DOKKU_GLOBAL_SKIP_DEPLOY"} dokku_log_info1 "Releasing $APP..." dokku_release "$APP" "$IMAGE_SOURCE_TYPE" "$IMAGE_TAG" if [[ "$DOKKU_SKIP_DEPLOY" != "true" ]]; then dokku_log_info1 "Deploying $APP..." cmd-deploy "$APP" "$IMAGE_TAG" dokku_log_info2 "Application deployed:" get_app_urls urls "$APP" | sed "s/^/ /" else dokku_log_info1 "Skipping deployment" fi echo fi } dokku_receive() { declare desc="receives an app kicks off deploy process" source "$PLUGIN_AVAILABLE_PATH/config/functions" local APP="$1" local IMAGE=$(get_app_image_name "$APP") local IMAGE_SOURCE_TYPE="$2" local TMP_WORK_DIR="$3" docker_cleanup "$APP" local DOKKU_CNB_EXPERIMENTAL="$(config_get "$APP" DOKKU_CNB_EXPERIMENTAL || true)" if [[ "$DOKKU_CNB_EXPERIMENTAL" == "1" ]]; then IMAGE_SOURCE_TYPE="pack" fi DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_TYPE="$IMAGE_SOURCE_TYPE" dokku_build "$APP" "$IMAGE_SOURCE_TYPE" "$TMP_WORK_DIR" plugn trigger release-and-deploy "$APP" } docker_cleanup() { declare desc="cleans up all exited/dead containers and removes all dangling images" declare APP="$1" FORCE_CLEANUP="$2" local DOKKU_APP_SKIP_CLEANUP source "$PLUGIN_AVAILABLE_PATH/config/functions" if [[ "$FORCE_CLEANUP" != "true" ]]; then [[ -n "$APP" ]] && DOKKU_APP_SKIP_CLEANUP="$(config_get "$APP" DOKKU_SKIP_CLEANUP || true)" if [[ -n "$DOKKU_SKIP_CLEANUP" ]] || [[ -n "$DOKKU_APP_SKIP_CLEANUP" ]]; then dokku_log_info1 "DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup" return fi fi dokku_log_info1 "Cleaning up..." local DOKKU_SCHEDULER=$(get_app_scheduler "$APP") if [[ "$APP" == "--global" ]]; then APP="" fi plugn trigger scheduler-docker-cleanup "$DOKKU_SCHEDULER" "$APP" "$FORCE_CLEANUP" if [[ -n "$APP" ]]; then # delete all non-running containers # shellcheck disable=SC2046 "$DOCKER_BIN" container rm $("$DOCKER_BIN" container list --all --filter "status=exited" --filter "label=$DOKKU_CONTAINER_LABEL" --filter "label=com.dokku.app-name=$APP" --quiet) &>/dev/null || true # delete all dead containers # shellcheck disable=SC2046 "$DOCKER_BIN" container rm $("$DOCKER_BIN" container list --all --filter "status=dead" --filter "label=$DOKKU_CONTAINER_LABEL" --filter "label=com.dokku.app-name=$APP" --quiet) &>/dev/null || true # delete danging images # shellcheck disable=SC2046 "$DOCKER_BIN" image rm $("$DOCKER_BIN" image list --filter 'dangling=true' --filter "label=com.dokku.app-name=$APP" --quiet) &>/dev/null || true # delete unused images "$DOCKER_BIN" image prune --all --filter "label=com.dokku.app-name=$APP" --force &>/dev/null || true else # delete all non-running containers # shellcheck disable=SC2046 "$DOCKER_BIN" container rm $("$DOCKER_BIN" container list --all --filter "status=exited" --filter "label=$DOKKU_CONTAINER_LABEL" --quiet) &>/dev/null || true # delete all dead containers # shellcheck disable=SC2046 "$DOCKER_BIN" container rm $("$DOCKER_BIN" container list --all --filter "status=dead" --filter "label=$DOKKU_CONTAINER_LABEL" --quiet) &>/dev/null || true # delete danging images # shellcheck disable=SC2046 "$DOCKER_BIN" image rm $("$DOCKER_BIN" image list --filter 'dangling=true' --quiet) &>/dev/null || true fi } get_available_port() { declare desc="returns first currently unused port > 1024" 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() { declare desc="calls user-auth plugin trigger" export SSH_USER=${SSH_USER:=$USER} export SSH_NAME=${NAME:="default"} # this plugin trigger exists in the core `20_events` plugin if [[ $(find "$PLUGIN_PATH"/enabled/*/user-auth 2>/dev/null | wc -l) == 1 ]]; then return 0 fi if ! plugn trigger user-auth "$SSH_USER" "$SSH_NAME" "$@"; then return 1 fi return 0 } _ipv4_regex() { declare desc="ipv4 regex" echo "([0-9]{1,3}[\.]){3}[0-9]{1,3}" } _ipv6_regex() { declare desc="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 local RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,7}:|" # TEST: 1:: 1:2:3:4:5:6:7:: local 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 local 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 local 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 local 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 local 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 local 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 local 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 :: local 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) local 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) local 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() { declare desc="returns 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() { declare desc="returns 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_dockerfile_exposed_ports() { declare desc="return all exposed ports from passed file path" local DOCKERFILE_PORTS=$(grep -E "^EXPOSE " "$1" | awk '{ print $2 }' | xargs) || true echo "$DOCKERFILE_PORTS" } get_exposed_ports_from_image() { declare desc="return all exposed ports from passed image name" local IMAGE="$1" verify_image "$IMAGE" # shellcheck disable=SC2016 local DOCKER_IMAGE_EXPOSED_PORTS="$("$DOCKER_BIN" image inspect --format '{{range $key, $value := .Config.ExposedPorts}}{{$key}} {{end}}' "$IMAGE")" echo "$DOCKER_IMAGE_EXPOSED_PORTS" } get_entrypoint_from_image() { declare desc="return .Config.Entrypoint from passed image name" local IMAGE="$1" verify_image "$IMAGE" local DOCKER_IMAGE_ENTRYPOINT="$("$DOCKER_BIN" image inspect --format '{{range .Config.Entrypoint}}{{.}} {{end}}' "$IMAGE")" echo "ENTRYPOINT $DOCKER_IMAGE_ENTRYPOINT" } get_cmd_from_image() { declare desc="return .Config.Cmd from passed image name" local IMAGE="$1" verify_image "$IMAGE" local DOCKER_IMAGE_CMD="$("$DOCKER_BIN" image inspect --format '{{range .Config.Cmd}}{{.}} {{end}}' "$IMAGE")" DOCKER_IMAGE_CMD="${DOCKER_IMAGE_CMD/\/bin\/sh -c/}" echo "CMD $DOCKER_IMAGE_CMD" } extract_directive_from_dockerfile() { declare desc="return requested directive from passed file path" local FILE_PATH="$1" local SEARCH_STRING="$2" local FOUND_LINE=$(grep -E "^${SEARCH_STRING} " "$FILE_PATH" | tail -n1) || true echo "$FOUND_LINE" } get_app_raw_tcp_ports() { declare desc="extracts raw tcp port numbers from DOCKERFILE_PORTS config variable" source "$PLUGIN_AVAILABLE_PATH/config/functions" local APP="$1" 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 local raw_tcp_ports="$(echo "$raw_tcp_ports" | xargs)" echo "$raw_tcp_ports" } get_container_ports() { declare desc="returns published ports from app containers" local APP="$1" local APP_CIDS="$(get_app_container_ids "$APP")" local cid for cid in $APP_CIDS; do local container_ports="$("$DOCKER_BIN" container port "$cid" | awk '{ print $3 "->" $1}' | awk -F ":" '{ print $2 }')" done echo "$container_ports" } get_app_urls() { declare desc="print an app's available urls" declare URL_TYPE="$1" APP="$2" local urls urls=$(plugn trigger app-urls "$APP" "$URL_TYPE") if [[ -n "$urls" ]]; then echo "$urls" | sort else internal_get_app_urls "$URL_TYPE" "$APP" | sort fi } internal_get_app_urls() { declare desc="print an app's available urls" source "$PLUGIN_AVAILABLE_PATH/certs/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" local APP="$2" local RAW_TCP_PORTS="$(get_app_raw_tcp_ports "$APP")" local URLS_FILE="$DOKKU_ROOT/$APP/URLS" local DOKKU_PROXY_PORT_MAP=$(config_get "$APP" DOKKU_PROXY_PORT_MAP || true) if [[ -s "$URLS_FILE" ]]; then local app_urls="$(grep -v -E "^#" "$URLS_FILE")" if [[ -n "$DOKKU_PROXY_PORT_MAP" ]]; then local port_map app_vhost local app_vhosts=$(plugn trigger domains-list "$APP") for port_map in $DOKKU_PROXY_PORT_MAP; do local scheme="$(awk -F ':' '{ print $1 }' <<<"$port_map")" local listen_port="$(awk -F ':' '{ print $2 }' <<<"$port_map")" for app_vhost in $app_vhosts; do if [[ "$listen_port" != "80" ]] && [[ "$listen_port" != "443" ]]; then port_urls+=" $scheme://$app_vhost:$listen_port " else port_urls+=" $scheme://$app_vhost " fi done done fi local port_urls="$(echo "$port_urls" | xargs)" local URLS="$(merge_dedupe_list "$port_urls $app_urls" " ")" case "$1" in url) if is_ssl_enabled "$APP"; then echo "$URLS" | tr ' ' '\n' | grep https | head -n1 else echo "$URLS" | tr ' ' '\n' | head -n1 fi ;; urls) echo "$URLS" | tr ' ' '\n' | sort ;; esac else local SCHEME="http" local SSL="$DOKKU_ROOT/$APP/tls" if [[ -e "$SSL/server.crt" && -e "$SSL/server.key" ]]; then local SCHEME="https" fi if [[ "$(plugn trigger proxy-is-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 local DOKKU_APP_WEB_LISTENERS PORT DOKKU_APP_WEB_LISTENERS="$(plugn trigger network-get-listeners "$APP" "web" | xargs)" for DOKKU_APP_WEB_LISTENER in $DOKKU_APP_WEB_LISTENERS; do PORT="$(echo "$DOKKU_APP_WEB_LISTENER" | cut -d ':' -f2)" echo "$SCHEME://$(<"$DOKKU_ROOT/HOSTNAME"):$PORT (container)" done shopt -u nullglob fi elif [[ -n "$DOKKU_PROXY_PORT_MAP" ]]; then local port_map for port_map in $DOKKU_PROXY_PORT_MAP; do local scheme="$(awk -F ':' '{ print $1 }' <<<"$port_map")" local listen_port="$(awk -F ':' '{ print $2 }' <<<"$port_map")" echo "$scheme://$(<"$DOKKU_ROOT/HOSTNAME"):$listen_port" done 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/HOSTNAME")" fi fi } get_json_value() { declare desc="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" cat | jq -r "${JSON_NODE} | select (.!=null)" 2>/dev/null } strip_inline_comments() { declare desc="removes bash-style comment from input line" local line="$1" local stripped_line="${line%[[:space:]]#*}" echo "$stripped_line" } is_val_in_list() { declare desc="return true if value ($1) is in list ($2) separated by delimiter ($3); delimiter defaults to comma" local value="$1" list="$2" delimiter="${3:-,}" local IFS="$delimiter" val_in_list=false for val in $list; do if [[ "$val" == "$value" ]]; then val_in_list=true fi done echo "$val_in_list" } fn-in-array() { declare desc="return true if value ($1) is in list (all other arguments)" local e for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0 done return 1 } remove_val_from_list() { declare desc="remove value ($1) from list ($2) separated by delimiter ($3) (delimiter defaults to comma) and return list" local value="$1" list="$2" delimiter="${3:-,}" list="${list//$value/}" list="${list//$delimiter$delimiter/$delimiter}" list="${list/#$delimiter/}" list="${list/%$delimiter/}" echo "$list" } add_val_to_list() { declare desc="add value ($1) to list ($2) separated by delimiter ($3) (delimiter defaults to comma) and return list" local value="$1" list="$2" delimiter="${3:-,}" list+="${delimiter}$value" echo "$list" } merge_dedupe_list() { declare desc="combine lists ($1) separated by delimiter ($2) (delimiter defaults to comma), dedupe and return list" local input_lists="$1" delimiter="${2:-,}" local merged_list="$(tr "$delimiter" $'\n' <<<"$input_lists" | sort | uniq | xargs)" echo "$merged_list" } acquire_app_deploy_lock() { declare desc="acquire advisory lock for use in git/tar deploys" local APP="$1" local LOCK_TYPE="${2:-waiting}" local APP_DEPLOY_LOCK_FILE="$DOKKU_ROOT/$APP/.deploy.lock" local LOCK_WAITING_MSG="$APP currently has a deploy lock in place. Waiting..." local LOCK_FAILED_MSG="$APP currently has a deploy lock in place. Exiting..." acquire_advisory_lock "$APP_DEPLOY_LOCK_FILE" "$LOCK_TYPE" "$LOCK_WAITING_MSG" "$LOCK_FAILED_MSG" } release_app_deploy_lock() { declare desc="release advisory lock used in git/tar deploys" local APP="$1" local APP_DEPLOY_LOCK_FILE="$DOKKU_ROOT/$APP/.deploy.lock" release_advisory_lock "$APP_DEPLOY_LOCK_FILE" } acquire_advisory_lock() { declare desc="acquire advisory lock" local LOCK_FILE="$1" LOCK_TYPE="$2" LOCK_WAITING_MSG="$3" LOCK_FAILED_MSG="$4" local LOCK_FD="200" local SHOW_MSG=true eval "exec $LOCK_FD>$LOCK_FILE" if [[ "$LOCK_TYPE" == "waiting" ]]; then while [[ $( flock -n "$LOCK_FD" &>/dev/null echo $? ) -ne 0 ]]; do if [[ "$SHOW_MSG" == "true" ]]; then echo "$LOCK_WAITING_MSG" SHOW_MSG=false fi sleep 1 done else flock -n "$LOCK_FD" &>/dev/null || dokku_log_fail "$LOCK_FAILED_MSG" fi } release_advisory_lock() { declare desc="release advisory lock" local LOCK_FILE="$1" local LOCK_FD="200" flock -u "$LOCK_FD" && rm -f "$LOCK_FILE" &>/dev/null } suppress_output() { declare desc="suppress all output from a given command unless there is an error" local TMP_COMMAND_OUTPUT TMP_COMMAND_OUTPUT=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") trap "rm -rf '$TMP_COMMAND_OUTPUT' >/dev/null" RETURN "$@" >"$TMP_COMMAND_OUTPUT" 2>&1 || { local exit_code="$?" cat "$TMP_COMMAND_OUTPUT" return "$exit_code" } return 0 }