-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,356 @@ | ||
#!/usr/bin/env bash | ||
|
||
if [[ "${DOND_COMPOSE_SHIM_DEBUG:-false}" == true ]]; then | ||
set -o xtrace | ||
fi | ||
|
||
# Exit on any kind of errors | ||
# https://unix.stackexchange.com/questions/23026 | ||
set -o errexit | ||
set -o nounset | ||
set -o pipefail | ||
set -o errtrace | ||
set -o functrace | ||
shopt -s inherit_errexit | ||
|
||
function echo_error() { | ||
echo "ERROR(dond-shim):" "${@}" >&2 | ||
} | ||
|
||
function error() { | ||
echo_error "${@}" | ||
uncaught_error=false | ||
exit 1 | ||
} | ||
|
||
# Ensure the user knows that the error was originated from the shim | ||
function handle_error_trap() { | ||
if [[ "${uncaught_error:-true}" == true ]]; then | ||
echo_error "Uncaught error at line ${LINENO}" | ||
fi | ||
} | ||
|
||
trap handle_error_trap ERR | ||
|
||
# Runs the docker command with the given arguments. | ||
function run_docker_compose() { | ||
if [[ "${print_command}" == true ]]; then | ||
echo "${docker_compose_path}" "${@}" | ||
exit 0 | ||
else | ||
DOND_COMPOSE_SHIM_SKIP=true exec "${docker_compose_path}" "${@}" | ||
fi | ||
} | ||
|
||
# Gets the current/parent container id on the host. | ||
function set_container_id() { | ||
local result | ||
|
||
local mount_info_lines=() | ||
readarray -t mount_info_lines </proc/self/mountinfo | ||
for line in "${mount_info_lines[@]}"; do | ||
if [[ "${line}" =~ /([a-z0-9]{12,128})/resolv.conf" " ]]; then | ||
result="${BASH_REMATCH[1]}" | ||
fi | ||
done | ||
unset mount_info_lines | ||
|
||
# Sanity check | ||
if [[ "${result}" =~ ^[a-z0-9]{12,128}$ ]]; then | ||
readonly container_id="${result}" | ||
else | ||
error "Could not get parent container id" | ||
fi | ||
} | ||
|
||
# Gets the root directory of the current/parent container on the host | ||
# filesystem. | ||
function set_container_root_on_host() { | ||
local result | ||
|
||
if [[ -z "${mock_container_root_on_host}" ]]; then | ||
result="$( | ||
"${docker_compose_path}" inspect --format '{{.GraphDriver.Data.MergedDir}}' "${container_id}" | ||
)" | ||
else | ||
result="${mock_container_root_on_host}" | ||
fi | ||
|
||
# Sanity check | ||
if [[ "${result}" =~ ^(/[^/]+)+$ ]]; then | ||
readonly container_root_on_host="${result}" | ||
else | ||
error "Could not get parent container root on host" | ||
fi | ||
} | ||
|
||
# Reads the mounts of the current/parent container and stores them in the | ||
# parent_container_mounts array. | ||
function set_parent_container_mounts() { | ||
local docker_output | ||
docker_output=$( | ||
"${docker_compose_path}" inspect \ | ||
--format '{{range .Mounts}}{{if or (eq .Type "bind") (eq .Type "volume")}}{{printf "%s:%s\n" .Source .Destination}}{{end}}{{end}}' \ | ||
"${container_id}" | ||
) | ||
|
||
readarray -t parent_container_mounts <<<"${docker_output}" | ||
readonly parent_container_mounts | ||
} | ||
|
||
# Performs the necessary transformations to the volume/mount argument. | ||
function fix_volume_arg() { | ||
local arg_type="" | ||
if [[ "${volume_arg}" =~ ^(/[^:]+):([^:]+)(:([^:]+))?$ ]]; then | ||
arg_type="volume" | ||
local source="${BASH_REMATCH[1]}" | ||
local destination="${BASH_REMATCH[2]}" | ||
if [[ -n "${BASH_REMATCH[4]}" ]]; then | ||
local mode_suffix=":${BASH_REMATCH[4]}" | ||
else | ||
local mode_suffix="" | ||
fi | ||
elif [[ "${volume_arg}" =~ (^|,)type\=bind(,|$) ]]; then | ||
# There must be a better way of doing this | ||
if [[ "${volume_arg}" =~ (^|,)((source|src)\=(/[^,]+))(,|$) ]]; then | ||
local source_key="${BASH_REMATCH[3]}" | ||
local source="${BASH_REMATCH[4]}" | ||
if [[ "${volume_arg}" =~ (^|,)((destination|dst|target)\=(/[^,]+))(,|$) ]]; then | ||
arg_type="mount" | ||
local destination_key="${BASH_REMATCH[3]}" | ||
local destination="${BASH_REMATCH[4]}" | ||
fi | ||
fi | ||
fi | ||
|
||
if [[ -z "${arg_type}" ]]; then | ||
# Leave volume_arg as is if it does not match any patterns | ||
return | ||
fi | ||
|
||
local fixed_source="" | ||
|
||
# Fetch data only once and if needed | ||
if [[ "${container_data_fetched}" == false ]]; then | ||
set_container_id | ||
set_container_root_on_host | ||
set_parent_container_mounts | ||
container_data_fetched=true | ||
fi | ||
|
||
# Check mounts of the parent container to identify whether the source | ||
# is from the host filesystem or from the container itself. If it is | ||
# from the host filesystem, then we need to transform the mount to | ||
# match the mount from the parent container. | ||
for container_volume in "${parent_container_mounts[@]}"; do | ||
local container_volume_source="${container_volume%%":"*}" | ||
local container_volume_destination="${container_volume#*":"}" | ||
|
||
if [[ -z "${fixed_source}" ]]; then | ||
if [[ "${source}" == "${container_volume_destination}" ]]; then | ||
fixed_source="${container_volume_source}" | ||
elif [[ "${source}" == "${container_volume_destination}/"* ]]; then | ||
fixed_source="${container_volume_source}${source#"${container_volume_destination}"}" | ||
fi | ||
fi | ||
|
||
# Check if there is some container_volume mounted within the source | ||
# Example: | ||
# First container mounts --volume "${PWD}/testfile:/home/rootless/testfile" | ||
# Second container mounts --volume /home/rootless:/wd | ||
# Then the second container should have an extra mount --volume "${PWD}/testfile:/wd/testfile" | ||
if [[ "${container_volume_destination}" == "${source}/"* ]]; then | ||
# Convert /home/rootless/testfile (container_volume_destination) to /wd/testfile (destination path) | ||
if [[ "${arg_type}" == "volume" ]]; then | ||
next_extra_args+=(--volume "${container_volume_source}:${destination}/${container_volume_destination#"${source}/"}${mode_suffix}") | ||
elif [[ "${arg_type}" == "mount" ]]; then | ||
# Use replace to generate an argument with the same options as the original | ||
local fixed_arg | ||
fixed_arg="${volume_arg//"${source_key}=${source}"/"${source_key}=${container_volume_source}"}" | ||
fixed_arg="${fixed_arg//"${destination_key}=${destination}"/"${destination_key}=${destination}/${container_volume_destination#"${source}/"}"}" | ||
next_extra_args+=(--mount "${fixed_arg}") | ||
fi | ||
fi | ||
done | ||
|
||
# If it was not possible to find a matching container_volume, then | ||
# we mount relative to the container root directory on the host | ||
# filesystem. | ||
if [[ -z "${fixed_source}" ]]; then | ||
fixed_source="${container_root_on_host}${source}" | ||
fi | ||
|
||
if [[ "${arg_type}" == "volume" ]]; then | ||
volume_arg="${fixed_source}:${destination}${mode_suffix}" | ||
elif [[ "${arg_type}" == "mount" ]]; then | ||
# Use replace to avoid removing other options and also to keep the order | ||
volume_arg="${volume_arg//"${source_key}=${source}"/"${source_key}=${fixed_source}"}" | ||
fi | ||
} | ||
|
||
script_path="$(realpath "$0")" | ||
readonly script_path | ||
|
||
# Parse supported environment variables | ||
readonly mock_container_root_on_host="${DOND_COMPOSE_SHIM_MOCK_CONTAINER_ROOT_ON_HOST:-}" | ||
readonly print_command="${DOND_COMPOSE_SHIM_PRINT_COMMAND:-false}" | ||
|
||
if [[ -n "${DOND_COMPOSE_SHIM_DOCKER_COMPOSE_PATH:-}" ]]; then | ||
# If DOND_COMPOSE_SHIM_DOCKER_COMPOSE_PATH is set, then use it to call the docker | ||
readonly docker_compose_path="${DOND_COMPOSE_SHIM_DOCKER_COMPOSE_PATH}" | ||
elif [[ "${0}" == *"/docker" ]]; then | ||
# If this shim is named docker, then we expect the original docker to be | ||
# named docker.orig | ||
readonly docker_compose_path="docker.orig" | ||
else | ||
# If this shim is not named docker, then we can simply call docker | ||
readonly docker_compose_path="docker" | ||
fi | ||
|
||
# Ensure docker_compose_path is different from this script to avoid infinite loop | ||
if [[ "${script_path}" == "${docker_compose_path}" ]]; then | ||
error "docker_compose_path (${docker_compose_path}) points to this script (${script_path})" | ||
fi | ||
|
||
# Ensure docker_compose_path actually exists | ||
if ! command -v "${docker_compose_path}" >/dev/null; then | ||
error "docker_compose_path (${docker_compose_path}) points to a non-existing file or command" | ||
fi | ||
|
||
# Save original arguments | ||
original_args=("$@") | ||
readonly original_args | ||
|
||
# Avoid infinite loop if this script calls itself at a different path | ||
if [[ "${DOND_COMPOSE_SHIM_SKIP:-false}" == true ]]; then | ||
exec "${docker_compose_path}" "${original_args[@]}" | ||
fi | ||
|
||
# Exit early if the original command does not have at least 3 args, like | ||
# run --volume=/tmp:/tmp ubuntu | ||
if [[ "${#original_args[@]}" -lt 3 ]]; then | ||
run_docker_compose "${original_args[@]}" | ||
fi | ||
|
||
# We need to identify which arguments are global, which are for the container | ||
# run/create command and which are for the image. That's to avoid transforming | ||
# arguments that are not meant to be transformed (we should only transform | ||
# arguments that are meant to be passed to the container run/create command). | ||
|
||
# Example: --host whatever container run | ||
global_args=() | ||
# Example: --volume /tmp:/tmp ubuntu | ||
command_args=() | ||
# Example: bash -c "echo hello" | ||
image_args=() | ||
|
||
skip_next_arg=false | ||
next_arg_type="global" | ||
docker_command=() | ||
global_options_with_value_fetched=false | ||
command_options_with_value_fetched=false | ||
first_global_positional_arg_found=false | ||
|
||
for arg in "${original_args[@]}"; do | ||
if [[ "${next_arg_type}" == "global" ]]; then | ||
global_args+=("${arg}") | ||
|
||
if [[ "${skip_next_arg}" == true ]]; then | ||
skip_next_arg=false | ||
continue | ||
fi | ||
|
||
if [[ "${arg}" == "-"* ]]; then | ||
# Only parse global options before the first positional command, because | ||
# docker does not allow an option between container and run/create like | ||
# this: docker container --host whatever run | ||
if [[ "${first_global_positional_arg_found}" == false ]]; then | ||
# Only fetch docker global options when needed and only once | ||
if [[ "${global_options_with_value_fetched}" == false ]]; then | ||
set_docker_options_with_value | ||
global_options_with_value_fetched=true | ||
fi | ||
|
||
# Skip next argument if it is a global option that accepts a value | ||
for option in "${docker_options_with_value[@]}"; do | ||
if [[ "${arg}" == "${option}" ]]; then | ||
skip_next_arg=true | ||
break | ||
fi | ||
done | ||
fi | ||
elif [[ "${arg}" == "run" || "${arg}" == "create" ]]; then | ||
docker_command+=("${arg}") | ||
next_arg_type="command" | ||
elif [[ "${arg}" == "container" ]]; then | ||
docker_command+=("${arg}") | ||
first_global_positional_arg_found=true | ||
else | ||
# Skip if command is not run, create, container run, or container create | ||
run_docker_compose "${original_args[@]}" | ||
fi | ||
elif [[ "${next_arg_type}" == "command" ]]; then | ||
command_args+=("${arg}") | ||
|
||
if [[ "${skip_next_arg}" == true ]]; then | ||
skip_next_arg=false | ||
continue | ||
fi | ||
|
||
if [[ "${arg}" == "-"* ]]; then | ||
# Only fetch docker command options when needed and only once | ||
if [[ "${command_options_with_value_fetched}" == false ]]; then | ||
set_docker_options_with_value "${docker_command[@]}" | ||
command_options_with_value_fetched=true | ||
fi | ||
|
||
for option in "${docker_options_with_value[@]}"; do | ||
if [[ "${arg}" == "${option}" ]]; then | ||
skip_next_arg=true | ||
continue | ||
fi | ||
done | ||
else | ||
# First non-option argument is the image | ||
next_arg_type="image" | ||
fi | ||
elif [[ "${next_arg_type}" == "image" ]]; then | ||
image_args+=("${arg}") | ||
fi | ||
done | ||
readonly global_args command_args image_args | ||
unset next_arg_type skip_next_arg docker_command global_options_with_value_fetched \ | ||
command_options_with_value_fetched first_global_positional_arg_found \ | ||
docker_options_with_value | ||
|
||
# Finally it's time to transform the volume|mount arguments | ||
container_data_fetched=false | ||
fix_next_arg=false | ||
fixed_args=() | ||
next_extra_args=() | ||
|
||
for arg in "${command_args[@]}"; do | ||
if [[ "${fix_next_arg}" == true ]]; then | ||
fix_next_arg=false | ||
volume_arg="${arg}" | ||
fix_volume_arg | ||
arg="${volume_arg}" | ||
elif [[ "${arg}" == "-v" || "${arg}" == "--volume" || "${arg}" == "--mount" ]]; then | ||
fix_next_arg=true | ||
elif [[ "${arg}" == "-v="* || "${arg}" == "--volume="* ]]; then | ||
option_name="${arg%%"="*}" | ||
volume_arg="${arg#*"="}" | ||
fix_volume_arg | ||
arg="${option_name}=${volume_arg}" | ||
elif [[ "${arg}" == "--mount="* ]]; then | ||
option_name="${arg%%"="*}" | ||
volume_arg="${arg#*"="}" | ||
fix_volume_arg | ||
arg="${option_name}=${volume_arg}" | ||
fi | ||
|
||
fixed_args+=("${arg}" "${next_extra_args[@]}") | ||
next_extra_args=() | ||
done | ||
|
||
run_docker_compose "${global_args[@]}" "${fixed_args[@]}" "${image_args[@]}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
services: | ||
test: | ||
image: busybox | ||
volumes: | ||
- /wd:/wd | ||
command: [grep, -q, ^test$$, /wd/testfile] |