Compare commits

...

6 Commits

Author SHA1 Message Date
Andi Powers-Holmes
d167da9d63 Fix external network name parsing (#152)
* fix: external network name parsing

Compose network definitions may have a "name" field defining the actual network name,
which may differ from the key used in the compose file e.g. when overriding the default
compose network, or using a network name containing special characters that are not valid YAML keys.

Fix: check for "name" field on definition and use that if present, else fall back to key.

* tests: Add test for external network name field parsing

Covers the case where a network definition has a "name" field that
differs from the YAML key (e.g., default key with name: compose-net).

---------

Co-authored-by: Bas Nijholt <bas@nijho.lt>
2026-01-07 02:48:35 -08:00
Bas Nijholt
a5eac339db compose: Quote arguments with shlex to preserve spaces (#151) 2026-01-06 15:37:55 +01:00
Bas Nijholt
9f3813eb72 docs: Add missing source files to architecture docs (#150) 2026-01-06 13:07:20 +01:00
Bas Nijholt
b9ae0ad4d5 docs: Add missing options, aliases, and config settings (#149)
- Add --pull and --build options to cf up (from #146)
- Add --no-strays option to cf apply
- Add command aliases section (a, l, r, u, p, s, c, rf, ck, tf)
- Add cf config init-env subcommand documentation
- Add glances_stack config option (from #124)
- Add Host Resource Monitoring section to architecture docs
2026-01-06 11:06:24 +01:00
Bas Nijholt
ca2a4dd6d9 cli: Add short command aliases (#148)
* cli: Add short command aliases

Add single and two-letter aliases for frequently used commands:

- a  → apply
- l  → logs
- r  → restart
- u  → update
- p  → pull
- s  → stats
- c  → compose
- rf → refresh
- ck → check
- tf → traefik-file

Aliases are hidden from --help to keep output clean.

* docs: Document command aliases in README
2026-01-05 18:46:57 +01:00
Bas Nijholt
fafdce5736 docs: Clarify Docker Compose vs Compose Farm commands (#147)
* docs: Clarify Docker Compose vs Compose Farm commands

Split the Usage section into two tables:
- Docker Compose Commands: wrappers with multi-host additions
- Compose Farm Commands: orchestration Docker Compose can't do

Also update the `update` command docstring to clarify it's
a shorthand for `up --pull --build`.

* chore(docs): update TOC

* docs: Add command type distinction to commands.md

Explain that commands are either Docker Compose wrappers with
multi-host superpowers, or Compose Farm originals for orchestration.
Also update `update` description to clarify it's a shorthand.

* Update README.md
2026-01-05 18:37:41 +01:00
11 changed files with 158 additions and 34 deletions

View File

@@ -59,18 +59,20 @@ Check:
- Config file search order is accurate
- Example YAML would actually work
### 4. Verify docs/architecture.md
### 4. Verify docs/architecture.md and CLAUDE.md
```bash
# What source files actually exist?
git ls-files "src/**/*.py"
```
Check:
Check **both** `docs/architecture.md` and `CLAUDE.md` (Architecture section):
- Listed files exist
- No files are missing from the list
- Descriptions match what the code does
Both files have architecture listings that can drift independently.
### 5. Check Examples
For examples in any doc:

View File

@@ -20,15 +20,17 @@ src/compose_farm/
│ ├── monitoring.py # logs, ps, stats commands
│ ├── ssh.py # SSH key management (setup, status, keygen)
│ └── web.py # Web UI server command
├── config.py # Pydantic models, YAML loading
├── compose.py # Compose file parsing (.env, ports, volumes, networks)
├── config.py # Pydantic models, YAML loading
├── console.py # Shared Rich console instances
├── executor.py # SSH/local command execution, streaming output
├── operations.py # Business logic (up, migrate, discover, preflight checks)
├── state.py # Deployment state tracking (which stack on which host)
├── glances.py # Glances API integration for host resource stats
├── logs.py # Image digest snapshots (dockerfarm-log.toml)
├── operations.py # Business logic (up, migrate, discover, preflight checks)
├── paths.py # Path utilities, config file discovery
├── registry.py # Container registry client for update checking
├── ssh_keys.py # SSH key path constants and utilities
├── state.py # Deployment state tracking (which stack on which host)
├── traefik.py # Traefik file-provider config generation from labels
└── web/ # Web UI (FastAPI + HTMX)
```

View File

@@ -51,6 +51,9 @@ A minimal CLI tool to run Docker Compose commands across multiple hosts via SSH.
- [Multi-Host Stacks](#multi-host-stacks)
- [Config Command](#config-command)
- [Usage](#usage)
- [Docker Compose Commands](#docker-compose-commands)
- [Compose Farm Commands](#compose-farm-commands)
- [Aliases](#aliases)
- [CLI `--help` Output](#cli---help-output)
- [Auto-Migration](#auto-migration)
- [Traefik Multihost Ingress (File Provider)](#traefik-multihost-ingress-file-provider)
@@ -363,24 +366,47 @@ Use `cf config init` to get started with a fully documented template.
The CLI is available as both `compose-farm` and the shorter `cf` alias.
### Docker Compose Commands
These wrap `docker compose` with multi-host superpowers:
| Command | Wraps | Compose Farm Additions |
|---------|-------|------------------------|
| `cf up` | `up -d` | `--all`, `--host`, parallel execution, auto-migration |
| `cf down` | `down` | `--all`, `--host`, `--orphaned`, state tracking |
| `cf stop` | `stop` | `--all`, `--service` |
| `cf restart` | `restart` | `--all`, `--service` |
| `cf pull` | `pull` | `--all`, `--service`, parallel execution |
| `cf logs` | `logs` | `--all`, `--host`, multi-stack output |
| `cf ps` | `ps` | `--all`, `--host`, unified cross-host view |
| `cf compose` | any | passthrough for commands not listed above |
### Compose Farm Commands
Multi-host orchestration that Docker Compose can't do:
| Command | Description |
|---------|-------------|
| **`cf apply`** | **Make reality match config (start + migrate + stop orphans)** |
| `cf up <stack>` | Start stack (auto-migrates if host changed) |
| `cf down <stack>` | Stop and remove stack containers |
| `cf stop <stack>` | Stop stack without removing containers |
| `cf restart <stack>` | Restart running containers |
| `cf update <stack>` | Pull, build, recreate only if changed |
| `cf pull <stack>` | Pull latest images |
| `cf logs -f <stack>` | Follow logs |
| `cf ps` | Show status of all stacks |
| `cf refresh` | Update state from running stacks |
| **`cf apply`** | **Reconcile: start missing, migrate moved, stop orphans** |
| `cf update` | Shorthand for `up --pull --build` |
| `cf refresh` | Sync state from what's actually running |
| `cf check` | Validate config, mounts, networks |
| `cf init-network` | Create Docker network on hosts |
| `cf init-network` | Create Docker network on all hosts |
| `cf traefik-file` | Generate Traefik file-provider config |
| `cf config <cmd>` | Manage config files (init, show, path, validate, edit, symlink) |
| `cf config` | Manage config files (init, show, validate, edit, symlink) |
| `cf ssh` | Manage SSH keys (setup, status, keygen) |
All commands support `--all` to operate on all stacks.
### Aliases
Short aliases for frequently used commands:
| Alias | Command | Alias | Command |
|-------|---------|-------|---------|
| `cf a` | `apply` | `cf s` | `stats` |
| `cf l` | `logs` | `cf c` | `compose` |
| `cf r` | `restart` | `cf rf` | `refresh` |
| `cf u` | `update` | `cf ck` | `check` |
| `cf p` | `pull` | `cf tf` | `traefik-file` |
Each command replaces: look up host → SSH → find compose file → run `ssh host "cd /opt/compose/plex && docker compose up -d"`.
@@ -474,7 +500,8 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
│ stop). │
│ pull Pull latest images (docker compose pull). │
│ restart Restart running containers (docker compose restart). │
│ update Update stacks. Only recreates containers if images changed.
│ update Update stacks (pull + build + up). Shorthand for 'up --pull
│ --build'. │
│ apply Make reality match config (start, migrate, stop │
│ strays/orphans as needed). │
│ compose Run any docker compose command on a stack. │
@@ -694,7 +721,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
Usage: cf update [OPTIONS] [STACKS]...
Update stacks. Only recreates containers if images changed.
Update stacks (pull + build + up). Shorthand for 'up --pull --build'.
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ stacks [STACKS]... Stacks to operate on │

View File

@@ -96,7 +96,7 @@ Typer-based CLI with subcommand modules:
cli/
├── app.py # Shared Typer app, version callback
├── common.py # Shared helpers, options, progress utilities
├── config.py # config subcommand (init, show, path, validate, edit, symlink)
├── config.py # config subcommand (init, init-env, show, path, validate, edit, symlink)
├── lifecycle.py # up, down, stop, pull, restart, update, apply, compose
├── management.py # refresh, check, init-network, traefik-file
├── monitoring.py # logs, ps, stats
@@ -343,3 +343,19 @@ For repeated connections to the same host, SSH reuses connections.
```
Icons use [Lucide](https://lucide.dev/). Add new icons as macros in `web/templates/partials/icons.html`.
### Host Resource Monitoring (`src/compose_farm/glances.py`)
Integration with [Glances](https://nicolargo.github.io/glances/) for real-time host stats:
- Fetches CPU, memory, and load from Glances REST API on each host
- Used by web UI dashboard to display host resource usage
- Requires `glances_stack` config option pointing to a Glances stack running on all hosts
### Container Registry Client (`src/compose_farm/registry.py`)
OCI Distribution API client for checking image updates:
- Parses image references (registry, namespace, name, tag, digest)
- Fetches available tags from Docker Hub, GHCR, and other registries
- Compares semantic versions to find newer releases

View File

@@ -8,6 +8,8 @@ The Compose Farm CLI is available as both `compose-farm` and the shorter alias `
## Command Overview
Commands are either **Docker Compose wrappers** (`up`, `down`, `stop`, `restart`, `pull`, `logs`, `ps`, `compose`) with multi-host superpowers, or **Compose Farm originals** (`apply`, `update`, `refresh`, `check`) for orchestration Docker Compose can't do.
| Category | Command | Description |
|----------|---------|-------------|
| **Lifecycle** | `apply` | Make reality match config |
@@ -15,7 +17,7 @@ The Compose Farm CLI is available as both `compose-farm` and the shorter alias `
| | `down` | Stop stacks |
| | `stop` | Stop services without removing containers |
| | `restart` | Restart running containers |
| | `update` | Update stacks (only recreates if images changed) |
| | `update` | Shorthand for `up --pull --build` |
| | `pull` | Pull latest images |
| | `compose` | Run any docker compose command |
| **Monitoring** | `ps` | Show stack status |
@@ -36,6 +38,18 @@ cf --version, -v # Show version
cf --help, -h # Show help
```
## Command Aliases
Short aliases for frequently used commands:
| Alias | Command | Alias | Command |
|-------|---------|-------|---------|
| `cf a` | `apply` | `cf s` | `stats` |
| `cf l` | `logs` | `cf c` | `compose` |
| `cf r` | `restart` | `cf rf` | `refresh` |
| `cf u` | `update` | `cf ck` | `check` |
| `cf p` | `pull` | `cf tf` | `traefik-file` |
---
## Lifecycle Commands
@@ -58,14 +72,16 @@ cf apply [OPTIONS]
|--------|-------------|
| `--dry-run, -n` | Preview changes without executing |
| `--no-orphans` | Skip stopping orphaned stacks |
| `--full, -f` | Also refresh running stacks |
| `--no-strays` | Skip stopping stray stacks (running on wrong host) |
| `--full, -f` | Also run up on all stacks (applies compose/env changes, triggers migrations) |
| `--config, -c PATH` | Path to config file |
**What it does:**
1. Stops orphaned stacks (in state but removed from config)
2. Migrates stacks on wrong host
3. Starts missing stacks (in config but not running)
2. Stops stray stacks (running on unauthorized hosts)
3. Migrates stacks on wrong host
4. Starts missing stacks (in config but not running)
**Examples:**
@@ -79,7 +95,10 @@ cf apply
# Only start/migrate, don't stop orphans
cf apply --no-orphans
# Also refresh all running stacks
# Don't stop stray stacks
cf apply --no-strays
# Also run up on all stacks (applies compose/env changes, triggers migrations)
cf apply --full
```
@@ -100,6 +119,8 @@ cf up [OPTIONS] [STACKS]...
| `--all, -a` | Start all stacks |
| `--host, -H TEXT` | Filter to stacks on this host |
| `--service, -s TEXT` | Target a specific service within the stack |
| `--pull` | Pull images before starting (`--pull always`) |
| `--build` | Build images before starting |
| `--config, -c PATH` | Path to config file |
**Examples:**
@@ -225,7 +246,7 @@ cf restart immich --service database
### cf update
Update stacks. Only recreates containers if images changed. With `--service`, updates just that service.
Update stacks (pull + build + up). Shorthand for `up --pull --build`. With `--service`, updates just that service.
<video autoplay loop muted playsinline>
<source src="/assets/update.webm" type="video/webm">
@@ -587,6 +608,7 @@ cf config COMMAND
| Command | Description |
|---------|-------------|
| `init` | Create new config with examples |
| `init-env` | Generate .env file for Docker deployment |
| `show` | Display config with highlighting |
| `path` | Print config file path |
| `validate` | Validate syntax and schema |
@@ -598,6 +620,7 @@ cf config COMMAND
| Subcommand | Options |
|------------|---------|
| `init` | `--path/-p PATH`, `--force/-f` |
| `init-env` | `--path/-p PATH`, `--output/-o PATH`, `--force/-f` |
| `show` | `--path/-p PATH`, `--raw/-r` |
| `edit` | `--path/-p PATH` |
| `path` | `--path/-p PATH` |
@@ -633,6 +656,12 @@ cf config symlink
# Create symlink to specific file
cf config symlink /opt/compose-farm/config.yaml
# Generate .env file for Docker deployment
cf config init-env
# Generate .env in current directory
cf config init-env -o .env
```
---

View File

@@ -121,6 +121,16 @@ Stack name running Traefik. Stacks on the same host are skipped in file-provider
traefik_stack: traefik
```
### glances_stack
Stack name running [Glances](https://nicolargo.github.io/glances/) for host resource monitoring. When set, the web UI displays CPU, memory, and load stats for all hosts.
```yaml
glances_stack: glances
```
The Glances stack should run on all hosts and expose port 61208. See the README for full setup instructions.
## Hosts Configuration
### Basic Host

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import shlex
from pathlib import Path
from typing import TYPE_CHECKING, Annotated
@@ -195,8 +196,7 @@ def update(
service: ServiceOption = None,
config: ConfigOption = None,
) -> None:
"""Update stacks. Only recreates containers if images changed."""
# update is just up --pull --build
"""Update stacks (pull + build + up). Shorthand for 'up --pull --build'."""
up(stacks=stacks, all_stacks=all_stacks, service=service, pull=True, build=True, config=config)
@@ -394,10 +394,10 @@ def compose(
else:
target_host = hosts[0]
# Build the full compose command
# Build the full compose command (quote args to preserve spaces)
full_cmd = command
if args:
full_cmd += " " + " ".join(args)
full_cmd += " " + " ".join(shlex.quote(arg) for arg in args)
# Run with raw=True for proper TTY handling (progress bars, interactive)
result = run_async(run_compose_on_host(cfg, resolved_stack, target_host, full_cmd, raw=True))
@@ -407,5 +407,9 @@ def compose(
raise typer.Exit(result.exit_code)
# Alias: cf a = cf apply
app.command("a", hidden=True)(apply)
# Aliases (hidden from help, shown in --help as "Aliases: ...")
app.command("a", hidden=True)(apply) # cf a = cf apply
app.command("r", hidden=True)(restart) # cf r = cf restart
app.command("u", hidden=True)(update) # cf u = cf update
app.command("p", hidden=True)(pull) # cf p = cf pull
app.command("c", hidden=True)(compose) # cf c = cf compose

View File

@@ -659,3 +659,9 @@ def init_network(
failed = [r for r in results if not r.success]
if failed:
raise typer.Exit(1)
# Aliases (hidden from help)
app.command("rf", hidden=True)(refresh) # cf rf = cf refresh
app.command("ck", hidden=True)(check) # cf ck = cf check
app.command("tf", hidden=True)(traefik_file) # cf tf = cf traefik-file

View File

@@ -201,3 +201,8 @@ def stats(
console.print()
console.print(_build_summary_table(cfg, state, pending))
# Aliases (hidden from help)
app.command("l", hidden=True)(logs) # cf l = cf logs
app.command("s", hidden=True)(stats) # cf s = cf stats

View File

@@ -280,8 +280,11 @@ def parse_external_networks(config: Config, stack: str) -> list[str]:
return []
external_networks: list[str] = []
for name, definition in networks.items():
for key, definition in networks.items():
if isinstance(definition, dict) and definition.get("external") is True:
# Networks may have a "name" field, which may differ from the key.
# Use it if present, else fall back to the key.
name = str(definition.get("name", key))
external_networks.append(name)
return external_networks

View File

@@ -338,6 +338,26 @@ def test_parse_external_networks_missing_compose(tmp_path: Path) -> None:
assert networks == []
def test_parse_external_networks_with_name_field(tmp_path: Path) -> None:
"""Network with 'name' field uses actual name, not key."""
cfg = Config(
compose_dir=tmp_path,
hosts={"host1": Host(address="192.168.1.10")},
stacks={"app": "host1"},
)
compose_path = tmp_path / "app" / "compose.yaml"
_write_compose(
compose_path,
{
"services": {"app": {"image": "nginx"}},
"networks": {"default": {"name": "compose-net", "external": True}},
},
)
networks = parse_external_networks(cfg, "app")
assert networks == ["compose-net"]
class TestExtractWebsiteUrls:
"""Test extract_website_urls function."""