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

34
cmd/edit.go Normal file
View File

@@ -0,0 +1,34 @@
package cmd
import (
"fmt"
"os"
"os/exec"
"github.com/spf13/cobra"
)
var editCmd = &cobra.Command{
Use: "edit",
Short: "Open the units config in $EDITOR",
RunE: runEdit,
}
func init() {
rootCmd.AddCommand(editCmd)
}
func runEdit(cmd *cobra.Command, args []string) error {
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
c := exec.Command(editor, configPath)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
return fmt.Errorf("editor exited with error: %w", err)
}
return nil
}

31
cmd/root.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var configPath string
var rootCmd = &cobra.Command{
Use: "unitdore",
Short: "A door you open and close for container units",
Long: `Unitdore manages container units via systemd.
It discovers running containers, stores them in a config file,
and generates + manages systemd .service units for each one.`,
Version: "0.1.0",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&configPath, "config", "/etc/unitdore/units.yaml", "path to units config file")
}

91
cmd/syncup.go Normal file
View File

@@ -0,0 +1,91 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/runtime"
)
var syncupCmd = &cobra.Command{
Use: "syncup",
Short: "Discover running containers and sync them into the config",
Long: `Syncup queries all available runtimes for running containers and adds
any new ones to the config file. Existing entries are never removed.
It also reconciles: if a configured unit's container no longer exists,
it is marked disabled with a reason.`,
RunE: runSyncup,
}
func init() {
rootCmd.AddCommand(syncupCmd)
}
func runSyncup(cmd *cobra.Command, args []string) error {
cfg, err := config.LoadOrEmpty(configPath)
if err != nil {
return err
}
added := 0
discovered := map[string]bool{}
// Discover running containers across all runtimes
for _, rt := range runtime.Available() {
containers, err := rt.ListRunning()
if err != nil {
fmt.Fprintf(os.Stderr, "warning: %s discovery failed: %v\n", rt.Name(), err)
continue
}
for _, c := range containers {
discovered[c.Name] = true
unit := config.Unit{
Name: c.Name,
Runtime: c.Runtime,
Order: len(cfg.Units) + 1,
Enabled: true,
}
if cfg.AddUnit(unit) {
fmt.Printf(" + added: %s (%s)\n", c.Name, c.Runtime)
added++
}
}
}
// Reconcile: check if existing units' containers still exist
disabled := 0
reenabled := 0
for i := range cfg.Units {
u := &cfg.Units[i]
rt := runtime.Get(u.Runtime)
if rt == nil {
continue
}
exists, err := rt.Exists(u.Name)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: checking %s: %v\n", u.Name, err)
continue
}
if !exists && u.Enabled {
u.Enabled = false
u.DisabledReason = "container not found"
fmt.Printf(" ! disabled: %s (container not found)\n", u.Name)
disabled++
} else if exists && !u.Enabled && u.DisabledReason == "container not found" {
u.Enabled = true
u.DisabledReason = ""
fmt.Printf(" ✓ re-enabled: %s (container found)\n", u.Name)
reenabled++
}
}
if err := cfg.Save(configPath); err != nil {
return err
}
fmt.Printf("\nDone. Added: %d Disabled: %d Re-enabled: %d\n", added, disabled, reenabled)
return nil
}