#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x

has_tty() {
  declare desc="return 0 if we have a tty"
  if [[ "$(/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 'tls' ! -name '.*' -printf "%f\n" | sort) || (dokku_log_fail "You haven't deployed any applications yet")
  [[ $INSTALLED_APPS ]] && echo "$INSTALLED_APPS"
}

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 "-----> $*"
  else
    return 0
  fi
}

dokku_log_info2_quiet() {
  declare desc="log info2 formatter (with quiet option)"
  if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then
    echo "=====> $*"
  else
    return 0
  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" "----->" "$@"
  else
    return 0
  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" "=====>" "$@"
  else
    return 0
  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" "$@"
  else
    return 0
  fi
}

dokku_log_verbose_quiet() {
  declare desc="log verbose formatter (with quiet option)"
  if [[ -z "$DOKKU_QUIET_OUTPUT" ]]; then
    echo "       $*"
  else
    return 0
  fi
}

dokku_log_verbose() {
  declare desc="log verbose formatter"
  echo "       $*"
}

dokku_log_warn() {
  declare desc="log warning formatter"
  echo " !     $*"
}

dokku_log_fail() {
  declare desc="log fail formatter"
  echo "$@" 1>&2
  exit 1
}

dokku_log_event() {
  declare desc="log dokku events"
  logger -t dokku -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_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 logs "$CID" 2>&1); do
    dokku_log_verbose_quiet "$line"
  done
  IFS=$OIFS
}

verify_app_name() {
  declare desc="verify app name format and app existence"
  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() {
  declare desc="verify image existence"
  local IMAGE="$1"
  if (docker 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"
  [[ -z "$APP" ]] && dokku_log_fail "(get_app_image_repo) APP must not be null"
  echo "$IMAGE_REPO"
}

get_deploying_app_image_name() {
  declare desc="return deployed image identifier for a given app, tag tuple. validate if tag is presented"
  local APP="$1"; local IMAGE_TAG="$2";
  [[ -z "$APP" ]] && dokku_log_fail "(get_app_image_name) APP must not be null"

  local IMAGE_REMOTE_REPOSITORY=$(plugn trigger deployed-app-repository "$APP")
  local NEW_IMAGE_TAG=$(plugn trigger deployed-app-image-tag "$APP")

  if [[ -n "$NEW_IMAGE_TAG" ]]; then
    IMAGE_TAG="$NEW_IMAGE_TAG"
  fi

  local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG")
  if [[ -n "$IMAGE_REMOTE_REPOSITORY" ]]; then
    IMAGE="${IMAGE_REMOTE_REPOSITORY}${IMAGE}"
  fi
  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"; 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
    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_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"
  [[ ! -n "$APP" ]] && dokku_log_fail "(get_running_image_tag) APP must not be null"
  verify_app_name "$APP"

  local CIDS=( $(get_app_container_ids "$APP") )
  local 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() {
  declare desc="returns true if app image is based on herokuish"
  # 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" &> /dev/null
}

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=("$@")
  for arg in "$@"; do
    $skip && skip=false && continue
    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
    local next_index=$(( next_index + 1 ))
  done
  return 0
}

copy_from_image() {
  declare desc="copy file from named image to destination"
  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" ]] && local WORKDIR=/app
      local SRC_FILE="$WORKDIR/$SRC_FILE"
    fi
    local 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() {
  declare desc="returns list of docker container ids for given app and optional container_type"
  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
    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"
  verify_app_name "$APP"

  ! (is_deployed "$APP") && dokku_log_fail "App $APP has not been deployed"
  local CIDS=$(get_app_container_ids "$APP" "$CONTAINER_TYPE")

  for CID in $CIDS; do
    local APP_CONTAINER_STATUS=$(docker inspect -f '{{.State.Running}}' "$CID" 2>/dev/null || true)
    [[ "$APP_CONTAINER_STATUS" == "true" ]] && 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
  verify_app_name "$APP"

  ! (is_deployed "$APP") && dokku_log_fail "App $APP has not been deployed"

  CONTAINER_TYPES="$(find "$DOKKU_ROOT/$APP" -maxdepth 1 -name "CONTAINER.*" -print0 | 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"
}

get_cmd_from_procfile() {
  declare desc="parse cmd from app Procfile"
  local APP=$1; local PROC_TYPE=$2; local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
  verify_app_name "$APP"

  if [[ -f $DOKKU_PROCFILE ]]; then
    local line; local name; local command
    while read line || [[ -n "$line" ]]; do
      if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then
        continue
      fi
      line=$(strip_inline_comments "$line")
      name="${line%%:*}"
      command="${line#*:[[:space:]]}"
      [[ "$name" == "$PROC_TYPE" ]] && echo "$command" && break
    done < "$DOKKU_PROCFILE"
  fi
}

is_deployed() {
  declare desc="return 0 if given app has a running container"
  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 () {
  declare desc="return 0 if given docker container id is in running state"
  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 () {
  declare desc="return 0 if given docker container id is in given state"
  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() {
  declare desc="return 0 if given app has a running container"
  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
}

dokku_build() {
  declare desc="build phase"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"

  local APP="$1"; local IMAGE_SOURCE_TYPE="$2"; local TMP_WORK_DIR="$3"; local IMAGE=$(get_app_image_name "$APP")
  verify_app_name "$APP"

  local CACHE_DIR="$DOKKU_ROOT/$APP/cache"

  eval "$(config_export app "$APP")"
  pushd "$TMP_WORK_DIR" &> /dev/null

  case "$IMAGE_SOURCE_TYPE" in
    herokuish)
      local id=$(tar -c . | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$DOKKU_IMAGE" /bin/bash -c "mkdir -p /app && tar -xC /app")
      test "$(docker wait "$id")" -eq 0
      docker commit "$id" "$IMAGE" > /dev/null
      [[ -d $CACHE_DIR ]] || mkdir -p "$CACHE_DIR"
      plugn trigger pre-build-buildpack "$APP"

      local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE")
      [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true "
      # shellcheck disable=SC2086
      local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -v $CACHE_DIR:/cache -e CACHE_PATH=/cache $DOCKER_ARGS $IMAGE /build)
      docker attach "$id"
      test "$(docker wait "$id")" -eq 0
      docker commit "$id" "$IMAGE" > /dev/null

      plugn trigger post-build-buildpack "$APP"
      ;;

    dockerfile)
      # extract first port from Dockerfile
      local DOCKERFILE_PORTS=$(get_dockerfile_exposed_ports Dockerfile)
      [[ -n "$DOCKERFILE_PORTS" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_PORTS="$DOCKERFILE_PORTS"

      # extract ENTRYPOINT/CMD from Dockerfile
      local DOCKERFILE_ENTRYPOINT=$(extract_directive_from_dockerfile Dockerfile ENTRYPOINT)
      [[ -n "$DOCKERFILE_ENTRYPOINT" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_ENTRYPOINT="$DOCKERFILE_ENTRYPOINT"
      local DOCKERFILE_CMD=$(extract_directive_from_dockerfile Dockerfile CMD)
      [[ -n "$DOCKERFILE_CMD" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_CMD="$DOCKERFILE_CMD"
      plugn trigger pre-build-dockerfile "$APP"

      [[ "$DOKKU_DOCKERFILE_CACHE_BUILD" == "false" ]] && DOKKU_DOCKER_BUILD_OPTS="$DOKKU_DOCKER_BUILD_OPTS --no-cache"
      local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE")
      # strip --volume and -v args from DOCKER_ARGS
      local DOCKER_ARGS=$(sed -e "s/--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<< "$DOCKER_ARGS")

      # shellcheck disable=SC2086
      docker build $DOCKER_ARGS $DOKKU_DOCKER_BUILD_OPTS -t $IMAGE .

      plugn trigger post-build-dockerfile "$APP"
      ;;

    *)
      dokku_log_fail "Building image source type $IMAGE_SOURCE_TYPE not supported!"
      ;;
  esac
}

dokku_release() {
  declare desc="release phase"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"

  local APP="$1"; local IMAGE_SOURCE_TYPE="$2"; local IMAGE_TAG="$3"; local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG")
  verify_app_name "$APP"

  case "$IMAGE_SOURCE_TYPE" in
    herokuish)
      plugn trigger pre-release-buildpack "$APP" "$IMAGE_TAG"
      if [[ -n $(config_export global) ]]; then
        local id=$(config_export global | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /app/.profile.d && cat > /app/.profile.d/00-global-env.sh")
        test "$(docker wait "$id")" -eq 0
        docker commit "$id" "$IMAGE" > /dev/null
      fi
      if [[ -n $(config_export app "$APP") ]]; then
        local id=$(config_export app "$APP" | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /app/.profile.d && cat > /app/.profile.d/01-app-env.sh")
        test "$(docker wait "$id")" -eq 0
        docker commit "$id" "$IMAGE" > /dev/null
      fi
      plugn trigger post-release-buildpack "$APP" "$IMAGE_TAG"
      ;;

    dockerfile)
      # buildstep plugins don't necessarily make sense for dockerfiles. call the new breed!!!
      plugn trigger pre-release-dockerfile "$APP" "$IMAGE_TAG"
      plugn trigger post-release-dockerfile "$APP" "$IMAGE_TAG"
      ;;

    *)
      dokku_log_fail "Releasing image source type $IMAGE_SOURCE_TYPE not supported!"
      ;;
  esac
}

dokku_deploy_cmd() {
  declare desc="deploy phase"
  local cmd="deploy"
  source "$PLUGIN_AVAILABLE_PATH/checks/functions"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"
  source "$PLUGIN_CORE_AVAILABLE_PATH/proxy/functions"

  [[ -z $1 ]] && dokku_log_fail "Please specify an app to deploy"
  local APP="$1"; local IMAGE_TAG="$2"; local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
  verify_app_name "$APP"
  plugn trigger pre-deploy "$APP" "$IMAGE_TAG"

  is_image_herokuish_based "$IMAGE" && local DOKKU_HEROKUISH=true
  local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE"
  local oldids=$(get_app_container_ids "$APP")

  local DOKKU_DEFAULT_DOCKER_ARGS=$(: | plugn trigger docker-args-deploy "$APP" "$IMAGE_TAG")
  local DOKKU_IS_APP_PROXY_ENABLED="$(is_app_proxy_enabled "$APP")"
  local DOKKU_DOCKER_STOP_TIMEOUT="$(config_get "$APP" DOKKU_DOCKER_STOP_TIMEOUT || true)"
  [[ $DOKKU_DOCKER_STOP_TIMEOUT ]] && DOCKER_STOP_TIME_ARG="--time=${DOKKU_DOCKER_STOP_TIMEOUT}"

  local line; local PROC_TYPE; local PROC_COUNT; local CONTAINER_INDEX
  while read -r line || [[ -n "$line" ]]; do
    [[ "$line" =~ ^#.* ]] && continue
    line="$(strip_inline_comments "$line")"
    PROC_TYPE=${line%%=*}
    PROC_COUNT=${line#*=}
    CONTAINER_INDEX=1

    if [[ "$(is_app_proctype_checks_disabled "$APP" "$PROC_TYPE")" == "true" ]]; then
      dokku_log_info1 "zero downtime is disabled for app ($APP.$PROC_TYPE). stopping currently running containers"
      local cid proctype_oldids="$(get_app_running_container_ids "$APP" "$PROC_TYPE")"
      for cid in $proctype_oldids; do
        dokku_log_info2 "stopping $APP.$PROC_TYPE ($cid)"
        # shellcheck disable=SC2086
        docker stop $DOCKER_STOP_TIME_ARG "$cid" &> /dev/null
      done
    fi

    while [[ $CONTAINER_INDEX -le $PROC_COUNT ]]; do
      local id=""; local port=""; local ipaddr=""
      local DOKKU_CONTAINER_ID_FILE="$DOKKU_ROOT/$APP/CONTAINER.$PROC_TYPE.$CONTAINER_INDEX"
      local DOKKU_IP_FILE="$DOKKU_ROOT/$APP/IP.$PROC_TYPE.$CONTAINER_INDEX"
      local DOKKU_PORT_FILE="$DOKKU_ROOT/$APP/PORT.$PROC_TYPE.$CONTAINER_INDEX"

      # start the app
      local DOCKER_ARGS="$DOKKU_DEFAULT_DOCKER_ARGS"
      local DOCKER_ARGS+=" -e DYNO='$PROC_TYPE.$CONTAINER_INDEX' "
      [[ "$DOKKU_TRACE" ]] && local DOCKER_ARGS+=" -e TRACE=true "

      [[ -n "$DOKKU_HEROKUISH" ]] && local START_CMD="/start $PROC_TYPE"

      if [[ -z "$DOKKU_HEROKUISH" ]]; then
        local DOKKU_DOCKERFILE_PORTS=($(config_get "$APP" DOKKU_DOCKERFILE_PORTS || true))
        local DOKKU_DOCKERFILE_START_CMD=$(config_get "$APP" DOKKU_DOCKERFILE_START_CMD || true)
        local DOKKU_PROCFILE_START_CMD=$(get_cmd_from_procfile "$APP" "$PROC_TYPE")
        local START_CMD=${DOKKU_DOCKERFILE_START_CMD:-$DOKKU_PROCFILE_START_CMD}
      fi

      if [[ "$PROC_TYPE" == "web" ]]; then
        if [[ -z "${DOKKU_DOCKERFILE_PORTS[*]}" ]]; then
          local port=5000
          local DOKKU_DOCKER_PORT_ARGS+="-p $port"
        else
          local p
          for p in ${DOKKU_DOCKERFILE_PORTS[*]};do
            if [[ ! "$p" =~ .*udp.* ]]; then
              # set port to first non-udp port
              local p=${p//\/tcp}
              local port=${port:="$p"}
            fi
            local DOKKU_DOCKER_PORT_ARGS+=" -p $p "
          done
        fi
        if [[ "$DOKKU_IS_APP_PROXY_ENABLED" == "true" ]]; then
          # shellcheck disable=SC2086
          local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -e PORT=$port $DOCKER_ARGS $IMAGE $START_CMD)
          local ipaddr=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$id")
          # Docker < 1.9 compatibility
          if [[ -z $ipaddr ]]; then
            local ipaddr=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$id")
          fi
        else
          # shellcheck disable=SC2086
          local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOKKU_DOCKER_PORT_ARGS -e PORT=$port $DOCKER_ARGS $IMAGE $START_CMD)
          local port=$(docker port "$id" "$port" | sed 's/[0-9.]*://')
          local ipaddr=127.0.0.1
        fi
      else
        # shellcheck disable=SC2086
        local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOCKER_ARGS $IMAGE $START_CMD)
      fi

      kill_new() {
        declare desc="wrapper function to kill newly started app container"
        local id="$1"
        docker inspect "$id" &> /dev/null && docker stop "$id" > /dev/null && docker kill "$id" &> /dev/null
        trap - INT TERM EXIT
        kill -9 $$
      }

      # run checks first, then post-deploy hooks, which switches proxy traffic
      trap 'kill_new $id' INT TERM EXIT
      if [[ "$(is_app_proctype_checks_disabled "$APP" "$PROC_TYPE")" == "false" ]]; then
        dokku_log_info1 "Attempting pre-flight checks"
        plugn trigger check-deploy  "$APP" "$id" "$PROC_TYPE" "$port" "$ipaddr"
      fi
      trap -        INT TERM EXIT

      # now using the new container
      [[ -n "$id" ]] && echo "$id" > "$DOKKU_CONTAINER_ID_FILE"
      [[ -n "$ipaddr" ]] && echo "$ipaddr" > "$DOKKU_IP_FILE"
      [[ -n "$port" ]] && echo "$port" > "$DOKKU_PORT_FILE"

      # cleanup pre-migration files
      rm -f "$DOKKU_ROOT/$APP/CONTAINER" "$DOKKU_ROOT/$APP/IP" "$DOKKU_ROOT/$APP/PORT"

      local CONTAINER_INDEX=$(( CONTAINER_INDEX + 1 ))
    done
    # cleanup when we scale down
    if [[ "$PROC_COUNT" == 0 ]]; then
      local CONTAINER_IDX_OFFSET=0
    else
      local CONTAINER_IDX_OFFSET=$((PROC_COUNT + 1))
    fi
    local container_state_filetype
    for container_state_filetype in CONTAINER IP PORT; do
      cd "$DOKKU_ROOT/$APP"
      find . -maxdepth 1 -name "$container_state_filetype.$PROC_TYPE.*" -printf "%f\n" | sort -t . -k 3 -n | tail -n +$CONTAINER_IDX_OFFSET | xargs rm -f
    done
  done < "$DOKKU_SCALE_FILE"

  dokku_log_info1 "Running post-deploy"
  plugn trigger post-deploy "$APP" "$port" "$ipaddr" "$IMAGE_TAG"

  # kill the old container
  if [[ -n "$oldids" ]]; then

    if [[ -z "$DOKKU_WAIT_TO_RETIRE" ]]; then
      local DOKKU_APP_DOKKU_WAIT_TO_RETIRE=$(config_get "$APP" DOKKU_WAIT_TO_RETIRE || true)
      local DOKKU_GLOBAL_DOKKU_WAIT_TO_RETIRE=$(config_get --global DOKKU_WAIT_TO_RETIRE || true)
      local DOKKU_WAIT_TO_RETIRE=${DOKKU_APP_DOKKU_WAIT_TO_RETIRE:="$DOKKU_GLOBAL_DOKKU_WAIT_TO_RETIRE"}
    fi

    # Let the old container finish processing requests, before terminating it
    local WAIT="${DOKKU_WAIT_TO_RETIRE:-60}"
    dokku_log_info1 "Shutting down old containers in $WAIT seconds"
    local oldid
    for oldid in $oldids; do
      dokku_log_info2 "$oldid"
    done
    (
      exec >/dev/null 2>/dev/null </dev/null
      trap '' INT HUP
      sleep "$WAIT"
      for oldid in $oldids; do
        # Attempt to stop, if that fails, then force a kill as docker seems
        # to not send SIGKILL as the docs would indicate. If that fails, move
        # on to the next.
        # shellcheck disable=SC2086
        docker stop $DOCKER_STOP_TIME_ARG "$oldid" \
        || docker kill "$oldid" \
        || plugn trigger retire-container-failed "$APP" # plugin trigger for event logging
      done
    ) & disown -a
    # Use trap since disown/nohup don't seem to keep child alive
    # Give child process just enough time to set the traps
    sleep 0.1
  fi
}

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")
  verify_app_name "$APP"

  if verify_image "$IMAGE"; then
    if is_image_herokuish_based "$IMAGE"; then
      local IMAGE_SOURCE_TYPE="herokuish"
    else
      local IMAGE_SOURCE_TYPE="dockerfile"
      local 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 ($IMAGE)..."
    dokku_release "$APP" "$IMAGE_SOURCE_TYPE" "$IMAGE_TAG"

    if [[ "$DOKKU_SKIP_DEPLOY" != "true" ]]; then
      dokku_log_info1 "Deploying $APP ($IMAGE)..."
      dokku_deploy_cmd "$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"
  if [[ -z "$DOKKU_SKIP_CLEANUP" ]]; then
    dokku_log_info1 "Cleaning up..."
    docker_cleanup
  else
    dokku_log_info1 "DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup"
  fi
  dokku_log_info1 "Building $APP from $IMAGE_SOURCE_TYPE..."
  config_set --no-restart "$APP" DOKKU_APP_TYPE="$IMAGE_SOURCE_TYPE" &> /dev/null
  dokku_build "$APP" "$IMAGE_SOURCE_TYPE" "$TMP_WORK_DIR"
  release_and_deploy "$APP"
}

docker_cleanup() {
  declare desc="cleans up all exited/dead containers and removes all dangling images"
  # delete all non-running containers
  # shellcheck disable=SC2046
  docker rm $(docker ps -a -f "status=exited" -f "label=$DOKKU_CONTAINER_LABEL" -q) &> /dev/null || true

  # delete all dead containers
  # shellcheck disable=SC2046
  docker rm $(docker ps -a -f "status=dead" -f "label=$DOKKU_CONTAINER_LABEL" -q) &> /dev/null || true

  # delete unused images
  # shellcheck disable=SC2046
  docker rmi $(docker images -f 'dangling=true' -q) &> /dev/null &
}

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"}
  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 *.<ip address>.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 *.<ip address>.xip.io
  echo "${RE_IPV6}\$"
}

get_global_vhost() {
  declare desc="return 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() {
  declare 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() {
  declare desc="returns true or false if vhost support is enabled for a given application"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"

  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() {
  declare desc="disable vhost support for given application"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"

  local APP=$1; verify_app_name "$APP"
  local APP_VHOST_FILE="$DOKKU_ROOT/$APP/VHOST"
  local APP_URLS_FILE="$DOKKU_ROOT/$APP/URLS"

  plugn trigger pre-disable-vhost "$APP"
  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

  [[ "$2" == "--no-restart" ]] && local CONFIG_SET_ARGS=$2
  # shellcheck disable=SC2086
  config_set $CONFIG_SET_ARGS $APP NO_VHOST=1
}

enable_app_vhost() {
  declare desc="enable vhost support for given application"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"

  local APP=$1; verify_app_name "$APP"

  plugn trigger pre-enable-vhost "$APP"
  [[ "$2" == "--no-restart" ]] && local CONFIG_SET_ARGS=$2
  # shellcheck disable=SC2086
  config_set $CONFIG_SET_ARGS "$APP" NO_VHOST=0
}

get_dockerfile_exposed_ports() {
  declare desc="return all exposed ports from passed file path"
  local DOCKERFILE_PORTS=$(egrep "^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 inspect -f '{{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 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 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=$(egrep "^${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"; 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
  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"; 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() {
  declare desc="print an app's available urls"
  source "$PLUGIN_AVAILABLE_PATH/config/functions"
  source "$PLUGIN_AVAILABLE_PATH/domains/functions"
  source "$PLUGIN_AVAILABLE_PATH/proxy/functions"

  local APP="$2"; verify_app_name "$APP"
  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="$(egrep -v "^#" "$URLS_FILE")"
    if [[ -n "$DOKKU_PROXY_PORT_MAP" ]]; then
      local port_map app_vhost
      local app_vhosts=$(get_app_domains "$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)
        echo "$URLS" | tr ' ' '\n' | head -n1
      ;;
      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 [[ "$(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_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/VHOST")"
    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"
  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() {
  declare desc="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
}

strip_inline_comments() {
  declare desc="removes bash-style comment from input line"
  local line="$1"
  local stripped_line="${line%[[:space:]]#*}"

  echo "$stripped_line"
}

is_valid_hostname() {
  declare desc="return 0 if argument is a valid hostname; else return 1"
  local hostname_string="$1"; local hostname_regex='^(([a-zA-Z0-9\*](-?[a-zA-Z0-9])*)\.)*[a-zA-Z\*](-?[a-zA-Z0-9])+\.[a-zA-Z]{2,}$'
  if [[ $hostname_string =~ $hostname_regex ]]; then
    return 0
  else
    return 1
  fi
}

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"
}

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"; verify_app_name "$APP"
  local LOCK_TYPE="${2:-waiting}"
  local APP_DEPLOY_LOCK_FD="200"
  local APP_DEPLOY_LOCK_FILE="$DOKKU_ROOT/$APP/.deploy.lock"
  local LOCK_WAITING_MSG="$APP is currently being deployed. Waiting..."
  local LOCK_FAILED_MSG="$APP is currently being deployed. Exiting..."
  local SHOW_MSG=true

  eval "exec $APP_DEPLOY_LOCK_FD>$APP_DEPLOY_LOCK_FILE"
  if [[ "$LOCK_TYPE" == "waiting" ]]; then
    while [[ $(flock -n "$APP_DEPLOY_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 "$APP_DEPLOY_LOCK_FD" &>/dev/null || dokku_log_fail "$LOCK_FAILED_MSG"
  fi
}

release_app_deploy_lock() {
  declare desc="release advisory lock used in git/tar deploys"
  local APP="$1"; verify_app_name "$APP"
  local APP_DEPLOY_LOCK_FD="200"
  local APP_DEPLOY_LOCK_FILE="$DOKKU_ROOT/$APP/.deploy.lock"

  flock -u "$APP_DEPLOY_LOCK_FD" && rm -f "$APP_DEPLOY_LOCK_FILE" &> /dev/null
}
