90 lines
3.6 KiB
Bash
Executable File
90 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Docker Compose cannot reuse a fixed `container_name` that is already owned by
|
|
# another compose project. Local development often leaves those containers behind
|
|
# after moving or renaming the repo, so remove only compose-owned stale conflicts
|
|
# before `docker compose up` reaches Docker's generic name-conflict failure.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
cd "${PROJECT_ROOT}"
|
|
|
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
printf 'python3 is required to inspect docker compose config before startup\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
compose_config="$(docker compose config --format json)"
|
|
compose_project="$(printf '%s' "${compose_config}" | python3 -c 'import json, sys; print(json.load(sys.stdin).get("name", ""))')"
|
|
if [[ -z "${compose_project}" ]]; then
|
|
printf 'docker compose project name is empty; cannot resolve container conflicts safely\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
spec_file="$(mktemp)"
|
|
trap 'rm -f "${spec_file}"' EXIT
|
|
|
|
printf '%s' "${compose_config}" | python3 -c 'import json, sys
|
|
config = json.load(sys.stdin)
|
|
for service_name, service in sorted(config.get("services", {}).items()):
|
|
container_name = service.get("container_name")
|
|
if container_name:
|
|
print(f"{service_name}\t{container_name}")
|
|
' > "${spec_file}"
|
|
|
|
for requested_service in "$@"; do
|
|
if ! awk -F '\t' -v service="${requested_service}" '$1 == service { found = 1 } END { exit !found }' "${spec_file}"; then
|
|
printf 'unknown compose service for conflict resolution: %s\n' "${requested_service}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
docker_label() {
|
|
local container_name="$1"
|
|
local label_name="$2"
|
|
|
|
docker inspect --format "{{ with index .Config.Labels \"${label_name}\" }}{{ . }}{{ end }}" "${container_name}" 2>/dev/null || true
|
|
}
|
|
|
|
requested_services=" $* "
|
|
|
|
while IFS=$'\t' read -r service_name container_name; do
|
|
if [[ -z "${service_name}" || -z "${container_name}" ]]; then
|
|
continue
|
|
fi
|
|
|
|
if [[ "$#" -gt 0 && "${requested_services}" != *" ${service_name} "* ]]; then
|
|
continue
|
|
fi
|
|
|
|
container_id="$(docker inspect --format '{{.Id}}' "${container_name}" 2>/dev/null || true)"
|
|
if [[ -z "${container_id}" ]]; then
|
|
continue
|
|
fi
|
|
|
|
label_project="$(docker_label "${container_name}" "com.docker.compose.project")"
|
|
label_service="$(docker_label "${container_name}" "com.docker.compose.service")"
|
|
label_workdir="$(docker_label "${container_name}" "com.docker.compose.project.working_dir")"
|
|
label_config_files="$(docker_label "${container_name}" "com.docker.compose.project.config_files")"
|
|
|
|
# A container created by this exact compose project can be reused by compose;
|
|
# deleting it would discard useful local process state without fixing anything.
|
|
if [[ "${label_project}" == "${compose_project}" ]] && \
|
|
{ [[ "${label_workdir}" == "${PROJECT_ROOT}" ]] || [[ "${label_config_files}" == *"${PROJECT_ROOT}/"* ]]; }; then
|
|
continue
|
|
fi
|
|
|
|
# Manual or unlabeled containers have unknown ownership. Fail with a precise
|
|
# command instead of guessing that this repo is allowed to remove them.
|
|
if [[ -z "${label_project}" || -z "${label_service}" ]]; then
|
|
printf 'container name conflict: %s (%s) is not managed by Docker Compose\n' "${container_name}" "${container_id:0:12}" >&2
|
|
printf 'remove or rename it before startup, for example: docker rm -f %s\n' "${container_name}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'removing stale compose container: %s (%s, project=%s, service=%s, working_dir=%s)\n' \
|
|
"${container_name}" "${container_id:0:12}" "${label_project}" "${label_service}" "${label_workdir:-unknown}"
|
|
docker rm -f "${container_name}" >/dev/null
|
|
done < "${spec_file}"
|