#!/bin/bash ## Bash wrap script to launch the ``compose`` docker with right options. ## ## ## Launcher ## - should need minimum requirement to run ## - no shell libs ## [[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true ## ## From kal-shlib ## read-0-err() { local ret="$1" eof="" idx=0 last= read -r -- "${ret?}" <<<"0" shift while [ "$1" ]; do last=$idx read -r -d '' -- "$1" || { ## Put this last value in ${!ret} eof="$1" read -r -- "$ret" <<<"${!eof}" break } ((idx++)) shift done [ -z "$eof" ] || { if [ "$last" != 0 ]; then echo "Error: read-0-err couldn't fill all value" >&2 read -r -- "$ret" <<<"127" else if [ -z "${!ret}" ]; then echo "Error: last value is not a number, did you finish with an errorlevel ?" >&2 read -r -- "$ret" <<<"126" fi fi false } } p-err() { "$@" echo "$?" } _sed_compat_load() { if get_path sed >/dev/null; then if sed --version >/dev/null 2>&1; then ## GNU sed_compat() { sed -r "$@"; } sed_compat_i() { sed -r -i "$@"; } else ## BSD sed_compat() { sed -E "$@"; } sed_compat_i() { sed -E -i "" "$@"; } fi else ## Look for ``gsed`` if (get_path gsed && gsed --version) >/dev/null 2>&1; then sed_compat() { gsed -r "$@"; } sed_compat_i() { gsed -r -i "$@"; } else die "$exname: required GNU or BSD sed not found" fi fi } ## BSD / GNU sed compatibility layer sed_compat() { _sed_compat_load; sed_compat "$@"; } hash_get() { if get_path sha256sum >/dev/null; then hash_get() { local x; x=$(sha256sum) || return 1; echo "${x:0:32}"; } elif get_path md5sum >/dev/null; then hash_get() { local x; x=$(md5sum) || return 1; echo "${x:0:32}"; } elif get_path md5 >/dev/null; then hash_get() { md5; } else err "required GNU md5sum or BSD md5 not found" return 1 fi hash_get } ## output on stdout the next record on stdin separated by a '\0' next-0() { local ans IFS='' read -r -d '' ans && printf "%s" "$ans" } array_read-0() { local elt aname while true; do for aname in "$@"; do declare -n cur="$aname" elt="$(next-0)" || return 0 cur+=("$elt") done done } str_pattern_matches() { local str="$1" shift for pattern in "$@"; do eval "[[ \"$str\" == $pattern ]]" && return 0 done return 1 } ansi_color() { local choice="$1" if [ "$choice" == "tty" ]; then if [ -t 1 ]; then choice="yes" else choice="no" fi fi ANSI_ESC=$'\e[' if [ "$choice" != "no" ]; then NORMAL="${ANSI_ESC}0m" GRAY="${ANSI_ESC}1;30m" RED="${ANSI_ESC}1;31m" GREEN="${ANSI_ESC}1;32m" YELLOW="${ANSI_ESC}1;33m" BLUE="${ANSI_ESC}1;34m" PINK="${ANSI_ESC}1;35m" CYAN="${ANSI_ESC}1;36m" WHITE="${ANSI_ESC}1;37m" DARKGRAY="${ANSI_ESC}0;30m" DARKRED="${ANSI_ESC}0;31m" DARKGREEN="${ANSI_ESC}0;32m" DARKYELLOW="${ANSI_ESC}0;33m" DARKBLUE="${ANSI_ESC}0;34m" DARKPINK="${ANSI_ESC}0;35m" DARKCYAN="${ANSI_ESC}0;36m" DARKWHITE="${ANSI_ESC}0;37m" else NORMAL= RED= GREEN= YELLOW= BLUE= GRAY= WHITE= DARKGRAY= DARKRED= DARKGREEN= DARKYELLOW= DARKBLUE= DARKPINK= DARKCYAN= DARKWHITE= fi ansi_color="$choice" export NORMAL GRAY RED GREEN YELLOW BLUE PINK CYAN WHITE DARKGRAY \ DARKRED DARKGREEN DARKYELLOW DARKBLUE DARKPINK DARKCYAN \ DARKWHITE SUCCESS WARNING FAILURE NOOP ON OFF ERROR ansi_color } e() { printf "%s" "$*"; } warn() { e "${YELLOW}Warning:$NORMAL" "$*"$'\n' >&2 ; } info() { e "${BLUE}II$NORMAL" "$*"$'\n' >&2 ; } verb() { [ -z "$VERBOSE" ] || e "$*"$'\n' >&2; } debug() { [ -z "$DEBUG" ] || e "$*"$'\n' >&2; } err() { e "${RED}Error:$NORMAL $*"$'\n' >&2 ; } die() { err "$@" ; exit 1; } ## equivalent of 'xargs echo' with builtins nspc() { local content content=$(printf "%s " $(cat -)) printf "%s" "${content::-1}" } get_path() { ( IFS=: for d in $PATH; do filename="$d/$1" [ -f "$filename" -a -x "$filename" ] && { echo "$d/$1" return 0 } done return 1 ) } depends() { ## Avoid colliding with variables that are created with depends. local __i __path __new_name for __i in "$@"; do if ! __path=$(get_path "$__i"); then __new_name="$(echo "${__i//-/_}")" if [ "$__new_name" != "$__i" ]; then depends "$__new_name" else err "dependency check: couldn't find '$__i' required command." exit 1 fi else if ! test -z "$__path" ; then export "$(echo "${__i//- /__}")"="$__path" fi fi done } get_os() { local uname_output uname_output="$(uname -s)" case "${uname_output}" in Linux*) if [[ "$(< /proc/version)" =~ "@(Microsoft|WSL)" ]]; then e wsl # elif [[ "$(< /proc/version)" =~ "@(microsoft|WSL)" ]]; then # e wsl2 else e linux fi ;; Darwin*) e mac;; CYGWIN*) e cygwin;; MINGW*) e mingw;; *) e "UNKNOWN:${uname_output}";; esac } read-0() { local eof= IFS='' while [ "$1" ]; do read -r -d '' -- "$1" || eof=1 shift done [ -z "$eof" ] } read-0a() { local eof= IFS='' while [ "$1" ]; do IFS='' read -r -d $'\n' -- "$1" || eof=1 shift done [ -z "$eof" ] } p0() { printf "%s\0" "$@" } cla.normalize() { local letters arg i while [ "$#" != 0 ]; do arg=$1 case "$arg" in --) p0 "$@" return 0 ;; --*=*|-*=*) shift set -- "${arg%%=*}" "${arg#*=}" "$@" continue ;; --*|-?) :;; -*) letters=${arg:1} shift i=${#letters} while ((i--)); do set -- -${letters:$i:1} "$@" done continue ;; esac p0 "$arg" shift done } docker_has_image() { local image="$1" images=$(docker images -q "$image" 2>/dev/null) || { err "docker images call has failed unexpectedly." return 1 } [ -n "$images" ] } docker_image_id() { local image="$1" image_id=$(docker inspect "$image" --format='{{.Id}}') || return 1 echo "$image_id" } ## ## Compose-core common functions ## list_compose_vars() { while read-0a def; do def="${def##* }" def="${def%=*}" p0 "$def" done < <(declare -p | grep "^declare -x COMPOSE_[A-Z_]\+=\"") } get_running_compose_containers() { ## XXXvlab: docker bug: there will be a final newline anyway docker ps --filter label="compose.service" --format='{{.ID}}' docker ps --filter label="compose" --format='{{.ID}}' } get_volumes_for_container() { local container="$1" docker inspect \ --format '{{range $mount := .Mounts}}{{$mount.Source}}{{"\x00"}}{{$mount.Destination}}{{"\x00"}}{{end}}' \ "$container" } is_volume_used() { local volume="$1" container_id src dst while read container_id; do while read-0 src dst; do [ "$src" == "$volume" ] && { return 0 } done < <(get_volumes_for_container "$container_id") done < <(get_running_compose_containers) return 1 } _MULTIOPTION_REGEX='^((-[a-zA-Z]|--[a-zA-Z0-9-]+)(, )?)+' _MULTIOPTION_REGEX_LINE_FILTER=$_MULTIOPTION_REGEX'(\s|=)' ## ## compose launcher functions ## clean_unused_sessions() { for f in "$SESSION_DIR/"*; do [ -e "$f" ] || continue is_volume_used "$f" && continue ## XXXvlab: the second rmdir should not be useful [ -d "$f" ] && { err "Unexpected directory as session remnant $(printf "%q" "$f")" >&2 echo " - can you contact support to report this issue ?" >&2 echo " - as a workaround, you can remove it manually using:" >&2 echo "" >&2 echo " rm -rf $(printf "%q" "$f")" >&2 echo "" >&2 return 1 } rm -f "$f" >/dev/null || { err "Couldn't delete $(printf "%q" "$f")" >&2 return 1 } done } check_no_links_subdirs() { local dir for dir in "$@"; do [ -d "$dir" ] || continue if [ -L "$dir" ]; then err "Unfortunately, this compose launcher do not support yet symlinks in charm-store." echo " Found symlink in charm-store: $dir" >&2 return 1 fi [ -e "$dir/metadata.yml" ] && continue check_no_links_subdirs "$dir"/* || return 1 done } get_override() { local override override=$(get_volume_opt "$@") || return 1 if [ -n "$override" ]; then if ! [ -f "$override" ]; then err "Invalid override of 'compose-core' detected." \ "File '$override' does not exist on host." return 1 fi echo "$override" fi } get_hash_image() { local compose_docker_image="$1" override="$2" { docker_image_id "$compose_docker_image" || { err "Failed to get docker image id of image '$compose_docker_image'." return 1 } p0 "" [ -n "$override" ] && cat "$override" true } | hash_get return "${PIPESTATUS[0]}" } get_compose_file_opt() { local hash_bin="$1" override="$2" \ cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$(p0 "$@" | hash_get)" if [ -e "$cache_file" ]; then cat "$cache_file" && touch "$cache_file" || return 1 return 0 fi shift 2 DC_MATCH_MULTI=$(get_compose_multi_opts_list "$hash_bin" "$override") || return 1 DC_MATCH_SINGLE=$(get_compose_single_opts_list "$hash_bin" "$override") || return 1 while read-0 arg; do case "$arg" in "-f"|"--file") read-0 value e "$value" return 0 ;; --*|-*) if str_pattern_matches "$arg" $DC_MATCH_MULTI; then read-0 value opts+=("$arg" "$value") shift elif str_pattern_matches "$arg" $DC_MATCH_SINGLE; then opts+=("$arg") else debug "Unknown option '$arg'. Didn't manage to pre-parse correctly options." return 1 fi ;; *) return 1 ;; esac done < <(cla.normalize "$@") | tee "$cache_file" } replace_compose_file_opt() { local hash_bin="$1" override="$2" args arg \ cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$(p0 "$@" | hash_get)" if [ -e "$cache_file" ]; then cat "$cache_file" && touch "$cache_file" || return 1 return 0 fi debug "Replacing '-f|--file' argument in command line." shift 2 DC_MATCH_MULTI=$(get_compose_multi_opts_list "$hash_bin" "$override") || return 1 DC_MATCH_SINGLE=$(get_compose_single_opts_list "$hash_bin" "$override") || return 1 args=() while read-0 arg; do case "$arg" in "-f"|"--file") read-0 value args+=("$arg" "${value##*/}") ;; --*|-*) if str_pattern_matches "$arg" $DC_MATCH_MULTI; then read-0 value args+=("$arg" "$value") shift elif str_pattern_matches "$arg" $DC_MATCH_SINGLE; then args+=("$arg") else err "Unknown option '$arg'. Didn't manage to pre-parse correctly options." return 1 fi ;; *) args+=("$arg") while read-0 arg; do args+=("$arg") done ;; esac done < <(cla.normalize "$@") p0 "${args[@]}" | tee "$cache_file" } get_compose_opts_list() { local hash_bin="$1" override="$2" \ cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$1" if [ -e "$cache_file" ]; then cat "$cache_file" && touch "$cache_file" || return 1 return 0 fi debug "Pre-Launching docker to retrieve command line argument definitions." opts_list=() if [ -n "$override" ]; then opts_list+=("-v" "$override:/usr/local/bin/compose-core:ro") fi compose_opts_help=$(docker run "${opts_list[@]}" "$COMPOSE_DOCKER_IMAGE" --help 2>/dev/null) echo "$compose_opts_help" | grep '^Options:' -A 20000 | tail -n +2 | { cat ; echo; } | grep -E -m 1 "^\S*\$" -B 10000 | head -n -1 | grep -E "^\s+-" | sed_compat 's/\s+((((-[a-zA-Z]|--[a-zA-Z0-9-]+)( [A-Z=]+|=[^ ]+)?)(, )?)+)\s+.*$/\1/g' | tee "$cache_file" || return 1 } multi_opts_filter() { grep -E "$_MULTIOPTION_REGEX_LINE_FILTER" | sed_compat "s/^($_MULTIOPTION_REGEX)(\s|=).*$/\1/g" | tr ',' "\n" | nspc } single_opts_filter() { grep -E -v "$_MULTIOPTION_REGEX_LINE_FILTER" | tr ',' "\n" | nspc } get_compose_multi_opts_list() { local hash_bin="$1" override="$2" \ cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$1" opts_list if [ -e "$cache_file" ]; then cat "$cache_file" && touch "$cache_file" || return 1 return 0 fi opts_list=$(get_compose_opts_list "$hash_bin" "$override") || return 1 echo "$opts_list" | multi_opts_filter | tee "$cache_file" } get_compose_single_opts_list() { local hash_bin="$1" override="$2" \ cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$1" opts_list if [ -e "$cache_file" ]; then cat "$cache_file" && touch "$cache_file" || return 1 return 0 fi opts_list=$(get_compose_opts_list "$hash_bin" "$override") || return 1 echo "$opts_list" | single_opts_filter | tee "$cache_file" } get_volume_opt() { local cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$(p0 "$@" | hash_get)" if [ -e "$cache_file" ]; then cat "$cache_file" && touch "$cache_file" || return 1 return 0 fi while [ "$#" != 0 ]; do case "$1" in "-v") dst="${2#*:}" dst="${dst%:*}" if [ "$dst" == "/usr/local/bin/compose-core" ]; then override="${2%%:*}" debug "Override of compose-core found: $override" fi shift;; "-e"|"-w") shift;; *) : ;; esac shift done { [ -n "$override" ] && echo "$override"; } | tee "$cache_file" } get_tz() { if [ -n "$TZ" ]; then : elif [ -n "$COMPOSE_LOCAL_ROOT" ] && ## previous compose run [ -e "$COMPOSE_LOCAL_ROOT/etc/timezone" ]; then read -r TZ < "$COMPOSE_LOCAL_ROOT/etc/timezone" elif [ -e "/etc/timezone" ]; then ## debian host system timezone read -r TZ < /etc/timezone elif [ -e "/etc/localtime" ]; then ## redhat and macosx sys timezone local fullpath dirname fullpath="$(readlink -f /etc/localtime)" dirname="${fullpath%/*}" TZ=${TZ:-${fullpath##*/}/${dirname##*/}} else err "Timezone not found nor inferable !" echo " compose relies on '/etc/timezone' or '/etc/localtime' to be present " >&2 echo " so as to ensure same timezone for all containers that need it." >&2 echo >&2 echo " You can set a default value for compose by create issuing:" >&2 echo >&2 if [ -n "$COMPOSE_LOCAL_ROOT" ] && [ "$UID" != 0 ]; then echo " mkdir -p $COMPOSE_LOCAL_ROOT/etc &&" >&2 echo " echo \"Europe/Paris\" > $COMPOSE_LOCAL_ROOT/etc/timezone" >&2 else echo " echo \"Europe/Paris\" > /etc/timezone" >&2 fi echo >&2 echo " Of course, you can change 'Europe/Paris' value by any other valid timezone." >&2 echo " timezone." >&2 echo >&2 echo " Notice you can also use \$TZ environment variable, but the value" >&2 echo " will be necessary each time you launch compose." >&2 echo >&2 return 1 fi e "$TZ" } pretty_print() { while [ "$#" != 0 ]; do case "$1" in "-v"|"-e"|"-w") e "$1" "$2" "\\"$'\n' shift;; *) e "$1 ";; esac shift done } win_env() { "$CMD" /c "/dev/null; } wsl_path_env() { wslpath "$(win_env "${1}")"; } set_os() { OS="$(get_os)" case "$OS" in linux) ## Order matters, files get to override vars compose_config_files=( ## DEFAULT LINUX VARS /etc/default/charm /etc/default/datastore /etc/default/compose ## COMPOSE SYSTEM-WIDE FILES /etc/compose.conf /etc/compose.local.conf /etc/compose/local.conf ## COMPOSE USER FILES ~/.compose/etc/local.conf ~/.compose.conf ) COMPOSE_LOCAL_ROOT=${COMPOSE_LOCAL_ROOT:-"$HOME/.compose"} COMPOSE_VAR=${COMPOSE_VAR:-/var/lib/compose} COMPOSE_CACHE=${COMPOSE_CACHE:-/var/cache/compose} DATASTORE=${DATASTORE:-/srv/datastore/data} CONFIGSTORE=${CONFIGSTORE:-/srv/datastore/config} if [ "$UID" == 0 ]; then SESSION_DIR=${SESSION_DIR:-"$COMPOSE_VAR"/sessions} CHARM_STORE=${CHARM_STORE:-/srv/charm-store} TZ_PATH=${TZ_PATH:-"$COMPOSE_VAR"/timezones} COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_CACHE"} else SESSION_DIR=${SESSION_DIR:-"$COMPOSE_LOCAL_ROOT"/sessions} CHARM_STORE=${CHARM_STORE:-"$HOME"/.charm-store} TZ_PATH=${TZ_PATH:-"$COMPOSE_LOCAL_ROOT"/timezones} COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache} fi mkdir -p "$COMPOSE_CACHE" || return 1 ;; mac) ## Order matters, files get to override vars compose_config_files=( ## COMPOSE SYSTEM-WIDE FILES /etc/compose.conf /etc/compose.local.conf /etc/compose/local.conf ## COMPOSE USER FILES ~/.compose/etc/local.conf ~/.compose.conf ) COMPOSE_LOCAL_ROOT=${COMPOSE_LOCAL_ROOT:-"$HOME/.compose"} COMPOSE_VAR=${COMPOSE_VAR:-"$COMPOSE_LOCAL_ROOT"/lib} COMPOSE_CACHE=${COMPOSE_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache} SESSION_DIR=${SESSION_DIR:-"$COMPOSE_LOCAL_ROOT"/sessions} DATASTORE=${DATASTORE:-"$COMPOSE_LOCAL_ROOT"/data} CONFIGSTORE=${CONFIGSTORE:-"$COMPOSE_LOCAL_ROOT"/config} CHARM_STORE=${CHARM_STORE:-"$HOME"/.charm-store} TZ_PATH=${TZ_PATH:-"$COMPOSE_LOCAL_ROOT"/timezones} COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache} ;; wsl) type -p cmd.exe >/dev/null || { die "cmd.exe is not found in \$PATH." } CMD=$(type -p cmd.exe >/dev/null) || { for p in {/mnt,}/c/WINDOWS/SYSTEM32; do if [ -x "$p"/cmd.exe ]; then CMD="$p"/cmd.exe fi done if [ -z "$CMD" ]; then die "cmd.exe is not found in \$PATH." \ "And could not find it in standard directories." fi } WIN_HOME="$(wsl_path_env UserProfile)" WIN_PROGRAM_FILES="$(wsl_path_env ProgramFiles)" ## Order matters, files get to override vars compose_config_files=( ## COMPOSE SYSTEM-WIDE FILES /etc/compose.conf /etc/compose.local.conf /etc/compose/local.conf ## COMPOSE USER FILES ~/.compose/etc/local.conf ~/.compose.conf ## COMPOSE USER FILES {~,"$WIN_HOME"}/.compose/etc/local.conf {~,"$WIN_HOME"}/.compose.conf ) APPDATA_BASE="$WIN_HOME/AppData/Local/Compose" COMPOSE_LAUNCHER_APP_DIR="$WIN_PROGRAM_FILES/Compose" COMPOSE_LOCAL_ROOT=${COMPOSE_LOCAL_ROOT:-"$APPDATA_BASE/Launcher"} COMPOSE_VAR=${COMPOSE_VAR:-"$APPDATA_BASE/lib"} COMPOSE_CACHE=${COMPOSE_CACHE:-"$APPDATA_BASE/cache"} DATASTORE=${DATASTORE:-"$APPDATA_BASE/data"} CONFIGSTORE=${CONFIGSTORE:-"$APPDATA_BASE/config"} mkdir -p "$COMPOSE_VAR" "$COMPOSE_CACHE" "$DATASTORE" "$CONFIGSTORE" || return 1 SESSION_DIR=${SESSION_DIR:-"$COMPOSE_LOCAL_ROOT"/sessions} CHARM_STORE=${CHARM_STORE:-"$COMPOSE_LAUNCHER_APP_DIR/charm-store"} TZ_PATH=${TZ_PATH:-"$COMPOSE_LOCAL_ROOT"/timezones} COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache} ;; *) echo "System '$os' not supported yet." >&2 return 1 ;; esac } mk_docker_run_options() { set_os || return 1 docker_run_opts=("-v" "/var/run/docker.sock:/var/run/docker.sock") ## ## Load config files ## if [ -z "$DISABLE_SYSTEM_CONFIG_FILE" ]; then ## XXXvlab: should provide YML config opportunities in possible parent dirs ? ## userdir ? and global /etc/compose.yml ? for cfgfile in "${compose_config_files[@]}"; do [ -e "$cfgfile" ] || continue docker_run_opts+=("-v" "$cfgfile:$cfgfile:ro") debug "Loading config file '$cfgfile'." . "$cfgfile" done else docker_run_opts+=("-e" "DISABLE_SYSTEM_CONFIG_FILE=$DISABLE_SYSTEM_CONFIG_FILE") fi mkdir -p "$COMPOSE_LAUNCHER_CACHE" || return 1 ## get TZ value and prepare TZ_PATH TZ=$(get_tz) || return 1 mkdir -p "${TZ_PATH}" TZ_PATH="${TZ_PATH}/$(e "$TZ" | hash_get | cut -c 1-8)" || return 1 [ -e "$TZ_PATH" ] || e "$TZ" > "$TZ_PATH" ## CACHE/DATA DIRS docker_run_opts+=("-v" "$COMPOSE_VAR:/var/lib/compose") docker_run_opts+=("-v" "$COMPOSE_CACHE:/var/cache/compose") docker_run_opts+=("-v" "$TZ_PATH:/etc/timezone:ro") ## ## Checking vars ## ## CHARM_STORE [ -e "$CHARM_STORE" ] || mkdir -p "$CHARM_STORE" || return 1 [ -L "$CHARM_STORE" ] && { CHARM_STORE=$(readlink -f "$CHARM_STORE") || return 1 } docker_run_opts+=( "-v" "$CHARM_STORE:/srv/charm-store:ro" "-e" "CHARM_STORE=/srv/charm-store" "-e" "HOST_CHARM_STORE=$CHARM_STORE" ) check_no_links_subdirs "$CHARM_STORE"/* || return 1 ## DATASTORE and CONFIGSTORE docker_run_opts+=( "-v" "$DATASTORE:/srv/datastore/data:rw" "-e" "DATASTORE=/srv/datastore/data" "-e" "HOST_DATASTORE=$DATASTORE" "-v" "$CONFIGSTORE:/srv/datastore/config:rw" "-e" "CONFIGSTORE=/srv/datastore/config" "-e" "HOST_CONFIGSTORE=$CONFIGSTORE" ) if [ "$OS" == "linux" ]; then [ -d "$HOME/.docker" ] && \ docker_run_opts+=("-v" "$HOME/.docker:/root/.docker:ro") [ -d "$HOME/.ssh" ] && \ docker_run_opts+=("-v" "$HOME/.ssh:/root/.ssh:ro") [ -d "/etc/ssh" ] && \ docker_run_opts+=("-v" "/etc/ssh:/etc/ssh:ro") fi COMPOSE_DOCKER_IMAGE=${COMPOSE_DOCKER_IMAGE:-docker.0k.io/compose} docker_run_opts+=("-e" "COMPOSE_DOCKER_IMAGE=$COMPOSE_DOCKER_IMAGE") if ! docker_has_image "$COMPOSE_DOCKER_IMAGE"; then docker pull "$COMPOSE_DOCKER_IMAGE" || return 1 fi COMPOSE_LAUNCHER_BIN=$(readlink -f "${BASH_SOURCE[0]}") docker_run_opts+=("-v" "$COMPOSE_LAUNCHER_BIN:/usr/local/bin/compose") COMPOSE_LAUNCHER_BIN_OVERRIDE=$(get_override "${docker_run_opts[@]}") || return 1 COMPOSE_LAUNCHER_HASH=$( get_hash_image "$COMPOSE_DOCKER_IMAGE" "$COMPOSE_LAUNCHER_BIN_OVERRIDE") || return 1 while read-0 var; do case "$var" in COMPOSE_YML_FILE|COMPOSE_LAUNCHER_BIN|COMPOSE_DOCKER_IMAGE|\ COMPOSE_LAUNCHER_OPTS|COMPOSE_VAR|COMPOSE_CACHE) : ;; *) docker_run_opts+=("-e" "$var=${!var}") ;; esac done < <(list_compose_vars) ARG_COMPOSE_FILE=$( get_compose_file_opt "$COMPOSE_LAUNCHER_HASH" "$COMPOSE_LAUNCHER_BIN_OVERRIDE" \ "$@") || return 1 compose_file="${ARG_COMPOSE_FILE:-$COMPOSE_YML_FILE}" if [ -z "$compose_file" ]; then ## Find a compose.yml in parents debug "No config file specified on command line arguments" debug "Looking for 'compose.yml' in self and parents.." if parent=$(while true; do [ -e "./compose.yml" ] && { echo "$PWD" exit 0 } [ "$PWD" == "/" ] && return 1 cd .. done ); then compose_file="$(realpath "$parent"/"compose.yml")" debug " .. found '$compose_file'" else debug " .. not found." fi fi if [ -z "$compose_file" ] && [ "${DEFAULT_COMPOSE_FILE+x}" ]; then debug "Using \$DEFAULT_COMPOSE_FILE value '$DEFAULT_COMPOSE_FILE' as compose file." compose_file="$DEFAULT_COMPOSE_FILE" fi if [ -n "$compose_file" ]; then if ! [ -f "$compose_file" ]; then die "Specified compose file '$compose_file' not found." fi compose_file="$(realpath "$compose_file")" if [ "$OS" == "wsl" ]; then ## Docker host is not same linux than WSL, so ## access to root files are not the same. ##YYYvlab, check on cp where is the base dst="$COMPOSE_LAUNCHER_CACHE/compose.$(hash_get < "$compose_file").yml" cp "$compose_file" "$dst" ## docker host start with /c/... whereas WSL could start with /mnt/c/... local="$dst" else local="$compose_file" fi parent_dir="${compose_file%/*}" docker_path=/var/lib/compose/root/${parent_dir##*/}/${compose_file##*/} docker_run_opts+=( "-e" "COMPOSE_YML_FILE=${compose_file##*/}" "-e" "HOST_COMPOSE_YML_FILE=${local}" "-v" "${local}:${docker_path}:ro" "-w" "${docker_path%/*}" ) else docker_path=/var/lib/compose/root docker_run_opts+=( "-w" "${docker_path}" ) fi clean_unused_sessions || return 1 filename=$(mktemp -p /tmp/ -t launch_opts-XXXXXXXXXXXXXXXX) p0 "${docker_run_opts[@]}" > "$filename" hash=$(hash_get < "$filename") || return 1 src="$SESSION_DIR/$UID-$hash" if [ -d "$src" ]; then err "Unexpected directory found in '$src'." return 1 fi dest="/var/lib/compose/sessions/$UID-$hash" additional_docker_run_opts=( "-v" "$SESSION_DIR/$UID-$hash:$dest:ro" "-e" "HOST_COMPOSE_LAUNCHER_OPTS=$SESSION_DIR/$UID-$hash" "-e" "COMPOSE_LAUNCHER_OPTS=$dest" "-e" "COMPOSE_LAUNCHER_BIN=$COMPOSE_LAUNCHER_BIN" "--label" "compose=1" ) p0 "${additional_docker_run_opts[@]}" >> "$filename" docker_run_opts+=("${additional_docker_run_opts[@]}") ## keep also some env vars: for var in ARG_COMPOSE_FILE COMPOSE_DOCKER_IMAGE COMPOSE_LAUNCHER_{BIN_OVERRIDE,HASH}; do p0 "!env:$var=${!var}" done >> "$filename" if [ -e "$src" ]; then ## compare content of $src and $filename if ! diff -q "$src" "$filename" >/dev/null; then return 0 fi warn "Session already exists but content is different. Squashing." fi mkdir -p "$SESSION_DIR" || return 1 cat "$filename" > "$src" || return 1 } load_env() { docker_run_opts=() if [ -z "$COMPOSE_LAUNCHER_OPTS" ]; then mk_docker_run_options "$@" || return 1 else [ -d "$COMPOSE_LAUNCHER_OPTS" ] && { err "Variable \$COMPOSE_LAUNCHER_OPTS provided but it points on a directory." return 1 } set_os || return 1 while read-0 opt; do if [[ "$opt" == "!env:"* ]]; then opt="${opt##!env:}" var="${opt%%=*}" value="${opt#*=}" debug "Loading var: $var=$value" export "$var"="$value" else docker_run_opts+=("$opt") fi done < "$COMPOSE_LAUNCHER_OPTS" fi if [ -n "$GLOBAL_ALL_RELATIONS" ]; then if ! [ -e "$GLOBAL_ALL_RELATIONS" ]; then err "Variable \$GLOBAL_ALL_RELATION provided but it doesn't point on an existing file." return 1 fi if ! [ -r "$GLOBAL_ALL_RELATIONS" ]; then err "Variable \$GLOBAL_ALL_RELATION provided but it doesn't point on a readable file." return 1 fi if [ -z "$GLOBAL_ALL_RELATIONS_HASH" ]; then err "Variable \$GLOBAL_ALL_RELATION provided but not \$GLOBAL_ALL_RELATIONS_HASH." return 1 fi NEW_GLOBAL_ALL_RELATIONS="$COMPOSE_CACHE/${GLOBAL_ALL_RELATIONS##*/}" if [ "$NEW_GLOBAL_ALL_RELATIONS" != "$GLOBAL_ALL_RELATIONS" ]; then cp "$GLOBAL_ALL_RELATIONS" "$NEW_GLOBAL_ALL_RELATIONS" || return 1 fi docker_run_opts+=("-e" "GLOBAL_ALL_RELATIONS=$NEW_GLOBAL_ALL_RELATIONS") docker_run_opts+=("-e" "GLOBAL_ALL_RELATIONS_HASH=$GLOBAL_ALL_RELATIONS_HASH") fi if [ -n "$PROJECT_NAME" ]; then docker_run_opts+=("-e" "PROJECT_NAME=$PROJECT_NAME") fi } show_env() { echo "${WHITE}Environment:${NORMAL}" echo " COMPOSE_DOCKER_IMAGE: $COMPOSE_DOCKER_IMAGE" echo " CHARM_STORE: $CHARM_STORE" echo " DATASTORE: $DATASTORE" echo " CONFIGSTORE: $CONFIGSTORE" echo " COMPOSE_VAR: $COMPOSE_VAR" echo " COMPOSE_CACHE: $COMPOSE_CACHE" echo " COMPOSE_LAUNCHER_CACHE: $COMPOSE_LAUNCHER_CACHE" echo " SESSION_DIR: $SESSION_DIR" echo " TZ_PATH: $TZ_PATH" [ -n "$GLOBAL_ALL_RELATIONS" ] && { echo " GLOBAL_ALL_RELATIONS: $GLOBAL_ALL_RELATIONS" echo " GLOBAL_ALL_RELATIONS_HASH: $GLOBAL_ALL_RELATIONS_HASH" } [ -n "$PROJECT_NAME" ] && echo " PROJECT_NAME: $PROJECT_NAME" } run() { local os docker_run_opts load_env "$@" || return 1 [ -n "$DEBUG" ] && show_env >&2 if [ -n "$ARG_COMPOSE_FILE" ]; then while read-0-err E cmd_arg; do cmd_args+=("$cmd_arg") done < <(p-err replace_compose_file_opt "$COMPOSE_LAUNCHER_HASH" \ "$COMPOSE_LAUNCHER_BIN_OVERRIDE" \ "$@") if [ "$E" != "0" ]; then err "Unexpected failure while trying to replace compose file option." return 1 fi set -- "${cmd_args[@]}" fi ## XXXvlab: can't see a place where we wouldn't want to link stdin ## to internal process be it a terminal or not. docker_run_opts+=("-i") ## If stdin is a not a tty, then adding ``-t`` will fail [ -t 0 -a -t 1 ] && docker_run_opts+=("-t") debug "${WHITE}Launching:${NORMAL}" if [ -n "$DEBUG" ] || [ -n "$DRY_RUN" ]; then echo "docker run --rm \\" pretty_print "${docker_run_opts[@]}" | sed_compat 's/^/ /g;s/([^\])$/\1\\\n/g' if [ -z "$ENTER" ]; then printf "%s\n" " ${COMPOSE_DOCKER_IMAGE} \\" printf " " printf "%s " "$@" printf "\n" else echo " --entrypoint bash \\" echo " ${COMPOSE_DOCKER_IMAGE}" fi fi | { if [ -n "$DEBUG" ]; then sed_compat 's/^/ /g'; else cat; fi } >&2 if [ -z "$DRY_RUN" ]; then debug "${WHITE}Execution:${NORMAL}" if [ -z "$ENTER" ]; then exec docker run --rm "${docker_run_opts[@]}" "${COMPOSE_DOCKER_IMAGE}" "$@" else exec docker run --rm "${docker_run_opts[@]}" \ --entrypoint bash \ "${COMPOSE_DOCKER_IMAGE}" fi fi } [ -n "$SOURCED" ] && return 0 ## ## Code ## depends docker cat readlink sed realpath tee sed grep tail ansi_color "${ansi_color:-tty}" if [ "$SHOW_ENV" ]; then load_env "$@" || return 1 show_env >&2 exit 0 fi if [ "$SHOW_CONFIG_LOCATIONS" ]; then set_os || return 1 echo "compose will read these files if existing in the given order:" for loc in "${compose_config_files[@]}"; do echo " - $loc" done exit 0 fi run "$@"