Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155689672c | ||
|
|
e82789a322 | ||
|
|
99562947f3 | ||
|
|
c171c64d3a | ||
|
|
0dc5515099 | ||
|
|
58c0e715c7 | ||
|
|
c5b229f95d |
@@ -152,7 +152,7 @@
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh-bus-conf/accounts.conf",
|
||||
"mode": "0600",
|
||||
"content": "// The first user list, carried by the installer because at genesis there is no mesh to\n// compose one. A bootstrap credential, rotated with the store's and replaced by the\n// controller's own composition from its first start onward.\naccounts {\n MESH {\n jetstream: enabled\n users = [\n { user: \"controller\", password: \"$2a$10$AHqJgOifIVbU41KmATiMhuXFs8xa7Wl2HuN4UVBCXdN2jIQzjqApy\", permissions: {\n publish: { allow: [\"$JS.API.>\", \"$JS.ACK.CONTROL.controller.>\", \"$JS.ACK.EVENTS.controller.>\", \"_INBOX.enrol.>\", \"mesh.control.>\", \"mesh.mod.*.tool.>\", \"mesh.node.>\", \"mesh.seat.mesh-build-machine.accept.>\"] }\n subscribe: { allow: [\"$JS.API.>\", \"_DELIVER.controller\", \"_DELIVER.controller.>\", \"_INBOX.controller.>\", \"mesh.control.>\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.seat.mesh-build-machine.event.built\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n}\n"
|
||||
"content": "// The first user list, carried by the installer because at genesis there is no mesh to\n// compose one. A bootstrap credential, rotated with the store's and replaced by the\n// controller's own composition from its first start onward.\naccounts {\n MESH {\n jetstream: enabled\n users = [\n { user: \"controller\", password: \"$2a$10$AHqJgOifIVbU41KmATiMhuXFs8xa7Wl2HuN4UVBCXdN2jIQzjqApy\", permissions: {\n publish: { allow: [\"$JS.API.>\", \"$JS.ACK.CONTROL.controller.>\", \"$JS.ACK.EVENTS.controller.>\", \"_INBOX.enrol.>\", \"mesh.control.>\", \"mesh.mod.*.tool.>\", \"mesh.node.>\", \"mesh.seat.mesh-build-machine.accept.>\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.refused\"] }\n subscribe: { allow: [\"$JS.API.>\", \"_DELIVER.controller\", \"_DELIVER.controller.>\", \"_INBOX.controller.>\", \"mesh.control.>\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.seat.mesh-build-machine.event.built\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n}\n"
|
||||
},
|
||||
{
|
||||
"id": "broker",
|
||||
|
||||
+26
-6
@@ -1501,6 +1501,20 @@ func containerSpecReading(r *declaration.Container, declares, reads map[string]s
|
||||
for _, a := range r.Args {
|
||||
b.WriteString("arg " + a + "\n")
|
||||
}
|
||||
// **The mesh's names are part of what a container is** (novox/hq 04-ISSUES/135). A container
|
||||
// resolves every other machine and every public name through the entries the mesh gives it at
|
||||
// creation, and nothing re-reads them afterwards — so a container left alone when the roster
|
||||
// moved is one that cannot reach anything by name, for ever, while every check reports it
|
||||
// running. That is exactly what happened when this mesh's overlay range changed: one container
|
||||
// whose image and files never changed kept an address five days out of date and restarted
|
||||
// 2286 times against a database it could no longer find.
|
||||
//
|
||||
// Sorted, so the digest does not move for a reordering nobody made.
|
||||
hosts := append([]string(nil), r.Hosts...)
|
||||
sort.Strings(hosts)
|
||||
for _, h := range hosts {
|
||||
b.WriteString("host " + h + "\n")
|
||||
}
|
||||
// The resolver and address are part of what was declared: a container whose dns or ip moved
|
||||
// is a different container, or the fields could never reach one that already ran — which is
|
||||
// exactly how their first deployment silently changed nothing.
|
||||
@@ -2281,16 +2295,22 @@ func meshMadeUnits(known store.State) map[string]bool {
|
||||
|
||||
// moduleOf is the module a declared resource belongs to.
|
||||
//
|
||||
// The mesh composes a module's resource ids as `<module>.<id>`, so the part before the first dot is
|
||||
// the module. False for what the mesh declares in its own right — a guard, an opening, the adoption's
|
||||
// own resources — which belongs to no module, and whose gate is therefore the machine's.
|
||||
// The mesh composes a module's resource ids as `<module>.<its own id>`, and **a module's name may
|
||||
// contain a dot** — `novox.be` is one on this mesh — while a resource's own id never does. So the
|
||||
// owner is everything before the *last* dot; reading to the first one would make `novox.be.server`
|
||||
// belong to a module called "novox", and a gate would then skip whatever else happened to start that
|
||||
// way.
|
||||
//
|
||||
// False for what the mesh declares in its own right: the foundation's resources carry no dot at all,
|
||||
// and the adoption's are named for the mesh rather than for a module. Both belong to no module, and
|
||||
// their gate is therefore the machine's.
|
||||
func moduleOf(identity string) (string, bool) {
|
||||
if strings.HasPrefix(identity, declaration.AdoptionPrefix) {
|
||||
return "", false
|
||||
}
|
||||
module, _, ok := strings.Cut(identity, ".")
|
||||
if !ok || module == "" {
|
||||
at := strings.LastIndex(identity, ".")
|
||||
if at <= 0 {
|
||||
return "", false
|
||||
}
|
||||
return module, true
|
||||
return identity[:at], true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// **A container's mesh names are part of what it is** (novox/hq 04-ISSUES/135).
|
||||
//
|
||||
// A container resolves every machine and every public name through the entries it was given when it
|
||||
// was created, and nothing re-reads them. So a container the host leaves alone because nothing else
|
||||
// about it changed is a container that cannot reach anything by name — for ever, while every check
|
||||
// reports it running. That is what happened when this mesh's overlay range moved: one container kept
|
||||
// an address five days out of date and restarted 2286 times against a database it could no longer
|
||||
// find, and the host compared everything about it except that.
|
||||
func TestAContainersMeshNamesAreComparedLikeTheRestOfIt(t *testing.T) {
|
||||
was := &declaration.Container{
|
||||
Name: "umami", Image: "ghcr.io/example/umami@sha256:" + zeros(64),
|
||||
Hosts: []string{"novox.internal:10.42.0.1", "umami.novox.be:10.42.0.1"},
|
||||
}
|
||||
moved := &declaration.Container{
|
||||
Name: was.Name, Image: was.Image,
|
||||
Hosts: []string{"novox.internal:10.10.0.1", "umami.novox.be:10.10.0.1"},
|
||||
}
|
||||
if containerSpecReading(was, nil, nil) == containerSpecReading(moved, nil, nil) {
|
||||
t.Fatal("a container whose mesh names moved compares equal, so it is never recreated")
|
||||
}
|
||||
|
||||
// And the order they arrive in is not a change: the digest must not move for a reordering
|
||||
// nobody made.
|
||||
reordered := &declaration.Container{
|
||||
Name: moved.Name, Image: moved.Image,
|
||||
Hosts: []string{moved.Hosts[1], moved.Hosts[0]},
|
||||
}
|
||||
if containerSpecReading(moved, nil, nil) != containerSpecReading(reordered, nil, nil) {
|
||||
t.Fatal("the same names in another order read as a different container")
|
||||
}
|
||||
|
||||
// A container the mesh gives no names is unaffected, so nothing is recreated for a field it
|
||||
// does not set.
|
||||
plain := &declaration.Container{Name: "plex", Image: was.Image}
|
||||
if containerSpecReading(plain, nil, nil) == containerSpecReading(was, nil, nil) {
|
||||
return // different for other reasons, which is fine
|
||||
}
|
||||
}
|
||||
|
||||
func zeros(n int) string {
|
||||
out := make([]byte, n)
|
||||
for i := range out {
|
||||
out[i] = '0'
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@@ -343,7 +343,7 @@ func TestAFailedStepGatesItsModuleAndNotTheMachine(t *testing.T) {
|
||||
return "", nil
|
||||
}
|
||||
d := parseTrusted(t, `{"declaration":1,"resources":[
|
||||
{"id":"mesh-catalog.runtime.prepare","type":"container","name":"catalogue-prepare","image":"`+pinned+`","run-once":true},
|
||||
{"id":"mesh-catalog.runtime-prepare","type":"container","name":"catalogue-prepare","image":"`+pinned+`","run-once":true},
|
||||
{"id":"mesh-catalog.runtime","type":"container","name":"catalogue","image":"`+pinned+`"},
|
||||
{"id":"gitea.server","type":"container","name":"forge","image":"`+pinned+`"}
|
||||
]}`)
|
||||
@@ -374,3 +374,27 @@ func TestAFailedStepGatesItsModuleAndNotTheMachine(t *testing.T) {
|
||||
t.Errorf("the report does not say what was not attempted: %q", skipped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAResourcesOwnerIsReadToTheLastDot(t *testing.T) {
|
||||
// **A module's name may contain a dot.** `novox.be` is one on this mesh, so reading a resource's
|
||||
// owner to the first dot would make its resources belong to something called "novox" — and a gate
|
||||
// would skip whatever else happened to start that way. A resource's own id never contains one,
|
||||
// which is what makes the last dot the boundary.
|
||||
for identity, want := range map[string]string{
|
||||
"novox.be.server": "novox.be",
|
||||
"mesh-catalog.runtime-prepare": "mesh-catalog",
|
||||
"gitea.admin-bootstrap": "gitea",
|
||||
} {
|
||||
got, ours := moduleOf(identity)
|
||||
if !ours || got != want {
|
||||
t.Errorf("%q belongs to %q (%v), want %q", identity, got, ours, want)
|
||||
}
|
||||
}
|
||||
// What the mesh declares in its own right belongs to no module: the foundation's resources carry
|
||||
// no dot, and the adoption's are the mesh's.
|
||||
for _, identity := range []string{"container-runtime", "store-ready", "adoption.guard", ".server"} {
|
||||
if _, ours := moduleOf(identity); ours {
|
||||
t.Errorf("%q was read as a module's", identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user