mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-12 01:44:30 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf94a62f37 | ||
|
|
81b4074827 | ||
|
|
455657c8df | ||
|
|
ee5a92788a | ||
|
|
2ba396a419 | ||
|
|
7144d58160 | ||
|
|
279fa2e5ef | ||
|
|
dbe0b8b597 |
@@ -11,7 +11,8 @@
|
||||
```
|
||||
compose_farm/
|
||||
├── cli/ # CLI subpackage
|
||||
│ ├── __init__.py # Main Typer app, version callback
|
||||
│ ├── __init__.py # Imports modules to trigger command registration
|
||||
│ ├── app.py # Shared Typer app instance, version callback
|
||||
│ ├── common.py # Shared helpers, options, progress bar utilities
|
||||
│ ├── config.py # Config subcommand (init, show, path, validate, edit)
|
||||
│ ├── lifecycle.py # up, down, pull, restart, update commands
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Bas Nijholt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
README.md
18
README.md
@@ -462,16 +462,16 @@ Update your Traefik config to use directory watching instead of a single file:
|
||||
|
||||
There are many ways to run containers on multiple hosts. Here is where Compose Farm sits:
|
||||
|
||||
| | Docker Contexts | K8s / Swarm | Ansible / Terraform | Portainer / Coolify | Compose Farm |
|
||||
| | Compose Farm | Docker Contexts | K8s / Swarm | Ansible / Terraform | Portainer / Coolify |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|
|
||||
| No compose rewrites | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Version controlled | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
| State tracking | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Auto-migration | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| Interactive CLI | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Parallel execution | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Agentless | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
| High availability | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| No compose rewrites | ✅ | ✅ | ❌ | ✅ | ✅ |
|
||||
| Version controlled | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| State tracking | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Auto-migration | ✅ | ❌ | ✅ | ❌ | ❌ |
|
||||
| Interactive CLI | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Parallel execution | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Agentless | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
| High availability | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||
|
||||
**Docker Contexts** — You can use `docker context create remote ssh://...` and `docker compose --context remote up`. But it's manual: you must remember which host runs which service, there's no global view, no parallel execution, and no auto-migration.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ dynamic = ["version"]
|
||||
description = "Compose Farm - run docker compose commands across multiple hosts"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
license-files = ["LICENSE"]
|
||||
authors = [
|
||||
{ name = "Bas Nijholt", email = "bas@nijho.lt" }
|
||||
]
|
||||
|
||||
@@ -130,6 +130,52 @@ async def _up_multi_host_service(
|
||||
return results
|
||||
|
||||
|
||||
async def _migrate_service(
|
||||
cfg: Config,
|
||||
service: str,
|
||||
current_host: str,
|
||||
target_host: str,
|
||||
prefix: str,
|
||||
*,
|
||||
raw: bool = False,
|
||||
) -> CommandResult | None:
|
||||
"""Migrate a service from current_host to target_host.
|
||||
|
||||
Pre-pulls/builds images on target, then stops service on current host.
|
||||
Returns failure result if migration prep fails, None on success.
|
||||
"""
|
||||
console.print(
|
||||
f"{prefix} Migrating from [magenta]{current_host}[/] → [magenta]{target_host}[/]..."
|
||||
)
|
||||
# Prepare images on target host before stopping old service to minimize downtime.
|
||||
# Pull handles image-based services; build handles Dockerfile-based services.
|
||||
# Each command is a no-op for the other type (exit 0, no work done).
|
||||
pull_result = await run_compose(cfg, service, "pull", raw=raw)
|
||||
if raw:
|
||||
print() # Ensure newline after raw output
|
||||
if not pull_result.success:
|
||||
err_console.print(
|
||||
f"{prefix} [red]✗[/] Pull failed on [magenta]{target_host}[/], "
|
||||
"leaving service on current host"
|
||||
)
|
||||
return pull_result
|
||||
build_result = await run_compose(cfg, service, "build", raw=raw)
|
||||
if raw:
|
||||
print() # Ensure newline after raw output
|
||||
if not build_result.success:
|
||||
err_console.print(
|
||||
f"{prefix} [red]✗[/] Build failed on [magenta]{target_host}[/], "
|
||||
"leaving service on current host"
|
||||
)
|
||||
return build_result
|
||||
down_result = await run_compose_on_host(cfg, service, current_host, "down", raw=raw)
|
||||
if raw:
|
||||
print() # Ensure newline after raw output
|
||||
if not down_result.success:
|
||||
return down_result
|
||||
return None
|
||||
|
||||
|
||||
async def up_services(
|
||||
cfg: Config,
|
||||
services: list[str],
|
||||
@@ -162,15 +208,11 @@ async def up_services(
|
||||
# If service is deployed elsewhere, migrate it
|
||||
if current_host and current_host != target_host:
|
||||
if current_host in cfg.hosts:
|
||||
console.print(
|
||||
f"{prefix} Migrating from "
|
||||
f"[magenta]{current_host}[/] → [magenta]{target_host}[/]..."
|
||||
failure = await _migrate_service(
|
||||
cfg, service, current_host, target_host, prefix, raw=raw
|
||||
)
|
||||
down_result = await run_compose_on_host(cfg, service, current_host, "down", raw=raw)
|
||||
if raw:
|
||||
print() # Ensure newline after raw output
|
||||
if not down_result.success:
|
||||
results.append(down_result)
|
||||
if failure:
|
||||
results.append(failure)
|
||||
continue
|
||||
else:
|
||||
err_console.print(
|
||||
|
||||
Reference in New Issue
Block a user