diff --git a/.gitignore b/.gitignore index bcd9c5a..ff9b7c6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,5 @@ ENV/ htmlcov/ # Local config (don't commit real configs) -sdc.yaml -!examples/sdc.yaml +compose-farm.yaml +!examples/compose-farm.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 60d7fa8..e7ae290 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# SDC Development Guidelines +# Compose Farm Development Guidelines ## Core Principles @@ -9,7 +9,7 @@ ## Architecture ``` -sdc/ +compose_farm/ ├── config.py # Pydantic models, YAML loading ├── ssh.py # asyncssh execution, streaming └── cli.py # Typer commands @@ -22,6 +22,7 @@ sdc/ 3. **Streaming output**: Real-time stdout/stderr with `[service]` prefix 4. **SSH key auth only**: Uses ssh-agent, no password handling (YAGNI) 5. **NFS assumption**: Compose files at same path on all hosts +6. **Local execution**: When host is `localhost`/`local`, skip SSH and run locally ## Development Notes diff --git a/README.md b/README.md index ba8d9d5..cc95268 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,27 @@ -# SDC - Simple Distributed Compose +# Compose Farm A minimal CLI tool to run Docker Compose commands across multiple hosts via SSH. -## Why SDC? +## Why Compose Farm? I run 100+ Docker Compose stacks on an LXC container that frequently runs out of memory. I needed a way to distribute services across multiple machines without the complexity of: - **Kubernetes**: Overkill for my use case. I don't need pods, services, ingress controllers, or YAML manifests 10x the size of my compose files. -- **Docker Swarm**: Effectively in maintenance modeno longer being invested in by Docker. +- **Docker Swarm**: Effectively in maintenance mode—no longer being invested in by Docker. -**SDC is intentionally simple**: one YAML config mapping services to hosts, and a CLI that runs `docker compose` commands over SSH. That's it. +**Compose Farm is intentionally simple**: one YAML config mapping services to hosts, and a CLI that runs `docker compose` commands over SSH. That's it. ## Installation ```bash -pip install sdc +pip install compose-farm # or -uv pip install sdc +uv pip install compose-farm ``` ## Configuration -Create `~/.config/sdc/sdc.yaml` (or `./sdc.yaml` in your working directory): +Create `~/.config/compose-farm/compose-farm.yaml` (or `./compose-farm.yaml` in your working directory): ```yaml compose_dir: /opt/compose @@ -47,27 +47,27 @@ Compose files are expected at `{compose_dir}/{service}/docker-compose.yml`. ```bash # Start services -sdc up plex jellyfin -sdc up --all +compose-farm up plex jellyfin +compose-farm up --all # Stop services -sdc down plex +compose-farm down plex # Pull latest images -sdc pull --all +compose-farm pull --all # Restart (down + up) -sdc restart plex +compose-farm restart plex # Update (pull + down + up) - the end-to-end update command -sdc update --all +compose-farm update --all # View logs -sdc logs plex -sdc logs -f plex # follow +compose-farm logs plex +compose-farm logs -f plex # follow # Show status -sdc ps +compose-farm ps ``` ## Requirements diff --git a/sdc.example.yaml b/compose-farm.example.yaml similarity index 71% rename from sdc.example.yaml rename to compose-farm.example.yaml index eb175b2..be29daf 100644 --- a/sdc.example.yaml +++ b/compose-farm.example.yaml @@ -1,5 +1,5 @@ -# Example SDC configuration -# Copy to ~/.config/sdc/sdc.yaml or ./sdc.yaml +# Example Compose Farm configuration +# Copy to ~/.config/compose-farm/compose-farm.yaml or ./compose-farm.yaml compose_dir: /opt/compose @@ -13,6 +13,9 @@ hosts: # Short form (just address, user defaults to current user) nas02: 192.168.1.11 + # Local execution (no SSH) + local: localhost + services: # Map service names to hosts # Compose file expected at: {compose_dir}/{service}/docker-compose.yml diff --git a/examples/README.md b/examples/README.md index 468d480..9a7460e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ -# SDC Examples +# Compose Farm Examples -This folder contains example Docker Compose services for testing SDC locally. +This folder contains example Docker Compose services for testing Compose Farm locally. ## Quick Start @@ -8,28 +8,28 @@ This folder contains example Docker Compose services for testing SDC locally. cd examples # Check status of all services -sdc ps +compose-farm ps # Pull images -sdc pull --all +compose-farm pull --all # Start hello-world (runs and exits) -sdc up hello +compose-farm up hello # Start nginx (stays running) -sdc up nginx +compose-farm up nginx # Check nginx is running curl localhost:8080 # View logs -sdc logs nginx +compose-farm logs nginx # Stop nginx -sdc down nginx +compose-farm down nginx # Update all (pull + restart) -sdc update --all +compose-farm update --all ``` ## Services @@ -39,4 +39,4 @@ sdc update --all ## Config -The `sdc.yaml` in this directory configures both services to run locally (no SSH). +The `compose-farm.yaml` in this directory configures both services to run locally (no SSH). diff --git a/examples/compose-farm.yaml b/examples/compose-farm.yaml new file mode 100644 index 0000000..056d35d --- /dev/null +++ b/examples/compose-farm.yaml @@ -0,0 +1,11 @@ +# Example Compose Farm config for local testing +# Run from the examples directory: cd examples && compose-farm ps + +compose_dir: . + +hosts: + local: localhost + +services: + hello: local + nginx: local diff --git a/examples/sdc.yaml b/examples/sdc.yaml deleted file mode 100644 index 67600cc..0000000 --- a/examples/sdc.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Example SDC config for local testing -# Run from the examples directory: cd examples && sdc ps - -compose_dir: . - -hosts: - local: localhost - -services: - hello: local - nginx: local diff --git a/pyproject.toml b/pyproject.toml index db1a63b..a02afde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "sdc" +name = "compose-farm" version = "0.1.0" -description = "Simple Distributed Compose - run docker compose commands across hosts" +description = "Compose Farm - run docker compose commands across multiple hosts" readme = "README.md" authors = [ { name = "Bas Nijholt", email = "bas@nijho.lt" } @@ -15,14 +15,14 @@ dependencies = [ ] [project.scripts] -sdc = "sdc.cli:app" +compose-farm = "compose_farm.cli:app" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/sdc"] +packages = ["src/compose_farm"] [tool.ruff] target-version = "py311" diff --git a/src/compose_farm/__init__.py b/src/compose_farm/__init__.py new file mode 100644 index 0000000..96a8928 --- /dev/null +++ b/src/compose_farm/__init__.py @@ -0,0 +1,3 @@ +"""Compose Farm - run docker compose commands across multiple hosts.""" + +__version__ = "0.1.0" diff --git a/src/sdc/cli.py b/src/compose_farm/cli.py similarity index 97% rename from src/sdc/cli.py rename to src/compose_farm/cli.py index 959b8e9..ebe6086 100644 --- a/src/sdc/cli.py +++ b/src/compose_farm/cli.py @@ -21,8 +21,8 @@ if TYPE_CHECKING: T = TypeVar("T") app = typer.Typer( - name="sdc", - help="Simple Distributed Compose - run docker compose commands across hosts", + name="compose-farm", + help="Compose Farm - run docker compose commands across multiple hosts", no_args_is_help=True, ) diff --git a/src/sdc/config.py b/src/compose_farm/config.py similarity index 93% rename from src/sdc/config.py rename to src/compose_farm/config.py index 02a6a3e..8760185 100644 --- a/src/sdc/config.py +++ b/src/compose_farm/config.py @@ -63,12 +63,12 @@ def load_config(path: Path | None = None) -> Config: Search order: 1. Explicit path if provided - 2. ./sdc.yaml - 3. ~/.config/sdc/sdc.yaml + 2. ./compose-farm.yaml + 3. ~/.config/compose-farm/compose-farm.yaml """ search_paths = [ - Path("sdc.yaml"), - Path.home() / ".config" / "sdc" / "sdc.yaml", + Path("compose-farm.yaml"), + Path.home() / ".config" / "compose-farm" / "compose-farm.yaml", ] if path: diff --git a/src/sdc/py.typed b/src/compose_farm/py.typed similarity index 100% rename from src/sdc/py.typed rename to src/compose_farm/py.typed diff --git a/src/sdc/ssh.py b/src/compose_farm/ssh.py similarity index 100% rename from src/sdc/ssh.py rename to src/compose_farm/ssh.py diff --git a/src/sdc/__init__.py b/src/sdc/__init__.py deleted file mode 100644 index e67bf31..0000000 --- a/src/sdc/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Simple Distributed Compose - run docker compose commands across hosts.""" - -__version__ = "0.1.0" diff --git a/tests/test_config.py b/tests/test_config.py index 803c120..7b3f94a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest import yaml -from sdc.config import Config, Host, load_config +from compose_farm.config import Config, Host, load_config class TestHost: diff --git a/tests/test_ssh.py b/tests/test_ssh.py index 8836751..564bdf3 100644 --- a/tests/test_ssh.py +++ b/tests/test_ssh.py @@ -4,8 +4,8 @@ from pathlib import Path import pytest -from sdc.config import Config, Host -from sdc.ssh import ( +from compose_farm.config import Config, Host +from compose_farm.ssh import ( CommandResult, _is_local, _run_local_command, diff --git a/uv.lock b/uv.lock index 5792c6e..83f53fb 100644 --- a/uv.lock +++ b/uv.lock @@ -124,6 +124,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "compose-farm" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "asyncssh" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, + { name = "types-pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "asyncssh", specifier = ">=2.14.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "typer", specifier = ">=0.9.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.19.0" }, + { name = "pre-commit", specifier = ">=4.5.0" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.14.8" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, +] + [[package]] name = "cryptography" version = "46.0.3" @@ -668,45 +707,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, ] -[[package]] -name = "sdc" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "asyncssh" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "typer" }, -] - -[package.dev-dependencies] -dev = [ - { name = "mypy" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "ruff" }, - { name = "types-pyyaml" }, -] - -[package.metadata] -requires-dist = [ - { name = "asyncssh", specifier = ">=2.14.0" }, - { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "typer", specifier = ">=0.9.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.19.0" }, - { name = "pre-commit", specifier = ">=4.5.0" }, - { name = "pytest", specifier = ">=9.0.2" }, - { name = "pytest-asyncio", specifier = ">=1.3.0" }, - { name = "ruff", specifier = ">=0.14.8" }, - { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, -] - [[package]] name = "shellingham" version = "1.5.4"