Take the catalogue's composition rules into the node-engine
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
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
func optionalShelf() map[string]mcat.Manifest {
|
||||
grafana := mcat.Manifest{Module: "grafana", Version: "1", Requires: []string{"influxdb-api", "shell"},
|
||||
OptionalRequires: map[string]bool{"influxdb-api": true, "shell": true}}
|
||||
return shelf(mcat.Manifest{Module: "influxdb", Version: "1", Provides: mcat.FromAnywhere("influxdb-api")}, mcat.Manifest{Module: "zsh", Version: "1", Provides: mcat.Offers("shell")}, grafana, mcat.
|
||||
// What wants the same names hard, for the cases where one consumer's optional is another's hard.
|
||||
Manifest{Module: "telegraf", Version: "1", Requires: []string{"influxdb-api"}}, mcat.Manifest{Module: "editor", Version: "1", Requires: []string{"shell"}}, mcat.Manifest{Module: "suite", Version: "1", Requires: []string{"editor"}},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAnOptionalRequirementWithNoProviderIsAbsentAndNothingIsInstalled(t *testing.T) {
|
||||
// Exactly one catalogue module could answer each — the case where a hard requirement installs it.
|
||||
// Brokered (influxdb-api, from anywhere) and on this machine (shell); both passes.
|
||||
for _, world := range []mcat.World{{}, {Unchecked: true}} {
|
||||
got, err := Resolve(optionalShelf(), []string{"grafana"}, reachable(), world)
|
||||
if err != nil {
|
||||
t.Fatalf("an optional requirement with no provider refused the assignment: %v", err)
|
||||
}
|
||||
if have := strings.Join(names(got), " "); have != "grafana" {
|
||||
t.Fatalf("assigning grafana put %s on the machine; nothing is installed for an optional requirement (ADR 0330 §3)", have)
|
||||
}
|
||||
if world.Unchecked {
|
||||
continue
|
||||
}
|
||||
want := []mcat.Absent{{Consumer: "grafana", Requirement: "influxdb-api"}, {Consumer: "grafana", Requirement: "shell"}}
|
||||
if len(got.Absent) != 2 || got.Absent[0] != want[0] || got.Absent[1] != want[1] {
|
||||
t.Fatalf("absent: %+v, want %+v", got.Absent, want)
|
||||
}
|
||||
if len(got.Needs) != 0 {
|
||||
t.Fatalf("an absent requirement was given a binding: %+v", got.Needs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnOptionalRequirementWithAProviderBindsAsAHardOne(t *testing.T) {
|
||||
world := mcat.World{Offered: map[string][]mcat.Provider{"influxdb-api": {
|
||||
{Node: "anchor", At: "anchor.internal", Module: "influxdb", Serves: map[string]any{"port": 8086}}}}}
|
||||
got, err := Resolve(optionalShelf(), []string{"grafana", "zsh"}, reachable(), world)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := boundFrom(t, got, "grafana", "influxdb-api"); n.From != "anchor" || n.Module != "influxdb" {
|
||||
t.Fatalf("bound to %s/%s", n.From, n.Module)
|
||||
}
|
||||
if len(got.Absent) != 0 {
|
||||
t.Fatalf("a requirement with a provider was left absent: %+v", got.Absent)
|
||||
}
|
||||
// And the same assignment without the provider is absent: what binds it later is only the
|
||||
// provider being assigned, with no act on grafana (ADR 0330 §7).
|
||||
before, err := Resolve(optionalShelf(), []string{"grafana", "zsh"}, reachable(), mcat.World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(before.Absent) != 1 || before.Absent[0].Requirement != "influxdb-api" {
|
||||
t.Fatalf("absent before the provider was assigned: %+v", before.Absent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOneConsumersOptionalRequirementIsAnothersHardOne(t *testing.T) {
|
||||
// telegraf wants influxdb-api hard: with nothing offering it, the assignment is refused as
|
||||
// before, naming what to assign — grafana's optional does not soften telegraf's.
|
||||
_, err := Resolve(optionalShelf(), []string{"grafana", "telegraf"}, reachable(), mcat.World{})
|
||||
if err == nil || !strings.Contains(err.Error(), "assign influxdb") {
|
||||
t.Fatalf("a hard requirement with no provider was not refused naming the candidate: %v", err)
|
||||
}
|
||||
// And the refusal names telegraf, whose remedy it is, not grafana, which met the name first.
|
||||
if !strings.Contains(err.Error(), "wanted by telegraf") {
|
||||
t.Errorf("the refusal does not name the module that wants it hard: %v", err)
|
||||
}
|
||||
// And a module pulled in later (suite → editor) that wants the shell hard installs it, even
|
||||
// though grafana, met first, wanted it only optionally — and grafana is then bound to it.
|
||||
got, err := Resolve(optionalShelf(), []string{"grafana", "suite"}, workstation(), mcat.World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if have := strings.Join(names(got), " "); !strings.Contains(have, "zsh") {
|
||||
t.Fatalf("a hard requirement met after an optional one was not installed: %s", have)
|
||||
}
|
||||
for _, a := range got.Absent {
|
||||
if a.Requirement == "shell" {
|
||||
t.Fatalf("the shell is installed and still listed absent: %+v", got.Absent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHardRequirementWithNoProviderIsStillRefused(t *testing.T) {
|
||||
_, err := Resolve(optionalShelf(), []string{"telegraf"}, reachable(), mcat.World{})
|
||||
if err == nil || !strings.Contains(err.Error(), "influxdb") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func optionalModule(resources string) string {
|
||||
return `{"module":"grafana","version":"1",
|
||||
"requires":["route",{"name":"influxdb-api","optional":true}],
|
||||
"secrets":{"influxdb-api":"/var/lib/grafana/influxdb-api"},
|
||||
"resources":[` + resources + `]}`
|
||||
}
|
||||
|
||||
func absentGrafana(t *testing.T, absent bool) mcat.Resolution {
|
||||
t.Helper()
|
||||
m := mustParse(t, optionalModule(`
|
||||
{"id":"env","type":"file","path":"/var/lib/grafana/env","mode":"0644",
|
||||
"content":"INFLUX=${bound:influxdb-api:at|default:none}:${bound:influxdb-api:port|default:0}\n"},
|
||||
{"id":"ds","type":"file","path":"/var/lib/grafana/ds.yaml","mode":"0400",
|
||||
"content":"url: ${bound:influxdb-api:at}\npassword: ${secret:influxdb-api}\n","only-when-bound":"influxdb-api"}`))
|
||||
m.Requires = []string{"influxdb-api"}
|
||||
m.Binds = map[string]string{"influxdb-api": "/var/lib/grafana/influxdb.json"}
|
||||
r := mcat.Resolution{Node: "workstation", Modules: []mcat.Manifest{m}}
|
||||
if absent {
|
||||
r.Absent = []mcat.Absent{{Consumer: "grafana", Requirement: "influxdb-api"}}
|
||||
} else {
|
||||
r.Needs = []mcat.Needed{{Name: "influxdb-api", From: "anchor", At: "anchor.internal", For: "grafana",
|
||||
Module: "influxdb", Sealed: "sealed-influx", Serves: map[string]any{"port": float64(8086)}}}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestAnAbsentRequirementIsComposedWithItsDefaultsAndWithoutItsFiles(t *testing.T) {
|
||||
out, err := Declaration(absentGrafana(t, true), mcat.Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := fileNamed(out, "grafana.env")
|
||||
if env == nil || env["content"] != "INFLUX=none:0\n" {
|
||||
t.Fatalf("the defaults were not rendered: %v", env)
|
||||
}
|
||||
for _, id := range []string{"grafana.ds", "grafana." + mcat.SecretID("influxdb-api"), "grafana." + mcat.BoundID("influxdb-api")} {
|
||||
if fileNamed(out, id) != nil {
|
||||
t.Errorf("%s was delivered while influxdb-api is absent (ADR 0330 §5, §6)", id)
|
||||
}
|
||||
}
|
||||
for _, r := range out {
|
||||
if _, has := r[mcat.OnlyWhenBound]; has {
|
||||
t.Errorf("%v carries %s to the node-engine, which knows no such field", r["id"], mcat.OnlyWhenBound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestABoundOptionalRequirementIsComposedAsAHardOne(t *testing.T) {
|
||||
out, err := Declaration(absentGrafana(t, false), mcat.Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if env := fileNamed(out, "grafana.env"); env == nil || env["content"] != "INFLUX=anchor.internal:8086\n" {
|
||||
t.Fatalf("the bound values were not rendered: %v", env)
|
||||
}
|
||||
ds := fileNamed(out, "grafana.ds")
|
||||
if ds == nil {
|
||||
t.Fatalf("the only-when-bound file was left out while bound: %v", out)
|
||||
}
|
||||
if _, has := ds[mcat.OnlyWhenBound]; has {
|
||||
t.Errorf("the file carries %s to the node-engine", mcat.OnlyWhenBound)
|
||||
}
|
||||
for _, id := range []string{"grafana." + mcat.SecretID("influxdb-api"), "grafana." + mcat.BoundID("influxdb-api")} {
|
||||
if fileNamed(out, id) == nil {
|
||||
t.Errorf("%s was not delivered while bound", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user