package catalogue import ( "encoding/json" "fmt" "strings" "testing" ) // A scheduled step may hold its module's own containers still while it runs (novox/hq ADR 0189). // // The host judges what it receives — whether each id is a container on that machine. What the // definition is the only place to see is judged here, near whoever wrote it. func aStoreManifest(step map[string]any) []byte { m := map[string]any{ "module": "distribution", "version": "1", "resources": []any{ map[string]any{"id": "store", "type": "container", "name": "mesh-registry", "image": "registry@sha256:" + strings.Repeat("a", 64)}, step, }, } raw, _ := json.Marshal(m) return raw } func TestAMaintenanceWindowOnItsOwnModulesContainerIsAccepted(t *testing.T) { raw := aStoreManifest(map[string]any{ "id": "collect", "type": "container", "name": "mesh-registry-collect", "image": "registry@sha256:" + strings.Repeat("a", 64), "schedule": "30 3 * * *", "while-stopped": []any{"store"}, }) if _, err := ParseManifest(raw); err != nil { t.Fatalf("a step holding its own module's container still was refused: %v", err) } } func TestAMaintenanceWindowIsRefusedWhereTheDefinitionShowsItCannotMean(t *testing.T) { for _, c := range []struct { name string step map[string]any says string }{ { "on a step with no schedule", map[string]any{"id": "collect", "type": "container", "name": "c", "image": "registry@sha256:" + strings.Repeat("a", 64), "while-stopped": []any{"store"}}, "gates what is declared after it", }, { "on a run-once step, which already has order", map[string]any{"id": "collect", "type": "container", "name": "c", "image": "registry@sha256:" + strings.Repeat("a", 64), "run-once": true, "while-stopped": []any{"store"}}, "A maintenance window is for a recurring step", }, { "naming a container this module does not declare", map[string]any{"id": "collect", "type": "container", "name": "c", "image": "registry@sha256:" + strings.Repeat("a", 64), "schedule": "30 3 * * *", "while-stopped": []any{"the-broker"}}, "could quiesce a neighbour could stop the mesh", }, { "naming itself", map[string]any{"id": "collect", "type": "container", "name": "c", "image": "registry@sha256:" + strings.Repeat("a", 64), "schedule": "30 3 * * *", "while-stopped": []any{"collect"}}, "naming itself", }, { "written as something that is not a list", map[string]any{"id": "collect", "type": "container", "name": "c", "image": "registry@sha256:" + strings.Repeat("a", 64), "schedule": "30 3 * * *", "while-stopped": "store"}, "a list of this module's container ids", }, } { _, err := ParseManifest(aStoreManifest(c.step)) if err == nil { t.Errorf("%s was accepted", c.name) continue } if !strings.Contains(err.Error(), c.says) { t.Errorf("%s: the refusal does not say %q:\n%v", c.name, c.says, err) } } } // A composed declaration names the step's held containers the way the machine knows them. // // **The gap that let a bug through to the control node.** The manifest says `while-stopped: // ["store"]`, because a module names its own resources locally; the declaration a machine // receives calls that container `distribution.store`, because every resource is composed under // its module. `restart-on` and `reload-on` are rewritten for exactly this reason, and // `while-stopped` was not — so the host found no container by that id and refused the whole // declaration, every push, until it was fixed. // // It passed every test on both sides: the controller's tests read manifests, the host's read // hand-written declarations with bare ids. Only composing one and judging the result catches it. func TestAComposedWindowNamesTheContainerAsTheMachineKnowsIt(t *testing.T) { store := Manifest{ Module: "distribution", Version: "1", Provides: FromAnywhere("artifact-store"), Listens: []Listening{{Port: 5000, Protocol: "tcp", From: FromMesh}}, Serves: map[string]map[string]any{"artifact-store": {"port": 5000}}, Resources: []map[string]any{ {"id": "store", "type": "container", "name": "mesh-registry", "image": "registry@sha256:" + strings.Repeat("a", 64), "ports": []any{"5000"}}, {"id": "collect", "type": "container", "name": "mesh-registry-collect", "image": "registry@sha256:" + strings.Repeat("a", 64), "schedule": "30 3 * * *", WhileStopped: []any{"store"}}, }, } r, err := Resolve(shelf(store), []string{"distribution"}, reachable(), World{}) if err != nil { t.Fatal(err) } out, err := r.Declaration(Rendering{}) if err != nil { t.Fatal(err) } collect := fileNamed(out, "distribution.collect") if collect == nil { for _, res := range out { if res["id"] == "distribution.collect" { collect = res } } } if collect == nil { t.Fatalf("the step was not composed at all: %v", out) } held, _ := collect[WhileStopped].([]any) if len(held) != 1 { t.Fatalf("the composed step holds %v still; want one container", collect[WhileStopped]) } if got := fmt.Sprint(held[0]); got != "distribution.store" { t.Fatalf("the composed step says it holds %q still, and the machine's container is "+ "called %q — the host refuses a declaration naming a container it does not have, "+ "whole, so the machine would take nothing at all", got, "distribution.store") } }