Files
unitdore/AGENTS.md
T
warkanum df9b226c7e feat: OpenRC and runit init backends behind an Init interface
Extract init-neutral unit resolution into service/, add initsys.Init with systemd, openrc and runit backends, selected by 'init:' config or autodetect. OpenRC and runit are root only; rootless units run as system services as that user. Add docs and generated examples for all three.
2026-10-12 20:19:58 +02:00

5.3 KiB

Project Overview

Unitdore is a Go CLI tool that bridges container runtimes (Podman, Docker) and the init system (systemd, OpenRC, runit). It discovers running containers, stores them in a YAML config, and generates + manages service files for each one.

Agent Rules

Keep answers short. Question everything, never assume or guess. Ask if unsure. You must use the AMCS MCP system tools. Set unitdore as the active project and always capture summaries of what was done as thoughts using the capture_thought tool. If AMCS is not available, write files to docs/llm/log/yyyymmdd_hh.md.

Tools to use

When writing Go: doc/tools/go-skill.md (if present), otherwise follow standard Go conventions.

Commands

make build          # Compile binary: ./unitdore
make install        # Install binary to /usr/bin, man page, create /etc/unitdore
make uninstall      # Remove binary and man page
make test           # go test ./... -v
make test-short     # go test ./...
make lint           # go vet ./...
make clean          # Remove built binary
make release-version  # Bump patch version, commit, tag, and push

Architecture

cmd/           # Cobra commands (edit, install, list, root, start, startall,
               #   status, stop, stopall, syncup, uninstall, update)
config/        # Config struct + YAML load/save (units.yaml)
runtime/       # Docker and Podman runtime abstraction, compose labels
sysuser/       # run commands as root/user (runuser), home/linger lookup
testdata/stack # podman-compose test stack + captured `podman ps` fixture
service/       # init-neutral unit resolution: commands, quoting, user env
initsys/       # Init interface, backend selection (config init: / autodetect)
systemd/       # .service generator + systemctl manager (Backend)
openrc/        # /etc/init.d script generator + rc-service manager (Backend)
runit/         # /etc/sv run/finish generator + sv manager (Backend)
docs/          # generated-units.md (unit file examples), unitdore.1 (man page)

Key packages

  • config — Unit and Config structs; Load, Save, FindUnit, AddUnit
  • runtime — ContainerRuntime interface; docker.go, podman.go implementations
  • service — Resolve(unit) → start/stop commands, type, workdir; shared by all backends
  • initsys — Init interface; New(cfg) picks backend; cmd/ only talks to Init
  • systemd — generator.go builds .service file content; manager.go calls systemctl
  • openrc, runit — same role; root only; tests stub command execution via package vars

Config file

Location: /etc/unitdore/units.yaml

init: ""        # systemd | openrc | runit (empty = autodetect)
service_dir: ""  # runit: supervised dir (empty = autodetect)
prefix: ""      # prepended to service file name (e.g. "prod-")
suffix: ""      # appended to service file name (e.g. "-svc")
users: []       # root only: rootless users scanned by syncup

units:
  - name: nginx
    runtime: podman        # podman | docker | script | exec
    user: ""               # empty = system unit; set for rootless user unit
    command: ""            # override ExecStart entirely
    order: 1               # startup order, lower = earlier; same = parallel
    delay: 0s              # delay after previous order group
    enabled: true
    disabled_reason: ""    # auto-set by syncup reconciliation
    restart: false         # Restart=on-failure
    restart_sec: 0         # RestartSec
    restart_retries: 0     # StartLimitBurst
    kind: container        # container | stack (podman-compose project)
    project: ""            # stack: compose project
    working_dir: ""        # stack: compose dir (from labels)
    compose_files: []      # stack: relative to working_dir
    compose_bin: ""        # stack: podman-compose path
    script: ""             # script: file run via shell (ExecStart)
    stop_script: ""        # script: optional ExecStop
    shell: ""              # script: interpreter, default /bin/bash
    executable: ""         # exec: binary run as ExecStart
    args: []               # exec: arguments
    stop_executable: ""    # exec: optional ExecStop
    service_type: ""       # script/exec: simple (default) | oneshot

Non-root runs use ~/.config/unitdore/units.yaml and manage only own user units.

Generated service file locations

Scenario Path
System unit (root) /etc/systemd/system/unitdore-<name>.service
User unit /home/<user>/.config/systemd/user/unitdore-<name>.service
With prefix prod- /etc/systemd/system/unitdore-prod-<name>.service
With suffix -svc /etc/systemd/system/unitdore-<name>-svc.service

Libraries

  • github.com/spf13/cobra — CLI framework
  • gopkg.in/yaml.v3 — config parsing

Testing

Tests exist in:

  • config/config_test.go
  • runtime/runtime_test.go
  • cmd/syncup_test.go
  • sysuser/sysuser_test.go
  • service/spec_test.go
  • initsys/initsys_test.go
  • openrc/openrc_test.go
  • runit/runit_test.go
  • systemd/generator_test.go

Always run make test before marking a task complete. Add tests for new behaviour in the same package as the code under test.

Additional documentation

  • docs/generated-units.md — full annotated examples of every generated unit file combination
  • docs/unitdore.1 — man page