ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
123 lines
4.9 KiB
Go
123 lines
4.9 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// anchored is a machine on the private network, which is where a name worth certifying comes from.
|
|
func anchored() mcat.Node {
|
|
n := workstation()
|
|
n.At = "workstation.internal"
|
|
return n
|
|
}
|
|
|
|
// authority is a module that must put its own machine's name into a file it writes.
|
|
func authority() mcat.Manifest {
|
|
return mcat.Manifest{
|
|
Module: "authority", Version: "1",
|
|
Provides: mcat.FromAnywhere("acme-ca"),
|
|
Serves: map[string]map[string]any{"acme-ca": {}},
|
|
Resources: []map[string]any{{
|
|
"id": "init", "type": "file", "path": "/var/lib/authority/init.env",
|
|
"content": "NAMES=${machine:at},${machine:name},localhost\n",
|
|
}},
|
|
}
|
|
}
|
|
|
|
func TestAModuleNamesTheMachineItWasAssignedTo(t *testing.T) {
|
|
got, err := Resolve(shelf(authority()), []string{"authority"}, anchored(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
content := ""
|
|
for _, r := range mustDeclare(t, got) {
|
|
if strings.HasSuffix(plainly(r["id"]), "init") {
|
|
content = plainly(r["content"])
|
|
}
|
|
}
|
|
if content == "" {
|
|
t.Fatal("the authority's file was not declared")
|
|
}
|
|
if strings.Contains(content, "${machine:") {
|
|
t.Fatalf("the placeholder was written into the file as a value: %q", content)
|
|
}
|
|
// The machine's own name on the private network — the one a consumer is handed as `at`, and so
|
|
// the one a certificate has to carry.
|
|
if !strings.Contains(content, "workstation.internal") {
|
|
t.Errorf("the file does not name the address consumers reach it at: %q", content)
|
|
}
|
|
}
|
|
|
|
// A machine not on the private network has no such address, and a module asking to be reached at
|
|
// one is named here rather than left to fail later as a certificate nobody can verify.
|
|
func TestAnAddressAMachineDoesNotHaveIsRefused(t *testing.T) {
|
|
got, err := Resolve(shelf(authority()), []string{"authority"}, workstation(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Declaration(got, mcat.Rendering{}); err == nil {
|
|
t.Fatal("an address the machine does not have was substituted rather than refused")
|
|
} else if !strings.Contains(err.Error(), "at") {
|
|
t.Errorf("the refusal does not name what was asked for: %v", err)
|
|
}
|
|
}
|
|
|
|
// A module that must give software the machine's ADDRESS rather than its name — a resolver pointing
|
|
// the container runtime at itself, whose list of resolvers cannot be a name — says
|
|
// ${machine:address}, and gets what the machine's name resolves to on the private network: the same
|
|
// address every other machine's hosts file carries for it.
|
|
func TestAModuleNamesTheAddressBehindItsMachinesName(t *testing.T) {
|
|
pointing := mcat.Manifest{Module: "pointing", Version: "1", Resources: []map[string]any{{
|
|
"id": "runtime", "type": "file", "path": "/etc/runtime.json",
|
|
"content": `{"dns":["${machine:address}"]}`,
|
|
}}}
|
|
got, err := Resolve(shelf(pointing), []string{"pointing"}, anchored(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Declaration(got, mcat.Rendering{Names: map[string]string{
|
|
"workstation.internal": "10.42.0.7", "anchor.internal": "10.42.0.1"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if content := plainly(out[0]["content"]); content != `{"dns":["10.42.0.7"]}` {
|
|
t.Fatalf("the machine's address was not the one its name resolves to: %q", content)
|
|
}
|
|
|
|
// Off the network there is no such address, and the refusal says what the machine does have —
|
|
// rather than a placeholder written into the runtime's file and read as an address.
|
|
got, err = Resolve(shelf(pointing), []string{"pointing"}, workstation(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Declaration(got, mcat.Rendering{}); err == nil || !strings.Contains(err.Error(), "${machine:address}") {
|
|
t.Fatalf("a machine off the network was given an address, or refused for another reason: %v", err)
|
|
}
|
|
}
|
|
|
|
// The mesh's private range is offered as ${machine:mesh-range}, so a module names it rather than
|
|
// hardcoding a value it cannot know (novox/hq ADR 0112) — the fail2ban ignoreip is the case.
|
|
func TestAModuleNamesTheMeshRange(t *testing.T) {
|
|
facts := machineFacts(mcat.Resolution{Node: "anchor", At: "anchor.internal"}, map[string]string{"anchor.internal": "10.10.0.1"}, "10.10.0.0/24")
|
|
if facts["mesh-range"] != "10.10.0.0/24" {
|
|
t.Fatalf("the mesh range is not a machine fact: %v", facts)
|
|
}
|
|
res := map[string]any{"type": "file", "id": "jail", "content": "ignoreip = 127.0.0.1/8 ${machine:mesh-range}\n"}
|
|
if err := machineInto(res, facts, "fail2ban"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := res["content"].(string); !strings.Contains(got, "10.10.0.0/24") || strings.Contains(got, "${machine:") {
|
|
t.Fatalf("the mesh range was not written in: %q", got)
|
|
}
|
|
// A mesh with no range gives no such fact, and a file that names it is refused rather than
|
|
// left with a literal placeholder in it.
|
|
none := machineFacts(mcat.Resolution{Node: "anchor"}, nil, "")
|
|
if _, has := none["mesh-range"]; has {
|
|
t.Fatal("a mesh with no range still offered one")
|
|
}
|
|
}
|