ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
143 lines
7.4 KiB
Go
143 lines
7.4 KiB
Go
package resolution
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// A module not handed has a plan for what a provider's helper answers of it, and for nothing else (issue 774: grafana
|
|
// had no plan, so no route key, and route-proxy served no route for it); a need the controller still answers stays out.
|
|
func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
|
|
consumer := notifierConsumer(t, false, false)
|
|
consumer.Requires = append(consumer.Requires, "route")
|
|
consumer.Contributes = map[string]map[string]any{"route": {"host": "notify.example.org"}}
|
|
consumer.ContributesMany = map[string]map[string]map[string]any{"route": {"api": {"label": "notify-api", "port": 9000}}}
|
|
node := workstation()
|
|
node.At, node.PublicDomain = "workstation.internal", "example.org"
|
|
proxy := mcat.Manifest{Module: "proxy", Version: "1", Provides: []mcat.Offer{{Name: "route", Scope: mcat.ScopeMesh, AnsweredBy: mcat.AnsweredByHelper}}}
|
|
store := mcat.Manifest{Module: "store", Version: "1", Provides: []mcat.Offer{{Name: "postgres-database", Scope: mcat.ScopeMesh,
|
|
Credential: &mcat.OfferCredential{Form: mcat.CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}}
|
|
w := twoStores("anchor")
|
|
w.Offered["route"] = []mcat.Provider{{Node: "anchor", At: "anchor.internal", Module: "proxy"}}
|
|
r, err := Resolve(shelf(consumer, proxy, store), []string{consumer.Module}, node, w)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.Account = "op"
|
|
for i, n := range r.Needs {
|
|
// As the plan sets it (compose's answeredByHelper).
|
|
r.Needs[i].AnsweredByHelper = n.Name == "route"
|
|
}
|
|
_, before := composeWith(t, r, mcat.Rendering{})
|
|
c, got := composeWith(t, r, handingTo("store:5000"))
|
|
if got != before {
|
|
t.Fatalf("the plan changed what is composed:\nbefore: %s\nnow: %s", before, got)
|
|
}
|
|
plan, ok := c.Plans["notify"]
|
|
if !ok || len(plan.Provisions) != 1 || plan.Provisions[0].Provision != "route" || plan.Provisions[0].ProviderModule != "proxy" {
|
|
t.Fatalf("the plan is %+v (%v), want the route alone", plan, ok)
|
|
}
|
|
if plan.Templates != (mcat.PlanTemplates{}) || plan.Files != nil || plan.Secrets != nil ||
|
|
!strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" {
|
|
t.Fatalf("the plan names %+v", plan)
|
|
}
|
|
// What it contributes, composed as a received file carried it, and where its machine is, as opaque bytes the
|
|
// provider's own contract reads (issue 774, research 072 row 34): route-proxy serves the route from the plan alone.
|
|
given, err := mcat.DecodeContributed(plan.Provisions[0].Contributed)
|
|
if err != nil || given.At != "workstation.internal" || len(given.Each) != 2 || given.Each[0].Local != "" ||
|
|
string(given.Each[0].Values) != `{"host":"notify.example.org"}` || given.Each[1].Local != "api" ||
|
|
string(given.Each[1].Values) != `{"internal-name":"notify-api.anchor.internal","label":"notify-api","name":"notify-api.example.org","port":9000}` {
|
|
t.Fatalf("the plan gives %+v (%v)", given, err)
|
|
}
|
|
// Its port is where the machine publishes it, the number ${port:9000} gives the module and its declaration publishes,
|
|
// never the software's own where the placement moved it (2026-10-31: route-proxy dialled gitea's 3000, published at
|
|
// 20000): its provider dials what the plan says. Where a plan carries it, a port the module does not listen on is
|
|
// refused by module, node and port.
|
|
for _, p := range []struct {
|
|
name string
|
|
ports map[string]map[int]int
|
|
given map[string]map[int]int
|
|
planned bool
|
|
port float64
|
|
refused string
|
|
}{
|
|
{"assigned by the placement (gitea 3000 at 20000)", map[string]map[int]int{"notify": {9000: 20009}}, nil, true, 20009, ""},
|
|
{"given by the node over the placement", map[string]map[int]int{"notify": {9000: 20009}},
|
|
map[string]map[int]int{"notify": {9000: 9900}}, true, 9900, ""},
|
|
{"a bare published port the placement holds none for keeps its number (influxdb 8086)",
|
|
map[string]map[int]int{"notify": {9000: 9000}}, nil, true, 9000, ""},
|
|
{"a mapping the module fixed (as mailu 7080:80)", map[string]map[int]int{"notify": {9000: 7080}}, nil, true, 7080, ""},
|
|
{"a port the module does not listen on", nil, nil, true, 0,
|
|
"notify contributing to route: its port 9000 on workstation is none of the ports it listens on"},
|
|
{"a declaration with no ports leaves it as written", nil, nil, false, 9000, ""},
|
|
} {
|
|
with := handingTo("store:5000")
|
|
with.Ports, with.Given, with.Planned = p.ports, p.given, p.planned
|
|
c, err := Compose(r, with)
|
|
if p.refused != "" {
|
|
if err == nil || !strings.Contains(err.Error(), p.refused) {
|
|
t.Errorf("%s: composed with %v, want refused with %q", p.name, err, p.refused)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("%s: %v", p.name, err)
|
|
continue
|
|
}
|
|
given, err := mcat.DecodeContributed(c.Plans["notify"].Provisions[0].Contributed)
|
|
var api map[string]any
|
|
if err == nil {
|
|
err = json.Unmarshal(given.Each[1].Values, &api)
|
|
}
|
|
if err != nil || api["port"] != p.port || given.At != "workstation.internal" {
|
|
t.Errorf("%s: the plan gives %v at %q (%v), want port %v", p.name, api, given.At, err, p.port)
|
|
}
|
|
}
|
|
// Read strictly: a form the SDK does not write is refused, whatever its contributions say.
|
|
for raw, refused := range map[string]string{
|
|
`{"v":2,"each":[{"values":{}}]}`: "version 2",
|
|
`{"v":1,"each":[]}`: "no contribution",
|
|
`{"v":1,"each":[{"values":{}}],"route":{}}`: "unknown field",
|
|
`{"v":1,"each":[{"values":{}},{"values":{}}]}`: "twice",
|
|
`{"v":1,"each":[{"local":"api","values":["a"]}]}`: "not a JSON object",
|
|
`{"v":1,"each":[{"values":{}}]} {}`: "more than one",
|
|
`{"v":1,"at":"10.0.0.2","each":[{"values":{"x":true}}]}`: "",
|
|
} {
|
|
_, err := mcat.DecodeContributed(json.RawMessage(raw))
|
|
if (refused == "") != (err == nil) || err != nil && !strings.Contains(err.Error(), refused) {
|
|
t.Errorf("DecodeContributed(%s) = %v, want %q", raw, err, refused)
|
|
}
|
|
}
|
|
// And it hands nothing: Plan.Hands, the one rule every reader of a plan asks, by each part a plan can name.
|
|
if plan.Hands() {
|
|
t.Fatalf("a plan naming the route alone hands its module: %+v", plan)
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
plan mcat.Plan
|
|
hands bool
|
|
}{
|
|
{"empty", mcat.Plan{}, false},
|
|
{"provisions alone", mcat.Plan{Provisions: plan.Provisions}, false},
|
|
{"a digest without an archive", mcat.Plan{Templates: mcat.PlanTemplates{Digest: "sha256:1"}}, false},
|
|
{"templates alone", mcat.Plan{Templates: mcat.PlanTemplates{Source: "notify.tar", Digest: "sha256:1"}}, true},
|
|
{"files alone", mcat.Plan{Files: []mcat.PlanFile{{ID: "conf", Path: "/etc/notify.conf"}}}, true},
|
|
{"secrets alone", mcat.Plan{Secrets: []mcat.PlanSecret{{Name: "admin"}}}, true},
|
|
{"items alone", mcat.Plan{Items: []mcat.PlanItem{{Collection: "routes", Template: "route.tmpl"}}}, true},
|
|
{"secrets and provisions", mcat.Plan{Secrets: []mcat.PlanSecret{{Name: "admin"}}, Provisions: plan.Provisions}, true},
|
|
} {
|
|
if got := c.plan.Hands(); got != c.hands {
|
|
t.Errorf("%s: Hands() is %v, want %v", c.name, got, c.hands)
|
|
}
|
|
}
|
|
if want := mcat.Awaited(consumer, func(p string) []mcat.Manifest {
|
|
return map[string][]mcat.Manifest{"route": {proxy}, "postgres-database": {store}}[p]
|
|
}); !reflect.DeepEqual(want, []string{"route"}) {
|
|
t.Fatalf("the assigner waits for %v, and the plan keys the route alone", want)
|
|
}
|
|
}
|