Files
unitdore/PLAN.md
sgcommand aa7d85822c 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
2026-04-03 14:39:09 +02:00

5.4 KiB

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

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)

# /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?