205 lines
7.6 KiB
Go
205 lines
7.6 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A module whose files cannot be written in advance.
|
|
//
|
|
// The mesh's private network is the case: a machine's peer list is derived from every other
|
|
// machine, so it differs on each one and changes when any of them changes. What matters in these
|
|
// tests is not that it works, but that being computed changes *nothing else* about being a
|
|
// module — it is assigned, resolved, settled and absent when nobody asked for it.
|
|
|
|
type fake struct {
|
|
on map[string]bool
|
|
asked []string
|
|
err error
|
|
}
|
|
|
|
func (f *fake) Resources(node string) ([]map[string]any, bool, error) {
|
|
f.asked = append(f.asked, node)
|
|
if f.err != nil {
|
|
return nil, false, f.err
|
|
}
|
|
if !f.on[node] {
|
|
return nil, false, nil
|
|
}
|
|
return []map[string]any{{"id": "peers", "type": "file", "path": "/etc/x.conf",
|
|
"merge": MergeJSON, "content": `{"peer":"` + node + `"}`}}, true, nil
|
|
}
|
|
|
|
func computedShelf() map[string]Manifest {
|
|
return shelf(Manifest{Module: "mesh-network", Computed: "mesh-network",
|
|
Provides: Offers("private-network")})
|
|
}
|
|
|
|
func TestAComputedModuleIsAskedAboutTheNodeItIsFor(t *testing.T) {
|
|
// The generator gets a node name, not a plan. Everything it needs is the whole mesh, which
|
|
// it was built with — this is the one thing a node could never work out for itself.
|
|
got, err := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gen.asked) != 1 || gen.asked[0] != "workstation" {
|
|
t.Fatalf("asked about %v, wanted workstation once", gen.asked)
|
|
}
|
|
if len(out) != 1 || !strings.Contains(out[0]["content"].(string), `"peer": "workstation"`) {
|
|
t.Fatalf("got %v", out)
|
|
}
|
|
}
|
|
|
|
func TestAMachineNobodyGaveItGetsNothing(t *testing.T) {
|
|
// The whole point of making the network a module. Before this, every machine with an address
|
|
// was on the private network and there was no way to say one should stay off.
|
|
got, err := Resolve(computedShelf(), nil, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out) != 0 {
|
|
t.Fatalf("a machine that was assigned nothing got %d resource(s)", len(out))
|
|
}
|
|
if len(gen.asked) != 0 {
|
|
t.Fatalf("the generator was asked about %v, and nobody had asked for it", gen.asked)
|
|
}
|
|
}
|
|
|
|
func TestAssignedAndNotYetPartOfItIsNotAFailure(t *testing.T) {
|
|
// A machine given the network module before it has a place on it. Brief and ordinary — the
|
|
// answer is "nothing yet", and treating it as an error would make an ordering a fault.
|
|
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
|
gen := &fake{on: map[string]bool{}}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
|
if err != nil {
|
|
t.Fatalf("refused a node that is not on the network yet: %v", err)
|
|
}
|
|
if len(out) != 0 {
|
|
t.Fatalf("got %v", out)
|
|
}
|
|
}
|
|
|
|
func TestAGeneratorThisControlPlaneDoesNotHaveIsRefused(t *testing.T) {
|
|
// Sending a machine a module with no files would look like it worked. Named in the message,
|
|
// because the only fix is a control plane that has it.
|
|
got, _ := Resolve(shelf(Manifest{Module: "weather", Computed: "the-weather"}), []string{"weather"}, workstation(), World{})
|
|
|
|
_, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": &fake{}}})
|
|
if err == nil {
|
|
t.Fatal("a module computed by nothing was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "the-weather") {
|
|
t.Fatalf("the refusal does not name what is missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAModuleIsEitherWrittenOrComputedNotBoth(t *testing.T) {
|
|
// Otherwise nobody could say where a given file on a machine came from.
|
|
_, err := ParseManifest([]byte(`{"module":"mesh-network","version":"1",
|
|
"computed":"mesh-network",
|
|
"resources":[{"id":"a","type":"package","package":"wireguard-tools"}]}`))
|
|
if err == nil {
|
|
t.Fatal("a module that both ships files and has them computed was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "one or the other") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSettingsApplyToAComputedModuleToo(t *testing.T) {
|
|
// Being computed is about where the files come from, not about whether they are configurable.
|
|
// A line drawn there would be arbitrary and nobody could predict it.
|
|
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
|
out, err := got.Declaration(Rendering{
|
|
Generators: map[string]Generator{"mesh-network": gen},
|
|
Settings: SettingsBy{"mesh-network": {{From: "the mesh",
|
|
Values: map[string]any{"keepalive": 25}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := out[0]["content"].(string)
|
|
if !strings.Contains(content, `"keepalive": 25`) {
|
|
t.Fatalf("the setting did not reach a computed file: %s", content)
|
|
}
|
|
if !strings.Contains(content, `"peer": "workstation"`) {
|
|
t.Fatalf("the setting replaced what the generator computed: %s", content)
|
|
}
|
|
}
|
|
|
|
func TestTwoWaysToBeOnAPrivateNetworkRefuseAndNameBoth(t *testing.T) {
|
|
// WireGuard is one way. The refusing rule is what makes a second one safe to add: assigning
|
|
// both is caught rather than producing a machine on two networks that each half-work.
|
|
_, err := Resolve(shelf(
|
|
Manifest{Module: "mesh-network", Computed: "mesh-network",
|
|
Provides: Offers("private-network")},
|
|
Manifest{Module: "tailscale", Provides: Offers("private-network")},
|
|
Manifest{Module: "backups", Requires: []string{"private-network"}},
|
|
), []string{"backups"}, workstation(), World{})
|
|
|
|
if err == nil {
|
|
t.Fatal("two answers to one requirement were taken silently")
|
|
}
|
|
for _, want := range []string{"mesh-network", "tailscale", "private-network"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("the refusal does not name %s: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// reloading answers the runtime's trust as the networking module does (novox/hq ADR 0102): a file
|
|
// written into, and the runtime reloaded on it.
|
|
type reloading struct{}
|
|
|
|
func (reloading) Resources(node string) ([]map[string]any, bool, error) {
|
|
return []map[string]any{
|
|
{"id": "registry-trust", "type": "file", "path": "/etc/docker/daemon.json",
|
|
"merge": MergeJSON, "into": "json", "content": `{"insecure-registries":["r:5000"]}`},
|
|
{"id": "registry-trust-reload", "type": "service", "unit": "docker.service",
|
|
"state": "running", "reload-on": []string{"registry-trust"}},
|
|
}, true, nil
|
|
}
|
|
|
|
func TestWhatAServiceIsReloadedOnIsNamedAsTheHostWillSeeIt(t *testing.T) {
|
|
// Ids are prefixed with their module on the way out. An unprefixed reload-on would name a
|
|
// resource the host never sees, and the runtime would never be reloaded for its trust.
|
|
got, err := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": reloading{}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var file, service map[string]any
|
|
for _, r := range out {
|
|
switch r["type"] {
|
|
case "file":
|
|
file = r
|
|
case "service":
|
|
service = r
|
|
}
|
|
}
|
|
if file == nil || service == nil {
|
|
t.Fatalf("got %v", out)
|
|
}
|
|
if file["into"] != "json" {
|
|
t.Errorf("into did not reach the host: %v", file)
|
|
}
|
|
if want := "[" + file["id"].(string) + "]"; fmt.Sprint(service["reload-on"]) != want {
|
|
t.Errorf("reload-on names %v, the file is %v", service["reload-on"], file["id"])
|
|
}
|
|
}
|