package catalogue import ( "encoding/json" "strings" "testing" ) // A module that must know its own address — a login redirect, a canonical URL, an issuer — had it // written into its manifest as a literal (novox/hq 04-ISSUES/122): a domain in a definition, wrong on // every other machine. It is told instead, from the same composition the provider receives. // selfAware contributes a labelled route, binds the requirement, and writes its own name into a file. func selfAware(label string) Manifest { m := labelled("board", label, 8080) m.Requires = []string{"reverse-proxy"} m.Binds = map[string]string{"reverse-proxy": "/var/lib/board/route.json"} m.Resources = []map[string]any{ {"id": "conf", "type": "file", "path": "/var/lib/board/app.conf", "content": "root = https://${bound:reverse-proxy:name}/\ninternal = ${bound:reverse-proxy:internal-name}\n"}, } return m } // servingProxy is proxy() as the catalogue's route providers are declared: the provision scoped to // the mesh, serving nothing a consumer must know (route-adapter, route-proxy: `"serves": {"route": {}}`). func servingProxy() Manifest { p := proxy() p.Provides = []Offer{{Name: "reverse-proxy", Scope: ScopeMesh}} p.Serves = map[string]map[string]any{"reverse-proxy": {}} return p } // nodeProxy is the same provider scoped to its node, whose answer on the same machine comes from // `here` rather than from the mesh's needs — the other path a binding is written by. func nodeProxy() Manifest { p := proxy() p.Serves = map[string]map[string]any{"reverse-proxy": {"scheme": "http"}} return p } // onBoth is a node with a public domain and a private-network address, so both names compose. func onBoth(domain string) Node { n := withDomain(domain) n.At = "anchor.internal" return n } func fileAt(t *testing.T, out []map[string]any, path string) string { t.Helper() for _, r := range out { if r["path"] == path { return r["content"].(string) } } t.Fatalf("nothing was declared at %s", path) return "" } func TestAModuleIsToldTheNameItsProviderServes(t *testing.T) { got, err := Resolve(shelf(servingProxy(), selfAware("git")), []string{"traefik", "board"}, onBoth("example.tld"), World{}) if err != nil { t.Fatal(err) } out := mustDeclare(t, got) served := received(t, out)[0].Values var binding map[string]any if err := json.Unmarshal([]byte(fileAt(t, out, "/var/lib/board/route.json")), &binding); err != nil { t.Fatal(err) } if binding["name"] != served["name"] || binding["name"] != "git.example.tld" { t.Fatalf("the module was told %v, the provider serves %v", binding["name"], served["name"]) } if binding["internal-name"] != served["internal-name"] || binding["internal-name"] == nil { t.Fatalf("internal name: module told %v, provider serves %v", binding["internal-name"], served["internal-name"]) } conf := fileAt(t, out, "/var/lib/board/app.conf") want := "root = https://git.example.tld/\ninternal = " + served["internal-name"].(string) + "\n" if conf != want { t.Fatalf("the file was rendered as\n%s\nwant\n%s", conf, want) } } func TestTheNameAModuleIsToldFollowsTheNodesDomain(t *testing.T) { // The whole point: the same definition, two machines, two names — nothing edited. for _, domain := range []string{"example.tld", "other.example"} { got, err := Resolve(shelf(servingProxy(), selfAware("git")), []string{"traefik", "board"}, onBoth(domain), World{}) if err != nil { t.Fatal(err) } conf := fileAt(t, mustDeclare(t, got), "/var/lib/board/app.conf") if !strings.HasPrefix(conf, "root = https://git."+domain+"/") { t.Fatalf("on %s the module wrote %q", domain, conf) } } } func TestAModuleWithNoPublicNameIsNotToldOne(t *testing.T) { // No public domain on the node: nothing composed, so no `name` — and a file asking for one is // refused rather than rendered with a placeholder or an empty host. m := selfAware("git") m.Resources[0]["content"] = "root = https://${bound:reverse-proxy:name}/\n" got, err := Resolve(shelf(servingProxy(), m), []string{"traefik", "board"}, workstation(), World{}) if err != nil { t.Fatal(err) } if _, err := got.Declaration(Rendering{}); err == nil || !strings.Contains(err.Error(), `"name"`) { t.Fatalf("a file asking for a name that was never composed was not refused: %v", err) } } func TestAModuleIsToldItsNameByANodeScopedProviderToo(t *testing.T) { m := selfAware("git") m.Resources[0]["content"] = "root = https://${bound:reverse-proxy:name}/\n" got, err := Resolve(shelf(nodeProxy(), m), []string{"board"}, withDomain("example.tld"), World{}) if err != nil { t.Fatal(err) } conf := fileAt(t, mustDeclare(t, got), "/var/lib/board/app.conf") if !strings.HasPrefix(conf, "root = https://git.example.tld/") { t.Fatalf("a same-machine, node-scoped answer did not tell the module its name: %q", conf) } }