moved compose commands to script for updating containers and compose files to new services folder structure
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# Home Services
|
||||
|
||||
This repository manages home-lab application stacks using compose files and a small Python CLI that works with either Docker or Podman.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```text
|
||||
services/
|
||||
media/
|
||||
compose.yaml
|
||||
composectl.py
|
||||
update_containers.sh
|
||||
```
|
||||
|
||||
- `services/` contains one directory per deployed stack.
|
||||
- Each stack directory contains its own `compose.yaml` file.
|
||||
- `composectl.py` infers the stack name, compose file path, and project name from that directory structure.
|
||||
- `update_containers.sh` is a compatibility shim that delegates to the Python tool.
|
||||
|
||||
## Runtime support
|
||||
|
||||
The CLI detects and uses whichever runtime is available:
|
||||
|
||||
- `docker compose ...`
|
||||
- `podman compose ...`
|
||||
|
||||
This means the same service directories can be managed from either backend without changing the compose files.
|
||||
|
||||
## Common commands
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
python3 composectl.py --help
|
||||
python3 composectl.py --dry-run --all refresh
|
||||
python3 composectl.py --stack media --dry-run up
|
||||
python3 composectl.py --stack media pull
|
||||
python3 composectl.py --stack media down
|
||||
python3 composectl.py --stack media status
|
||||
```
|
||||
|
||||
The shell entrypoint delegates to the Python tool:
|
||||
|
||||
```bash
|
||||
bash update_containers.sh --dry-run --all refresh
|
||||
```
|
||||
|
||||
## Refresh flow
|
||||
|
||||
The repo-wide refresh flow does the following for each configured stack:
|
||||
|
||||
1. `pull`
|
||||
2. `down`
|
||||
3. `up -d`
|
||||
4. `prune`
|
||||
|
||||
This preserves the same maintenance behavior as the old shell updater, but moves the logic into a portable Python implementation.
|
||||
Binary file not shown.
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Runtime-agnostic compose deployment helper for home services.
|
||||
|
||||
This lightweight CLI keeps compose files as the source of truth and selects
|
||||
Docker or Podman as the backend at runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def detect_runtime(preferred: str | None = None) -> str:
|
||||
"""Return a supported runtime name.
|
||||
|
||||
The preferred runtime is honored when available; otherwise the first
|
||||
installed runtime from the standard Docker/Podman lookup order is used.
|
||||
"""
|
||||
if preferred and shutil.which(preferred):
|
||||
return preferred
|
||||
|
||||
for runtime in ("docker", "podman"):
|
||||
if shutil.which(runtime):
|
||||
return runtime
|
||||
|
||||
raise RuntimeError("Neither Docker nor Podman is installed or available on PATH")
|
||||
|
||||
|
||||
def build_compose_command(
|
||||
runtime: str,
|
||||
compose_file: Path,
|
||||
project_name: str,
|
||||
action: str,
|
||||
detached: bool = True,
|
||||
) -> list[str]:
|
||||
"""Build a compose command for the chosen runtime."""
|
||||
base = [runtime, "compose", "-f", str(compose_file), "-p", project_name]
|
||||
|
||||
if action == "up":
|
||||
return [*base, "up", "-d"]
|
||||
if action == "pull":
|
||||
return [*base, "pull"]
|
||||
if action == "down":
|
||||
return [*base, "down"]
|
||||
if action == "status":
|
||||
return [*base, "ps"]
|
||||
|
||||
raise ValueError(f"Unsupported action '{action}'")
|
||||
|
||||
|
||||
def discover_compose_files(root: Path) -> list[Path]:
|
||||
"""Return compose files under the services directory."""
|
||||
services_dir = root / "services"
|
||||
if not services_dir.exists():
|
||||
return []
|
||||
|
||||
return sorted(path for path in services_dir.rglob("compose.yaml") if path.is_file())
|
||||
|
||||
|
||||
def discover_stack_names(root: Path) -> list[str]:
|
||||
"""Infer stack names from the services directory layout."""
|
||||
services_dir = root / "services"
|
||||
if not services_dir.exists():
|
||||
return []
|
||||
|
||||
return sorted(path.name for path in services_dir.iterdir() if path.is_dir())
|
||||
|
||||
|
||||
def build_update_commands(runtime: str, compose_file: Path, project_name: str) -> list[list[str]]:
|
||||
"""Return the maintenance sequence used for a full stack refresh."""
|
||||
return [
|
||||
build_compose_command(runtime=runtime, compose_file=compose_file, project_name=project_name, action="pull"),
|
||||
build_compose_command(runtime=runtime, compose_file=compose_file, project_name=project_name, action="down"),
|
||||
build_compose_command(runtime=runtime, compose_file=compose_file, project_name=project_name, action="up"),
|
||||
[runtime, "image", "prune", "-f"],
|
||||
]
|
||||
|
||||
|
||||
def run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, check=False, text=True, capture_output=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Manage compose stacks with Docker or Podman")
|
||||
parser.add_argument("action", choices=["pull", "up", "down", "prune", "status", "refresh"])
|
||||
parser.add_argument("--stack", default=None)
|
||||
parser.add_argument("--compose-file", type=Path, default=None)
|
||||
parser.add_argument("--project-name", default=None)
|
||||
parser.add_argument("--runtime", choices=["docker", "podman"], default=None)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--all", action="store_true", help="Run the requested action for every configured stack")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
runtime = detect_runtime(args.runtime)
|
||||
|
||||
if args.action == "prune":
|
||||
command = [runtime, "image", "prune", "-f"]
|
||||
if args.dry_run:
|
||||
print(" ".join(command))
|
||||
return 0
|
||||
|
||||
result = run_command(command)
|
||||
if result.stdout:
|
||||
print(result.stdout, end="")
|
||||
if result.stderr:
|
||||
print(result.stderr, end="", file=sys.stderr)
|
||||
return result.returncode
|
||||
|
||||
stack_names = discover_stack_names(Path(".")) if args.all else [args.stack or "media"]
|
||||
failure_count = 0
|
||||
|
||||
for stack_name in stack_names:
|
||||
compose_file = args.compose_file or Path(f"services/{stack_name}/compose.yaml")
|
||||
project_name = args.project_name or f"{stack_name}_services"
|
||||
|
||||
if args.action == "refresh":
|
||||
commands = build_update_commands(runtime=runtime, compose_file=compose_file, project_name=project_name)
|
||||
if args.dry_run:
|
||||
for command in commands:
|
||||
print(" ".join(command))
|
||||
continue
|
||||
|
||||
for command in commands:
|
||||
result = run_command(command)
|
||||
if result.stdout:
|
||||
print(result.stdout, end="")
|
||||
if result.stderr:
|
||||
print(result.stderr, end="", file=sys.stderr)
|
||||
if result.returncode != 0:
|
||||
failure_count += 1
|
||||
continue
|
||||
|
||||
command = build_compose_command(
|
||||
runtime=runtime,
|
||||
compose_file=compose_file,
|
||||
project_name=project_name,
|
||||
action=args.action,
|
||||
)
|
||||
|
||||
if args.dry_run:
|
||||
print(" ".join(command))
|
||||
continue
|
||||
|
||||
result = run_command(command)
|
||||
if result.stdout:
|
||||
print(result.stdout, end="")
|
||||
if result.stderr:
|
||||
print(result.stderr, end="", file=sys.stderr)
|
||||
if result.returncode != 0:
|
||||
failure_count += 1
|
||||
|
||||
return 1 if failure_count else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Binary file not shown.
@@ -0,0 +1,58 @@
|
||||
import shutil
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from composectl import (
|
||||
build_compose_command,
|
||||
build_update_commands,
|
||||
detect_runtime,
|
||||
discover_compose_files,
|
||||
discover_stack_names,
|
||||
)
|
||||
|
||||
|
||||
class ComposeCtlTests(unittest.TestCase):
|
||||
def test_detect_runtime_returns_installed_backend(self):
|
||||
runtime = detect_runtime(preferred="docker")
|
||||
expected = "docker" if shutil.which("docker") else "podman"
|
||||
self.assertEqual(runtime, expected)
|
||||
|
||||
def test_build_compose_command_uses_selected_runtime(self):
|
||||
cmd = build_compose_command(
|
||||
runtime="podman",
|
||||
compose_file=Path("media/compose.yaml"),
|
||||
project_name="media_services",
|
||||
action="up",
|
||||
)
|
||||
self.assertEqual(
|
||||
cmd,
|
||||
["podman", "compose", "-f", "media/compose.yaml", "-p", "media_services", "up", "-d"],
|
||||
)
|
||||
|
||||
def test_discover_compose_files_finds_existing_stacks(self):
|
||||
stack_files = discover_compose_files(Path("."))
|
||||
self.assertIn(Path("services/media/compose.yaml"), stack_files)
|
||||
|
||||
def test_discover_stack_names_returns_inferred_stack_names(self):
|
||||
stack_names = discover_stack_names(Path("."))
|
||||
self.assertIn("media", stack_names)
|
||||
|
||||
def test_build_update_commands_returns_full_stack_refresh_sequence(self):
|
||||
commands = build_update_commands(
|
||||
runtime="podman",
|
||||
compose_file=Path("services/media/compose.yaml"),
|
||||
project_name="media_services",
|
||||
)
|
||||
self.assertEqual(
|
||||
commands,
|
||||
[
|
||||
["podman", "compose", "-f", "services/media/compose.yaml", "-p", "media_services", "pull"],
|
||||
["podman", "compose", "-f", "services/media/compose.yaml", "-p", "media_services", "down"],
|
||||
["podman", "compose", "-f", "services/media/compose.yaml", "-p", "media_services", "up", "-d"],
|
||||
["podman", "image", "prune", "-f"],
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+9
-13
@@ -1,15 +1,11 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
sudo -i
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
cd /volume1/docker/git_services/compose
|
||||
docker compose -p git_services pull
|
||||
docker compose -p git_services down
|
||||
docker compose -p git_services up -d --remove-orphans
|
||||
docker image prune -f
|
||||
|
||||
cd /volume1/docker/media_services/compose
|
||||
docker compose -p media_services pull
|
||||
docker compose -p media_services down
|
||||
docker compose -p media_services up -d --remove-orphans
|
||||
docker image prune -f
|
||||
if [ "$#" -gt 0 ]; then
|
||||
python3 composectl.py "$@"
|
||||
else
|
||||
python3 composectl.py refresh --all
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user