Skip to content

Commit

Permalink
Add shim for docker-compose
Browse files Browse the repository at this point in the history
  • Loading branch information
felipecrs committed Oct 17, 2024
1 parent 5d2c5bc commit 03bb6b3
Show file tree
Hide file tree
Showing 3 changed files with 366 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ ARG DOCKER_PATH="/usr/local/bin/docker"
RUN mv -f "${DOCKER_PATH}" "${DOCKER_PATH}.orig"
COPY --from=dond-shim-bin /dond "${DOCKER_PATH}"

ARG DOCKER_COMPOSE_PATH="/usr/local/bin/docker-compose"
RUN mv -f "${DOCKER_COMPOSE_PATH}" "${DOCKER_COMPOSE_PATH}.orig"
COPY ./dond-compose "${DOCKER_COMPOSE_PATH}"

FROM dond-shim AS test

# Create fixtures
Expand Down
356 changes: 356 additions & 0 deletions dond-compose
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[@]}"
6 changes: 6 additions & 0 deletions tests/fixtures/docker-compose.yaml
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]

0 comments on commit 03bb6b3

Please sign in to comment.