Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shim for docker-compose #17

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Loading