package resolution import ( "encoding/json" "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) func published(module, host string, port int) mcat.Manifest { return mcat.Manifest{Module: module, Version: "1", Contributes: map[string]map[string]any{ "reverse-proxy": {"host": host, "port": port}, }} } func proxy() mcat.Manifest { return mcat.Manifest{Module: "traefik", Version: "1", Provides: mcat.Offers("reverse-proxy"), Receives: map[string]string{"reverse-proxy": "/etc/traefik/mesh.json"}, Resources: []map[string]any{ {"id": "up", "type": "service", "unit": "traefik", "state": "running", "restart-on": []any{mcat.ReceivedID("reverse-proxy")}}, }} } // received digs the delivered contributions back out of a declaration. func received(t *testing.T, out []map[string]any) []mcat.Contribution { t.Helper() for _, r := range out { if r["path"] != "/etc/traefik/mesh.json" { continue } body := r["content"].(string) var parsed struct { Requirement string `json:"requirement"` Given []mcat.Contribution `json:"given"` } if err := json.Unmarshal([]byte(body), &parsed); err != nil { t.Fatalf("the file the proxy is given is not readable: %v\n%s", err, body) } if parsed.Requirement != "reverse-proxy" { t.Fatalf("the file does not say what it is about: %q", parsed.Requirement) } return parsed.Given } t.Fatalf("the provider was given no file at all: %v", out) return nil } func TestTheFileTheProviderGetsIsMachineReadable(t *testing.T) { // It is written for a program, and the first version put a `//` header above the JSON — a // file that says "do not edit" to a person and fails to parse for the thing meant to read it. got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) for _, r := range mustDeclare(t, got) { if r["path"] != "/etc/traefik/mesh.json" { continue } var any map[string]any if err := json.Unmarshal([]byte(r["content"].(string)), &any); err != nil { t.Fatalf("the file is not parseable: %v\n%s", err, r["content"]) } if note, _ := any["generated"].(string); !strings.Contains(note, "do not edit") { // Inside the document rather than above it, so it reaches a person without // breaking the parse. t.Fatalf("the file does not say it is generated: %v", any["generated"]) } return } t.Fatal("no received file") } func TestAModuleTellsItsProviderWhatItNeeds(t *testing.T) { got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(got, mcat.Rendering{}) if err != nil { t.Fatal(err) } given := received(t, out) if len(given) != 1 || given[0].From != "board" { t.Fatalf("the proxy was not told about board: %v", given) } if given[0].Values["host"] != "board" || given[0].Values["port"] != float64(8080) { t.Fatalf("the contribution did not survive: %v", given[0].Values) } } func TestContributingToSomethingIsRequiringIt(t *testing.T) { // Asking to be published means a publisher must exist. A module that had to say both would // eventually say one, and the failure would be a machine where nothing serves the route. got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) if err != nil { t.Fatal(err) } if !strings.Contains(strings.Join(names(got), " "), "traefik") { t.Fatalf("contributing to reverse-proxy did not bring one in: %v", names(got)) } } func TestNothingToContributeToIsRefused(t *testing.T) { _, err := Resolve(shelf(published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) if err == nil { t.Fatal("a module was published through a proxy that does not exist") } if !strings.Contains(err.Error(), "reverse-proxy") { t.Fatalf("the refusal does not name what is missing: %v", err) } } func TestEveryPublisherOnTheMachineIsInOneFile(t *testing.T) { got, err := Resolve(shelf(proxy(), published("board", "board", 8080), published("archive", "archive", 9000), ), []string{"board", "archive"}, workstation(), mcat.World{}) if err != nil { t.Fatal(err) } given := received(t, mustDeclare(t, got)) if len(given) != 2 { t.Fatalf("the proxy was told about %d of 2: %v", len(given), given) } // Ordered by module, because this becomes a file and a file whose lines move about looks // changed when nothing changed — which would restart the proxy for ever. if given[0].From != "archive" || given[1].From != "board" { t.Fatalf("the order is not stable: %v", given) } } func TestAProviderWithNoConsumersStillGetsTheFile(t *testing.T) { // Empty rather than absent. A provider that finds no file cannot tell "nothing asked for me" // from "the mesh never wrote it", and those want completely different responses — the same // rule the host follows about a service that does not exist. got, err := Resolve(shelf(proxy()), []string{"traefik"}, workstation(), mcat.World{}) if err != nil { t.Fatal(err) } if given := received(t, mustDeclare(t, got)); len(given) != 0 { t.Fatalf("got %v", given) } } func TestTheProviderCanReloadWhenTheRoutesChange(t *testing.T) { // A proxy that got a new route and did not reload is a route that silently does not work. // The same fault the private network had when a peer list changed under a running interface, // which is why the received file has a name a module can point at. got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) out := mustDeclare(t, got) for _, r := range out { if r["type"] != "service" { continue } reflects, ok := r["restart-on"].([]any) if !ok || len(reflects) != 1 { t.Fatalf("the proxy does not reflect anything: %v", r) } if reflects[0] != "traefik."+mcat.ReceivedID("reverse-proxy") { t.Fatalf("it reflects %v, which is not the file it was given", reflects[0]) } return } t.Fatal("no service in the declaration") } func TestARouteCanBeSetPerMesh(t *testing.T) { // The hostname is exactly the kind of thing that differs between one mesh and the next. A // module whose route could not be set would have to be edited to be reused anywhere. got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) out, err := Declaration(got, mcat.Rendering{Settings: mcat.SettingsBy{ "board": {{From: "the mesh", Values: map[string]any{"host": "dashboard"}}}, }}) if err != nil { t.Fatal(err) } given := received(t, out) if given[0].Values["host"] != "dashboard" { t.Fatalf("the setting did not reach the route: %v", given[0].Values) } if given[0].Values["port"] != float64(8080) { t.Fatalf("setting the host dropped the port: %v", given[0].Values) } } func TestASettingReachesAContributionOnlyWhereItDeclaresTheKey(t *testing.T) { // novox/hq 04-ISSUES/173: the mail module's site name, set so its environment file could read // it, arrived in every route it contributed. A setting overrides a key the contribution // declares and adds none — the provider reads the contribution as a contract. got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), mcat.World{}) out, err := Declaration(got, mcat.Rendering{Settings: mcat.SettingsBy{ "board": {{From: "the mesh", Values: map[string]any{"host": "dashboard", "sitename": "Board"}}}, }}) if err != nil { t.Fatal(err) } given := received(t, out) if given[0].Values["host"] != "dashboard" { t.Fatalf("the setting did not override the route's host: %v", given[0].Values) } if _, leaked := given[0].Values["sitename"]; leaked { t.Fatalf("a setting the route never declared reached the proxy: %v", given[0].Values) } } func provider() mcat.Manifest { return mcat.Manifest{Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database"), Receives: map[string]string{"postgres-database": "/var/lib/postgres/grants/mesh.json"}, Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"}, } } func grantedTo(t *testing.T, out []map[string]any) []mcat.Contribution { t.Helper() for _, r := range out { if r["path"] != "/var/lib/postgres/grants/mesh.json" { continue } var parsed struct { Given []mcat.Contribution `json:"given"` } if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil { t.Fatal(err) } return parsed.Given } t.Fatalf("the provider was given no manifest: %v", out) return nil } func TestAConsumersOwnContributionsAreStillThere(t *testing.T) { // Cross-node grants are merged in with this machine's own, because from the provider's side // they are the same thing — somebody wanting something — and a provider that had to read two // lists would read one of them. got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } given := received(t, mustDeclare(t, got)) if len(given) != 1 || given[0].Node != "" { t.Fatalf("a local contribution grew a node: %v", given) } } func TestARequireOnlyConsumerOfAParameterlessProvisionStillAsks(t *testing.T) { // A consumer that requires a parameterless provision (one whose serves names no consumer key — // redis-cache, amqp) contributes no payload, but it still ASKS for the provision and must be // granted a credential. Keying "asks" on contributions alone gave its grant From="" — read as // withdrawn — so the provider never created the account and the consumer authenticated nowhere. r := mcat.Resolution{ Node: "laptop", Modules: []mcat.Manifest{{ Module: "amqp-ping", Requires: []string{"amqp"}, }}, } values, asks, err := ContributionsFrom(r, "amqp", "amqp-ping", mcat.SettingsBy{}) if err != nil { t.Fatal(err) } if !asks { t.Fatal("a require-only consumer was treated as not asking; its grant would be withdrawn and it would never be provisioned") } if values == nil { t.Fatalf("asks is true but values is nil; expected an empty contribution, got %v", values) } } func TestAConsumerThatStoppedAskingIsWithdrawn(t *testing.T) { // Withdrawal is how a credential is taken away. The provisioner removes what nobody asks for, // and it can only do that if the mesh stops asking — a consumer that was unassigned would // otherwise keep a working login for ever, and nothing would say so. // // A grant with no asking module is exactly that state: the mesh still holds the secret, // because it is sealed and unusable to the mesh anyway, and the machine no longer wants it. got, err := Resolve(shelf(provider()), []string{"postgres"}, reachable(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(got, mcat.Rendering{Grants: []mcat.Grant{ {Provision: "postgres-database", Consumer: "still-here", From: "meshboard", Values: map[string]any{"name": "meshboard"}, Sealed: "c2VhbGVk"}, {Provision: "postgres-database", Consumer: "gone-away", Sealed: "c3RhbGU="}, }}) if err != nil { t.Fatal(err) } given := grantedTo(t, out) if len(given) != 1 || given[0].Node != "still-here" { t.Fatalf("the provider is still told about a machine that stopped asking: %v", given) } // And no credential is written for it either, or the provisioner would find a file for a // consumer its manifest does not mention and have to guess what that means. for _, r := range out { if path, _ := r["path"].(string); strings.Contains(path, "gone-away") { t.Fatalf("a withdrawn consumer's credential is still delivered: %v", r) } } }