Files
dokku/plugins/common/functions
Jose Diaz-Gonzalez dbac12e9f1 refactor: route bash app name validation through go
The bash and go validators previously each kept their own copy of the regex, which had to be updated in lockstep. Both bash wrappers now invoke the existing common binary via the same pattern as `verify_app_name`, leaving go as the single source of truth. The legacy `IsValidAppNameOld` rule is also widened to allow underscores again so apps created under the old naming rules can still be looked up through `VerifyAppName`'s either-rule fallback.
2026-05-09 16:00:51 -04:00

1066 lines
31 KiB
Bash
Executable File

#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
if [[ -f "${PLUGIN_CORE_AVAILABLE_PATH:-/var/lib/dokku/core-plugins/available}/common/archive-functions" ]]; then
source "${PLUGIN_CORE_AVAILABLE_PATH:-/var/lib/dokku/core-plugins/available}/common/archive-functions"
fi
has_tty() {
declare desc="return 0 if we have a tty"
if [[ "$DOKKU_FORCE_TTY" == "true" ]]; then
return 0
fi
if [[ "$DOKKU_DISABLE_TTY" == "true" ]]; then
return 1
fi
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"
declare FILTER="$1"
local INSTALLED_APPS="$(plugn trigger app-list "$FILTER")"
if [[ -z "$INSTALLED_APPS" ]]; then
dokku_log_fail "You haven't deployed any applications yet"
fi
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-force-arm64-image() {
declare IMAGE="$1"
local image_architecture="$("$DOCKER_BIN" image inspect --format '{{.Architecture}}' "$IMAGE")"
if [[ "$image_architecture" == "amd64" ]] && [[ "$(dpkg --print-architecture 2>/dev/null || true)" != "amd64" ]]; then
return 0
fi
return 1
}
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"
"$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet is-valid-app-name "$APP"
}
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"
"$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet is-valid-app-name-old "$APP"
}
fn-plugn-trigger-exists() {
declare desc="checks if a plugn trigger exists"
declare TRIGGER_NAME="$1"
local exists
exists="$("$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet plugn-trigger-exists "$TRIGGER_NAME" || true)"
if [[ "$exists" == "true" ]]; then
return
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 may only contain lowercase alphanumerics, dots, and hyphens"
}
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 may only contain lowercase alphanumerics, dots, and hyphens"
}
verify_app_name() {
declare desc="verify app name format and app existence"
declare APP="$1"
if "$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet verify-app-name "$APP"; then
return 0
fi
DOKKU_FAIL_EXIT_CODE=20 dokku_log_fail "App $APP does not exist"
}
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"
local IMAGE_REPO="$3"
IMAGE_TAG="$(get_running_image_tag "$APP" "$IMAGE_TAG")"
local IMAGE_REMOTE_REPOSITORY=$(plugn trigger deployed-app-repository "$APP")
local NEW_IMAGE_REPO=$(plugn trigger deployed-app-image-repo "$APP")
[[ -n "$NEW_IMAGE_REPO" ]] && IMAGE_REPO="$NEW_IMAGE_REPO"
[[ -z "$IMAGE_REPO" ]] && IMAGE_REPO=$(get_app_image_repo "$APP")
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"
"$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet scheduler-detect "$APP"
}
get_running_image_tag() {
declare desc="retrieves current deployed image tag for a given app"
local APP="$1" IMAGE_TAG="$2"
local NEW_IMAGE_TAG=$(plugn trigger deployed-app-image-tag "$APP")
[[ -n "$NEW_IMAGE_TAG" ]] && IMAGE_TAG="$NEW_IMAGE_TAG"
[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG="latest"
echo "$IMAGE_TAG"
}
get_image_builder_type() {
declare desc="returns the image builder_type"
declare IMAGE="$1" APP="$2"
if [[ -z "$IMAGE" ]]; then
echo "dockerfile"
return
fi
BUILDER_TYPE="$("$DOCKER_BIN" image inspect --format '{{ index .Config.Labels "com.dokku.builder-type" }}' "$IMAGE")"
if [[ -n "$BUILDER_TYPE" ]]; then
echo "$BUILDER_TYPE"
return
fi
if is_image_herokuish_based "$IMAGE" "$APP"; then
echo "herokuish"
return
fi
echo "dockerfile"
}
is_image_cnb_based() {
declare desc="returns true if app image is based on cnb"
declare IMAGE="$1"
if [[ "$("$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet image-is-cnb-based "$IMAGE")" != "true" ]]; then
return 1
fi
}
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"
if [[ "$("$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet image-is-herokuish-based "$IMAGE" "$APP")" != "true" ]]; then
return 1
fi
}
get_docker_version() {
CLIENT_VERSION_STRING="$("$DOCKER_BIN" version --format "{{ .Client.Version }}")"
echo "$CLIENT_VERSION_STRING"
}
fn-is-compose-installed() {
declare desc="check if the compose docker plugin is installed"
local COMPOSE_INSTALLED
COMPOSE_INSTALLED="$(docker info -f '{{range .ClientInfo.Plugins}}{{if eq .Name "compose"}}true{{end}}{{end}}')"
if [[ "$COMPOSE_INSTALLED" == "true" ]]; then
return 0
fi
return 1
}
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
;;
--trace)
export DOKKU_TRACE=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"
if ! "$PLUGIN_CORE_AVAILABLE_PATH/common/common" copy-from-image "$APP" "$IMAGE" "$SRC_FILE" "$DST_FILE"; then
return 1
fi
}
copy_dir_from_image() {
declare desc="copy a directory from named image to destination"
declare IMAGE="$1" SRC_DIR="$2" DST_DIR="$3"
if ! "$PLUGIN_CORE_AVAILABLE_PATH/common/common" copy-dir-from-image "$APP" "$IMAGE" "$SRC_DIR" "$DST_DIR"; then
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_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
}
dokku_build() {
declare desc="build phase"
declare APP="$1" IMAGE_SOURCE_TYPE="$2" SOURCECODE_WORK_DIR="$3"
local IMAGE=$(get_app_image_name "$APP")
local RELEASED_IMAGE_ID="$(docker image ls --filter "label=com.dokku.image-stage=release" --filter "label=com.dokku.app-name=$APP" --format "{{.ID}}")"
if plugn trigger builder-build "$IMAGE_SOURCE_TYPE" "$APP" "$SOURCECODE_WORK_DIR"; then
return
fi
if [[ -n "$RELEASED_IMAGE_ID" ]]; then
dokku_log_warn "Retagging old image $RELEASED_IMAGE_ID as $IMAGE"
"$DOCKER_BIN" image tag "$RELEASED_IMAGE_ID" "$IMAGE" 2>/dev/null || true
else
dokku_log_warn "Removing invalid image tag $IMAGE"
"$DOCKER_BIN" image remove --force --no-prune "$IMAGE" &>/dev/null || true
fi
dokku_log_fail "App build failed"
}
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" PROCESS_TYPE="$3"
verify_app_name "$APP"
local DOKKU_SCHEDULER=$(get_app_scheduler "$APP")
plugn trigger scheduler-deploy "$DOKKU_SCHEDULER" "$APP" "$IMAGE_TAG" "$PROCESS_TYPE"
}
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:-latest}"
local SOURCE="${3:-deploy}"
dokku_setup_build_capture "$APP" "$SOURCE"
local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG")
local exit_code=0
if verify_image "$IMAGE"; then
if ! declare -f -F fn-plugin-property-get-default >/dev/null; then
source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions"
fi
IMAGE_SOURCE_TYPE="$(get_image_builder_type "$IMAGE" "$APP")"
local DOKKU_APP_SKIP_DEPLOY="$(fn-plugin-property-get-default "ps" "$APP" "skip-deploy" "")"
local DOKKU_GLOBAL_SKIP_DEPLOY="$(fn-plugin-property-get-default "ps" "--global" "skip-deploy" "")"
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" || exit_code="$?"
if [[ "$exit_code" -eq 0 ]] && [[ "$DOKKU_SKIP_DEPLOY" != "true" ]]; then
local DOKKU_SCHEDULER=$(get_app_scheduler "$APP")
dokku_log_info1 "Deploying $APP via the $DOKKU_SCHEDULER scheduler..."
cmd-deploy "$APP" "$IMAGE_TAG" || exit_code="$?"
if [[ "$exit_code" -eq 0 ]]; then
dokku_log_info2 "Application deployed:"
plugn trigger domains-urls "$APP" urls | sed "s/^/ /"
fi
elif [[ "$exit_code" -eq 0 ]]; then
dokku_log_info1 "Skipping deployment"
fi
echo
fi
if [[ -n "$DOKKU_BUILD_ID" ]]; then
plugn trigger builds-record-finalize "$APP" "$DOKKU_BUILD_ID" "$exit_code" || true
fi
return "$exit_code"
}
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"
plugn trigger builder-set-property "$APP" "detected" "$IMAGE_SOURCE_TYPE"
if ! dokku_build "$APP" "$IMAGE_SOURCE_TYPE" "$TMP_WORK_DIR"; then
return 1
fi
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
"$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet docker-cleanup "$APP"
}
dokku_auth() {
declare desc="calls user-auth plugin trigger"
export SSH_USER=${SSH_USER:=$USER}
export SSH_NAME=${NAME:="default"}
export DOKKU_COMMAND="$1"
local user_auth_count=$(find "$PLUGIN_PATH"/enabled/*/user-auth 2>/dev/null | wc -l)
# no plugin trigger exists
if [[ $user_auth_count == 0 ]]; then
return 0
fi
# this plugin trigger exists in the core `20_events` plugin
if [[ "$user_auth_count" == 1 ]] && [[ -f "$PLUGIN_PATH"/enabled/20_events/user-auth ]]; 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 sslip.io which allows for *.<ip address>.sslip.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 sslip.io which allows for *.<ip address>.sslip.io
echo "${RE_IPV6}\$"
}
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_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_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"
}
dokku_setup_build_capture() {
declare desc="generate a build-id, write a record, and redirect output to log + journald"
declare APP="$1" SOURCE="${2:-unknown}"
[[ "$DOKKU_REDIRECT_OUTPUT" == "true" ]] && return 0
if [[ -z "$APP" ]]; then
return 0
fi
export DOKKU_REDIRECT_OUTPUT=true
if [[ -z "$DOKKU_BUILD_ID" ]]; then
DOKKU_BUILD_ID="$(plugn trigger builds-generate-id 2>/dev/null)"
if [[ -z "$DOKKU_BUILD_ID" ]]; then
DOKKU_BUILD_ID="pid-${DOKKU_PID}"
fi
export DOKKU_BUILD_ID
fi
export DOKKU_BUILD_SOURCE="$SOURCE"
local DIR="$DOKKU_LIB_ROOT/data/builds/$APP"
local LOG="$DIR/${DOKKU_BUILD_ID}.log"
mkdir -p "$DIR" && : >"$LOG"
export DOKKU_BUILD_LOG_FILE="$LOG"
plugn trigger builds-record-start "$APP" "$DOKKU_BUILD_ID" "$$" "$SOURCE" || true
exec &> >(tee -a "$LOG" >(logger -i -t "dokku-${DOKKU_BUILD_ID}"))
}
acquire_app_deploy_lock() {
declare desc="acquire advisory lock for use in deploys"
local APP="$1"
local LOCK_TYPE="${2:-waiting}"
local APP_DEPLOY_LOCK_FILE="$DOKKU_LIB_ROOT/data/apps/$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..."
if [[ -z "$DOKKU_BUILD_ID" ]]; then
DOKKU_BUILD_ID="$(plugn trigger builds-generate-id 2>/dev/null)"
if [[ -z "$DOKKU_BUILD_ID" ]]; then
DOKKU_BUILD_ID="pid-${DOKKU_PID}"
fi
export DOKKU_BUILD_ID
fi
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 deploys"
local APP="$1"
local EXIT_CODE="${2:-0}"
local APP_DEPLOY_LOCK_FILE="$DOKKU_LIB_ROOT/data/apps/$APP/.deploy.lock"
if [[ -n "$DOKKU_BUILD_ID" ]]; then
plugn trigger builds-record-finalize "$APP" "$DOKKU_BUILD_ID" "$EXIT_CODE" || true
fi
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
if ! flock -n "$LOCK_FD" &>/dev/null; then
dokku_log_warn "$LOCK_FAILED_MSG"
dokku_log_fail "Run 'apps:unlock' to release the existing deploy lock"
fi
fi
echo "${DOKKU_BUILD_ID:-$DOKKU_PID}" >"$LOCK_FILE"
}
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")
"$@" >"$TMP_COMMAND_OUTPUT" 2>&1 || {
local exit_code="$?"
cat "$TMP_COMMAND_OUTPUT"
rm -rf "$TMP_COMMAND_OUTPUT" >/dev/null
return "$exit_code"
}
rm -rf "$TMP_COMMAND_OUTPUT" >/dev/null
return 0
}
fn-migrate-config-to-property() {
declare desc="[deprecated] migrates deprecated config variables to property counterpart. Use 'prop migrate-config-to-property' instead."
declare PLUGIN="$1" KEY="$2" CONFIG_VAR="$3" GLOBAL_CONFIG_VAR="$4"
# todo: refactor to remove config_unset usage
if ! declare -f -F config_unset >/dev/null; then
source "$PLUGIN_AVAILABLE_PATH/config/functions"
fi
if ! declare -f -F fn-plugin-property-write >/dev/null; then
source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions"
fi
if [[ -n "$GLOBAL_CONFIG_VAR" ]]; then
local value=$(plugn trigger config-get-global "$GLOBAL_CONFIG_VAR" || true)
if [[ -n "$value" ]]; then
dokku_log_info1 "Migrating deprecated global $GLOBAL_CONFIG_VAR to $PLUGIN $KEY property."
fn-plugin-property-write "$PLUGIN" --global "$KEY" "$value"
DOKKU_QUIET_OUTPUT=1 config_unset --global "$GLOBAL_CONFIG_VAR" || true
fi
fi
for app in $(dokku_apps "false"); do
local value=$(plugn trigger config-get "$app" "$CONFIG_VAR" || true)
if [[ -n "$value" ]]; then
dokku_log_info1 "Migrating deprecated $CONFIG_VAR to $PLUGIN $KEY property for $app."
fn-plugin-property-write "$PLUGIN" "$app" "$KEY" "$value"
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$app" "$CONFIG_VAR" || true
fi
done
}
fn-registry-docker-config-dir() {
declare desc="returns docker config file path if per-app registry credentials exist"
declare APP="$1"
local config_dir="/var/lib/dokku/config/registry/$APP"
local config_file="$config_dir/config.json"
if [[ -f "$config_file" ]] && [[ "$(jq -r '.auths | length' "$config_file" 2>/dev/null)" != "0" ]]; then
echo "$config_dir"
fi
}
fn-report-parse-args() {
declare desc="strips --format and --global flags from a report invocation, populating REPORT_FORMAT, REPORT_IS_GLOBAL, REPORT_ARGS"
REPORT_FORMAT="stdout"
REPORT_IS_GLOBAL="false"
REPORT_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--format)
REPORT_FORMAT="$2"
shift 2
;;
--global)
REPORT_IS_GLOBAL="true"
shift
;;
*)
REPORT_ARGS+=("$1")
shift
;;
esac
done
}
fn-report-emit-json() {
declare desc="serializes a flag_map array (passed by name) as a JSON object, stripping the --<prefix>- prefix from each key"
declare flag_map_name="$1" prefix="$2"
local -n flag_map_ref="$flag_map_name"
local json_output="{}"
for flag in "${flag_map_ref[@]}"; do
local key value
key="$(echo "$flag" | cut -d':' -f1 | sed "s/^--${prefix}-//")"
value="$(echo "$flag" | cut -d':' -f2- | sed 's/^ //')"
json_output="$(echo "$json_output" | jq -c --arg key "$key" --arg value "$value" '. + {($key): $value}')"
done
echo "$json_output"
}
fn-report-filter-global() {
declare desc="filters a flag_map array (passed by name) in place, keeping only entries with -global- in their flag name"
declare flag_map_name="$1"
local -n flag_map_ref="$flag_map_name"
local filtered=()
for flag in "${flag_map_ref[@]}"; do
local key="$(echo "$flag" | cut -d':' -f1)"
if [[ "$key" == *"-global-"* ]]; then
filtered+=("$flag")
fi
done
flag_map_ref=("${filtered[@]}")
}
fn-report-validate-format() {
declare desc="rejects --format json when an info flag is also specified"
declare FORMAT="$1" INFO_FLAG="$2"
if [[ "$FORMAT" != "stdout" ]] && [[ -n "$INFO_FLAG" ]]; then
dokku_log_fail "--format flag cannot be specified when specifying an info flag"
fi
}