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) } } }