diff --git a/cmd/mesh-controller/sendable_test.go b/cmd/mesh-controller/sendable_test.go index cecfbe0..c434265 100644 --- a/cmd/mesh-controller/sendable_test.go +++ b/cmd/mesh-controller/sendable_test.go @@ -214,17 +214,19 @@ func TestConsumersAreToldTheGivenPort(t *testing.T) { } } + // A port for the whole mesh is refused where it is set, not stored to refuse every node's + // declaration afterwards. if err := open.inventory.SetSettings(ctx, "", "store", - map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5434}}); err != nil { - t.Fatal(err) + map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5434}}); err == nil || + !strings.Contains(err.Error(), "per node") { + t.Fatalf("a port given for the whole mesh was not refused: %v", err) } plan, settings, err := planFor(ctx, open, "anchor") if err != nil { t.Fatal(err) } - if _, err := declarationFor(ctx, open, "anchor", plan, settings); err == nil || - !strings.Contains(err.Error(), "per node") { - t.Fatalf("a port given for the whole mesh was not refused: %v", err) + if _, err := declarationFor(ctx, open, "anchor", plan, settings); err != nil { + t.Fatalf("the refused mesh-wide layer was stored anyway: %v", err) } } diff --git a/internal/inventory/catalogue.go b/internal/inventory/catalogue.go index 65a6e5d..5ca6d12 100644 --- a/internal/inventory/catalogue.go +++ b/internal/inventory/catalogue.go @@ -585,6 +585,17 @@ func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, va return err } if nodeName == "" { + // A port is a fact about one machine (novox/hq ADR 0100). Refused here, in composition's + // words: stored, it refuses every node running the module at composition, and the mesh + // cannot be pushed at all until somebody finds the layer that did it. + given, err := givenIn(module, raw) + if err != nil { + return err + } + if len(given) > 0 { + return fmt.Errorf("%s: %s is given per node — a port is a fact about one machine; "+ + "set it with --node", module, catalogue.PortsSetting) + } _, err = i.store.Pool().Exec(ctx, `insert into settings (node, module, values) values (null, $1, $2) on conflict (module) where node is null diff --git a/internal/inventory/ports_test.go b/internal/inventory/ports_test.go index 21a3761..2ac93c8 100644 --- a/internal/inventory/ports_test.go +++ b/internal/inventory/ports_test.go @@ -2,6 +2,7 @@ package inventory import ( "errors" + "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" @@ -318,3 +319,28 @@ func TestAGivenPortHasOneHolderAndReplacesTheAssignment(t *testing.T) { t.Fatalf("the released port %d was not free again (got %d)", assigned.Machine, other.Machine) } } + +// novox/hq ADR 0100: a port is a fact about one machine, so a layer for the whole mesh cannot give +// one. Refused where it is set — stored, it refuses every node running the module at composition, +// and the mesh cannot be pushed until somebody finds the layer that did it. +func TestAPortGivenForTheWholeMeshIsRefusedWhereItIsSet(t *testing.T) { + inv, node := aNodeWithModules(t, "postgres") + ctx := t.Context() + err := inv.SetSettings(ctx, "", "postgres", + map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5433}}) + if err == nil || !strings.Contains(err.Error(), "per node") { + t.Fatalf("a port given for the whole mesh was accepted: %v", err) + } + layers, err := inv.SettingsFor(ctx, node, "postgres") + if err != nil { + t.Fatal(err) + } + if len(layers) != 0 { + t.Fatalf("the refused layer was stored: %v", layers) + } + // The same values for one machine are the ordinary setting. + if err := inv.SetSettings(ctx, node, "postgres", + map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5433}}); err != nil { + t.Fatal(err) + } +}