diff --git a/internal/inventory/catalogue.go b/internal/inventory/catalogue.go index 0c1def5..b6d23c1 100644 --- a/internal/inventory/catalogue.go +++ b/internal/inventory/catalogue.go @@ -66,11 +66,15 @@ func (s Source) Current() bool { // gains a requirement, a claim, a resource. What matters is that the change is visible the next // time a node is resolved, which it is. func (i *Inventory) RegisterModule(ctx context.Context, m catalogue.Manifest, from Source) error { - // **Once the node's tool runtime is in the catalogue, the pattern it retires is refused** + // **Once the node's tool runtime is in the catalogue, the pattern it retires may not spread** // (novox/hq ADR 0175, to-be 38 WP2.4): a module serving its tools from a container built on the - // runtime's image. Refused at registration, by name, because this is the mechanism that keeps - // the old pattern from returning by habit — a rebuild of an unmoved module stops here with the - // record that says why. Before the runtime exists the pattern is accepted as it always was. + // runtime's image. Refused at registration, by name, for a module that is new to the catalogue + // or that was registered in another shape — the mechanism that keeps the old pattern from + // returning by habit. **Not refused for a module already registered in that shape**: the + // catalogue holds some thirty of them the day the runtime arrives, each moves to a bundle in + // its own change (to-be 38 WP4 onward), and a gate that refused every rebuild of every unmoved + // module in the meantime would stop the whole pipeline to make a point the record already makes. + // Before the runtime exists the pattern is accepted as it always was. if m.Module != catalogue.RuntimeModule { if why := catalogue.ToolContainerOnTheRuntime(m, from.Against); why != "" { runtime, err := i.hasModule(ctx, catalogue.RuntimeModule) @@ -78,7 +82,13 @@ func (i *Inventory) RegisterModule(ctx context.Context, m catalogue.Manifest, fr return err } if runtime { - return fmt.Errorf("%s is not registered: %s", m.Module, why) + already, err := i.registeredInThatShape(ctx, m.Module) + if err != nil { + return err + } + if !already { + return fmt.Errorf("%s is not registered: %s", m.Module, why) + } } } } @@ -110,6 +120,26 @@ func (i *Inventory) RegisterModule(ctx context.Context, m catalogue.Manifest, fr return err } +// registeredInThatShape is whether the catalogue already holds this module as a tools container on +// the runtime's image — judged from the manifest it holds and what that module's newest build stood +// on, the same two things the gate judges a new registration by. False for a module the catalogue +// does not hold. +func (i *Inventory) registeredInThatShape(ctx context.Context, name string) (bool, error) { + held, err := i.Catalogue(ctx) + if err != nil { + return false, err + } + stored, has := held[name] + if !has { + return false, nil + } + against, err := i.BuiltAgainst(ctx) + if err != nil { + return false, err + } + return catalogue.ToolContainerOnTheRuntime(stored, against[name]) != "", nil +} + // hasModule is whether the catalogue holds a module of that name. func (i *Inventory) hasModule(ctx context.Context, name string) (bool, error) { var one int diff --git a/internal/inventory/catalogue_test.go b/internal/inventory/catalogue_test.go index 7940ccb..dbca7af 100644 --- a/internal/inventory/catalogue_test.go +++ b/internal/inventory/catalogue_test.go @@ -688,29 +688,58 @@ func TestRegisteringWithoutProvenanceKeepsTheSeat(t *testing.T) { // Once the node's tool runtime is in the catalogue, a module serving its tools from a container // built on the runtime's image is refused at registration, naming the record (novox/hq ADR 0175, -// to-be 38 WP2.4). Before, it is accepted as it always was — so a mesh converts in the order the -// design says and nothing is refused before there is anything to move to. +// to-be 38 WP2.4) — for a module new to the catalogue or one that had moved away from it; a module +// already standing in that shape is rebuilt as before, so the catalogue's pipeline keeps running +// while each moves (WP3's amendment). Before the runtime, it is accepted as it always was — so a +// mesh converts in the order the design says and nothing is refused before there is anything to +// move to. func TestAToolContainerIsRefusedOnceTheRuntimeIsRegistered(t *testing.T) { inv := fresh(t) + ctx := t.Context() filter := catalogue.Manifest{Module: "nftables", Version: "1", Tools: []string{"firewall_rules"}, Resources: []map[string]any{{"id": "runtime", "type": "container", "name": "mesh-nftables"}}} stoodOn := []string{catalogue.ArtifactStoreScheme + "mesh-tools/runtime@sha256:" + strings.Repeat("d", 64)} - if err := inv.RegisterModule(t.Context(), filter, Source{Repository: "/r", Against: stoodOn}); err != nil { + // Before the runtime exists the old pattern is accepted as it always was — and built, which is + // how the catalogue comes to know what the module stood on. + if err := inv.RegisterModule(ctx, filter, Source{Repository: "/r", Against: stoodOn}); err != nil { t.Fatalf("before the runtime exists the old pattern is accepted: %v", err) } - runtime := catalogue.Manifest{Module: catalogue.RuntimeModule, Version: "1"} - if err := inv.RegisterModule(t.Context(), runtime, Source{Repository: "/r"}); err != nil { + built := aBuild("nf1", "nftables", "") + built.Against = stoodOn + if err := inv.RecordBuild(ctx, built); err != nil { t.Fatal(err) } - err := inv.RegisterModule(t.Context(), filter, Source{Repository: "/r", Against: stoodOn}) - if err == nil || !strings.Contains(err.Error(), "ADR 0175") { - t.Fatalf("the old pattern was registered beside the runtime: %v", err) + runtime := catalogue.Manifest{Module: catalogue.RuntimeModule, Version: "1"} + if err := inv.RegisterModule(ctx, runtime, Source{Repository: "/r"}); err != nil { + t.Fatal(err) } - // A module that moved its tools to a bundle registers. + + // **A module already registered in that shape is rebuilt without complaint** (to-be 38 WP2.4 as + // amended by WP3): some thirty of them stand the day the runtime arrives, and each moves in its + // own change. The gate is against the pattern spreading, not against the pipeline running. + if err := inv.RegisterModule(ctx, filter, Source{Repository: "/r", Against: stoodOn}); err != nil { + t.Fatalf("a rebuild of a module that already had the pattern was refused: %v", err) + } + // A module new to the catalogue in that shape is refused, naming the record. + newcomer := filter + newcomer.Module = "lamp" + err := inv.RegisterModule(ctx, newcomer, Source{Repository: "/r", Against: stoodOn}) + if err == nil || !strings.Contains(err.Error(), "ADR 0175") { + t.Fatalf("a new module in the old pattern was registered beside the runtime: %v", err) + } + // And a module that had moved its tools to a bundle may not come back to a container. moved := filter moved.Resources = nil - if err := inv.RegisterModule(t.Context(), moved, Source{Repository: "/r", Against: stoodOn}); err != nil { + if err := inv.RegisterModule(ctx, moved, Source{Repository: "/r", Against: stoodOn}); err != nil { t.Fatalf("a module whose tools are a bundle was refused: %v", err) } + unbuilt := aBuild("nf2", "nftables", "") + if err := inv.RecordBuild(ctx, unbuilt); err != nil { + t.Fatal(err) + } + err = inv.RegisterModule(ctx, filter, Source{Repository: "/r", Against: stoodOn}) + if err == nil || !strings.Contains(err.Error(), "ADR 0175") { + t.Fatalf("a module that had moved returned to the old pattern unrefused: %v", err) + } }