package composer import ( "context" "encoding/json" "os" "reflect" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/frombus" "github.com/novox/mesh-host/internal/compose/frombus/frombustest" ) const demo = `{"module": "demo", "version": "1", "claims": [{"name": "node-files"}]}` // withInput is r with in as its input, as a caller puts it on the wire: the request's shape carries the input as JSON. func withInput(t *testing.T, r Request, in compose.Input) Request { t.Helper() raw, err := json.Marshal(in) if err != nil { t.Fatal(err) } if err := json.Unmarshal(raw, &r.Input); err != nil { t.Fatal(err) } return r } // The seats a node's assigned modules claim are held there when the request says the holdings come from the claims. func TestHoldingsComeFromTheClaims(t *testing.T) { m, err := catalogue.Decode([]byte(demo)) if err != nil { t.Fatal(err) } got := HoldingsOf(map[string]catalogue.Manifest{"demo": m}, []compose.Node{{Name: "b", Assigned: []string{"demo"}}, {Name: "a", Assigned: []string{"demo"}}}) if len(got) != 2 || got[0].Node != "a" || got[0].Claim != "node-files" || got[0].Scope != "node" { t.Fatalf("holdings = %+v", got) } } // A request the command cannot run is refused, never answered as a verdict: a case named twice, an unknown depth. func TestARequestItCannotRunIsRefused(t *testing.T) { if _, err := Run(Request{V: Version, Cases: []Case{{Name: "a"}, {Name: "a"}}}); err == nil { t.Error("a case named twice runs") } if _, err := Run(Request{V: Version, Depth: "deep"}); err == nil { t.Error("an unknown depth runs") } // A case's own placement (issue 1126) composes over a node the input holds, and is refused for one it does not. placed := withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthResolve, Cases: []Case{{Name: "trunk", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}, {Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}, Placements: map[string]*delivery.Placement{"a": {Node: "a", Modules: []delivery.PlacedModule{{Module: "ghost"}}}}}}}, compose.Input{Nodes: []compose.Node{{Name: "a"}}}) if v, err := Run(placed); err != nil || !v.Cases[0].Machines["a"].Composes || v.Cases[1].Machines["a"].Composes || !strings.Contains(v.Cases[1].Machines["a"].Reason, "ghost") { t.Errorf("a case composes its own placement: %+v, %v", v, err) } placed.Cases[1].Placements = map[string]*delivery.Placement{"z": {Node: "z"}} if _, err := Run(placed); err == nil || !strings.Contains(err.Error(), "holds no node for") { t.Errorf("a case placing a node the input lacks: %v", err) } // Issue 1126 (check of mesh-host #200): the module the mesh provides itself (the private network) comes from mesh-sdk, // a second source beside the indexer's manifests the cases carry, and the input carries no catalogue: a node it is // assigned to still composes, and what it provides is provided. wg := delivery.ProvidedModules()[0] provided := withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthResolve, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}}, compose.Input{Nodes: []compose.Node{{Name: "a", Assigned: []string{wg, "demo"}}}}) if v, err := Run(provided); err != nil || !v.Cases[0].Machines["a"].Composes { t.Errorf("a node assigned the mesh's own module: %+v, %v", v, err) } // Issue 1126 (checks of mesh-host #200, fix-a-bug-met-twice): the sandbox composes one node from the same inputs as // the bus reader, through one path. A mesh whose restic is built (its files from an archive) and handed to the // provisioner composes n1 through frombus, and the request a node-engine answers for it (the input as the engine // sends it, the indexer's manifests in the case) declares the same paths for n1. t.Run("the sandbox composes a node as the bus reader does", func(t *testing.T) { b, err := frombustest.Provisioned() if err != nil { t.Fatal(err) } ctx, scope := context.Background(), frombus.Scope{Node: frombustest.Node} read, err := frombus.Input(ctx, b, frombus.Mesh{}, scope) if err != nil { t.Fatal(err) } d, problems, err := compose.Compose(read, frombustest.Node) if err != nil || len(problems) > 0 { t.Fatalf("the bus reader's input composes n1: %v %v", err, problems) } want := pathsOf(d) sent := SentInput(read, frombustest.Node) cases := Case{Name: "change", Manifests: map[string]Manifest{}} for name, m := range b.Module { cases.Manifests[name] = Manifest{Raw: b.Manifests[m.Manifest]} } r := withInput(t, Request{V: Version, Machines: []string{frombustest.Node}, Declarations: true, Cases: []Case{cases}}, sent) v, err := Run(r) if err != nil { t.Fatal(err) } got := v.Cases[0].Machines[frombustest.Node] if !got.Composes || !reflect.DeepEqual(got.Declared, want) { t.Errorf("the sandbox composed n1 %+v; the bus reader declared %+v", got, want) } }) // Issue 774 (the check of the bus want, the third of a kind: the sandbox's input differs from what the bus reader // composes): a node like the bus node, its bus module folded in its own composition at a build of the old form and no // build published for the module's server artifact, composes in the sandbox as its node-engine composes it, from the // input that says how (compose.Input.ComposedHow); from an older engine's input, which does not, it is named not // judged, never failed and never passed in silence. t.Run("a module folded in the node's own composition", func(t *testing.T) { bus := `{"module": "bus", "version": "1", "claims": [{"name": "node-files"}], ` + `"resources": [{"id": "server", "type": "container", "name": "bus-server", "artifact": "server"}], ` + `"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}` man, err := catalogue.Decode([]byte(bus)) if err != nil { t.Fatal(err) } node := compose.Node{Name: "a", Assigned: []string{"bus"}, InOwn: map[string]bool{"bus": true}, RunsOwn: &delivery.Composed{Node: "a"}} in := compose.Input{Catalogue: map[string]catalogue.Manifest{"bus": man}, Nodes: []compose.Node{node}} request := func(in compose.Input) Request { return withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{}}}}, in) } older, err := Run(request(in)) if err != nil { t.Fatal(err) } if m := older.Cases[0].Machines["a"]; m.Composes || !strings.HasPrefix(m.NotJudged, NotJudged+"bus") || m.Reason != "" { t.Errorf("an older engine's input of a node with a folded module: %+v", m) } in.ComposedHow = &compose.ComposedHow{Node: "a", Builds: map[string]string{}} sent, err := Run(request(in)) if err != nil { t.Fatal(err) } if m := sent.Cases[0].Machines["a"]; !m.Composes || m.Reason != "" { t.Errorf("the input that says how the node composes, its bus folded: %+v", m) } // A module the change refuses beside the folded one fails the node in its words, never passed with the rest // composed (review of the sandbox input); held already where the node-engine composed, it stays held as there. app, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", ` + `"resources": [{"id": "env", "type": "file", "path": "/var/lib/app/env", "mode": "0600", "content": "N=${secret:repository}\n"}]}`)) if err != nil { t.Fatal(err) } in.Catalogue["app"] = app in.Nodes[0].Assigned = []string{"bus", "app"} refused, err := Run(request(in)) if err != nil { t.Fatal(err) } if m := refused.Cases[0].Machines["a"]; m.Composes || !strings.Contains(m.Reason, "app: ") || !strings.Contains(m.Reason, "repository") { t.Errorf("a module the change refuses on a node composed as its node-engine does: %+v", m) } // Held composing without the change, blamed for a refusal beyond the read (issue 1960's unset names): no refusal of // the change either (review round 1). in.ComposedHow.HeldBefore = map[string]string{"app": "its route names are unset"} before, err := Run(request(in)) if err != nil { t.Fatal(err) } if m := before.Cases[0].Machines["a"]; !m.Composes { t.Errorf("a module the node holds composing without the change: %+v", m) } in.ComposedHow.HeldBefore = nil in.ComposedHow.Held = map[string]string{"app": "held on the node"} held, err := Run(request(in)) if err != nil { t.Fatal(err) } if m := held.Cases[0].Machines["a"]; !m.Composes { t.Errorf("a module the node-engine holds too: %+v", m) } }) // Issue 774 (the group check of feat/l3-final 278aa5f0ccbe): a tool runner its node-engine holds by its manifest's // verdict is sent as its stub, and the input says it holds it: the sandbox composes the node with it held, never refuses // it for its stub's missing bundle. An input from an engine before the mark is read by Refused alone, the same way. t.Run("a refused tool runner the engine sends as its stub", func(t *testing.T) { raw := `{"module": "runner", "version": "1", "tool-runner": true, ` + `"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}` man, err := catalogue.Decode([]byte(raw)) if err != nil { t.Fatal(err) } in := compose.Input{Catalogue: map[string]catalogue.Manifest{"runner": compose.Stub(man)}, Refused: map[string]string{"runner": "the indexer's judge refused the manifest of runner"}, Nodes: []compose.Node{{Name: "a", Assigned: []string{"runner"}}}} wire, err := json.Marshal(in) if err != nil { t.Fatal(err) } var back compose.Input if err := json.Unmarshal(wire, &back); err != nil || !back.Catalogue["runner"].Stubbed || !strings.Contains(string(wire), `"Held"`) { t.Fatalf("the input's held mark across the wire: %s, %v", wire, err) } older := strings.Replace(string(wire), `"Held":{"Catalogue":["runner"]}`, `"Unsaid":null`, 1) for name, sent := range map[string]string{"with the mark": string(wire), "from an engine before it": older} { var r Request r = Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"runner": {Raw: json.RawMessage(raw)}}}}} if err := json.Unmarshal([]byte(sent), &r.Input); err != nil { t.Fatal(err) } v, err := Run(r) if err != nil { t.Fatal(err) } if m := v.Cases[0].Machines["a"]; !m.Composes || strings.Contains(m.Reason, "bundle") { t.Errorf("%s: the node with its tool runner held: %+v", name, m) } if read := v.Cases[0].Manifests["runner"]; read.Accepted || !strings.Contains(read.Refused, "the indexer's judge refused the manifest of runner") { t.Errorf("%s: the runner's read names not why its node-engine holds it: %+v", name, read) } } }) v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}}) if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" { t.Fatalf("a request with no machines: %+v, %v", v, err) } } // The composer judges no manifest (novox/hq ADR 0578 ยง5): xterm's module.json from mesh-catalog at 835bf22b, whose // config value falls back to "font-size", is read whatever list of predefined keys a request carries (a group touching // xterm failed its composition on this, group check 18df81380db3); a module whose verdict in the input refuses it is // held, composed as its stub, and named in the verdict's words. func TestTheComposerTakesTheVerdictsOfItsInput(t *testing.T) { raw, err := os.ReadFile("testdata/catalogue-835bf22b-xterm.module.json") if err != nil { t.Fatal(err) } r := Request{V: Version, Predefined: catalogue.PredefinedList{"something-else": {Type: "string", Description: "x"}}, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"xterm": {Raw: raw}}}}} v, err := Run(r) if err != nil { t.Fatal(err) } if got := v.Cases[0].Manifests["xterm"]; !got.Accepted { t.Fatalf("xterm: %+v, want accepted", got) } r.Cases[0].Verdicts = map[string]delivery.ManifestVerdict{"xterm": {Module: "xterm", State: delivery.VerdictRefused, Problems: []string{"falls back to a key not listed"}, Judge: "src2:j"}} shelf, err := parse(r.Cases[0].Manifests, map[string]ManifestRead{}, r.Cases[0].Verdicts) if err != nil || !shelf["xterm"].Stubbed { t.Fatalf("the refused module is not held as its stub: %+v %v", shelf["xterm"], err) } if v, err = Run(r); err != nil { t.Fatal(err) } if got := v.Cases[0].Manifests["xterm"]; got.Accepted || !strings.Contains(got.Refused, "the indexer's judge (src2:j) refused the manifest of xterm: falls back to a key not listed") { t.Fatalf("xterm refused by its verdict: %+v, want refused in the verdict's words", got) } }