From b602b223e11f6afb37d35abe15744df152cedf5b Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 03:27:32 +0200 Subject: [PATCH] A changed maintenance window is a changed spec (hq ADR 0189) containerSpec says a changed cadence moves the marker so the install is reported updated and re-established. Which containers are held still is the same kind of statement, and a declaration that changed it while the machine reported no change would be a machine quietly holding yesterday's containers. --- internal/apply/apply.go | 7 ++++ internal/apply/maintenance_window_test.go | 42 +++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 33b8a3c..f4a858e 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -1707,6 +1707,13 @@ func containerSpecReading(r *declaration.Container, declares, reads map[string]s // The cadence is part of what was declared, so a changed schedule is a changed spec — the marker // moves and the install is reported "updated" and re-established. Added only when present, so no // ordinary container's or run-once step's digest moves for a field it does not set. + // And which of its module's containers it holds still while it runs (novox/hq ADR 0189), for + // the same reason: a declaration that changed the window while the machine reported no change + // would be a machine quietly holding yesterday's containers. In declared order, which is the + // order they are stopped in. + for _, id := range r.WhileStopped { + b.WriteString("while-stopped " + id + "\n") + } if r.Schedule != "" { b.WriteString("schedule " + r.Schedule + "\n") } diff --git a/internal/apply/maintenance_window_test.go b/internal/apply/maintenance_window_test.go index 1649207..0acc358 100644 --- a/internal/apply/maintenance_window_test.go +++ b/internal/apply/maintenance_window_test.go @@ -194,3 +194,45 @@ func TestAMaintenanceWindowIsRefusedWhereItCannotMean(t *testing.T) { } } } + +// A changed window is a changed declaration, and the install says so. +// +// The cadence already works this way: "a changed schedule is a changed spec — the marker moves and +// the install is reported updated and re-established" (containerSpec). Which containers are held +// still for the run is the same kind of statement, and a declaration that changed it while the +// machine reported no change would be a machine quietly running the old window. +func TestAChangedWindowMovesTheSpec(t *testing.T) { + one := parseTrusted(t, `{"declaration":1,"resources":[ + {"id":"store","type":"container","name":"mesh-registry","image":"`+pinned+`"}, + {"id":"other","type":"container","name":"other","image":"`+pinned+`"}, + {"id":"collect","type":"container","name":"collect","image":"`+pinned+`", + "schedule":"30 3 * * *","while-stopped":["store"]} + ]}`) + two := parseTrusted(t, `{"declaration":1,"resources":[ + {"id":"store","type":"container","name":"mesh-registry","image":"`+pinned+`"}, + {"id":"other","type":"container","name":"other","image":"`+pinned+`"}, + {"id":"collect","type":"container","name":"collect","image":"`+pinned+`", + "schedule":"30 3 * * *","while-stopped":["store","other"]} + ]}`) + stepOf := func(d *declaration.Declaration) *declaration.Container { + for _, r := range d.Resources { + if c, ok := r.(*declaration.Container); ok && c.ID == "collect" { + return c + } + } + t.Fatal("no step in the fixture") + return nil + } + if containerSpec(stepOf(one), inputs{}) == containerSpec(stepOf(two), inputs{}) { + t.Fatal("the window changed and the spec did not; the machine would report no change " + + "and keep holding the containers it held yesterday") + } + // And a container with no window is untouched by the field existing at all. + plain := parseTrusted(t, `{"declaration":1,"resources":[ + {"id":"store","type":"container","name":"mesh-registry","image":"`+pinned+`"} + ]}`) + spec := containerSpec(plain.Resources[0].(*declaration.Container), inputs{}) + if strings.Contains(spec, "while-stopped") || strings.Contains(spec, "held") { + t.Errorf("an ordinary container's spec mentions a field it does not set:\n%s", spec) + } +}