feat: order/delay in service files, enable cmd, --all on unit commands
- install/uninstall/start/stop/enable take <name> or --all; *all kept as shortcuts - add enable (enable at boot + start); startall no longer enables - order/delay resolved by config.ResolveOrder and written into systemd, OpenRC and runit service files (After=/sleep), so they apply at boot - install --all stops and disables units before removing their files - update regenerates all installed, enabled units and removes disabled ones - edit creates the config dir; docs and man page updated - tests: fake Init for cmd, ResolveOrder, backend ordering output
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -43,6 +45,10 @@ type Unit struct {
|
||||
RestartSec int `yaml:"restart_sec,omitempty"` // seconds between restarts
|
||||
RestartRetries int `yaml:"restart_retries,omitempty"` // max restart attempts (StartLimitBurst)
|
||||
|
||||
// Resolved from order/delay by Config.ResolveOrder; never stored.
|
||||
After []string `yaml:"-"` // enabled same-user units of the previous order group
|
||||
StartDelay time.Duration `yaml:"-"` // wait before start (zero for the first group)
|
||||
|
||||
// Stack fields (kind: stack) — one unit for a whole podman-compose project.
|
||||
Kind string `yaml:"kind,omitempty"` // container (default) | stack
|
||||
Project string `yaml:"project,omitempty"` // compose project name
|
||||
@@ -102,9 +108,60 @@ func Load(path string) (*Config, error) {
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
if err := cfg.ResolveOrder(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ResolveOrder fills After and StartDelay on every unit from order and delay.
|
||||
// After lists the enabled units of the nearest lower order group in the same
|
||||
// scope (user). StartDelay is the unit's own delay, ignored for the first
|
||||
// group. Call again after changing order, delay or enabled.
|
||||
func (c *Config) ResolveOrder() error {
|
||||
delays := make([]time.Duration, len(c.Units))
|
||||
for i := range c.Units {
|
||||
if c.Units[i].Delay == "" {
|
||||
continue
|
||||
}
|
||||
d, err := time.ParseDuration(c.Units[i].Delay)
|
||||
if err != nil || d < 0 {
|
||||
return fmt.Errorf("unit %q: invalid delay %q", c.Units[i].Name, c.Units[i].Delay)
|
||||
}
|
||||
delays[i] = d
|
||||
}
|
||||
|
||||
for i := range c.Units {
|
||||
u := &c.Units[i]
|
||||
u.After, u.StartDelay = nil, 0
|
||||
if !u.Enabled {
|
||||
continue
|
||||
}
|
||||
hasLower, best := false, 0
|
||||
for j := range c.Units {
|
||||
o := c.Units[j]
|
||||
if !o.Enabled || o.Order >= u.Order {
|
||||
continue
|
||||
}
|
||||
hasLower = true
|
||||
if o.User != u.User {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case u.After == nil || o.Order > best:
|
||||
best, u.After = o.Order, []string{o.Name}
|
||||
case o.Order == best:
|
||||
u.After = append(u.After, o.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(u.After)
|
||||
if hasLower {
|
||||
u.StartDelay = delays[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOrEmpty loads the config file, or returns an empty config if it doesn't exist.
|
||||
func LoadOrEmpty(path string) (*Config, error) {
|
||||
_, err := os.Stat(path)
|
||||
|
||||
@@ -3,7 +3,9 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadOrEmpty_NoFile(t *testing.T) {
|
||||
@@ -183,3 +185,70 @@ func TestUserConfigPath(t *testing.T) {
|
||||
t.Errorf("UserConfigPath() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOrder(t *testing.T) {
|
||||
cfg := &Config{Units: []Unit{
|
||||
{Name: "db", Order: 1, Enabled: true, Delay: "9s"},
|
||||
{Name: "cache", Order: 1, Enabled: true},
|
||||
{Name: "off", Order: 2, Enabled: false},
|
||||
{Name: "web", Order: 3, Enabled: true, Delay: "5s"},
|
||||
{Name: "web-user", Order: 3, Enabled: true, User: "hein"},
|
||||
{Name: "user-db", Order: 1, Enabled: true, User: "hein"},
|
||||
}}
|
||||
if err := cfg.ResolveOrder(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
get := func(n string) Unit { return *cfg.FindUnit(n) }
|
||||
|
||||
if u := get("db"); len(u.After) != 0 || u.StartDelay != 0 {
|
||||
t.Errorf("first group must not wait: %+v", u)
|
||||
}
|
||||
if u := get("off"); len(u.After) != 0 || u.StartDelay != 0 {
|
||||
t.Errorf("disabled unit gets no ordering: %+v", u)
|
||||
}
|
||||
// skips the disabled order-2 unit, picks the enabled order-1 same-scope units
|
||||
if u := get("web"); !reflect.DeepEqual(u.After, []string{"cache", "db"}) || u.StartDelay != 5*time.Second {
|
||||
t.Errorf("web: %+v", u)
|
||||
}
|
||||
// user units only order after units of the same user
|
||||
if u := get("web-user"); !reflect.DeepEqual(u.After, []string{"user-db"}) || u.StartDelay != 0 {
|
||||
t.Errorf("web-user: %+v", u)
|
||||
}
|
||||
|
||||
// re-resolving after a change resets stale values
|
||||
cfg.FindUnit("db").Order = 9
|
||||
if err := cfg.ResolveOrder(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u := get("web"); !reflect.DeepEqual(u.After, []string{"cache"}) {
|
||||
t.Errorf("web after reorder: %+v", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOrder_InvalidDelay(t *testing.T) {
|
||||
for _, d := range []string{"abc", "-2s"} {
|
||||
cfg := &Config{Units: []Unit{{Name: "a", Delay: d, Enabled: true}}}
|
||||
if err := cfg.ResolveOrder(); err == nil {
|
||||
t.Errorf("delay %q should be rejected", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_ResolvesOrderAndRejectsBadDelay(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
good := filepath.Join(dir, "good.yaml")
|
||||
os.WriteFile(good, []byte("units:\n - {name: a, order: 1, enabled: true}\n - {name: b, order: 2, delay: 4s, enabled: true}\n"), 0644)
|
||||
cfg, err := Load(good)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b := cfg.Units[1]; !reflect.DeepEqual(b.After, []string{"a"}) || b.StartDelay != 4*time.Second {
|
||||
t.Errorf("b = %+v", b)
|
||||
}
|
||||
|
||||
bad := filepath.Join(dir, "bad.yaml")
|
||||
os.WriteFile(bad, []byte("units:\n - {name: a, delay: nope, enabled: true}\n"), 0644)
|
||||
if _, err := Load(bad); err == nil {
|
||||
t.Error("Load must reject invalid delay")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user