Compare commits

...

4 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
8 changed files with 94 additions and 13 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

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

@@ -38,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
@@ -60,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:**
@@ -81,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
```
@@ -102,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:**
@@ -589,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 |
@@ -600,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` |
@@ -635,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
@@ -393,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))

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