diff --git a/compose.watch-build-server.yml b/compose.watch-build-server.yml index 726e641..36c3a75 100644 --- a/compose.watch-build-server.yml +++ b/compose.watch-build-server.yml @@ -14,13 +14,10 @@ services: command: ["run", "watch-build"] entrypoint: [/entrypoint, --, npm] environment: - CAPABILITIES: # setpriv --bounding-set options. Must be a subset of cap_add. See https://www.man7.org/linux/man-pages/man1/setpriv.1.html#OPTIONS - CHOWN_LIST: # Set by Docker User Mirror - HOST_MAPPED_GROUP: # Set by Docker User Mirror - HOST_MAPPED_GID: # Set by Docker User Mirror - HOST_MAPPED_USER: # Set by Docker User Mirror - HOST_MAPPED_UID: # Set by Docker User Mirror - SERVICE_NAME: watch-build # Used to filter CHOWN_LIST with Docker User Mirror + USER_MIRROR_CAPABILITIES: # setpriv --bounding-set options. Must be a subset of cap_add. See https://www.man7.org/linux/man-pages/man1/setpriv.1.html#OPTIONS + USER_MIRROR_CHOWN_LIST: # Set by Docker User Mirror + USER_MIRROR_HOST_USER: # Set by Docker User Mirror + USER_MIRROR_SERVICE_NAME: watch-build # USER_MIRROR_CHOWN_LIST filter used by Docker User Mirror healthcheck: test: ["CMD", "sh", "-c", "[ $(ps | grep nodemon | grep -v grep | wc -l) -ge 1 ]"] interval: 5s @@ -89,13 +86,10 @@ services: command: ["run", "serve-release"] entrypoint: [/entrypoint, --, npm] environment: - CAPABILITIES: # setpriv --bounding-set options. Must be a subset of cap_add. See https://www.man7.org/linux/man-pages/man1/setpriv.1.html#OPTIONS - CHOWN_LIST: # Set by Docker User Mirror - HOST_MAPPED_GROUP: # Set by Docker User Mirror - HOST_MAPPED_GID: # Set by Docker User Mirror - HOST_MAPPED_USER: # Set by Docker User Mirror - HOST_MAPPED_UID: # Set by Docker User Mirror - SERVICE_NAME: server # Used to filter CHOWN_LIST with Docker User Mirror + USER_MIRROR_CAPABILITIES: # setpriv --bounding-set options. Must be a subset of cap_add. See https://www.man7.org/linux/man-pages/man1/setpriv.1.html#OPTIONS + USER_MIRROR_CHOWN_LIST: # Set by Docker User Mirror + USER_MIRROR_HOST_USER: # Set by Docker User Mirror + USER_MIRROR_SERVICE_NAME: server # USER_MIRROR_CHOWN_LIST filter used by Docker User Mirror healthcheck: test: ["CMD", "sh", "-c", "[ $(ps | grep http-server | grep -v npm | grep -v grep | wc -l) -eq 1 ]"] start_period: 30s diff --git a/compose.yml b/compose.yml index e78e3ba..69d6474 100644 --- a/compose.yml +++ b/compose.yml @@ -14,14 +14,11 @@ services: command: ["run", "clean-verify"] entrypoint: [/entrypoint, --, npm] environment: - CAPABILITIES: # setpriv --bounding-set options. Must be a subset of cap_add. See https://www.man7.org/linux/man-pages/man1/setpriv.1.html#OPTIONS - CHOWN_LIST: # Set by Docker User Mirror - HOST_MAPPED_GROUP: # Set by Docker User Mirror - HOST_MAPPED_GID: # Set by Docker User Mirror - HOST_MAPPED_USER: # Set by Docker User Mirror - HOST_MAPPED_UID: # Set by Docker User Mirror MINISIGN_CONFIG_DIR: /run/secrets - SERVICE_NAME: npm # Used to filter CHOWN_LIST with Docker User Mirror + USER_MIRROR_CAPABILITIES: # setpriv --bounding-set options. Must be a subset of cap_add. See https://www.man7.org/linux/man-pages/man1/setpriv.1.html#OPTIONS + USER_MIRROR_CHOWN_LIST: # Set by Docker User Mirror + USER_MIRROR_HOST_USER: # Set by Docker User Mirror + USER_MIRROR_SERVICE_NAME: npm # USER_MIRROR_CHOWN_LIST filter used by Docker User Mirror network_mode: "host" secrets: - source: minisign diff --git a/images/node/entrypoint b/images/node/entrypoint index 27a5a5a..7fe2653 100755 --- a/images/node/entrypoint +++ b/images/node/entrypoint @@ -3,7 +3,7 @@ # License: MIT set -eC; -VERSION='0.1.1-0336f43'; +VERSION='1.0.0-911dd14'; exec 3>/dev/null; exec 4>/dev/null; @@ -19,13 +19,28 @@ check_setpriv() { setpriv --reuid="$(id -u nobody)" --regid="$(id -g nobody)" --clear-groups true; } +# Split strings by a delimiter and extract an index. Uses space as a delimiter by default. +extract_index() ( + str="$1"; + count="${2:-0}"; + delimiter="${3:- }"; + while [ "$count" -gt 0 ]; do + : $((count -= 1)); + case "$str" in + *${delimiter}*) str="${str#*${delimiter}}";; + *) str='';; + esac + done + echo "${str%%${delimiter}*}"; +) + # Download a release file. # Args: release filename, output filename, release tag (defaults to latest release it not provided) download_file() { if [ $# -eq 3 ]; then - curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/download/$3/$1"; + curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/download/${3}/${1}"; elif [ $# -eq 2 ]; then - curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/latest/download/$1"; + curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/latest/download/${1}"; else echo 'Expected at least two arguments.' >&2; return 1; @@ -51,7 +66,7 @@ get_release_version() { if [ $# -eq 0 ]; then response="$(curl -Ls -w '\n%{http_code}' 'https://github.com/AJGranowski/docker-user-mirror/releases/latest/download/version')"; else - response="$(curl -Ls -w '\n%{http_code}' "https://github.com/AJGranowski/docker-user-mirror/releases/download/$1/version")"; + response="$(curl -Ls -w '\n%{http_code}' "https://github.com/AJGranowski/docker-user-mirror/releases/download/${1}/version")"; fi case "$(echo "$response" | tail -1)" in @@ -63,12 +78,12 @@ get_release_version() { } print_user() { - grep -e "^$1:" /etc/passwd; + grep -e "^${1}:" /etc/passwd; } # Query GitHub to check if a release tag exists. release_tag_exists() { - case "$(curl -Ls -o /dev/null -w '%{http_code}' "https://api.github.com/repos/AJGranowski/docker-user-mirror/releases/tags/$1")" in + case "$(curl -Ls -o /dev/null -w '%{http_code}' "https://api.github.com/repos/AJGranowski/docker-user-mirror/releases/tags/${1}")" in 2*) true;; *) false;; esac @@ -92,8 +107,8 @@ update() { fi fi - echo " Current version: $VERSION"; - echo " Latest version: $latest_version"; + printf ' Current version: %s\n' "$VERSION"; + printf ' Latest version: %s\n' "$latest_version"; if [ "$VERSION" = "$latest_version" ]; then return 0; @@ -102,7 +117,7 @@ update() { if [ "$o_yes" != true ]; then echo '\nInstall the update? (y/n)'; read yn; - case $yn in + case "$yn" in y*) ;; n*) return 0;; esac @@ -110,7 +125,7 @@ update() { echo 'Installing update...'; download_file 'entrypoint' "${0}.tmp"; - mv "${0}.tmp" "$0" && chmod +x "$0" && echo "${latest_version} installed!"; exit 0; + mv "${0}.tmp" "$0" && chmod +x "$0" && printf '%s installed!\n' "$latest_version"; exit 0; } # Convert "$VERSION" to "release-**" format. @@ -124,7 +139,7 @@ version_to_release_tag() { # Parse options. while [ $# -gt 0 ]; do - case $1 in + case "$1" in --setup) o_setup=true; ;; @@ -136,7 +151,7 @@ while [ $# -gt 0 ]; do exec 4>&2; ;; --version) - echo "Version: ${VERSION}"; + printf 'Version: %s\n' "$VERSION"; exit 0; ;; -y|--yes) @@ -146,9 +161,7 @@ while [ $# -gt 0 ]; do shift; break; ;; - *) - break; - ;; + *) break;; esac shift; done @@ -169,14 +182,14 @@ if [ "$o_setup" = true ]; then # Attempt to install setpriv with apk. if (apk update && apk -s add setpriv) >/dev/null 2>&1; then - apk add --no-cache "setpriv>=$SETPRIV_VERSION"; + apk add --no-cache "setpriv>=${SETPRIV_VERSION}"; hash -r; check_setpriv; echo 'setpriv installed!'; # Attempt to install setpriv from util-linux with apt-get. elif (apt-get update && apt-get install --dry-run util-linux) >/dev/null 2>&1; then - apt-get satisfy --no-install-recommends -y "util-linux (>=$UTIL_LINUX_VERSION)"; + apt-get satisfy --no-install-recommends -y "util-linux (>=${UTIL_LINUX_VERSION})"; hash -r; check_setpriv; echo 'setpriv installed!'; @@ -193,12 +206,12 @@ if [ "$o_setup" = true ]; then ppc64le) dpkgArch='ppc64el' ;; riscv64 | s390x) dpkgArch="$unameArch" ;; x86_64) dpkgArch='amd64' ;; - *) echo >&2 "error: unknown/unsupported architecture '$unameArch'"; exit 1 ;; + *) printf 'error: unknown/unsupported architecture: %s\n' "$unameArch" >&2; exit 1;; esac mkdir -p /usr/local/bin/; - wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; - wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; + wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${dpkgArch}"; + wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${dpkgArch}.asc"; GNUPGHOME="$(mktemp -d)"; if command -v gpg >/dev/null; then @@ -214,23 +227,8 @@ if [ "$o_setup" = true ]; then else set -eC; - if [ -z "$HOST_MAPPED_GID" ]; then - printf 'HOST_MAPPED_GID is unset\n' >&2; - guard_failure=true; - fi - - if [ -z "$HOST_MAPPED_GROUP" ]; then - printf 'HOST_MAPPED_GROUP is unset\n' >&2; - guard_failure=true; - fi - - if [ -z "$HOST_MAPPED_UID" ]; then - printf 'HOST_MAPPED_UID is unset\n' >&2; - guard_failure=true; - fi - - if [ -z "$HOST_MAPPED_USER" ]; then - printf 'HOST_MAPPED_USER is unset\n' >&2; + if [ -z "$USER_MIRROR_HOST_USER" ]; then + echo 'USER_MIRROR_HOST_USER is unset' >&2; guard_failure=true; fi @@ -238,27 +236,22 @@ else exit 1; fi - echo "CAPABILITIES=$CAPABILITIES" >&3; - echo "HOST_MAPPED_GROUP=$HOST_MAPPED_GROUP" >&3; - echo "HOST_MAPPED_GID=$HOST_MAPPED_GID" >&3; - echo "HOST_MAPPED_USER=$HOST_MAPPED_USER" >&3; - echo "HOST_MAPPED_UID=$HOST_MAPPED_UID" >&3; - echo "SERVICE_NAME=$SERVICE_NAME" >&3; + printf 'USER_MIRROR_CAPABILITIES=%s\n' "$USER_MIRROR_CAPABILITIES" >&3; + printf 'USER_MIRROR_HOST_USER=%s\n' "$USER_MIRROR_HOST_USER" >&3; + printf 'USER_MIRROR_SERVICE_NAME=%s\n' "$USER_MIRROR_SERVICE_NAME" >&3; + + host_user_name="$(extract_index "$USER_MIRROR_HOST_USER" 0 ':')"; + host_user_id="$(extract_index "$USER_MIRROR_HOST_USER" 1 ':')"; + host_user_group="$(extract_index "$USER_MIRROR_HOST_USER" 2 ':')"; + host_user_gid="$(extract_index "$USER_MIRROR_HOST_USER" 3 ':')"; # Create/replace user if non-root. - if [ $HOST_MAPPED_UID -ne 0 ] && [ "$HOST_MAPPED_USER" != 'root' ]; then - if id -u "$HOST_MAPPED_USER" >/dev/null 2>&1; then - echo "Removing user matching $HOST_MAPPED_USER: $(print_user $HOST_MAPPED_USER)" >&3; - if command -v userdel >/dev/null; then - userdel "$HOST_MAPPED_USER"; - else - deluser "$HOST_MAPPED_USER" 2>/dev/null; - fi - fi + if [ "$host_user_id" -ne 0 ] && [ "$host_user_name" != 'root' ]; then + export SHELL='/bin/sh'; - uid_username="$(awk -F: "\$3 == $HOST_MAPPED_UID {print \$1}" /etc/passwd)"; + uid_username="$(awk -F: "\$3 == ${host_user_id} {print \$1}" /etc/passwd)"; if id -u "$uid_username" >/dev/null 2>&1; then - echo "Removing user matching UID $HOST_MAPPED_UID: $(print_user $uid_username)" >&3; + printf 'Removing user matching UID %s: %s\n' "$host_user_id" "$(print_user "$uid_username")" >&3; if command -v userdel >/dev/null; then userdel "$uid_username"; else @@ -267,31 +260,34 @@ else fi if command -v groupadd >/dev/null; then - groupadd -g $HOST_MAPPED_GID "$HOST_MAPPED_GROUP" || true; + groupadd -g "$host_user_gid" "$host_user_group" || true; else - addgroup -g $HOST_MAPPED_GID "$HOST_MAPPED_GROUP" || true; + addgroup -g "$host_user_gid" "$host_user_group" || true; fi - gid_groupname="$(awk -F: "\$3 == $HOST_MAPPED_GID {print \$1}" /etc/group)"; + gid_groupname="$(awk -F: "\$3 == ${host_user_gid} {print \$1}" /etc/group)"; if command -v useradd >/dev/null; then - useradd -g $HOST_MAPPED_GID -m -s /bin/sh -u $HOST_MAPPED_UID $HOST_MAPPED_USER 2>&4; + useradd -c 'Mirrored Host User' -g "$host_user_gid" -m -s "$SHELL" -u "$host_user_id" "$host_user_name" 2>&4; else - adduser -D -G "$gid_groupname" -s /bin/sh -u $HOST_MAPPED_UID $HOST_MAPPED_USER 2>&4; + adduser -D -g 'Mirrored Host User' -G "$gid_groupname" -s "$SHELL" -u "$host_user_id" "$host_user_name" 2>&4; fi - echo "Created user: $(print_user "$HOST_MAPPED_USER")" >&3; + printf 'Created user: %s\n' "$(print_user "$host_user_name")" >&3; - HOME_DIR="/home/$HOST_MAPPED_USER"; + export HOME="/home/${host_user_name}"; else - HOME_DIR="/root"; + export HOME='/root'; fi + export LOGNAME="$host_user_name"; + export USER="$host_user_name"; + # Set the ownership of a set of items to the user. while read service_name; do if [ -z "$service_name" ]; then continue; fi - case $service_name in + case "$service_name" in /*) chown_path="$service_name"; unset service_name; @@ -299,33 +295,38 @@ else *) read chown_path;; esac - if [ -n "$service_name" ] && [ "$service_name" != "$SERVICE_NAME" ]; then + if [ -n "$service_name" ] && [ "$service_name" != "$USER_MIRROR_SERVICE_NAME" ]; then continue; fi - if ! [ -e "$chown_path" ]; then + if [ ! -e "$chown_path" ]; then printf "cannot access '%s': No such file or directory\n" "$chown_path" >&4; continue; fi - chown -c "$HOST_MAPPED_UID:$HOST_MAPPED_GID" "$chown_path" >&3; + chown -c "${host_user_id}:${host_user_gid}" "$chown_path" >&3; done </dev/null 2>&1; then - if [ -n "$CAPABILITIES" ]; then - capabilities="-all,$CAPABILITIES"; - else - capabilities='-all'; - fi - - echo "HOME=$HOME_DIR LOGNAME=$HOST_MAPPED_USER SHELL=/bin/sh USER=$HOST_MAPPED_USER exec setpriv --bounding-set \"$capabilities\" --init-groups --no-new-privs --reuid=$HOST_MAPPED_UID --regid=$HOST_MAPPED_GID ..." >&3; - HOME="$HOME_DIR" LOGNAME="$HOST_MAPPED_USER" SHELL='/bin/sh' USER="$HOST_MAPPED_USER" exec setpriv --bounding-set "$capabilities" --init-groups --no-new-privs --reuid=$HOST_MAPPED_UID --regid=$HOST_MAPPED_GID "$@"; + echo "exec setpriv --bounding-set \"${capabilities}\" --init-groups --no-new-privs --reuid=${host_user_id} --regid=${host_user_gid} ..." >&3; + exec setpriv --bounding-set "$capabilities" --init-groups --no-new-privs --reuid=${host_user_id} --regid=${host_user_gid} "$@"; elif command -v gosu >/dev/null; then - echo "HOME=$HOME_DIR LOGNAME=$HOST_MAPPED_USER SHELL=/bin/sh USER=$HOST_MAPPED_USER exec gosu "$HOST_MAPPED_UID:$HOST_MAPPED_GID" ..." >&3; - HOME="$HOME_DIR" LOGNAME="$HOST_MAPPED_USER" SHELL='/bin/sh' USER="$HOST_MAPPED_USER" exec gosu "$HOST_MAPPED_UID:$HOST_MAPPED_GID" "$@"; + echo "exec gosu \"${host_user_id}:${host_user_gid}\" ..." >&3; + exec gosu "${host_user_id}:${host_user_gid}" "$@"; else printf 'Unable to set user\n' >&2; exit 1; diff --git a/user-mirror b/user-mirror index 3c59817..05a7280 100755 --- a/user-mirror +++ b/user-mirror @@ -2,18 +2,83 @@ # Source: https://github.com/AJGranowski/docker-user-mirror # License: MIT set -eC; -VERSION='0.1.1-0336f43'; +VERSION='1.0.0-911dd14'; # Create container(s) and return the container ID(s). container_create() { - if [ -n "$REPLACED_COMMAND" ]; then + if [ "$COMMAND_TYPE" = 'compose' ]; then + # For compose commands, we just need to create the containers used in the command. + # TODO: Be selective when creating containers. https://github.com/AJGranowski/docker-user-mirror/issues/83 + for arg do + if [ "$is_file" = true ]; then + unset is_file; + if [ -z "$compose_configuration_files" ]; then + compose_configuration_files="-f ${arg}"; + else + compose_configuration_files="${compose_configuration_files} -f ${arg}"; + fi + else + case "$arg" in + -f|--file) is_file=true;; + esac + fi + done + + $COMPOSE_ENGINE --progress quiet $compose_configuration_files create >/dev/null 2>&1; + $COMPOSE_ENGINE --progress quiet ps --status created --format '{{.ID}}'; + elif [ "$COMMAND_TYPE" = 'container' ]; then + # For a non-compose commands, we can just change "exec" and "run" to "create", and then use that new command + # to create (but not start) a container for parsing. We're modifying the argument list since that's the only + # array supported in POSIX shell. + first_iteration=true; + index=0; + search_for_replace=true; + for arg do + if [ "$first_iteration" = true ]; then + unset first_iteration; + shift $#; + fi + + if [ "$search_for_replace" = true ]; then + case "$arg" in + exec|run) + replaced_command="$arg"; + replaced_command_index="$index"; + set -- "$@" "create"; + unset search_for_replace; + ;; + *) set -- "$@" "$arg";; + esac + else + set -- "$@" "$arg"; + fi + + : $((index += 1)); + done + "$@"; - elif [ -n "$COMPOSE_FILES" ]; then - $COMPOSE_ENGINE --progress quiet --file "$COMPOSE_FILES" create >/dev/null 2>&1; - $COMPOSE_ENGINE --progress quiet --file "$COMPOSE_FILES" ps -a --status created --format '{{.ID}}'; + + if [ -n "$replaced_command" ] && [ -n "$replaced_command_index" ]; then + first_iteration=true; + index=0; + for arg do + if [ "$first_iteration" = true ]; then + unset first_iteration; + shift $#; + fi + + if [ "$index" -eq "$replaced_command_index" ]; then + set -- "$@" "$replaced_command"; + else + set -- "$@" "$arg"; + fi + + : $((index += 1)); + done + fi else - $COMPOSE_ENGINE --progress quiet create >/dev/null 2>&1; - $COMPOSE_ENGINE --progress quiet ps -a --status created --format '{{.ID}}'; + printf 'Unknown COMMAND_TYPE state: %s\n' "$COMMAND_TYPE" >&2; + exit 1; fi } @@ -21,9 +86,9 @@ container_create() { # Args: release filename, output filename, release tag (defaults to latest release it not provided) download_file() { if [ $# -eq 3 ]; then - curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/download/$3/$1"; + curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/download/${3}/${1}"; elif [ $# -eq 2 ]; then - curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/latest/download/$1"; + curl --fail -Ls -o "$2" "https://github.com/AJGranowski/docker-user-mirror/releases/latest/download/${1}"; else echo 'Expected at least two arguments.' >&2; return 1; @@ -49,7 +114,7 @@ get_release_version() { if [ $# -eq 0 ]; then response="$(curl -Ls -w '\n%{http_code}' 'https://github.com/AJGranowski/docker-user-mirror/releases/latest/download/version')"; else - response="$(curl -Ls -w '\n%{http_code}' "https://github.com/AJGranowski/docker-user-mirror/releases/download/$1/version")"; + response="$(curl -Ls -w '\n%{http_code}' "https://github.com/AJGranowski/docker-user-mirror/releases/download/${1}/version")"; fi case "$(echo "$response" | tail -1)" in @@ -63,12 +128,12 @@ get_release_version() { # Create host items and populate chown list # Args: container ID (retrieved from container_create) prepare_environment() { - # Grab the SERVICE_NAME environment variable if it exists. - service_name="$($CONTAINER_ENGINE inspect --format '{{range .Config.Env}}{{if and (gt (len .) 13) (eq (slice . 0 13) "SERVICE_NAME=")}}{{slice . 13}}{{end}}{{end}}' "$1")"; + # Grab the USER_MIRROR_SERVICE_NAME environment variable if it exists. + service_name="$($CONTAINER_ENGINE inspect --format '{{range .Config.Env}}{{if and (gt (len .) 25) (eq (slice . 0 25) "USER_MIRROR_SERVICE_NAME=")}}{{slice . 25}}{{end}}{{end}}' "$1")"; # Create a list of all read/write mount destinations so they can be chown'd in the container at runtime. - CHOWN_LIST="$CHOWN_LIST -$($CONTAINER_ENGINE inspect --format "{{range .Mounts}}{{if .RW}}{{println \"$service_name\"}}{{println .Destination}}{{end}}{{end}}" "$1")"; + USER_MIRROR_CHOWN_LIST="$USER_MIRROR_CHOWN_LIST +$($CONTAINER_ENGINE inspect --format "{{range .Mounts}}{{if .RW}}{{println \"${service_name}\"}}{{println .Destination}}{{end}}{{end}}" "$1")"; # Loop through bind mounts that have a non-empty mode (`create_host_path: true` sets this), and mkdir those source paths on the host. while read create_path; do @@ -121,7 +186,7 @@ EOF # Query GitHub to check if a release tag exists. release_tag_exists() { - case "$(curl -Ls -o /dev/null -w '%{http_code}' "https://api.github.com/repos/AJGranowski/docker-user-mirror/releases/tags/$1")" in + case "$(curl -Ls -o /dev/null -w '%{http_code}' "https://api.github.com/repos/AJGranowski/docker-user-mirror/releases/tags/${1}")" in 2*) true;; *) false;; esac @@ -145,8 +210,8 @@ update() { fi fi - echo " Current version: $VERSION"; - echo " Latest version: $latest_version"; + printf ' Current version: %s\n' "$VERSION"; + printf ' Latest version: %s\n' "$latest_version"; if [ "$VERSION" = "$latest_version" ]; then return 0; @@ -155,7 +220,7 @@ update() { if [ "$o_yes" != true ]; then echo '\nInstall the update? (y/n)'; read yn; - case $yn in + case "$yn" in y*) ;; n*) return 0;; esac @@ -163,7 +228,7 @@ update() { echo 'Installing update...'; download_file 'user-mirror' "${0}.tmp"; - mv "${0}.tmp" "$0" && chmod +x "$0" && echo "${latest_version} installed!"; exit 0; + mv "${0}.tmp" "$0" && chmod +x "$0" && printf '%s installed!\n' "$latest_version"; exit 0; } # Convert "$VERSION" to "release-**" format. @@ -177,7 +242,7 @@ version_to_release_tag() { # Parse options. while [ $# -gt 0 ]; do - case $1 in + case "$1" in --docker) COMPOSE_ENGINE='docker compose'; CONTAINER_ENGINE='docker'; @@ -198,7 +263,7 @@ while [ $# -gt 0 ]; do o_update=true; ;; --version) - echo "Version: ${VERSION}"; + printf 'Version: %s\n' "$VERSION"; exit 0; ;; -y|--yes) @@ -209,12 +274,10 @@ while [ $# -gt 0 ]; do break; ;; -*) - printf 'Unknown option %s\n' $1 >&2; + printf 'Unknown option %s\n' "$1" >&2; exit 1; ;; - *) - break; - ;; + *) break;; esac shift; done @@ -229,7 +292,7 @@ fi # Parse the command if no engine declared. if [ -z "$COMPOSE_ENGINE" ] || [ -z "$CONTAINER_ENGINE" ]; then - case $1 in + case "$1" in docker-compose) COMPOSE_ENGINE='docker-compose'; CONTAINER_ENGINE='docker'; @@ -249,62 +312,28 @@ if [ -z "$COMPOSE_ENGINE" ] || [ -z "$CONTAINER_ENGINE" ]; then esac fi -# To get the arguments from a non-compose invocation, we'll change "exec" and "run" to "create", and then use that new -# command string to create (but not start) a container for parsing. This is a workaround for POSIX shell only having -# one array object (the argument list). -COMPOSE_FILES=""; -add_compose_file=false; -first_iteration=true; -search_for_replace=4; +# Duck type the command for arg do - if [ "$first_iteration" = true ]; then - unset first_iteration; - shift $#; - fi - - if [ $search_for_replace -gt 0 ]; then - case $arg in - compose) - set -- "$@" "$arg"; - search_for_replace=0; - ;; - run) - REPLACED_COMMAND="$arg"; - set -- "$@" "create"; - search_for_replace=0; - ;; - *) - set -- "$@" "$arg"; - ;; - esac - - # Decrement for non-option arguments. - case $arg in - -*) - ;; - *) - search_for_replace=$(($search_for_replace - 1)); - ;; - esac - else - if [ "$add_compose_file" = true ]; then - unset add_compose_file; - COMPOSE_FILES="$arg"; - else - case $arg in - -f|--file) add_compose_file=true;; - esac - fi - set -- "$@" "$arg"; - fi + case "$arg" in + compose|up) + COMMAND_TYPE='compose'; + break; + ;; + container|create|exec|run) + COMMAND_TYPE='container'; + break; + ;; + esac done +if [ -z "$COMMAND_TYPE" ]; then + exec "$@"; + exit 0; +fi + # If Docker and not rootless, then we have some work to do. if [ "$CONTAINER_ENGINE" = "docker" ] && ! docker info -f '{{println .SecurityOptions}}' | grep 'rootless' 1>/dev/null; then - HOST_MAPPED_GROUP="$(id -gn)"; - HOST_MAPPED_GID="$(id -g)"; - HOST_MAPPED_USER="$(id -un)"; - HOST_MAPPED_UID="$(id -u)"; + USER_MIRROR_HOST_USER="$(id -un):$(id -u):$(id -gn):$(id -g)"; # Loop through the containers created by this command. while read temp_container_id; do @@ -316,10 +345,7 @@ if [ "$CONTAINER_ENGINE" = "docker" ] && ! docker info -f '{{println .SecurityOp $(container_create "$@") EOF else - HOST_MAPPED_GROUP='root'; - HOST_MAPPED_GID=0; - HOST_MAPPED_USER='root'; - HOST_MAPPED_UID=0; + USER_MIRROR_HOST_USER='root:0:root:0'; fi # Restore the args list we may have mangled before, and insert runtime arguments. @@ -331,46 +357,33 @@ for arg do shift $#; fi - if [ $search_for_insert -gt 0 ]; then - case $arg in + if [ "$search_for_insert" -gt 0 ]; then + case "$arg" in create|exec|run) - if [ -n "$REPLACED_COMMAND" ]; then - set -- "$@" "$REPLACED_COMMAND"; - else - set -- "$@" "$arg"; - fi - - set -- "$@" '-e' "HOST_MAPPED_GID=$HOST_MAPPED_GID" '-e' "HOST_MAPPED_GROUP=$HOST_MAPPED_GROUP" '-e' "HOST_MAPPED_UID=$HOST_MAPPED_UID" '-e' "HOST_MAPPED_USER=$HOST_MAPPED_USER"; - if [ -n "$CHOWN_LIST" ]; then - set -- "$@" '-e' "CHOWN_LIST=$CHOWN_LIST"; + set -- "$@" "$arg"; + set -- "$@" '-e' "USER_MIRROR_HOST_USER=${USER_MIRROR_HOST_USER}"; + if [ -n "$USER_MIRROR_CHOWN_LIST" ]; then + set -- "$@" '-e' "USER_MIRROR_CHOWN_LIST=${USER_MIRROR_CHOWN_LIST}"; fi search_for_insert=0; ;; - *) - set -- "$@" "$arg"; - ;; + *) set -- "$@" "$arg";; esac # Decrement for non-option arguments. - case $arg in - -*) - ;; - *) - search_for_insert=$(($search_for_insert - 1)); - ;; + case "$arg" in + -*) ;; + *) : $((search_for_insert -= 1));; esac else set -- "$@" "$arg"; fi done -export HOST_MAPPED_GROUP; -export HOST_MAPPED_GID; -export HOST_MAPPED_USER; -export HOST_MAPPED_UID; -if [ -n "$CHOWN_LIST" ]; then - export CHOWN_LIST; +export USER_MIRROR_HOST_USER; +if [ -n "$USER_MIRROR_CHOWN_LIST" ]; then + export USER_MIRROR_CHOWN_LIST; fi exec "$@"; \ No newline at end of file