Contributions were node-local, so a mesh-scoped provider — the one case that most needs them — never heard from its consumers. A database was given a password and no idea what to create it for. Cross-node consumers now reach the provider's `receives` file, merged in with the ones on its own machine: from the provider's side they are the same thing, and a provider that had to read two lists would read one of them. Each names the file its credential is in rather than carrying it, because the mesh discarded the value and could not put it there. The readable half therefore stays readable. And examples/postgres-provisioner, which is the last step: it reads what the host wrote and makes PostgreSQL accept it. Explicitly not part of the control plane — the control plane decides and never touches a machine. This runs on the machine and touches it, and a real one ships with the module that ships PostgreSQL. It lives here because this is where the contract is defined, written as something that runs so it can be read. It reconciles rather than applying a change, because it is never told what changed. Three things that follow, and each is a fault somebody has shipped: - the password is set every time, not only on creation, or a rotation reports success and changes nothing - what it made and nobody asks for any more is revoked, or a departed consumer keeps a working login for ever - what it did not make is left alone, or it cannot be run on a database that predates it Proven in the lab against a real PostgreSQL, each assertion confirmed to fail with the behaviour removed. The suite is in mesh-lab, which also records the two ways the test itself was wrong first.
333 lines
12 KiB
Go
333 lines
12 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The other half of an edge.
|
|
//
|
|
// `requires` says a thing must be there. It never said what to do with it — a web application
|
|
// requiring a reverse proxy has to say *which name, which port*, and there was nowhere to put
|
|
// that. The two modules that needed it most, the proxy and the VPN, went round the outside and
|
|
// opened a connection to the control plane's database, which is why every node held a credential
|
|
// to it permanently.
|
|
|
|
func published(module, host string, port int) Manifest {
|
|
return Manifest{Module: module, Version: "1",
|
|
Contributes: map[string]map[string]any{
|
|
"reverse-proxy": {"host": host, "port": port},
|
|
}}
|
|
}
|
|
|
|
func proxy() Manifest {
|
|
return Manifest{Module: "traefik", Version: "1",
|
|
Provides: 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{ReceivedID("reverse-proxy")}},
|
|
}}
|
|
}
|
|
|
|
// received digs the delivered contributions back out of a declaration.
|
|
func received(t *testing.T, out []map[string]any) []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 []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(), 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(), World{})
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := got.Declaration(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(), 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(), 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(), 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(), 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(), 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."+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(), World{})
|
|
|
|
out, err := got.Declaration(Rendering{Settings: 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 TestReceivingWhatYouDoNotProvideIsRefused(t *testing.T) {
|
|
// It would create a file nobody ever writes to, on a machine where nothing asked for it.
|
|
_, err := ParseManifest([]byte(`{"module":"traefik","version":"1",
|
|
"receives":{"reverse-proxy":"/etc/traefik/mesh.json"}}`))
|
|
if err == nil {
|
|
t.Fatal("a module received contributions to something it does not provide")
|
|
}
|
|
if !strings.Contains(err.Error(), "does not provide") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyContributionIsRefused(t *testing.T) {
|
|
// Either a mistake or a requirement written the long way round, and both are better said.
|
|
_, err := ParseManifest([]byte(`{"module":"board","version":"1",
|
|
"contributes":{"reverse-proxy":{}}}`))
|
|
if err == nil {
|
|
t.Fatal("a module contributed nothing and was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "require it") {
|
|
t.Fatalf("the refusal does not say what to do instead: %v", err)
|
|
}
|
|
}
|
|
|
|
// A provision answered from anywhere in the mesh has consumers on other machines, and the
|
|
// provider has to know who they are. Contributions were node-local until this, which meant the
|
|
// one case that most needed them was the one they did not reach.
|
|
|
|
func provider() Manifest {
|
|
return Manifest{Module: "postgres", Version: "1",
|
|
Provides: FromAnywhere("database"),
|
|
Receives: map[string]string{"database": "/var/lib/postgres/grants/mesh.json"},
|
|
Grants: map[string]string{"database": "/var/lib/postgres/grants"},
|
|
}
|
|
}
|
|
|
|
// oneGrant is a declaration with a single consumer on another machine.
|
|
func oneGrant(t *testing.T) []map[string]any {
|
|
t.Helper()
|
|
got, err := Resolve(shelf(provider()), []string{"postgres"}, reachable(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := got.Declaration(Rendering{Grants: []Grant{{
|
|
Provision: "database", Consumer: "workstation", From: "meshboard",
|
|
Values: map[string]any{"name": "meshboard"}, Sealed: "c2VhbGVk",
|
|
}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func grantedTo(t *testing.T, out []map[string]any) []Contribution {
|
|
t.Helper()
|
|
for _, r := range out {
|
|
if r["path"] != "/var/lib/postgres/grants/mesh.json" {
|
|
continue
|
|
}
|
|
var parsed struct {
|
|
Given []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 TestAProviderIsToldAboutConsumersOnOtherMachines(t *testing.T) {
|
|
// A database told to create a password and not who for can do nothing with it.
|
|
given := grantedTo(t, oneGrant(t))
|
|
if len(given) != 1 {
|
|
t.Fatalf("got %v", given)
|
|
}
|
|
if given[0].Node != "workstation" || given[0].From != "meshboard" {
|
|
t.Fatalf("it does not say who asked: %v", given[0])
|
|
}
|
|
if given[0].Values["name"] != "meshboard" {
|
|
t.Fatalf("it does not say what was asked for: %v", given[0].Values)
|
|
}
|
|
}
|
|
|
|
func TestTheManifestNamesTheFileRatherThanCarryingTheCredential(t *testing.T) {
|
|
// The mesh discarded the value and could not put it here if it wanted to. What is here is
|
|
// where to find it — and the readable half therefore stays readable.
|
|
out := oneGrant(t)
|
|
given := grantedTo(t, out)
|
|
if given[0].Secret != "/var/lib/postgres/grants/workstation.secret" {
|
|
t.Fatalf("the manifest does not name the credential's file: %q", given[0].Secret)
|
|
}
|
|
for _, r := range out {
|
|
if r["path"] != "/var/lib/postgres/grants/mesh.json" {
|
|
continue
|
|
}
|
|
if strings.Contains(r["content"].(string), "c2VhbGVk") {
|
|
t.Fatal("the readable manifest carries the sealed credential")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheCredentialItselfLandsSealedBesideIt(t *testing.T) {
|
|
for _, r := range oneGrant(t) {
|
|
if r["path"] != "/var/lib/postgres/grants/workstation.secret" {
|
|
continue
|
|
}
|
|
if r["sealed"] != "c2VhbGVk" {
|
|
t.Fatalf("got %v", r)
|
|
}
|
|
if r["content"] != nil {
|
|
t.Fatal("a credential was written in the clear")
|
|
}
|
|
return
|
|
}
|
|
t.Fatal("no credential file")
|
|
}
|
|
|
|
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(), 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)
|
|
}
|
|
}
|