hyapp-server/scripts/resolve-compose-container-conflicts.sh
2026-05-02 13:02:38 +08:00

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}"