Closes the half of 04-ISSUES/026 that would otherwise come back. The fourteen mounts across the forge, the mail system, the store and the object store are all declared now — but nothing said they had to be, so they were right by coincidence and the next volume added would not be. A bind mount whose source does not exist is created by the container runtime, as root, with a mode it picks. So `owner` and `mode` — which exist precisely so a module can say who its data belongs to — were silently not applied to the only directories holding data. And the rule written for exactly this case did not reach them. A directory the mesh declared and no longer wants is kept, not removed, when it holds anything the mesh did not put there (ADR 0030). That is the answer to *what happens to my data when a module goes away*, and it is written in terms of declared directories: an undeclared one sits outside it, because the mesh does not know it is there. Refused where it is written rather than on the machine, which cannot tell the difference — by the time the host sees the mount it is being asked to make a directory, which it is perfectly able to do. The fault is in the manifest, so it is named at the manifest. Same argument as the action refusal directly above it. A path under a declared directory counts as declared, as do the files a module already names: its own secrets, its grants, what it receives. Every real manifest is checked to still parse, and the refusal bites.
96 lines
4.2 KiB
Go
96 lines
4.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A module with nothing that publishes binds what it binds, and the mesh may not move it.
|
|
//
|
|
// This is the case that made novox/hq 04-ISSUES/028's fix wrong on its first pass: a port was
|
|
// assigned to every module that declared one, so a service listening directly had the rule set
|
|
// opened on a number nothing was listening on, and its real port shut. The firewall reported
|
|
// success and blocked the service, which is the exact failure the mechanism exists to prevent.
|
|
func TestAPortNothingPublishesIsNotTheMeshsToMove(t *testing.T) {
|
|
m := Manifest{Module: "talker", Listens: []Listening{{Port: 9101, From: FromMesh}}}
|
|
at, mayAssign := m.MachineSide(9101)
|
|
if mayAssign {
|
|
t.Fatal("the mesh took a port it cannot move: nothing translates it, so assigning one " +
|
|
"opens the wrong number and leaves the service unreachable")
|
|
}
|
|
if at != 9101 {
|
|
t.Fatalf("a port nothing publishes reaches the machine where it binds, not at %d", at)
|
|
}
|
|
}
|
|
|
|
// A container publishing in short form is exactly the case the mesh may choose.
|
|
func TestAContainerPublishingShortIsTheMeshsToChoose(t *testing.T) {
|
|
m := Manifest{Module: "store", Resources: []map[string]any{
|
|
{"type": "container", "id": "server", "ports": []any{"5432"}},
|
|
}}
|
|
if _, mayAssign := m.MachineSide(5432); !mayAssign {
|
|
t.Fatal("a container's mapping is what translates a port, so this one is the mesh's to " +
|
|
"choose; refusing it puts every module back on a number it guessed")
|
|
}
|
|
}
|
|
|
|
// A manifest that wrote its own mapping already chose, and the machine side is the outer one.
|
|
func TestAMappingTheManifestWroteIsNotReassigned(t *testing.T) {
|
|
m := Manifest{Module: "mail", Resources: []map[string]any{
|
|
{"type": "container", "id": "front", "ports": []any{"7080:80"}},
|
|
}}
|
|
for _, named := range []int{7080, 80} {
|
|
at, mayAssign := m.MachineSide(named)
|
|
if mayAssign {
|
|
t.Fatalf("%d was reassigned though the manifest published it explicitly, which "+
|
|
"would open a rule on a port the container does not publish", named)
|
|
}
|
|
if at != 7080 {
|
|
t.Fatalf("naming %d gave %d; the machine side of 7080:80 is 7080", named, at)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A port some other container publishes is not this port.
|
|
func TestAPortNotInTheMappingIsNotFound(t *testing.T) {
|
|
m := Manifest{Module: "mail", Resources: []map[string]any{
|
|
{"type": "container", "id": "front", "ports": []any{"25", "7080:80"}},
|
|
}}
|
|
if at, mayAssign := m.MachineSide(993); mayAssign || at != 993 {
|
|
t.Fatalf("993 is published by nothing here, so it binds where it binds: got %d, %v",
|
|
at, mayAssign)
|
|
}
|
|
}
|
|
|
|
// A bind mount the module never declared is refused where it is written.
|
|
//
|
|
// The container runtime creates a missing bind source itself, as root, with a mode it picks. So
|
|
// the module's own owner and mode never reach the directory holding its data, and the rule that
|
|
// keeps data when a module goes away (novox/hq ADR 0030) does not cover it — that rule is written
|
|
// in terms of declared directories, and the mesh has never heard of this one.
|
|
func TestAMountNothingDeclaresIsRefused(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"store","resources":[` +
|
|
`{"id":"server","type":"container","name":"store","image":"x@sha256:` +
|
|
`0000000000000000000000000000000000000000000000000000000000000000",` +
|
|
`"volumes":["/services/store/data:/var/lib/data"]}]}`))
|
|
if err == nil {
|
|
t.Fatal("a container mounting a path no resource declares was accepted; the runtime " +
|
|
"would create it as root and the module's owner and mode would never apply")
|
|
}
|
|
if !strings.Contains(err.Error(), "/services/store/data") {
|
|
t.Fatalf("refused without naming the path, which leaves the author guessing: %v", err)
|
|
}
|
|
}
|
|
|
|
// And declaring it is enough — including declaring the directory above it.
|
|
func TestAMountUnderADeclaredDirectoryIsAccepted(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"store","resources":[` +
|
|
`{"id":"state","type":"directory","path":"/services/store","mode":"0700"},` +
|
|
`{"id":"server","type":"container","name":"store","image":"x@sha256:` +
|
|
`0000000000000000000000000000000000000000000000000000000000000000",` +
|
|
`"volumes":["/services/store/data:/var/lib/data"]}]}`))
|
|
if err != nil {
|
|
t.Fatalf("a module that said where its data lives was refused anyway: %v", err)
|
|
}
|
|
}
|