Initial commit: unitdore scaffold + syncup/edit commands

- config: load/save/add unit, Unit struct
- runtime: podman + docker discovery and exists check
- cmd: syncup (discover + reconcile), edit, cobra root
- PLAN.md: full project plan
This commit is contained in:
2026-04-03 14:39:09 +02:00
commit aa7d85822c
12 changed files with 633 additions and 0 deletions

182
PLAN.md Normal file
View File

@@ -0,0 +1,182 @@
# 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 / Compose / Quadlet
- 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 |
---
## 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?