Rename project from sdc to compose-farm

- Package: sdc → compose_farm
- CLI command: sdc → compose-farm
- Config file: sdc.yaml → compose-farm.yaml
- Config path: ~/.config/sdc/ → ~/.config/compose-farm/
- Updated all documentation, tests, and examples
This commit is contained in:
Bas Nijholt
2025-12-11 09:54:03 -08:00
parent 6feb2bbad9
commit 68aab82ef9
17 changed files with 102 additions and 98 deletions

4
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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 modeno 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

View File

@@ -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

View File

@@ -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).

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1,3 @@
"""Compose Farm - run docker compose commands across multiple hosts."""
__version__ = "0.1.0"

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -1,3 +0,0 @@
"""Simple Distributed Compose - run docker compose commands across hosts."""
__version__ = "0.1.0"

View File

@@ -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:

View File

@@ -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,

78
uv.lock generated
View File

@@ -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"