diff --git a/cmd/mesh-controller/network.go b/cmd/mesh-controller/network.go index 7c561b8..e883298 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -152,6 +152,15 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, return overlay.Empty(), nil } g, err := overlay.From(nodes, overlayCIDR(), "") + 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). + if at, port, found := artifactStoreOnNetwork(ctx, inv, on); found { + g.TrustRegistry(overlay.InternalName(at) + ":" + port) + } + } if err != nil && len(refused) > 0 { // 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 @@ -383,3 +392,39 @@ func namesInTheMesh(ctx context.Context, inv *inventory.Inventory) (map[string]s } return out, nil } + +// artifactStoreOnNetwork is the machine and port the mesh's artifact store answers on, when a +// module providing it is assigned to a machine that is on the private network. +func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory, + on map[string]bool) (node, port string, found bool) { + + shelf, err := inv.Catalogue(ctx) + if err != nil || shelf == nil { + return "", "", false + } + providers := map[string]string{} // module -> served port + for name, m := range shelf { + served, offers := m.Serves[catalogue.ArtifactStoreProvision] + if !offers { + continue + } + if p, ok := served["port"]; ok { + providers[name] = fmt.Sprintf("%v", p) + } + } + if len(providers) == 0 { + return "", "", false + } + for machine := range on { + assigned, err := inv.Assigned(ctx, machine) + if err != nil { + continue + } + for _, a := range assigned { + if p, ok := providers[a]; ok { + return machine, p, true + } + } + } + return "", "", false +} diff --git a/internal/overlay/generator.go b/internal/overlay/generator.go index 3b10895..54995ed 100644 --- a/internal/overlay/generator.go +++ b/internal/overlay/generator.go @@ -83,8 +83,17 @@ type Generator struct { // keyPath is where each node keeps the private half it generated. Named rather than carried: // the mesh has never seen it and never will. keyPath string + // registry is the mesh's artifact store as the network reaches it (host:port), or empty when + // the mesh has none. Being on the network is what grants a machine the right to pull from it + // (novox/hq ADR 0082), so the module that puts a machine on the network is what writes the + // runtime's trust — the same reasoning that has it write /etc/hosts. + 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. // // The nodes given are the ones assigned the module — not every node the mesh knows. A machine @@ -123,7 +132,29 @@ func (g *Generator) Resources(node string) ([]map[string]any, bool, error) { if err := json.Unmarshal(raw, &parsed); err != nil { return nil, false, err } - return parsed.Resources, true, nil + resources := parsed.Resources + 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{ + // Merged, not owned: the runtime's daemon file is the machine's, and this states + // one fact into it. The registry speaks plain HTTP because every path to it is + // already inside the overlay's encryption (ADR 0082) — this line is the runtime + // being told what the mesh already means. + "id": "registry-trust", "type": "file", "path": "/etc/docker/daemon.json", + "content": string(trust) + "\n", "mode": "0644", "merge": "json", + }, + map[string]any{ + // The runtime reloads nothing for this setting, so it is restarted when the fact + // changes — once, at joining, before the machine runs anything that would mind. + "id": "registry-trust-reload", "type": "service", "unit": "docker.service", + "state": "running", "restart-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. diff --git a/internal/overlay/generator_test.go b/internal/overlay/generator_test.go new file mode 100644 index 0000000..0cc935a --- /dev/null +++ b/internal/overlay/generator_test.go @@ -0,0 +1,56 @@ +package overlay + +import ( + "strings" + "testing" +) + +func TestTheNetworkCarriesRegistryTrust(t *testing.T) { + // novox/hq ADR 0082: being on the network is what grants a machine the right to pull from the + // mesh's artifact store in the clear, so the network module writes the runtime's trust — and + // writes nothing when the mesh has no store to trust. + nodes := []Node{ + {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"}, + } + g, err := From(nodes, "10.42.0.0/16", "") + if err != nil { + t.Fatal(err) + } + plain, _, err := g.Resources("node2") + if err != nil { + t.Fatal(err) + } + for _, r := range plain { + if r["id"] == "registry-trust" { + t.Fatal("trust was written with no artifact store to trust") + } + } + + g.TrustRegistry("anchor.internal:5000") + 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" { + t.Fatalf("the trust is not a merged daemon.json: %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 does not restart the runtime: %v", service) + } +}