hostfiles: importing wake state for a host with no route writes nothing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+25
−1
internal/hostfiles/wake.go+10 −1internal/hostfiles/wake_test.go+15 −0
diff --git a/internal/hostfiles/wake.go b/internal/hostfiles/wake.go| index 5421a3e22..fae6854da 100644 |
| --- a/internal/hostfiles/wake.go |
| +++ b/internal/hostfiles/wake.go |
| @@ -69,7 +69,9 @@ func ExportWake(reg *registry.Registry, host string) (WakeState, error) { |
| } |
| // ImportWake makes host's wake state exactly ws. It validates everything |
| -// before it writes anything. |
| +// and checks that host has a route before it writes anything. It is |
| +// retry-safe (a repeat is the same full replace) but not atomic across its |
| +// three writes. |
| func ImportWake(reg *registry.Registry, host string, ws WakeState) error { |
| if ws.V != 1 { |
| return fmt.Errorf("hostfiles: wake state version %d; this binary reads 1", ws.V) |
| @@ -80,6 +82,13 @@ func ImportWake(reg *registry.Registry, host string, ws WakeState) error { |
| if !registry.ValidReplication(ws.Replication) { |
| return fmt.Errorf("hostfiles: wake state for %s names replication class %q", host, ws.Replication) |
| } |
| + rt, err := reg.Get(host) |
| + if err != nil { |
| + return err |
| + } |
| + if rt == nil { |
| + return fmt.Errorf("hostfiles: wake state for %s: no route", host) |
| + } |
| rows := make([]registry.Timer, 0, len(ws.Timers)) |
| for _, t := range ws.Timers { |
| rows = append(rows, registry.Timer{Host: host, Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path, |
diff --git a/internal/hostfiles/wake_test.go b/internal/hostfiles/wake_test.go| index 36b01d234..435d1dc1b 100644 |
| --- a/internal/hostfiles/wake_test.go |
| +++ b/internal/hostfiles/wake_test.go |
| @@ -174,3 +174,18 @@ func TestImportWakeOfTheDefaultClassReplacesAnotherClass(t *testing.T) { |
| t.Fatalf("Replication = %q, want the default", rt.Replication) |
| } |
| } |
| + |
| +func TestImportWakeForAHostWithNoRouteWritesNothing(t *testing.T) { |
| + r := wakeReg(t) |
| + if err := r.ReplaceTimers("ghost.test", []registry.Timer{{Host: "ghost.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + ws := WakeState{V: 1, Host: "ghost.test", Timers: []WakeTimer{{Name: "new", Kind: "schedule", Spec: "@every 2m", Path: "/n", NextAt: 2, CreatedAt: 2}}} |
| + if err := ImportWake(r, "ghost.test", ws); err == nil { |
| + t.Fatal("imported wake state for a host with no route") |
| + } |
| + rows, _ := r.TimersFor("ghost.test") |
| + if len(rows) != 1 || rows[0].Name != "keep" { |
| + t.Fatalf("a refused import changed the timers: %+v", rows) |
| + } |
| +} |