Merge pull request 'The private network writes nothing into the runtime's file; generated resources are collision-checked (hq ADR 0222, issue 190 — 3 of 3)' (#63) from fix/190-the-overlay-writes-no-runtime-file into main
This commit was merged in pull request #63.
This commit is contained in:
@@ -275,25 +275,9 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// No registry trust is composed here any more: the container runtime's module states it, told
|
||||||
|
// where the store is reached by ${seat:mesh-artifact-store:reach} (novox/hq ADR 0222, issue 190).
|
||||||
g, err := overlay.From(nodes, cidr, "")
|
g, err := overlay.From(nodes, cidr, "")
|
||||||
if g != nil {
|
|
||||||
// The artifact store, as this network reaches it. Found rather than configured: the
|
|
||||||
// provider is whichever module offers it, on whichever machine holds that module — and if
|
|
||||||
// nothing does yet (genesis raises the registry before the catalogue knows it), there is
|
|
||||||
// no trust to write and nothing is written (novox/hq ADR 0082).
|
|
||||||
//
|
|
||||||
// Refused rather than composed without it when the question could not be answered: a
|
|
||||||
// declaration missing the trust because a lookup failed is a machine that cannot pull,
|
|
||||||
// delivered by a push that reported success — and nothing recomposes it until the next
|
|
||||||
// push (the shape of novox/hq issues 042/048, reappearing as a race).
|
|
||||||
at, port, found, storeErr := artifactStoreOnNetwork(ctx, inv, on)
|
|
||||||
if storeErr != nil {
|
|
||||||
return nil, fmt.Errorf("finding the artifact store this network reaches: %w", storeErr)
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
g.TrustRegistry(overlay.InternalName(at) + ":" + port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil && len(refused) > 0 {
|
if err != nil && len(refused) > 0 {
|
||||||
// The network is missing something, and some machines could not be resolved at all. Those
|
// The network is missing something, and some machines could not be resolved at all. Those
|
||||||
// are almost always the same fact: a node that does not resolve contributes nothing, so
|
// are almost always the same fact: a node that does not resolve contributes nothing, so
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ func TestTwoWaysToBeOnAPrivateNetworkRefuseAndNameBoth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reloading answers the runtime's trust as the networking module does (novox/hq ADR 0102): a file
|
// reloading answers the runtime's trust as the networking module once did (novox/hq ADR 0102; it no
|
||||||
|
// longer does, ADR 0222 — and beside the runtime's module it is refused, generated_collision_test): a file
|
||||||
// written into, and the runtime reloaded on it.
|
// written into, and the runtime reloaded on it.
|
||||||
type reloading struct{}
|
type reloading struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -431,6 +431,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generated, err := r.generatedHere(with)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var out []map[string]any
|
var out []map[string]any
|
||||||
for _, m := range r.Modules {
|
for _, m := range r.Modules {
|
||||||
if with.Adopted && m.Filtering != nil {
|
if with.Adopted && m.Filtering != nil {
|
||||||
@@ -697,23 +702,14 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
first = append(first, file)
|
first = append(first, file)
|
||||||
}
|
}
|
||||||
if m.Computed != "" {
|
if m.Computed != "" {
|
||||||
generator, known := with.Generators[m.Computed]
|
mine, part := generated[m.Module]
|
||||||
if !known {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%s says its resources are computed by %q, and this control plane has no %q",
|
|
||||||
m.Module, m.Computed, m.Computed)
|
|
||||||
}
|
|
||||||
generated, part, err := generator.Resources(r.Node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !part {
|
if !part {
|
||||||
// Assigned, and not yet part of what this generates. Nothing to put on the
|
// Assigned, and not yet part of what this generates. Nothing to put on the
|
||||||
// machine, which is different from an error: a node given the network module
|
// machine, which is different from an error: a node given the network module
|
||||||
// before it has an address is in exactly that state, briefly.
|
// before it has an address is in exactly that state, briefly.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resources = generated
|
resources = mine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, and not before: a module whose resources are computed replaces them wholesale, and
|
// Now, and not before: a module whose resources are computed replaces them wholesale, and
|
||||||
@@ -2353,3 +2349,49 @@ func accountsFirst(resources []map[string]any) (accounts, rest []map[string]any)
|
|||||||
}
|
}
|
||||||
return accounts, rest
|
return accounts, rest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generatedHere is what each computed module's generator answers for this machine, by module —
|
||||||
|
// absent for a module whose machine is not yet part of what it generates — held to the rule every
|
||||||
|
// module is held to: no two modules on a machine declare one path, unit, name or package (novox/hq
|
||||||
|
// issue 190, step 5; ADR 0222).
|
||||||
|
//
|
||||||
|
// Resolution checks the catalogue's manifests, and a computed module's manifest has none of the
|
||||||
|
// resources it will declare — they exist only once its generator has answered for this machine,
|
||||||
|
// here — so a generated resource writing into another module's file was never seen. That is how
|
||||||
|
// the private network came to declare the container runtime's daemon file and service beside the
|
||||||
|
// runtime's own module. Asked once, so what is checked is exactly what is declared.
|
||||||
|
func (r Resolution) generatedHere(with Rendering) (map[string][]map[string]any, error) {
|
||||||
|
generated := map[string][]map[string]any{}
|
||||||
|
placed := make([]Manifest, 0, len(r.Modules))
|
||||||
|
for _, m := range r.Modules {
|
||||||
|
if m.Computed == "" {
|
||||||
|
placed = append(placed, m)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
generator, known := with.Generators[m.Computed]
|
||||||
|
if !known {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%s says its resources are computed by %q, and this control plane has no %q",
|
||||||
|
m.Module, m.Computed, m.Computed)
|
||||||
|
}
|
||||||
|
resources, part, err := generator.Resources(r.Node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
computed := m
|
||||||
|
computed.Resources = nil
|
||||||
|
if part {
|
||||||
|
generated[m.Module] = resources
|
||||||
|
computed.Resources = resources
|
||||||
|
}
|
||||||
|
placed = append(placed, computed)
|
||||||
|
}
|
||||||
|
if len(generated) == 0 {
|
||||||
|
return generated, nil // nothing resolution has not already judged
|
||||||
|
}
|
||||||
|
if problems := checkResources(placed); len(problems) > 0 {
|
||||||
|
return nil, fmt.Errorf("what the mesh computes for this machine collides with a module's own: %s",
|
||||||
|
strings.Join(problems, "; "))
|
||||||
|
}
|
||||||
|
return generated, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq issue 190, step 5 (ADR 0222): what the mesh computes for a module is held to the rule
|
||||||
|
// every module is — no two modules on a machine declare one path, unit, name or package. The
|
||||||
|
// private network once declared the container runtime's file and service beside the runtime's own
|
||||||
|
// module, and nothing refused it, because the collision check only ever saw catalogue manifests.
|
||||||
|
|
||||||
|
func runtimesOwn() Manifest {
|
||||||
|
return Manifest{Module: "docker", Resources: []map[string]any{
|
||||||
|
{"id": "daemon", "type": "file", "path": "/etc/docker/daemon.json", "into": "json",
|
||||||
|
"content": `{"live-restore": true}`},
|
||||||
|
{"id": "runtime", "type": "service", "unit": "docker.service", "state": "running",
|
||||||
|
"reload-on": []any{"daemon"}},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAGeneratedResourceCollidingWithAModulesIsRefused(t *testing.T) {
|
||||||
|
r := Resolution{Node: "workstation", Modules: []Manifest{
|
||||||
|
{Module: "mesh-network", Computed: "mesh-network", Provides: Offers("private-network")},
|
||||||
|
runtimesOwn(),
|
||||||
|
}}
|
||||||
|
_, err := r.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": reloading{}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("a generated resource declaring the runtime's file beside the runtime's module was accepted")
|
||||||
|
}
|
||||||
|
for _, want := range []string{"mesh-network", "docker", "/etc/docker/daemon.json", "docker.service"} {
|
||||||
|
if !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("the refusal does not name %s: %v", want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAGeneratedResourceBesideAModulesOwnIsComposed(t *testing.T) {
|
||||||
|
r := Resolution{Node: "workstation", Modules: []Manifest{
|
||||||
|
{Module: "mesh-network", Computed: "mesh-network", Provides: Offers("private-network")},
|
||||||
|
runtimesOwn(),
|
||||||
|
}}
|
||||||
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
||||||
|
out, err := r.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("disjoint resources were refused: %v", err)
|
||||||
|
}
|
||||||
|
if fileNamed(out, "docker.daemon") == nil {
|
||||||
|
t.Fatalf("the runtime's own file is missing: %v", out)
|
||||||
|
}
|
||||||
|
// And a machine not on the network yet generates nothing, which collides with nothing.
|
||||||
|
if _, err := r.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": &fake{}}}); err != nil {
|
||||||
|
t.Fatalf("a machine off the network was refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The catalogue's container runtime module states the mesh's registry itself (ADR 0222).
|
||||||
|
func TestTheRuntimesModuleTrustsTheMeshsRegistry(t *testing.T) {
|
||||||
|
docker := catalogueManifest(t, "docker")
|
||||||
|
r := Resolution{Node: "workstation", Modules: []Manifest{docker}}
|
||||||
|
out, err := r.Declaration(Rendering{
|
||||||
|
SeatReach: map[string]string{"mesh-artifact-store": "anchor.internal:5100"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
daemon := fileNamed(out, "docker.daemon")
|
||||||
|
if daemon == nil || daemon["into"] != "json" {
|
||||||
|
t.Fatalf("the runtime's file is not written into: %v", daemon)
|
||||||
|
}
|
||||||
|
content, _ := daemon["content"].(string)
|
||||||
|
if !strings.Contains(content, `"insecure-registries": ["anchor.internal:5100"]`) ||
|
||||||
|
!strings.Contains(content, `"live-restore": true`) {
|
||||||
|
t.Fatalf("the runtime's file says %q", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err = r.Declaration(Rendering{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if content, _ := fileNamed(out, "docker.daemon")["content"].(string); strings.Contains(content, "insecure-registries") {
|
||||||
|
t.Fatalf("with no store on the network, the runtime is told %q", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,18 +78,13 @@ type Generator struct {
|
|||||||
// keyPath is where each node keeps the private half it generated. Named rather than carried:
|
// keyPath is where each node keeps the private half it generated. Named rather than carried:
|
||||||
// the mesh has never seen it and never will.
|
// the mesh has never seen it and never will.
|
||||||
keyPath string
|
keyPath string
|
||||||
// registry is the mesh's artifact store as the network reaches it (host:port), or empty when
|
// No registry. Being on the network is still what grants a machine the right to pull from the
|
||||||
// the mesh has none. Being on the network is what grants a machine the right to pull from it
|
// mesh's artifact store in the clear (novox/hq ADR 0082), but the runtime's file is the runtime's
|
||||||
// (novox/hq ADR 0082), so the module that puts a machine on the network is what writes the
|
// module's: the private network writes nothing into it, and that module states the registry
|
||||||
// runtime's trust. (That is still a write into another module's file, the container runtime's;
|
// itself, told where the store is reached by ${seat:mesh-artifact-store:reach} (novox/hq ADR
|
||||||
// novox/hq issue 190 has it handed to that module as a value.)
|
// 0222, issue 190).
|
||||||
registry string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrustRegistry names the artifact store this network's machines pull from in the clear —
|
|
||||||
// the overlay is the transport security (ADR 0082).
|
|
||||||
func (g *Generator) TrustRegistry(hostPort string) { g.registry = hostPort }
|
|
||||||
|
|
||||||
// From builds a generator over the machines that are part of the network.
|
// From builds a generator over the machines that are part of the network.
|
||||||
//
|
//
|
||||||
// The nodes given are the ones assigned the module — not every node the mesh knows. A machine
|
// The nodes given are the ones assigned the module — not every node the mesh knows. A machine
|
||||||
@@ -128,31 +123,7 @@ func (g *Generator) Resources(node string) ([]map[string]any, bool, error) {
|
|||||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
resources := parsed.Resources
|
return parsed.Resources, true, nil
|
||||||
if g.registry != "" {
|
|
||||||
trust, err := json.Marshal(map[string]any{"insecure-registries": []string{g.registry}})
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
resources = append(resources,
|
|
||||||
map[string]any{
|
|
||||||
// Written into, not over (novox/hq ADR 0102): the runtime's daemon file is the
|
|
||||||
// machine's — its data directory, its logging, whatever a predecessor set — and
|
|
||||||
// this states one fact in it. The host sets this key and keeps every other.
|
|
||||||
// ("merge" is the operator's settings merged into this content; "into" is the
|
|
||||||
// content written into the machine's file.) The registry speaks plain HTTP
|
|
||||||
// because every path to it is already inside the overlay's encryption (ADR 0082).
|
|
||||||
"id": "registry-trust", "type": "file", "path": "/etc/docker/daemon.json",
|
|
||||||
"content": string(trust) + "\n", "mode": "0644", "merge": "json", "into": "json",
|
|
||||||
},
|
|
||||||
map[string]any{
|
|
||||||
// Reloaded, not restarted: the runtime re-reads its trusted registries on a reload,
|
|
||||||
// and a restart stops every container on the machine (measured; ADR 0102).
|
|
||||||
"id": "registry-trust-reload", "type": "service", "unit": "docker.service",
|
|
||||||
"state": "running", "reload-on": []string{"registry-trust"},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return resources, true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nodes are the machines this generator was built over, so a caller can say who is on the network.
|
// Nodes are the machines this generator was built over, so a caller can say who is on the network.
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package overlay
|
package overlay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTheNetworkCarriesRegistryTrust(t *testing.T) {
|
// novox/hq ADR 0222, issue 190: the runtime's file and service are the runtime's module's. The
|
||||||
// novox/hq ADR 0082: being on the network is what grants a machine the right to pull from the
|
// private network writes nothing into either — being on it still grants the right to pull from the
|
||||||
// mesh's artifact store in the clear, so the network module writes the runtime's trust — and
|
// mesh's store in the clear (ADR 0082), and the runtime's module states that trust itself.
|
||||||
// writes nothing when the mesh has no store to trust.
|
func TestTheNetworkWritesNothingOfTheRuntimes(t *testing.T) {
|
||||||
nodes := []Node{
|
nodes := []Node{
|
||||||
{Name: "anchor", Site: "lab", Hub: true, Endpoint: "192.0.2.10:51820", Key: "k1", Address: "10.42.0.1"},
|
{Name: "anchor", Site: "lab", Hub: true, Endpoint: "192.0.2.10:51820", Key: "k1", Address: "10.42.0.1"},
|
||||||
{Name: "node2", Site: "lab", Key: "k2", Address: "10.42.0.2"},
|
{Name: "node2", Site: "lab", Key: "k2", Address: "10.42.0.2"},
|
||||||
@@ -18,47 +16,15 @@ func TestTheNetworkCarriesRegistryTrust(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
plain, _, err := g.Resources("node2")
|
for _, node := range []string{"anchor", "node2"} {
|
||||||
if err != nil {
|
resources, part, err := g.Resources(node)
|
||||||
t.Fatal(err)
|
if err != nil || !part {
|
||||||
}
|
t.Fatalf("%s: resources: %v part=%v", node, err, part)
|
||||||
for _, r := range plain {
|
|
||||||
if r["id"] == "registry-trust" {
|
|
||||||
t.Fatal("trust was written with no artifact store to trust")
|
|
||||||
}
|
}
|
||||||
}
|
for _, r := range resources {
|
||||||
|
if r["path"] == "/etc/docker/daemon.json" || r["unit"] == "docker.service" {
|
||||||
g.TrustRegistry("anchor.internal:5000")
|
t.Errorf("%s: the private network declares the runtime's %v", node, r)
|
||||||
trusted, part, err := g.Resources("node2")
|
}
|
||||||
if err != nil || !part {
|
|
||||||
t.Fatalf("resources: %v part=%v", err, part)
|
|
||||||
}
|
|
||||||
var file, service map[string]any
|
|
||||||
for _, r := range trusted {
|
|
||||||
switch r["id"] {
|
|
||||||
case "registry-trust":
|
|
||||||
file = r
|
|
||||||
case "registry-trust-reload":
|
|
||||||
service = r
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if file == nil || service == nil {
|
|
||||||
t.Fatalf("the trust file or its reload is missing: %v", trusted)
|
|
||||||
}
|
|
||||||
if file["path"] != "/etc/docker/daemon.json" || file["merge"] != "json" || file["into"] != "json" {
|
|
||||||
t.Fatalf("the trust is not written into daemon.json (ADR 0102): %v", file)
|
|
||||||
}
|
|
||||||
if content, _ := file["content"].(string); !strings.Contains(content, `"anchor.internal:5000"`) {
|
|
||||||
t.Fatalf("the trust does not name the store: %v", file["content"])
|
|
||||||
}
|
|
||||||
if service["unit"] != "docker.service" {
|
|
||||||
t.Fatalf("the reload is not the runtime's: %v", service)
|
|
||||||
}
|
|
||||||
// Reloaded, never restarted: a restart stops every container on the machine (ADR 0102).
|
|
||||||
if _, restarts := service["restart-on"]; restarts {
|
|
||||||
t.Fatalf("the runtime is restarted for its trust: %v", service)
|
|
||||||
}
|
|
||||||
if fmt.Sprint(service["reload-on"]) != "[registry-trust]" {
|
|
||||||
t.Fatalf("the runtime is not reloaded for its trust: %v", service)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user