From 6eabed63ebb29883c248c338478ebe198fb25634 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 17:38:17 +0200 Subject: [PATCH] Reload the service manager's units before restarting a service whose files changed, and start the guard before the network as the controller declares it (hq ADR 0100) --- internal/apply/apply.go | 16 ++++++++++++++++ internal/apply/apply_test.go | 12 ++++++++++++ internal/bootstrap/adopted.go | 2 +- internal/system/arch.go | 8 ++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/internal/apply/apply.go b/internal/apply/apply.go index d8a4926..6aa2b14 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -688,11 +688,27 @@ func reflected(r *declaration.Service, changed map[string]bool) []string { return restartedBy(r.RestartOn, changed) } +// unitReloader is a service manager that caches unit files and must be told to read them again. +type unitReloader interface { + ReloadUnits(ctx context.Context, run system.Runner) error +} + func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner, changed map[string]bool) (Outcome, error) { out := begin(r) var changes []string + // A file the service reflects changed, and it may be the unit's own file or a drop-in: the + // service manager reads those again only when told to, and a restart without it runs the unit + // it had already loaded. + if reflects(r, changed) { + if u, ok := sys.(unitReloader); ok { + if err := u.ReloadUnits(ctx, run); err != nil { + return out, fmt.Errorf("reloading the service manager's units for %s: %w", r.Unit, err) + } + } + } + // Boot first. A unit asked to be running and enabled should survive this apply failing // half way in the more useful direction: enabled-and-stopped comes back at the next boot, // where running-and-disabled does not. diff --git a/internal/apply/apply_test.go b/internal/apply/apply_test.go index 52a179c..ba5c5aa 100644 --- a/internal/apply/apply_test.go +++ b/internal/apply/apply_test.go @@ -1114,6 +1114,18 @@ func TestAServiceIsRestartedWhenWhatItReflectsChanges(t *testing.T) { if !stopped || !started { t.Errorf("the file changed and the service was not restarted; commands were %v", commands) } + reloaded, stop := -1, -1 + for i, c := range commands { + if strings.Contains(c, "daemon-reload") && reloaded < 0 { + reloaded = i + } + if strings.Contains(c, "stop thing.service") && stop < 0 { + stop = i + } + } + if reloaded < 0 || reloaded > stop { + t.Errorf("the service was restarted without the unit files being read again first; commands were %v", commands) + } } } diff --git a/internal/bootstrap/adopted.go b/internal/bootstrap/adopted.go index dbbe181..0e3dedd 100644 --- a/internal/bootstrap/adopted.go +++ b/internal/bootstrap/adopted.go @@ -65,7 +65,7 @@ func AsGuard(ports []int) string { func guardUnitText() string { return "[Unit]\n" + "Description=The mesh's guard: refuses its own ports from outside (novox/hq ADR 0100)\n" + - "After=network-pre.target\n" + + "Before=network-pre.target\n" + "Wants=network-pre.target\n" + "\n" + "[Service]\n" + diff --git a/internal/system/arch.go b/internal/system/arch.go index 178570d..abee02b 100644 --- a/internal/system/arch.go +++ b/internal/system/arch.go @@ -246,3 +246,11 @@ func (arch) AddUserToGroup(ctx context.Context, run Runner, name, group string) } return nil } + +// ReloadUnits has systemd read its unit files again. A unit file that changed on disk is otherwise +// ignored: a restart runs the unit systemd already loaded, and the new text only takes effect +// after a reload nobody asked for. +func (arch) ReloadUnits(ctx context.Context, run Runner) error { + _, err := run(ctx, "systemctl", "daemon-reload") + return err +}