On an adopted machine the private network takes the predecessor's tunnel over in place (hq ADR 0105). Genesis finds the one interface up besides the mesh's own, settles the hub's port and the mesh's range on it, and skips ADR 0100's non-overlap check for a range that is now the tunnel's; a --hub-port or --overlay-range that disagrees is refused naming the tunnel's. At enrolment the found interface's private key becomes this node's overlay key — the one credential the mesh takes rather than mints — stored where a generated one is stored, never printed and never sent; the tunnel (port, address, range, peers) travels with the keys so the mesh composes from it before the first declaration. The interface's service may say what it takes over. Before the mesh's unit starts, the found configuration is kept like any held file and the found unit is stopped and disabled; nothing is flushed, and an interface still up after its unit stopped refuses the takeover rather than half-working. The report says what was carried: interface, port, range, peer count, taken or not, and where the original was kept.
133 lines
5.6 KiB
Go
133 lines
5.6 KiB
Go
package declaration
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0100: every declaration says whether the node is adopted and which of its
|
|
// modules are taken, and the host refuses one it cannot read that from unambiguously.
|
|
|
|
const adoptedResources = `"resources":[
|
|
{"id":"hello-web.page","type":"file","path":"/var/lib/hello-web/index.html","content":"a\n"},
|
|
{"id":"hello-web.server","type":"container","name":"hello-web","image":"sha256:` + sixtyFour + `"},
|
|
{"id":"hello-web.data","type":"directory","path":"/var/lib/hello-web"}
|
|
]`
|
|
|
|
const sixtyFour = "0000000000000000000000000000000000000000000000000000000000000000"
|
|
|
|
func TestAnAdoptionIsReadWithTheDeclaration(t *testing.T) {
|
|
d, err := Parse([]byte(`{"adoption":{"taken":["postgres"],"untaken":{"hello-web":["hello-web.page","hello-web.server"]}},
|
|
"declaration":1,` + adoptedResources + `}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d.Adoption == nil {
|
|
t.Fatal("the adoption was dropped")
|
|
}
|
|
if len(d.Adoption.Taken) != 1 || d.Adoption.Taken[0] != "postgres" {
|
|
t.Errorf("taken read as %v", d.Adoption.Taken)
|
|
}
|
|
if module, ok := d.Adoption.UntakenModuleOf("hello-web.server"); !ok || module != "hello-web" {
|
|
t.Errorf("the container's untaken module read as %q, %v", module, ok)
|
|
}
|
|
if _, ok := d.Adoption.UntakenModuleOf("hello-web.data"); ok {
|
|
t.Error("a resource the adoption does not name was said to be untaken")
|
|
}
|
|
}
|
|
|
|
func TestADeclarationWithNoAdoptionIsConverged(t *testing.T) {
|
|
d, err := Parse([]byte(`{"declaration":1,` + adoptedResources + `}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d.Adoption != nil {
|
|
t.Errorf("a declaration saying nothing about adoption read as adopted: %+v", d.Adoption)
|
|
}
|
|
if _, ok := d.Adoption.UntakenModuleOf("hello-web.page"); ok {
|
|
t.Error("a converged node has an untaken module")
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptionNamingAnUnknownIDIsRefused(t *testing.T) {
|
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"hello-web":["hello-web.missing"]}},
|
|
"declaration":1,`+adoptedResources+`}`)
|
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "hello-web.missing") {
|
|
t.Errorf("the unknown id was not named: %v", refusal.Problems)
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptionMayNameAResourceOfAnyKind(t *testing.T) {
|
|
// A directory, a service or an action can reach what was found as surely as a file can, so
|
|
// the controller lists every resource of an untaken module (novox/hq ADR 0103).
|
|
d, err := Parse([]byte(`{"adoption":{"taken":[],"untaken":{"hello-web":["hello-web.data"]}},
|
|
"declaration":1,` + adoptedResources + `}`))
|
|
if err != nil {
|
|
t.Fatalf("a directory of an untaken module was refused: %v", err)
|
|
}
|
|
if module, ok := d.Adoption.UntakenModuleOf("hello-web.data"); !ok || module != "hello-web" {
|
|
t.Errorf("the directory is not its module's: %q %v", module, ok)
|
|
}
|
|
}
|
|
|
|
func TestAnIDUnderTwoModulesIsRefused(t *testing.T) {
|
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"a":["hello-web.page"],"b":["hello-web.page"]}},
|
|
"declaration":1,`+adoptedResources+`}`)
|
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "both") {
|
|
t.Errorf("an id under two modules was accepted: %v", refusal.Problems)
|
|
}
|
|
}
|
|
|
|
func TestAModuleBothTakenAndUntakenIsRefused(t *testing.T) {
|
|
refusal := refusalFor(t, `{"adoption":{"taken":["hello-web"],"untaken":{"hello-web":["hello-web.page"]}},
|
|
"declaration":1,`+adoptedResources+`}`)
|
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "both taken and untaken") {
|
|
t.Errorf("a module both taken and untaken was accepted: %v", refusal.Problems)
|
|
}
|
|
}
|
|
|
|
func TestTheMeshsOwnResourcesAreNeverUntaken(t *testing.T) {
|
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"x":["adoption.guard"]}},
|
|
"declaration":1,"resources":[
|
|
{"id":"adoption.guard","type":"file","path":"/etc/mesh/guard.nft","content":"x"}]}`)
|
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "belongs to no module") {
|
|
t.Errorf("an adoption. id was accepted as untaken: %v", refusal.Problems)
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptionWithAnUnknownFieldIsRefused(t *testing.T) {
|
|
refusalFor(t, `{"adoption":{"taken":[],"held":["x"]},"declaration":1,`+adoptedResources+`}`)
|
|
}
|
|
|
|
func TestACarriedBundleCannotSayTheNodeIsAdopted(t *testing.T) {
|
|
_, err := ParseTrusted([]byte(`{"adoption":{"taken":[]},"declaration":1,` + adoptedResources + `}`))
|
|
if err == nil || !strings.Contains(err.Error(), "only the mesh can say") {
|
|
t.Fatalf("a bundle claiming adoption was not refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0105: a service may take over a found tunnel, said whole and on an adopted node.
|
|
func TestTakingOverATunnelIsSaidWholeAndForARunningService(t *testing.T) {
|
|
adoptedWith := func(service string) error {
|
|
_, err := Parse([]byte(`{"declaration":1,"adoption":{"taken":[]},"resources":[` + service + `]}`))
|
|
return err
|
|
}
|
|
good := `{"id":"up","type":"service","unit":"wg-quick@mesh0","state":"running",
|
|
"takes-over":{"interface":"wg0","unit":"wg-quick@wg0","config":"/etc/wireguard/wg0.conf"}}`
|
|
if err := adoptedWith(good); err != nil {
|
|
t.Fatalf("a whole takeover on an adopted node was refused: %v", err)
|
|
}
|
|
for name, bad := range map[string]string{
|
|
"its own unit": `{"id":"up","type":"service","unit":"wg-quick@wg0","state":"running",
|
|
"takes-over":{"interface":"wg0","unit":"wg-quick@wg0","config":"/etc/wireguard/wg0.conf"}}`,
|
|
"no config": `{"id":"up","type":"service","unit":"wg-quick@mesh0","state":"running",
|
|
"takes-over":{"interface":"wg0","unit":"wg-quick@wg0"}}`,
|
|
"a stopped service": `{"id":"up","type":"service","unit":"wg-quick@mesh0","state":"stopped",
|
|
"takes-over":{"interface":"wg0","unit":"wg-quick@wg0","config":"/etc/wireguard/wg0.conf"}}`,
|
|
} {
|
|
if err := adoptedWith(bad); err == nil {
|
|
t.Errorf("a takeover naming %s was accepted", name)
|
|
}
|
|
}
|
|
}
|