scripts/check-dev-config.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Check the MERGED Compose configuration (not the files one by one): published ports, builds,
# images, mounts, commands, users and project name, for development and for an installation.
#
# Usage: scripts/check-dev-config.sh
#
# Development: docker-compose.yml + docker-compose.dev.yml with .env.dev.example.
# Installation: docker-compose.yml alone and with docker-compose.prod.yml, with .env.example:
# the published images, never a build. With docker-compose.build.yml: the same images built
# locally, to test them before a release.
# Overrides add ports and volumes to the main file, so a missing "!override" would silently
# publish more than intended: this is what the script catches.
set -euo pipefail
cd "$(dirname "$0")/.."
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# A library path that exists, so that the check does not depend on this machine.
mkdir -p "$TMP/library"
config() { # <env-file> <compose files...>
local env_file=$1
shift
local args=()
for file in "$@"; do args+=(-f "$file"); done
env -u COMPOSE_FILE -u COMPOSE_PROJECT_NAME VHS_HOST_LIBRARY="$TMP/library" \
docker compose --env-file "$env_file" "${args[@]}" config --format json
}
config .env.dev.example docker-compose.yml docker-compose.dev.yml >"$TMP/dev.json"
config .env.example docker-compose.yml >"$TMP/base.json"
config .env.example docker-compose.yml docker-compose.prod.yml >"$TMP/prod.json"
config .env.example docker-compose.yml docker-compose.prod.yml docker-compose.build.yml >"$TMP/build.json"
docker run --rm -i -e REPO="$PWD" -v "$TMP:/check:ro" python:3.13-slim python - <<'PY'
import json, os, sys
REPO = os.environ["REPO"]
failures = []
def load(name):
with open(f"/check/{name}.json") as handle:
return json.load(handle)
def ports(service):
return sorted(
f"{p.get('host_ip', '0.0.0.0') or '0.0.0.0'}:{p['published']}->{p['target']}"
for p in service.get("ports", [])
)
def mounts(service):
return sorted(
(m["type"], m.get("source", "").replace("/check/", "").rstrip("/"), m["target"], bool(m.get("read_only")))
for m in service.get("volumes", [])
)
def expect(label, actual, wanted):
ok = actual == wanted
print(f" {'ok ' if ok else 'ERR'} {label}: {actual}")
if not ok:
print(f" expected: {wanted}")
failures.append(label)
def library_source(config):
for service in config["services"].values():
for m in service.get("volumes", []):
if m["target"] == "/srv/video-library":
return m["source"]
print("== Development (docker-compose.yml + docker-compose.dev.yml, .env.dev.example)")
dev = load("dev")
lib = library_source(dev)
s = dev["services"]
expect("project", dev["name"], "vhs-dev")
expect("services", sorted(s), ["backend", "db", "frontend", "migrate", "nginx", "worker"])
published = {name: ports(svc) for name, svc in s.items() if ports(svc)}
expect("published ports", published, {"nginx": ["127.0.0.1:8081->80"]})
for name in ("migrate", "backend", "worker"):
svc = s[name]
expect(f"{name}: build target", svc.get("build", {}).get("target"), "dev")
expect(f"{name}: image", svc.get("image"), "vhs-backend-dev")
expect(f"{name}: user", svc.get("user"), "1000:1000")
targets = {m[2]: m for m in mounts(svc)}
expect(f"{name}: repository mounted", targets.get("/app", (None, None))[:2], ("bind", REPO))
expect(f"{name}: library writable", targets.get("/srv/video-library", (0, 0, 0, None))[3], False)
expect("backend: command", s["backend"].get("command"), ["python", "manage.py", "runserver", "0.0.0.0:8000"])
expect("worker: command", s["worker"].get("command")[:1], ["watchfiles"])
expect("nginx: build", "build" in s["nginx"], False)
expect("nginx: image", s["nginx"].get("image"), "nginx:alpine")
expect("nginx: mounts", [m[2:] for m in mounts(s["nginx"])], [("/etc/nginx/conf.d/default.conf", True), ("/srv/video-library", True)])
expect("frontend: ports", ports(s["frontend"]), [])
REGISTRY = "ghcr.io/rocchidavide/"
for label, name in (("Installation (docker-compose.yml, .env.example)", "base"),
("Installation (docker-compose.yml + docker-compose.prod.yml, .env.example)", "prod"),
("Installation built locally (+ docker-compose.build.yml)", "build")):
print(f"== {label}")
cfg = load(name)
s = cfg["services"]
built = name == "build"
expect("project", cfg["name"], "vhs")
expect("services", sorted(s), ["backend", "db", "migrate", "nginx", "worker"])
published = {n: ports(svc) for n, svc in s.items() if ports(svc)}
wanted = {"nginx": ["0.0.0.0:1976->80"]} if name == "base" else {"nginx": ["0.0.0.0:1976->80", "0.0.0.0:1977->443"]}
expect("published ports", published, wanted)
for n in ("migrate", "backend", "worker"):
svc = s[n]
expect(f"{n}: build target", svc.get("build", {}).get("target"), "prod" if built else None)
image = svc.get("image", "")
expect(f"{n}: image", image if built else image.rsplit(":", 1)[0], "vhs-backend" if built else REGISTRY + "vhs-backend")
expect(f"{n}: repository mounted", any(m[2] == "/app" for m in mounts(svc)), False)
expect("nginx: build", "build" in s["nginx"], built)
image = s["nginx"].get("image", "")
expect("nginx: image", image if built else image.rsplit(":", 1)[0], "vhs-nginx" if built else REGISTRY + "vhs-nginx")
if name != "base":
certs = [m for m in mounts(s["nginx"]) if m[2] == "/etc/nginx/certs"]
expect("nginx: certs folder read-only", [(m[1].rsplit("/", 1)[-1], m[3]) for m in certs], [("certs", True)])
expect("nginx: VHS_HTTPS", s["nginx"].get("environment", {}).get("VHS_HTTPS"), "false")
lib_mounts = {n: [m[3] for m in mounts(svc) if m[2] == "/srv/video-library"] for n, svc in s.items()}
expect("library read-only per service", lib_mounts, {"backend": [False], "db": [], "migrate": [False], "nginx": [True], "worker": [False]})
print()
if failures:
print(f"CONFIGURATION NOT AS EXPECTED: {len(failures)} checks failed.")
sys.exit(1)
print("Merged configuration as expected.")
PY