A module is told the name it is served under (hq 122)
A module contributes a label; the mesh joins it with the node's domains and
the provider serves the result — and the module itself was never told.
Software that must know its own address (a login redirect, a canonical URL,
an issuer) had it written into the manifest as a literal: a domain in a
definition, wrong on every other machine (ADR 0112). Found converting
grafana's keycloak login for ace, where it forced GF_SERVER_ROOT_URL and
keycloak's issuer back into manifests.
The binding for a requirement a module contributes to now carries `name`
and `internal-name` (or `names` by local name for several contributions),
and `${bound:<requirement>:name}` / `:internal-name` (`:name-<local>`) fill
files from it. Both come from the one function the provider's received
file is composed by, so the proxy and the module cannot disagree about the
name. Absent when nothing was composed, so a file asking for a name on a
node with no public domain is refused, not rendered empty.
Also: `${bound:…}` could not name a requirement answered by a node-scoped
provider on the same machine — its binding file was written (from `here`)
but the placeholders only looked at the mesh's needs. Filled from the same
answer now.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user