#!/usr/bin/env bash
# scripts/port-conflict-detect.sh
# Parse every stack's docker-compose.yml, collect "HOST:CONTAINER" port
# mappings, and warn if two services try to bind the same host port.
# Runs before `docker compose up` in my deploy flow.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
. "${HERE}/lib/log.sh"
REPO="${REPO:-/srv/homelab}"
declare -A owner
emit() {
local stack="$1" port="$2" svc="$3"
if [[ -n "${owner[${port}]:-}" && "${owner[${port}]}" != "${stack}/${svc}" ]]; then
log_err "CONFLICT port ${port}: ${owner[${port}]} vs ${stack}/${svc}"
return 1
fi
owner[${port}]="${stack}/${svc}"
return 0
}
rc=0
while IFS= read -r compose; do
stack=$(basename "$(dirname "${compose}")")
# Pull "- HOST:CONTAINER" lines out of the ports: blocks.
while IFS= read -r line; do
case "${line}" in
*ports:*) in_ports=1; continue ;;
*) ;;
esac
done < "${compose}"
python3 - "${compose}" "${stack}" <<'PY' || rc=1
import sys, yaml
path, stack = sys.argv[1], sys.argv[2]
with open(path) as f:
data = yaml.safe_load(f) or {}
for svc, spec in (data.get('services') or {}).items():
for p in spec.get('ports', []) or []:
s = str(p)
# strip interface prefix (e.g. "127.0.0.1:8080:8080")
parts = s.split(':')
if len(parts) >= 2:
host = parts[-2]
print(f"{stack}\t{svc}\t{host}")
PY
done < <(find "${REPO}/stacks" -maxdepth 3 -name 'docker-compose.yml') \
| while IFS=$'\t' read -r stack svc port; do
emit "${stack}" "${port}" "${svc}" || rc=1
done
exit "${rc}"