Compare commits

...

3 Commits

Author SHA1 Message Date
Bas Nijholt
cdb3b1d257 Show friendly error when config file not found
Instead of a Python traceback, display a clean error message with
the red ✗ symbol when the config file cannot be found.
2025-12-14 00:31:36 -08:00
Bas Nijholt
0913769729 Fix check command to validate all services with check_all flag 2025-12-14 00:23:23 -08:00
Bas Nijholt
3a1d5b77b5 Add traefik port validation to check command 2025-12-14 00:19:17 -08:00
2 changed files with 43 additions and 9 deletions

View File

@@ -33,6 +33,15 @@ console = Console(highlight=False)
err_console = Console(stderr=True, highlight=False)
def _load_config_or_exit(config_path: Path | None) -> Config:
"""Load config or exit with a friendly error message."""
try:
return load_config(config_path)
except FileNotFoundError as e:
err_console.print(f"[red]✗[/] {e}")
raise typer.Exit(1) from e
def _maybe_regenerate_traefik(cfg: Config) -> None:
"""Regenerate traefik config if traefik_file is configured."""
if cfg.traefik_file is None:
@@ -87,7 +96,7 @@ def _get_services(
config_path: Path | None,
) -> tuple[list[str], Config]:
"""Resolve service list and load config."""
config = load_config(config_path)
config = _load_config_or_exit(config_path)
if all_services:
return list(config.services.keys()), config
@@ -262,7 +271,7 @@ def ps(
config: ConfigOption = None,
) -> None:
"""Show status of all services."""
cfg = load_config(config)
cfg = _load_config_or_exit(config)
results = _run_async(run_on_services(cfg, list(cfg.services.keys()), "ps"))
_report_results(results)
@@ -370,7 +379,7 @@ def sync(
file, and captures image digests. Combines service discovery with
image snapshot into a single command.
"""
cfg = load_config(config)
cfg = _load_config_or_exit(config)
current_state = load_state(cfg)
console.print("Discovering running services...")
@@ -416,7 +425,7 @@ def check(
config: ConfigOption = None,
) -> None:
"""Check for compose directories not in config (and vice versa)."""
cfg = load_config(config)
cfg = _load_config_or_exit(config)
configured = set(cfg.services.keys())
on_disk = cfg.discover_compose_dirs()
@@ -440,6 +449,20 @@ def check(
for name in missing_from_config:
console.print(f"[dim] {name}: docker-debian[/]")
# Check traefik labels have matching ports
try:
_, traefik_warnings = generate_traefik_config(
cfg, list(cfg.services.keys()), check_all=True
)
if traefik_warnings:
console.print(f"\n[yellow]Traefik issues[/] ({len(traefik_warnings)}):")
for warning in traefik_warnings:
console.print(f" [yellow]![/] {warning}")
elif not missing_from_config and not missing_from_disk:
console.print("[green]✓[/] All traefik services have published ports.")
except (FileNotFoundError, ValueError):
pass # Skip traefik check if config can't be loaded
if __name__ == "__main__":
app()

View File

@@ -470,10 +470,19 @@ def _process_service_labels(
def generate_traefik_config(
config: Config,
services: list[str],
*,
check_all: bool = False,
) -> tuple[dict[str, Any], list[str]]:
"""Generate Traefik dynamic config from compose labels.
Args:
config: The compose-farm config.
services: List of service names to process.
check_all: If True, check all services for warnings (ignore host filtering).
Used by the check command to validate all traefik labels.
Returns (config_dict, warnings).
"""
dynamic: dict[str, Any] = {}
warnings: list[str] = []
@@ -481,7 +490,7 @@ def generate_traefik_config(
# Determine Traefik's host from service assignment
traefik_host = None
if config.traefik_service:
if config.traefik_service and not check_all:
traefik_host = config.services.get(config.traefik_service)
for stack in services:
@@ -489,10 +498,12 @@ def generate_traefik_config(
stack_host = config.services.get(stack)
# Skip services on Traefik's host - docker provider handles them directly
if host_address.lower() in LOCAL_ADDRESSES:
continue
if traefik_host and stack_host == traefik_host:
continue
# (unless check_all is True, for validation purposes)
if not check_all:
if host_address.lower() in LOCAL_ADDRESSES:
continue
if traefik_host and stack_host == traefik_host:
continue
for compose_service, definition in raw_services.items():
if not isinstance(definition, dict):