diff --git a/commands/cmd_to_run_generator.sh b/commands/cmd_to_run_generator.sh new file mode 100644 index 00000000..87b86bda --- /dev/null +++ b/commands/cmd_to_run_generator.sh @@ -0,0 +1,84 @@ +#!/bin/bash +set -uo pipefail + +function generate_cmd() { + local -n cmds="$1" + local -n display="$2" + + shell_disabled=1 + result=() + + if [[ -n "${BUILDKITE_COMMAND}" ]]; then + shell_disabled='' + fi + + # Handle shell being disabled + if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL:-}" =~ ^(false|off|0)$ ]] ; then + shell_disabled=1 + + # Show a helpful error message if a string version of shell is used + elif [[ -n "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL:-}" ]] ; then + echo -n "🚨 The Docker Compose Plugin’s shell configuration option must be specified as an array. " + echo -n "Please update your pipeline.yml to use an array, " + echo "for example: [\"/bin/sh\", \"-e\", \"-u\"]." + echo + echo -n "Note that a shell will be inferred if one is required, so you might be able to remove" + echo "the option entirely" + exit 1 + + # Handle shell being provided as a string or list + elif plugin_read_list_into_result BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL ; then + shell_disabled='' + for arg in "${result[@]}" ; do + cmds+=("$arg") + done + fi + + # Set a default shell if one is needed + if [[ -z $shell_disabled ]] && [[ ${#cmds[@]} -eq 0 ]] ; then + if is_windows ; then + cmds=("CMD.EXE" "/c") + # else + # cmds=("/bin/sh" "-e" "-c") + fi + fi + + if [[ ${#cmds[@]} -gt 0 ]] ; then + for shell_arg in "${cmds[@]}" ; do + display+=("$shell_arg") + done + fi + + # Show a helpful error message if string version of command is used + if [[ -n "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_COMMAND:-}" ]] ; then + echo "🚨 The Docker Compose Plugin’s command configuration option must be an array." + exit 1 + fi + + # Parse plugin command if provided + if plugin_read_list_into_result BUILDKITE_PLUGIN_DOCKER_COMPOSE_COMMAND ; then + if [[ "${#result[@]}" -gt 0 ]] && [[ -n "${BUILDKITE_COMMAND}" ]] ; then + echo "+++ Error: Can't use both a step level command and the command parameter of the plugin" + exit 1 + elif [[ "${#result[@]}" -gt 0 ]] ; then + echo "compose plugin command: ${result[@]}" + for arg in "${result[@]}" ; do + cmds+=("$arg") + display+=("$arg") + done + fi + fi + if [[ -n "${BUILDKITE_COMMAND}" ]] ; then + echo "buildkite command: ${BUILDKITE_COMMAND}" + if [[ $(echo "$BUILDKITE_COMMAND" | wc -l) -gt 1 ]]; then + # FIXME: This is easy to fix, just need to do at end + + # An array of commands in the step will be a single string with multiple lines + # This breaks a lot of things here so we will print a warning for user to be aware + echo "⚠️ Warning: The command received has multiple lines." + echo "⚠️ The Docker Compose Plugin does not correctly support step-level array commands." + fi + cmds+=("${BUILDKITE_COMMAND}") + display+=("'${BUILDKITE_COMMAND}'") + fi +} \ No newline at end of file diff --git a/commands/pull.sh b/commands/pull.sh new file mode 100644 index 00000000..cb62219d --- /dev/null +++ b/commands/pull.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -uo pipefail + +function pull() { + prebuilt_candidates=("$1") + + pull_services=() + pull_params=() + + override_file="docker-compose.buildkite-${BUILDKITE_BUILD_NUMBER}-override.yml" + pull_retries="$(plugin_read_config PULL_RETRIES "0")" + + # Build a list of services that need to be pulled down + while read -r name ; do + if [[ -n "$name" ]] ; then + pull_services+=("$name") + + if ! in_array "$name" "${prebuilt_candidates[@]}" ; then + prebuilt_candidates+=("$name") + fi + fi + done <<< "$(plugin_read_list PULL)" + + # A list of tuples of [service image cache_from] for build_image_override_file + prebuilt_service_overrides=() + prebuilt_services=() + + # We look for a prebuilt images for all the pull services and the run_service. + prebuilt_image_override="$(plugin_read_config RUN_IMAGE)" + for service_name in "${prebuilt_candidates[@]}" ; do + if [[ -n "$prebuilt_image_override" ]] && [[ "$service_name" == "$1" ]] ; then + echo "~~~ :docker: Overriding run image for $service_name" + prebuilt_image="$prebuilt_image_override" + elif prebuilt_image=$(get_prebuilt_image "$service_name") ; then + echo "~~~ :docker: Found a pre-built image for $service_name" + fi + + if [[ -n "$prebuilt_image" ]] ; then + prebuilt_service_overrides+=("$service_name" "$prebuilt_image" "" 0 0) + prebuilt_services+=("$service_name") + + # If it's prebuilt, we need to pull it down + if [[ -z "${pull_services:-}" ]] || ! in_array "$service_name" "${pull_services[@]}" ; then + pull_services+=("$service_name") + fi + fi + done + + exitcode=1 + # If there are any prebuilts, we need to generate an override docker-compose file + if [[ ${#prebuilt_services[@]} -gt 0 ]] ; then + echo "~~~ :docker: Creating docker-compose override file for prebuilt services" + build_image_override_file "${prebuilt_service_overrides[@]}" | tee "$override_file" + pull_params+=(-f "$override_file") + exitcode=0 + fi + + # If there are multiple services to pull, run it in parallel (although this is now the default) + if [[ ${#pull_services[@]} -gt 1 ]] ; then + pull_params+=("pull" "--parallel" "${pull_services[@]}") + elif [[ ${#pull_services[@]} -eq 1 ]] ; then + pull_params+=("pull" "${pull_services[0]}") + fi + + # Pull down specified services + if [[ ${#pull_services[@]} -gt 0 ]] && [[ "$(plugin_read_config SKIP_PULL "false")" != "true" ]]; then + echo "~~~ :docker: Pulling services ${pull_services[0]}" + retry "$pull_retries" run_docker_compose "${pull_params[@]}" + fi + + echo "done pulling. exitcode: $exitcode" + return $exitcode +} \ No newline at end of file diff --git a/commands/run.sh b/commands/run.sh index 4c912854..708383e5 100755 --- a/commands/run.sh +++ b/commands/run.sh @@ -1,5 +1,15 @@ #!/bin/bash -set -ueo pipefail +set -uo pipefail + +. "$DIR/../commands/pull.sh" +. "$DIR/../commands/run_params_generator.sh" +. "$DIR/../commands/cmd_to_run_generator.sh" + +# Can't set both user and propagate-uid-gid +if [[ -n "$(plugin_read_config USER)" ]] && [[ -n "$(plugin_read_config PROPAGATE_UID_GID)" ]]; then + echo "+++ Error: Can't set both user and propagate-uid-gid" + exit 1 +fi # Run takes a service name, pulls down any pre-built image for that name # and then runs docker-compose run a generated project name @@ -7,209 +17,46 @@ set -ueo pipefail run_service="$(plugin_read_config RUN)" container_name="$(docker_compose_project_name)_${run_service}_build_${BUILDKITE_BUILD_NUMBER}" override_file="docker-compose.buildkite-${BUILDKITE_BUILD_NUMBER}-override.yml" -pull_retries="$(plugin_read_config PULL_RETRIES "0")" -mount_checkout="$(plugin_read_config MOUNT_CHECKOUT "false")" -workdir='' - -expand_headers_on_error() { - echo "^^^ +++" -} -trap expand_headers_on_error ERR test -f "$override_file" && rm "$override_file" -run_params=() -pull_params=() -up_params=() -pull_services=() -prebuilt_candidates=("$run_service") +pulled_status=0 +pull "$run_service" || pulled_status=$? +echo "pulled_status: $pulled_status" -# Build a list of services that need to be pulled down -while read -r name ; do - if [[ -n "$name" ]] ; then - pull_services+=("$name") - - if ! in_array "$name" "${prebuilt_candidates[@]}" ; then - prebuilt_candidates+=("$name") - fi - fi -done <<< "$(plugin_read_list PULL)" - -# A list of tuples of [service image cache_from] for build_image_override_file -prebuilt_service_overrides=() -prebuilt_services=() - -# We look for a prebuilt images for all the pull services and the run_service. -prebuilt_image_override="$(plugin_read_config RUN_IMAGE)" -for service_name in "${prebuilt_candidates[@]}" ; do - if [[ -n "$prebuilt_image_override" ]] && [[ "$service_name" == "$run_service" ]] ; then - echo "~~~ :docker: Overriding run image for $service_name" - prebuilt_image="$prebuilt_image_override" - elif prebuilt_image=$(get_prebuilt_image "$service_name") ; then - echo "~~~ :docker: Found a pre-built image for $service_name" - fi - - if [[ -n "$prebuilt_image" ]] ; then - prebuilt_service_overrides+=("$service_name" "$prebuilt_image" "" 0 0) - prebuilt_services+=("$service_name") +if [[ ! -f "$override_file" ]] ; then + echo "+++ 🚨 No pre-built image found from a previous 'build' step for this service and config file." - # If it's prebuilt, we need to pull it down - if [[ -z "${pull_services:-}" ]] || ! in_array "$service_name" "${pull_services[@]}" ; then - pull_services+=("$service_name") - fi + if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_REQUIRE_PREBUILD:-}" =~ ^(true|on|1)$ ]]; then + echo "The step specified that it was required" + exit 1 fi -done - -# If there are any prebuilts, we need to generate an override docker-compose file -if [[ ${#prebuilt_services[@]} -gt 0 ]] ; then - echo "~~~ :docker: Creating docker-compose override file for prebuilt services" - build_image_override_file "${prebuilt_service_overrides[@]}" | tee "$override_file" - run_params+=(-f "$override_file") - pull_params+=(-f "$override_file") - up_params+=(-f "$override_file") -fi - -# If there are multiple services to pull, run it in parallel (although this is now the default) -if [[ ${#pull_services[@]} -gt 1 ]] ; then - pull_params+=("pull" "--parallel" "${pull_services[@]}") -elif [[ ${#pull_services[@]} -eq 1 ]] ; then - pull_params+=("pull" "${pull_services[0]}") -fi - -# Pull down specified services -if [[ ${#pull_services[@]} -gt 0 ]] && [[ "$(plugin_read_config SKIP_PULL "false")" != "true" ]]; then - echo "~~~ :docker: Pulling services ${pull_services[0]}" - retry "$pull_retries" run_docker_compose "${pull_params[@]}" +else + echo "~~~ :docker: Using pre-built image for $run_service" fi -# We set a predictable container name so we can find it and inspect it later on +up_params=() +declare -a run_params run_params+=("run" "--name" "$container_name") +generate_run_args "run_params" $pulled_status +echo "run_params after func: ${run_params[@]}" -if [[ "$(plugin_read_config RUN_LABELS "true")" =~ ^(true|on|1)$ ]]; then - # Add useful labels to run container - run_params+=( - "--label" "com.buildkite.pipeline_name=${BUILDKITE_PIPELINE_NAME}" - "--label" "com.buildkite.pipeline_slug=${BUILDKITE_PIPELINE_SLUG}" - "--label" "com.buildkite.build_number=${BUILDKITE_BUILD_NUMBER}" - "--label" "com.buildkite.job_id=${BUILDKITE_JOB_ID}" - "--label" "com.buildkite.job_label=${BUILDKITE_LABEL}" - "--label" "com.buildkite.step_key=${BUILDKITE_STEP_KEY}" - "--label" "com.buildkite.agent_name=${BUILDKITE_AGENT_NAME}" - "--label" "com.buildkite.agent_id=${BUILDKITE_AGENT_ID}" - ) -fi - -# append env vars provided in ENV or ENVIRONMENT, these are newline delimited -while IFS=$'\n' read -r env ; do - [[ -n "${env:-}" ]] && run_params+=("-e" "${env}") -done <<< "$(printf '%s\n%s' \ - "$(plugin_read_list ENV)" \ - "$(plugin_read_list ENVIRONMENT)")" - -# Propagate all environment variables into the container if requested -if [[ "$(plugin_read_config PROPAGATE_ENVIRONMENT "false")" =~ ^(true|on|1)$ ]] ; then - if [[ -n "${BUILDKITE_ENV_FILE:-}" ]] ; then - # Read in the env file and convert to --env params for docker - # This is because --env-file doesn't support newlines or quotes per https://docs.docker.com/compose/env-file/#syntax-rules - while read -r var; do - run_params+=("-e" "${var%%=*}") - done < "${BUILDKITE_ENV_FILE}" - else - echo -n "🚨 Not propagating environment variables to container as \$BUILDKITE_ENV_FILE is not set" - fi -fi +# We set a predictable container name so we can find it and inspect it later on +run_params+=("$run_service") +up_params+=("up") # this ensures that the array has elements to avoid issues with bash 4.3 -# Propagate AWS credentials if requested -if [[ "$(plugin_read_config PROPAGATE_AWS_AUTH_TOKENS "false")" =~ ^(true|on|1)$ ]] ; then - if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]] ; then - run_params+=( --env "AWS_ACCESS_KEY_ID" ) - fi - if [[ -n "${AWS_SECRET_ACCESS_KEY:-}" ]] ; then - run_params+=( --env "AWS_SECRET_ACCESS_KEY" ) - fi - if [[ -n "${AWS_SESSION_TOKEN:-}" ]] ; then - run_params+=( --env "AWS_SESSION_TOKEN" ) - fi - if [[ -n "${AWS_REGION:-}" ]] ; then - run_params+=( --env "AWS_REGION" ) - fi - if [[ -n "${AWS_DEFAULT_REGION:-}" ]] ; then - run_params+=( --env "AWS_DEFAULT_REGION" ) - fi - if [[ -n "${AWS_ROLE_ARN:-}" ]] ; then - run_params+=( --env "AWS_ROLE_ARN" ) - fi - if [[ -n "${AWS_STS_REGIONAL_ENDPOINTS:-}" ]] ; then - run_params+=( --env "AWS_STS_REGIONAL_ENDPOINTS" ) - fi - # Pass ECS variables when the agent is running in ECS - # https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html - if [[ -n "${AWS_CONTAINER_CREDENTIALS_FULL_URI:-}" ]] ; then - run_params+=( --env "AWS_CONTAINER_CREDENTIALS_FULL_URI" ) - fi - if [[ -n "${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI:-}" ]] ; then - run_params+=( --env "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ) - fi - if [[ -n "${AWS_CONTAINER_AUTHORIZATION_TOKEN:-}" ]] ; then - run_params+=( --env "AWS_CONTAINER_AUTHORIZATION_TOKEN" ) - fi - # Pass EKS variables when the agent is running in EKS - # https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html - if [[ -n "${AWS_WEB_IDENTITY_TOKEN_FILE:-}" ]] ; then - run_params+=( --env "AWS_WEB_IDENTITY_TOKEN_FILE" ) - # Add the token file as a volume - run_params+=( --volume "${AWS_WEB_IDENTITY_TOKEN_FILE}:${AWS_WEB_IDENTITY_TOKEN_FILE}" ) - fi +if [[ "$(plugin_read_config WAIT "false")" == "true" ]] ; then + up_params+=("--wait") fi -# If requested, propagate a set of env vars as listed in a given env var to the -# container. -if [[ -n "$(plugin_read_config ENV_PROPAGATION_LIST)" ]]; then - env_propagation_list_var="$(plugin_read_config ENV_PROPAGATION_LIST)" - if [[ -z "${!env_propagation_list_var:-}" ]]; then - echo -n "env-propagation-list desired, but ${env_propagation_list_var} is not defined!" - exit 1 - fi - for var in ${!env_propagation_list_var}; do - run_params+=("-e" "$var") - done +if [[ "$(plugin_read_config QUIET_PULL "false")" == "true" ]] ; then + up_params+=("--quiet-pull") fi -while IFS=$'\n' read -r vol ; do - [[ -n "${vol:-}" ]] && run_params+=("-v" "$(expand_relative_volume_path "$vol")") -done <<< "$(plugin_read_list VOLUMES)" - -# Parse BUILDKITE_DOCKER_DEFAULT_VOLUMES delimited by semi-colons, normalized to -# ignore spaces and leading or trailing semi-colons -IFS=';' read -r -a default_volumes <<< "${BUILDKITE_DOCKER_DEFAULT_VOLUMES:-}" -for vol in "${default_volumes[@]:-}" ; do - trimmed_vol="$(echo -n "$vol" | sed -e 's/^[[:space:]]*//' | sed -e 's/[[:space:]]*$//')" - [[ -n "$trimmed_vol" ]] && run_params+=("-v" "$(expand_relative_volume_path "$trimmed_vol")") -done -# If there's a git mirror, mount it so that git references can be followed. -if [[ -n "${BUILDKITE_REPO_MIRROR:-}" ]]; then - run_params+=("-v" "$BUILDKITE_REPO_MIRROR:$BUILDKITE_REPO_MIRROR:ro") -fi +dependency_exitcode=0 -tty_default='false' -workdir_default="/workdir" -pwd_default="$PWD" run_dependencies="true" - -# Set operating system specific defaults -if is_windows ; then - workdir_default="C:\\workdir" - # escaping /C is a necessary workaround for an issue with Git for Windows 2.24.1.2 - # https://github.com/git-for-windows/git/issues/2442 - pwd_default="$(cmd.exe //C "echo %CD%")" -fi - -# Disable allocating a TTY -if [[ "$(plugin_read_config TTY "$tty_default")" == "false" ]] ; then - run_params+=(-T) -fi - # Optionally disable dependencies if [[ "$(plugin_read_config DEPENDENCIES "true")" == "false" ]] ; then run_params+=(--no-deps) @@ -218,128 +65,6 @@ elif [[ "$(plugin_read_config PRE_RUN_DEPENDENCIES "true")" == "false" ]]; then run_dependencies="false" fi -if [[ -n "$(plugin_read_config WORKDIR)" ]] || [[ "${mount_checkout}" == "true" ]]; then - workdir="$(plugin_read_config WORKDIR "$workdir_default")" -fi - -if [[ -n "${workdir}" ]] ; then - run_params+=("--workdir=${workdir}") -fi - -if [[ "${mount_checkout}" == "true" ]]; then - run_params+=("-v" "${pwd_default}:${workdir}") -elif [[ "${mount_checkout}" =~ ^/.*$ ]]; then - run_params+=("-v" "${pwd_default}:${mount_checkout}") -elif [[ "${mount_checkout}" != "false" ]]; then - echo -n "🚨 mount-checkout should be either true or an absolute path to use as a mountpoint" - exit 1 -fi - -# Can't set both user and propagate-uid-gid -if [[ -n "$(plugin_read_config USER)" ]] && [[ -n "$(plugin_read_config PROPAGATE_UID_GID)" ]]; then - echo "+++ Error: Can't set both user and propagate-uid-gid" - exit 1 -fi - -# Optionally run as specified username or uid -if [[ -n "$(plugin_read_config USER)" ]] ; then - run_params+=("--user=$(plugin_read_config USER)") -fi - -# Optionally run as specified username or uid -if [[ "$(plugin_read_config PROPAGATE_UID_GID "false")" == "true" ]] ; then - run_params+=("--user=$(id -u):$(id -g)") -fi - -# Enable alias support for networks -if [[ "$(plugin_read_config USE_ALIASES "false")" == "true" ]] ; then - run_params+=(--use-aliases) -fi - -# Optionally remove containers after run -if [[ "$(plugin_read_config RM "true")" == "true" ]]; then - run_params+=(--rm) -fi - -# Optionally sets --entrypoint -if [[ -n "$(plugin_read_config ENTRYPOINT)" ]] ; then - run_params+=(--entrypoint) - run_params+=("$(plugin_read_config ENTRYPOINT)") -fi - -# Mount ssh-agent socket and known_hosts -if [[ ! "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_MOUNT_SSH_AGENT:-false}" = 'false' ]] ; then - if [[ -z "${SSH_AUTH_SOCK:-}" ]] ; then - echo "+++ 🚨 \$SSH_AUTH_SOCK isn't set, has ssh-agent started?" - exit 1 - fi - if [[ ! -S "${SSH_AUTH_SOCK}" ]] ; then - echo "+++ 🚨 The file at ${SSH_AUTH_SOCK} does not exist or is not a socket, was ssh-agent started?" - exit 1 - fi - - if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_MOUNT_SSH_AGENT:-''}" =~ ^(true|on|1)$ ]]; then - MOUNT_PATH=/root - else - MOUNT_PATH="${BUILDKITE_PLUGIN_DOCKER_COMPOSE_MOUNT_SSH_AGENT}" - fi - - run_params+=( - "-e" "SSH_AUTH_SOCK=/ssh-agent" - "-v" "${SSH_AUTH_SOCK}:/ssh-agent" - "-v" "${HOME}/.ssh/known_hosts:${MOUNT_PATH}/.ssh/known_hosts" - ) -fi - -# Optionally handle the mount-buildkite-agent option -if [[ "$(plugin_read_config MOUNT_BUILDKITE_AGENT "false")" == "true" ]]; then - if [[ -z "${BUILDKITE_AGENT_BINARY_PATH:-}" ]] ; then - if ! command -v buildkite-agent >/dev/null 2>&1 ; then - echo -n "+++ 🚨 Failed to find buildkite-agent in PATH to mount into container, " - echo "you can disable this behaviour with 'mount-buildkite-agent:false'" - else - BUILDKITE_AGENT_BINARY_PATH=$(command -v buildkite-agent) - fi - fi -fi - -# Mount buildkite-agent if we have a path for it -if [[ -n "${BUILDKITE_AGENT_BINARY_PATH:-}" ]] ; then - run_params+=( - "-e" "BUILDKITE_JOB_ID" - "-e" "BUILDKITE_BUILD_ID" - "-e" "BUILDKITE_AGENT_ACCESS_TOKEN" - "-v" "$BUILDKITE_AGENT_BINARY_PATH:/usr/bin/buildkite-agent" - ) -fi - -# Optionally expose service ports -if [[ "$(plugin_read_config SERVICE_PORTS "false")" == "true" ]]; then - run_params+=(--service-ports) -fi - -run_params+=("$run_service") - -if [[ ! -f "$override_file" ]] ; then - echo "+++ 🚨 No pre-built image found from a previous 'build' step for this service and config file." - - if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_REQUIRE_PREBUILD:-}" =~ ^(true|on|1)$ ]]; then - echo "The step specified that it was required" - exit 1 - fi -fi - -up_params+=("up") # this ensures that the array has elements to avoid issues with bash 4.3 - -if [[ "$(plugin_read_config WAIT "false")" == "true" ]] ; then - up_params+=("--wait") -fi - -if [[ "$(plugin_read_config QUIET_PULL "false")" == "true" ]] ; then - up_params+=("--quiet-pull") -fi - -dependency_exitcode=0 if [[ "${run_dependencies}" == "true" ]] ; then # Start up service dependencies in a different header to keep the main run with less noise echo "~~~ :docker: Starting dependencies" @@ -347,7 +72,6 @@ if [[ "${run_dependencies}" == "true" ]] ; then fi if [[ $dependency_exitcode -ne 0 ]] ; then - # Dependent services failed to start. echo "^^^ +++" echo "+++ 🚨 Failed to start dependencies" @@ -357,104 +81,14 @@ if [[ $dependency_exitcode -ne 0 ]] ; then upload_container_logs "$run_service" fi - return $dependency_exitcode + exit $dependency_exitcode fi -shell=() -shell_disabled=1 -result=() - -if [[ -n "${BUILDKITE_COMMAND}" ]]; then - shell_disabled='' -fi - -# Handle shell being disabled -if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL:-}" =~ ^(false|off|0)$ ]] ; then - shell_disabled=1 - -# Show a helpful error message if a string version of shell is used -elif [[ -n "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL:-}" ]] ; then - echo -n "🚨 The Docker Compose Plugin’s shell configuration option must be specified as an array. " - echo -n "Please update your pipeline.yml to use an array, " - echo "for example: [\"/bin/sh\", \"-e\", \"-u\"]." - echo - echo -n "Note that a shell will be inferred if one is required, so you might be able to remove" - echo "the option entirely" - exit 1 - -# Handle shell being provided as a string or list -elif plugin_read_list_into_result BUILDKITE_PLUGIN_DOCKER_COMPOSE_SHELL ; then - shell_disabled='' - for arg in "${result[@]}" ; do - shell+=("$arg") - done -fi - -# Set a default shell if one is needed -if [[ -z $shell_disabled ]] && [[ ${#shell[@]} -eq 0 ]] ; then - if is_windows ; then - shell=("CMD.EXE" "/c") - else - shell=("/bin/sh" "-e" "-c") - fi -fi - -command=() - -# Show a helpful error message if string version of command is used -if [[ -n "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_COMMAND:-}" ]] ; then - echo "🚨 The Docker Compose Plugin’s command configuration option must be an array." - exit 1 -fi - -# Parse plugin command if provided -if plugin_read_list_into_result BUILDKITE_PLUGIN_DOCKER_COMPOSE_COMMAND ; then - for arg in "${result[@]}" ; do - command+=("$arg") - done -fi - -if [[ ${#command[@]} -gt 0 ]] && [[ -n "${BUILDKITE_COMMAND}" ]] ; then - echo "+++ Error: Can't use both a step level command and the command parameter of the plugin" - exit 1 -fi # Assemble the shell and command arguments into the docker arguments - display_command=() - -if [[ ${#shell[@]} -gt 0 ]] ; then - for shell_arg in "${shell[@]}" ; do - run_params+=("$shell_arg") - display_command+=("$shell_arg") - done -fi - -if [[ -n "${BUILDKITE_COMMAND}" ]] ; then - if [[ $(echo "$BUILDKITE_COMMAND" | wc -l) -gt 1 ]]; then - # An array of commands in the step will be a single string with multiple lines - # This breaks a lot of things here so we will print a warning for user to be aware - echo "⚠️ Warning: The command received has multiple lines." - echo "⚠️ The Docker Compose Plugin does not correctly support step-level array commands." - fi - run_params+=("${BUILDKITE_COMMAND}") - display_command+=("'${BUILDKITE_COMMAND}'") -elif [[ ${#command[@]} -gt 0 ]] ; then - for command_arg in "${command[@]}" ; do - run_params+=("$command_arg") - display_command+=("${command_arg}") - done -fi - -ensure_stopped() { - echo '+++ :warning: Signal received, stopping container' - docker stop "${container_name}" || true - echo '~~~ Last log lines that may be missing above (if container was not already removed)' - docker logs "${container_name}" || true - exitcode='TRAP' -} - -trap ensure_stopped SIGINT SIGTERM SIGQUIT +commands=() +generate_cmd "commands" "display_command" if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_COLLAPSE_LOGS:-false}" = "true" ]]; then group_type="---" @@ -462,21 +96,21 @@ else group_type="+++" fi -# Disable -e to prevent cancelling step if the command fails for whatever reason -set +e -( # subshell is necessary to trap signals (compose v2 fails to stop otherwise) - echo "${group_type} :docker: Running ${display_command[*]:-} in service $run_service" >&2 - run_docker_compose "${run_params[@]}" +echo "${group_type} :docker: Running ${display_command[*]:-} in service $run_service" +echo "commands is: ${commands[@]}" +# printf -v cmd_lit ' "%s" ' "${commands[@]}" +cmd_lit=( "${run_params[@]}" "${commands[@]}" ) +# cmd_lit=( "${run_params[@]}" "echo hello world, I'm starting here; sleep 10000" ) +echo "PID is: $BASHPID" + +exitcode=0 +( + echo "docker compose being called. PID is: $BASHPID" + run_docker_compose "${cmd_lit[@]}" || exitcode=$? ) -exitcode=$? -# Restore -e as an option. -set -e -if [[ $exitcode = "TRAP" ]]; then - # command failed due to cancellation signal, make sure there is an error but no further output - exitcode=-1 -elif [[ $exitcode -ne 0 ]] ; then +if [[ $exitcode -ne 0 ]] ; then echo "^^^ +++" echo "+++ :warning: Failed to run command, exited with $exitcode, run params:" echo "${run_params[@]}" @@ -490,4 +124,4 @@ if [[ -n "${BUILDKITE_AGENT_ACCESS_TOKEN:-}" ]] ; then fi fi -return "$exitcode" +exit "$exitcode" \ No newline at end of file diff --git a/commands/run_params_generator.sh b/commands/run_params_generator.sh new file mode 100644 index 00000000..fd390613 --- /dev/null +++ b/commands/run_params_generator.sh @@ -0,0 +1,233 @@ +#!/bin/bash +set -uo pipefail + +function generate_run_args() { + local -n params="$1" + service_was_pulled=$2 + + if [[ $service_was_pulled -eq 0 ]] ; then + echo "~~~ :docker: Creating docker-compose override file for prebuilt services" + params+=(-f "$override_file") + up_params+=(-f "$override_file") + fi + + if [[ "$(plugin_read_config RUN_LABELS "true")" =~ ^(true|on|1)$ ]]; then + # Add useful labels to run container + params+=( + "--label" "com.buildkite.pipeline_name=${BUILDKITE_PIPELINE_NAME}" + "--label" "com.buildkite.pipeline_slug=${BUILDKITE_PIPELINE_SLUG}" + "--label" "com.buildkite.build_number=${BUILDKITE_BUILD_NUMBER}" + "--label" "com.buildkite.job_id=${BUILDKITE_JOB_ID}" + "--label" "com.buildkite.job_label=${BUILDKITE_LABEL}" + "--label" "com.buildkite.step_key=${BUILDKITE_STEP_KEY}" + "--label" "com.buildkite.agent_name=${BUILDKITE_AGENT_NAME}" + "--label" "com.buildkite.agent_id=${BUILDKITE_AGENT_ID}" + ) + fi + + # append env vars provided in ENV or ENVIRONMENT, these are newline delimited + while IFS=$'\n' read -r env ; do + [[ -n "${env:-}" ]] && params+=("-e" "${env}") + done <<< "$(printf '%s\n%s' \ + "$(plugin_read_list ENV)" \ + "$(plugin_read_list ENVIRONMENT)")" + + # Propagate all environment variables into the container if requested + if [[ "$(plugin_read_config PROPAGATE_ENVIRONMENT "false")" =~ ^(true|on|1)$ ]] ; then + if [[ -n "${BUILDKITE_ENV_FILE:-}" ]] ; then + # Read in the env file and convert to --env params for docker + # This is because --env-file doesn't support newlines or quotes per https://docs.docker.com/compose/env-file/#syntax-rules + while read -r var; do + params+=("-e" "${var%%=*}") + done < "${BUILDKITE_ENV_FILE}" + else + echo -n "🚨 Not propagating environment variables to container as \$BUILDKITE_ENV_FILE is not set" + fi + fi + + # Propagate AWS credentials if requested + if [[ "$(plugin_read_config PROPAGATE_AWS_AUTH_TOKENS "false")" =~ ^(true|on|1)$ ]] ; then + if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]] ; then + params+=( --env "AWS_ACCESS_KEY_ID" ) + fi + if [[ -n "${AWS_SECRET_ACCESS_KEY:-}" ]] ; then + params+=( --env "AWS_SECRET_ACCESS_KEY" ) + fi + if [[ -n "${AWS_SESSION_TOKEN:-}" ]] ; then + params+=( --env "AWS_SESSION_TOKEN" ) + fi + if [[ -n "${AWS_REGION:-}" ]] ; then + params+=( --env "AWS_REGION" ) + fi + if [[ -n "${AWS_DEFAULT_REGION:-}" ]] ; then + params+=( --env "AWS_DEFAULT_REGION" ) + fi + if [[ -n "${AWS_ROLE_ARN:-}" ]] ; then + params+=( --env "AWS_ROLE_ARN" ) + fi + if [[ -n "${AWS_STS_REGIONAL_ENDPOINTS:-}" ]] ; then + params+=( --env "AWS_STS_REGIONAL_ENDPOINTS" ) + fi + # Pass ECS variables when the agent is running in ECS + # https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html + if [[ -n "${AWS_CONTAINER_CREDENTIALS_FULL_URI:-}" ]] ; then + params+=( --env "AWS_CONTAINER_CREDENTIALS_FULL_URI" ) + fi + if [[ -n "${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI:-}" ]] ; then + params+=( --env "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ) + fi + if [[ -n "${AWS_CONTAINER_AUTHORIZATION_TOKEN:-}" ]] ; then + params+=( --env "AWS_CONTAINER_AUTHORIZATION_TOKEN" ) + fi + # Pass EKS variables when the agent is running in EKS + # https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html + if [[ -n "${AWS_WEB_IDENTITY_TOKEN_FILE:-}" ]] ; then + params+=( --env "AWS_WEB_IDENTITY_TOKEN_FILE" ) + # Add the token file as a volume + params+=( --volume "${AWS_WEB_IDENTITY_TOKEN_FILE}:${AWS_WEB_IDENTITY_TOKEN_FILE}" ) + fi + fi + + # If requested, propagate a set of env vars as listed in a given env var to the + # container. + if [[ -n "$(plugin_read_config ENV_PROPAGATION_LIST)" ]]; then + env_propagation_list_var="$(plugin_read_config ENV_PROPAGATION_LIST)" + if [[ -z "${!env_propagation_list_var:-}" ]]; then + echo -n "env-propagation-list desired, but ${env_propagation_list_var} is not defined!" + exit 1 + fi + for var in ${!env_propagation_list_var}; do + params+=("-e" "$var") + done + fi + + while IFS=$'\n' read -r vol ; do + [[ -n "${vol:-}" ]] && params+=("-v" "$(expand_relative_volume_path "$vol")") + done <<< "$(plugin_read_list VOLUMES)" + + # Parse BUILDKITE_DOCKER_DEFAULT_VOLUMES delimited by semi-colons, normalized to + # ignore spaces and leading or trailing semi-colons + IFS=';' read -r -a default_volumes <<< "${BUILDKITE_DOCKER_DEFAULT_VOLUMES:-}" + for vol in "${default_volumes[@]:-}" ; do + trimmed_vol="$(echo -n "$vol" | sed -e 's/^[[:space:]]*//' | sed -e 's/[[:space:]]*$//')" + [[ -n "$trimmed_vol" ]] && params+=("-v" "$(expand_relative_volume_path "$trimmed_vol")") + done + + # If there's a git mirror, mount it so that git references can be followed. + if [[ -n "${BUILDKITE_REPO_MIRROR:-}" ]]; then + params+=("-v" "$BUILDKITE_REPO_MIRROR:$BUILDKITE_REPO_MIRROR:ro") + fi + + # Disable allocating a TTY + tty_default='true' + if [[ "$(plugin_read_config TTY "$tty_default")" == "false" ]] ; then + params+=(-T) + fi + + workdir='' + workdir_default="/workdir" + pwd_default="$PWD" + + # Set operating system specific defaults + if is_windows ; then + workdir_default="C:\\workdir" + # escaping /C is a necessary workaround for an issue with Git for Windows 2.24.1.2 + # https://github.com/git-for-windows/git/issues/2442 + pwd_default="$(cmd.exe //C "echo %CD%")" + fi + + mount_checkout="$(plugin_read_config MOUNT_CHECKOUT "false")" + if [[ -n "$(plugin_read_config WORKDIR)" ]] || [[ "${mount_checkout}" == "true" ]]; then + workdir="$(plugin_read_config WORKDIR "$workdir_default")" + fi + + if [[ -n "${workdir}" ]] ; then + params+=("--workdir=${workdir}") + fi + + if [[ "${mount_checkout}" == "true" ]]; then + params+=("-v" "${pwd_default}:${workdir}") + elif [[ "${mount_checkout}" =~ ^/.*$ ]]; then + params+=("-v" "${pwd_default}:${mount_checkout}") + elif [[ "${mount_checkout}" != "false" ]]; then + echo -n "🚨 mount-checkout should be either true or an absolute path to use as a mountpoint" + exit 1 + fi + + # Optionally run as specified username or uid + if [[ -n "$(plugin_read_config USER)" ]] ; then + params+=("--user=$(plugin_read_config USER)") + fi + + # Optionally run as specified username or uid + if [[ "$(plugin_read_config PROPAGATE_UID_GID "false")" == "true" ]] ; then + params+=("--user=$(id -u):$(id -g)") + fi + + # Enable alias support for networks + if [[ "$(plugin_read_config USE_ALIASES "false")" == "true" ]] ; then + params+=(--use-aliases) + fi + + # Optionally remove containers after run + if [[ "$(plugin_read_config RM "true")" == "true" ]]; then + params+=(--rm) + fi + + # Optionally sets --entrypoint + if [[ -n "$(plugin_read_config ENTRYPOINT)" ]] ; then + params+=(--entrypoint) + params+=("$(plugin_read_config ENTRYPOINT)") + fi + + # Mount ssh-agent socket and known_hosts + if [[ ! "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_MOUNT_SSH_AGENT:-false}" = 'false' ]] ; then + if [[ -z "${SSH_AUTH_SOCK:-}" ]] ; then + echo "+++ 🚨 \$SSH_AUTH_SOCK isn't set, has ssh-agent started?" + exit 1 + fi + if [[ ! -S "${SSH_AUTH_SOCK}" ]] ; then + echo "+++ 🚨 The file at ${SSH_AUTH_SOCK} does not exist or is not a socket, was ssh-agent started?" + exit 1 + fi + + if [[ "${BUILDKITE_PLUGIN_DOCKER_COMPOSE_MOUNT_SSH_AGENT:-''}" =~ ^(true|on|1)$ ]]; then + MOUNT_PATH=/root + else + MOUNT_PATH="${BUILDKITE_PLUGIN_DOCKER_COMPOSE_MOUNT_SSH_AGENT}" + fi + + params+=( + "-e" "SSH_AUTH_SOCK=/ssh-agent" + "-v" "${SSH_AUTH_SOCK}:/ssh-agent" + "-v" "${HOME}/.ssh/known_hosts:${MOUNT_PATH}/.ssh/known_hosts" + ) + fi + + # Optionally handle the mount-buildkite-agent option + if [[ "$(plugin_read_config MOUNT_BUILDKITE_AGENT "false")" == "true" ]]; then + if [[ -z "${BUILDKITE_AGENT_BINARY_PATH:-}" ]] ; then + if ! command -v buildkite-agent >/dev/null 2>&1 ; then + echo -n "+++ 🚨 Failed to find buildkite-agent in PATH to mount into container, " + echo "you can disable this behaviour with 'mount-buildkite-agent:false'" + else + BUILDKITE_AGENT_BINARY_PATH=$(command -v buildkite-agent) + fi + fi + fi + + # Mount buildkite-agent if we have a path for it + if [[ -n "${BUILDKITE_AGENT_BINARY_PATH:-}" ]] ; then + params+=( + "-e" "BUILDKITE_JOB_ID" + "-e" "BUILDKITE_BUILD_ID" + "-e" "BUILDKITE_AGENT_ACCESS_TOKEN" + "-v" "$BUILDKITE_AGENT_BINARY_PATH:/usr/bin/buildkite-agent" + ) + fi + + # Optionally expose service ports + if [[ "$(plugin_read_config SERVICE_PORTS "false")" == "true" ]]; then + params+=(--service-ports) + fi +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 10c6e0b6..926bc540 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,5 +2,6 @@ version: '2' services: tests: image: buildkite/plugin-tester:v4.1.1 + command: ["bats", "tests/run.bats", "tests/cleanup.bats"] volumes: - ".:/plugin" diff --git a/hooks/command b/hooks/command index 233399b7..10820aca 100755 --- a/hooks/command +++ b/hooks/command @@ -1,12 +1,31 @@ #!/bin/bash -set -ueo pipefail +set -uo pipefail DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)" +expand_headers_on_error() { + echo "trapped an error" + echo "^^^ +++" + exit $1 +} +trap 'expand_headers_on_error "$?"' ERR + +ensure_stopped() { + echo '+++ :warning: Trapped fired, signal received, stopping container gracefully' + # docker stop "${container_name}" || true + compose_cleanup ${run_service} + echo '~~~ Last log lines that may be missing above (if container was not already removed)' + docker logs "${container_name}" || true + exit $1 +} +trap 'ensure_stopped "$?"' SIGINT SIGTERM SIGQUIT + # shellcheck source=lib/shared.bash . "$DIR/../lib/shared.bash" # shellcheck source=lib/metadata.bash . "$DIR/../lib/metadata.bash" +# shellcheck source=lib/run.bash +. "$DIR/../lib/run.bash" commands=() @@ -31,8 +50,6 @@ if in_array "BUILD" "${commands[@]}" ; then . "$DIR/../commands/build.sh" fi if in_array "RUN" "${commands[@]}" ; then - # shellcheck source=lib/run.bash - . "$DIR/../lib/run.bash" # shellcheck source=commands/run.sh . "$DIR/../commands/run.sh" fi diff --git a/hooks/pre-exit b/hooks/pre-exit index 8278e338..37b59f03 100755 --- a/hooks/pre-exit +++ b/hooks/pre-exit @@ -18,5 +18,5 @@ if [[ -n "$(plugin_read_list RUN)" ]] && [[ "$(plugin_read_config CLEANUP "true" . "$DIR/../lib/run.bash" echo "~~~ :docker: Cleaning up after docker-compose" >&2 - compose_cleanup + compose_cleanup "" fi diff --git a/lib/run.bash b/lib/run.bash index f531292a..250f2b5f 100644 --- a/lib/run.bash +++ b/lib/run.bash @@ -1,14 +1,15 @@ #!/bin/bash -compose_cleanup() { - if [[ "$(plugin_read_config GRACEFUL_SHUTDOWN 'false')" == "false" ]]; then - # Send all containers a SIGKILL - run_docker_compose kill || true - else - # Send all containers a friendly SIGTERM, followed by a SIGKILL after exceeding the stop_grace_period - run_docker_compose stop || true +kill_or_wait_for_stop() { + + if [[ "$(plugin_read_config GRACEFUL_SHUTDOWN 'false')" == "true" ]]; then + # This will block until the container exits + run_docker_compose wait "$1" + container_exit_code=$? + echo "exit code was $container_exit_code" fi + # This will kill the container if it hasn't exited yet # `compose down` doesn't support force removing images if [[ "$(plugin_read_config LEAVE_VOLUMES 'false')" == "false" ]]; then run_docker_compose rm --force -v || true @@ -24,6 +25,19 @@ compose_cleanup() { fi } +compose_cleanup() { + kill_or_wait_for_stop "$1" & + sleep 1 + + # No need to call kill directly for GRACEFUL_SHUTDOWN == false since rm --force will send the same kill signal + if [[ "$(plugin_read_config GRACEFUL_SHUTDOWN 'false')" == "true" ]]; then + echo "graceful shutdown was true, stopping ${1}" + # Send all containers a friendly SIGTERM, followed by a SIGKILL after exceeding the stop_grace_period + # run_docker_compose stop "$1" || true + run_docker_compose kill -s SIGTERM "$1" + fi +} + # Checks for failed containers and writes logs for them the the provided dir check_linked_containers_and_save_logs() { local service="$1" diff --git a/lib/shared.bash b/lib/shared.bash index 26d48a98..2e24ac84 100644 --- a/lib/shared.bash +++ b/lib/shared.bash @@ -255,6 +255,8 @@ function run_docker_compose() { command+=(-p "$(docker_compose_project_name)") + echo "running: ${command[@]}" + plugin_prompt_and_run "${command[@]}" "$@" } diff --git a/plugin.yml b/plugin.yml index a0aaba46..771cb13a 100644 --- a/plugin.yml +++ b/plugin.yml @@ -107,6 +107,10 @@ configuration: type: [ boolean, array ] skip-checkout: type: boolean + stop-signal: + type: string + leave-volumes: + type: boolean skip-pull: type: boolean ssh: diff --git a/tests/cleanup.bats b/tests/cleanup.bats old mode 100644 new mode 100755 index 77d55bb1..5a2c601e --- a/tests/cleanup.bats +++ b/tests/cleanup.bats @@ -17,8 +17,7 @@ load '../lib/run' export BUILDKITE_PLUGIN_DOCKER_COMPOSE_CLEANUP=true stub docker \ - "compose -f docker-compose.yml -p buildkite1111 kill : echo killing containers" \ - "compose -f docker-compose.yml -p buildkite1111 rm --force -v : echo removing stopped containers" \ + "compose -f docker-compose.yml -p buildkite1111 rm --force -v : echo killing and removing stopped containers" \ "compose -f docker-compose.yml -p buildkite1111 down --remove-orphans --volumes : echo removing everything" run "$PWD"/hooks/pre-exit diff --git a/tests/docker-compose-cleanup.bats b/tests/docker-compose-cleanup.bats index a8843434..7cd3ada6 100644 --- a/tests/docker-compose-cleanup.bats +++ b/tests/docker-compose-cleanup.bats @@ -15,19 +15,19 @@ setup () { run compose_cleanup assert_success - assert_equal "${lines[0]}" "kill" - assert_equal "${lines[1]}" "rm --force -v" - assert_equal "${lines[2]}" "down --remove-orphans --volumes" + assert_equal "${lines[0]}" "rm --force -v" + assert_equal "${lines[1]}" "down --remove-orphans --volumes" } @test "Possible to gracefully shutdown containers in docker-compose cleanup" { - export BUILDKITE_PLUGIN_DOCKER_COMPOSE_GRACEFUL_SHUTDOWN=1 + export BUILDKITE_PLUGIN_DOCKER_COMPOSE_GRACEFUL_SHUTDOWN="true" run compose_cleanup assert_success - assert_equal "${lines[0]}" "stop" - assert_equal "${lines[1]}" "rm --force -v" - assert_equal "${lines[2]}" "down --remove-orphans --volumes" + assert_output --partial "wait" + assert_equal "${lines[1]}" "exit code was 0" + assert_equal "${lines[2]}" "rm --force -v" + assert_equal "${lines[3]}" "down --remove-orphans --volumes" } @test "Possible to skip volume destruction in docker-compose cleanup" { @@ -35,7 +35,6 @@ setup () { run compose_cleanup assert_success - assert_equal "${lines[0]}" "kill" - assert_equal "${lines[1]}" "rm --force" - assert_equal "${lines[2]}" "down --remove-orphans" + assert_equal "${lines[0]}" "rm --force" + assert_equal "${lines[1]}" "down --remove-orphans" }