Files
unitdore/PLAN.md
T
warkanum f0827fcdd7 feat: podman-compose stack units and root/user execution scopes
- syncup: podman first, docker only as fallback (podman wins on name clash;
  existing docker units found in podman switch to podman)
- syncup: group podman-compose containers into one `kind: stack` unit per
  project (project, working_dir, compose_files, compose_bin from labels);
  disable existing member units ("part of stack <project>")
- generator: compose mode (podman-compose up/stop, WorkingDirectory) with
  pod start/stop fallback when compose binary/dir/files are missing
- reconcile: stacks disabled with "stack not found" when neither compose
  files nor pod exist
- root: scan root + rootless users listed in `users:` (podman only)
- non-root: config ~/.config/unitdore/units.yaml, own user units only
- sysuser package: runuser with XDG_RUNTIME_DIR/DBUS env, home/linger lookup
- install: warn when linger is not enabled for user units
- testdata/stack: podman-compose test stack and captured podman ps fixture
- docs: README, generated-units, man page, AGENTS, PLAN
2026-10-12 19:05:51 +02:00

186 lines
5.6 KiB
Markdown

# Unitdore — Project Plan
> *A door you open and close for container units.*
> *Unitdore bridges your container runtime and systemd.*
---
## What It Is
A Go CLI tool that bridges your container runtime (Podman, Docker, or custom) and systemd.
It discovers running containers, stores them in a config file, and generates + manages systemd
`.service` units for each one.
Think of it as: **config → systemd units → running services.**
---
## Goals
- Unified config for containers across runtimes and users
- Auto-generate systemd service files (no manual unit writing)
- Reconcile: disable units whose containers no longer exist
- Simple CLI — no daemon, no background process, no magic
---
## Non-Goals
- Not a container runtime (doesn't replace Podman/Docker)
- Not Kubernetes / Quadlet
- Compose: only wraps existing podman-compose stacks (one unit per project); docker compose planned
- Not a daemon — runs on demand
---
## Config File
**Location:** `/etc/unitdore/units.yaml`
**Owned by:** root
**Modified by:** `unitdore edit` (opens in $EDITOR) or automatically by `syncup`
```yaml
units:
- name: nginx
runtime: podman # podman | docker | exec
user: hein # empty = root/system unit; set = user unit for this user
command: "" # optional: override the ExecStart command entirely
order: 1 # startup order (lower = earlier)
delay: 0s # delay after previous order group finishes
enabled: true # false = unit exists but is not installed/started
disabled_reason: "" # auto-set by reconcile: "container not found", etc.
```
---
## Commands
### `unitdore syncup`
Discovers all currently running containers across configured runtimes and adds
any new ones to the config. Does **not** remove existing entries.
- Runs `podman ps --format json` and/or `docker ps --format json`
- Compares against existing config entries by name
- Appends new units with `enabled: true`
- Writes updated config back to disk
- Also reconciles: checks if containers for existing units still exist.
If not → sets `enabled: false` + `disabled_reason`
### `unitdore edit`
Opens `/etc/unitdore/units.yaml` in `$EDITOR` (fallback: `vi`).
### `unitdore install`
Generates and installs systemd `.service` files for all **enabled** units.
- For units with `user: ""` → writes to `/etc/systemd/system/unitdore-<name>.service`
then runs `systemctl daemon-reload`
- For units with `user: hein` → writes to `/home/hein/.config/systemd/user/unitdore-<name>.service`
then runs `systemctl --user daemon-reload` as that user
- Does **not** enable or start — just installs the unit files
- Removes unit files for disabled units and reloads
### `unitdore active`
Enables and starts all installed, enabled units.
- Runs `systemctl enable --now unitdore-<name>.service` for system units
- Runs `systemctl --user enable --now unitdore-<name>.service` for user units
- Reports success/failure per unit
### `unitdore status`
Prints a summary table of all managed units.
```
NAME RUNTIME USER ENABLED SYSTEMD STATUS REASON
nginx podman — yes active (running)
myapp docker hein yes active (running)
oldthing podman hein no inactive container not found
```
---
## Generated Service File (example)
```ini
# /etc/systemd/system/unitdore-nginx.service
# Generated by unitdore — do not edit manually
[Unit]
Description=Unitdore: nginx (podman)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/podman start -a nginx
ExecStop=/usr/bin/podman stop nginx
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
For user units, `User=` and `Group=` are set and it targets `default.target`.
---
## Project Structure
```
unitdore/
├── main.go
├── go.mod
├── cmd/
│ ├── root.go # cobra root, version, config path flag
│ ├── syncup.go # discover + reconcile
│ ├── edit.go # open $EDITOR
│ ├── install.go # generate + write unit files
│ ├── active.go # enable + start units
│ └── status.go # status table
├── config/
│ └── config.go # load, save, Unit struct, YAML marshal/unmarshal
├── runtime/
│ ├── runtime.go # interface: ListContainers() []Container
│ ├── podman.go # podman ps --format json
│ └── docker.go # docker ps --format json
└── systemd/
├── generator.go # build .service file content from a Unit
└── manager.go # install, reload, enable, start, status via systemctl
```
---
## Dependencies
- `github.com/spf13/cobra` — CLI framework
- `gopkg.in/yaml.v3` — config file
- Standard library for everything else (exec, os, text/template)
---
## Phased Build
| Phase | Deliverable |
|---|---|
| 1 | Scaffold: project, go.mod, cobra root, config load/save |
| 2 | `syncup` — discover containers, update config |
| 3 | `install` — generate + write service files |
| 4 | `active` + `status` |
| 5 | Reconcile (auto-disable missing units) in syncup |
| 6 | User unit support (rootless containers) |
| 7 | Polish: `edit`, error handling, --dry-run flag |
| 8 | podman-compose stacks (compose + pod fallback), root/user scopes |
| 9 | docker compose stacks (`com.docker.compose.*` labels) |
---
## Open Questions
- [ ] Should `syncup` also accept a `--runtime` flag to limit discovery to one runtime?
- [ ] Should `install` imply a `syncup --reconcile` first, or be kept separate?
- [ ] Packaging: just a binary, or also a Makefile / install script?