Retire the networking bundle and what the control plane stops shipping (hq ADR 0226)
networking required mesh-wireguard and nothing else; machines are assigned the network directly. module forget refuses a provided module, so a retired one is removed at start once no machine has it. Guard route-proxy's public account directory against a reissue.
This commit is contained in:
@@ -40,7 +40,12 @@ func providedModules() []catalogue.Manifest {
|
|||||||
// and neither could be swapped for anything, which is the test of whether a thing is a
|
// and neither could be swapped for anything, which is the test of whether a thing is a
|
||||||
// module at all (novox/hq ADR 0040). They existed because computed output needed somewhere
|
// module at all (novox/hq ADR 0040). They existed because computed output needed somewhere
|
||||||
// to live, and now a module says where it wants it — `facts` in its own manifest.
|
// to live, and now a module says where it wants it — `facts` in its own manifest.
|
||||||
overlay.Manifest(), overlay.DomainManifest(),
|
//
|
||||||
|
// **And the bundle that required it is gone** (novox/hq ADR 0226): `networking` named this
|
||||||
|
// module's requirement and nothing else, so every machine carried two modules for one
|
||||||
|
// network. A machine is assigned the private network itself; what a release stops shipping
|
||||||
|
// is retired at the next start (stores.go, Inventory.RetireUnshipped).
|
||||||
|
overlay.Manifest(),
|
||||||
} {
|
} {
|
||||||
var m catalogue.Manifest
|
var m catalogue.Manifest
|
||||||
b, _ := json.Marshal(raw)
|
b, _ := json.Marshal(raw)
|
||||||
@@ -494,8 +499,8 @@ const theBrokerSeat = "mesh-broker"
|
|||||||
// says. Only when nothing holds the seat yet (genesis raised the broker as plumbing and no module
|
// says. Only when nothing holds the seat yet (genesis raised the broker as plumbing and no module
|
||||||
// has adopted it) does the hub stand in, which is where the foundation is by convention.
|
// has adopted it) does the hub stand in, which is where the foundation is by convention.
|
||||||
//
|
//
|
||||||
// "On the overlay" is what `whereEveryoneIs` answers — a machine that RESOLVED the networking
|
// "On the overlay" is what `whereEveryoneIs` answers — a machine that RESOLVED the private network
|
||||||
// module — not "has an address", which is true of every placed machine and says nothing about
|
// — not "has an address", which is true of every placed machine and says nothing about
|
||||||
// whether anything can reach it (novox/hq issue 059). A node not on the overlay — at genesis,
|
// whether anything can reach it (novox/hq issue 059). A node not on the overlay — at genesis,
|
||||||
// before any `overlay place`, which is when the builder's account is issued — keeps the genesis
|
// before any `overlay place`, which is when the builder's account is issued — keeps the genesis
|
||||||
// address, so nothing about bring-up changes. This is issue 055, corrected by 059.
|
// address, so nothing about bring-up changes. This is issue 055, corrected by 059.
|
||||||
|
|||||||
@@ -73,6 +73,22 @@ func migrate(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("provided %s\n", m.Module)
|
fmt.Printf("provided %s\n", m.Module)
|
||||||
}
|
}
|
||||||
|
// And what an earlier release shipped and this one does not goes (novox/hq ADR 0226) — unless a
|
||||||
|
// machine still has it, which is said rather than overridden.
|
||||||
|
var shipped []string
|
||||||
|
for _, m := range provided {
|
||||||
|
shipped = append(shipped, m.Module)
|
||||||
|
}
|
||||||
|
retired, kept, err := inv.RetireUnshipped(ctx, shipped)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, name := range retired {
|
||||||
|
fmt.Printf("retired %s: the control plane no longer ships it\n", name)
|
||||||
|
}
|
||||||
|
for name, why := range kept {
|
||||||
|
fmt.Printf("kept %s, which the control plane no longer ships: %s\n", name, why)
|
||||||
|
}
|
||||||
// The seats the mesh ships with, into the table that now holds the set (novox/hq ADR 0122).
|
// The seats the mesh ships with, into the table that now holds the set (novox/hq ADR 0122).
|
||||||
// Idempotent: fills an empty table on first boot, adds a seat a release ships, and leaves an
|
// Idempotent: fills an empty table on first boot, adds a seat a release ships, and leaves an
|
||||||
// operator's changes in the table as they are.
|
// operator's changes in the table as they are.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func provided(t *testing.T) map[string]catalogue.Manifest {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
out := map[string]catalogue.Manifest{}
|
out := map[string]catalogue.Manifest{}
|
||||||
for _, raw := range []map[string]any{
|
for _, raw := range []map[string]any{
|
||||||
overlay.Manifest(), overlay.DomainManifest(),
|
overlay.Manifest(),
|
||||||
} {
|
} {
|
||||||
b, err := json.Marshal(raw)
|
b, err := json.Marshal(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,20 +34,16 @@ func provided(t *testing.T) map[string]catalogue.Manifest {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTheShippedNetworkingModulesResolveOnTheirOwn(t *testing.T) {
|
func TestTheShippedPrivateNetworkResolvesOnItsOwn(t *testing.T) {
|
||||||
got, err := catalogue.Resolve(provided(t), []string{overlay.Domain}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
// **Assigned directly** (novox/hq ADR 0226): the `networking` bundle that required it is
|
||||||
|
// retired, so the private network's own module is what a machine is given, and it must need
|
||||||
|
// nothing the control plane does not ship beside it.
|
||||||
|
got, err := catalogue.Resolve(provided(t), []string{overlay.Name}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("assigning %s does not work out of the box: %v", overlay.Domain, err)
|
t.Fatalf("assigning %s does not work out of the box: %v", overlay.Name, err)
|
||||||
}
|
}
|
||||||
var have []string
|
if len(got.Modules) != 1 || got.Modules[0].Module != overlay.Name {
|
||||||
for _, m := range got.Modules {
|
t.Fatalf("assigning %s brought %v", overlay.Name, got.Modules)
|
||||||
have = append(have, m.Module)
|
|
||||||
}
|
|
||||||
for _, want := range []string{overlay.Domain, overlay.Name} {
|
|
||||||
if !strings.Contains(strings.Join(have, " "), want) {
|
|
||||||
t.Fatalf("%s did not bring in %s: %v", overlay.Domain, want, have)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// **The network writes no names** (novox/hq ADR 0199): /etc/hosts is the hosts seat holder's
|
// **The network writes no names** (novox/hq ADR 0199): /etc/hosts is the hosts seat holder's
|
||||||
@@ -55,23 +51,52 @@ func TestTheShippedNetworkingModulesResolveOnTheirOwn(t *testing.T) {
|
|||||||
// may name that file.
|
// may name that file.
|
||||||
for _, m := range got.Modules {
|
for _, m := range got.Modules {
|
||||||
for name, f := range m.Facts {
|
for name, f := range m.Facts {
|
||||||
if m.Module == overlay.Name && f.Path == "/etc/hosts" {
|
if f.Path == "/etc/hosts" {
|
||||||
t.Fatalf("the network's provider still writes /etc/hosts, as its %q fact", name)
|
t.Fatalf("the network's provider still writes /etc/hosts, as its %q fact", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnotherVPNSatisfiesNetworkingWithoutDraggingWireGuardIn(t *testing.T) {
|
func TestTheControlPlaneShipsNoNetworkingBundle(t *testing.T) {
|
||||||
// **This inverted, and the inversion is the improvement.** The names used to be a module that
|
// ADR 0226: one module for the one private network. A bundle shipped beside it again would be
|
||||||
// required the mesh's own addressing, which only WireGuard provided — so choosing another VPN
|
// a second name every machine carries for the same thing.
|
||||||
// dragged WireGuard in anyway, and the node-scoped claim existed to at least make that
|
|
||||||
// collision loud. With the names a fact rather than a provision, a person who chose tailscale
|
|
||||||
// gets tailscale, and there is nothing left to collide.
|
|
||||||
shipped := provided(t)
|
shipped := provided(t)
|
||||||
got, err := catalogue.Resolve(
|
if len(shipped) != 1 {
|
||||||
withTailscale(shipped),
|
t.Fatalf("the control plane ships %d modules, want only %s", len(shipped), overlay.Name)
|
||||||
[]string{overlay.Domain, "tailscale"},
|
}
|
||||||
|
if _, ok := shipped["networking"]; ok {
|
||||||
|
t.Fatal("the retired networking bundle is shipped again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARefusalForWantOfThePrivateNetworkNamesItsModule(t *testing.T) {
|
||||||
|
// The hint in a refusal is a string in the resolver rather than an import of this package. It
|
||||||
|
// named `networking` until ADR 0226; a hint naming a module nobody ships sends a person to
|
||||||
|
// assign something that does not exist.
|
||||||
|
shelf := map[string]catalogue.Manifest{
|
||||||
|
"app": {Module: "app", Version: "1", Requires: []string{"postgres-database"}},
|
||||||
|
"postgres": {Module: "postgres", Version: "1", Provides: catalogue.FromAnywhere("postgres-database")},
|
||||||
|
}
|
||||||
|
_, err := catalogue.Resolve(shelf, []string{"app"}, catalogue.Node{Name: "workstation"},
|
||||||
|
catalogue.World{Offered: map[string][]catalogue.Provider{
|
||||||
|
"postgres-database": {{Node: "anchor", At: "anchor.internal"}}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("an app off the private network was pointed at a database on it")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "assign "+overlay.Name) {
|
||||||
|
t.Fatalf("the refusal does not name the private network's module %s: %v", overlay.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnotherVPNIsChosenByAssigningItInstead(t *testing.T) {
|
||||||
|
// What the bundle was for, kept without it: a machine given another VPN answers
|
||||||
|
// private-network from that VPN, and WireGuard is not dragged in by anything.
|
||||||
|
shipped := provided(t)
|
||||||
|
shelf := withTailscale(shipped)
|
||||||
|
shelf["needs-network"] = catalogue.Manifest{Module: "needs-network", Version: "1",
|
||||||
|
Requires: []string{overlay.Requirement}}
|
||||||
|
got, err := catalogue.Resolve(shelf, []string{"needs-network", "tailscale"},
|
||||||
catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("choosing another VPN was refused: %v", err)
|
t.Fatalf("choosing another VPN was refused: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The public issuer is the proxy's own fact (novox/hq ADR 0226), and the folding of it must not
|
||||||
|
// move the proxy's account.
|
||||||
|
//
|
||||||
|
// route-proxy keeps each ACME authority's account and certificates in a directory named after a
|
||||||
|
// digest of the directory URL and of the root bundle its `trust` container copies in
|
||||||
|
// (examples/route-proxy, forThisAuthority). Until ADR 0226 the URL was rendered from a binding to
|
||||||
|
// `public-acme`'s `acme-ca`, spelled with the port. A URL spelled any other way — even the same
|
||||||
|
// authority without `:443` — or a root bundle from another image is a new authority to the proxy:
|
||||||
|
// a new account, and every routed name ordered again, on two machines at once, against Let's
|
||||||
|
// Encrypt's rate limits. So what the module now states is held to exactly what the binding rendered.
|
||||||
|
|
||||||
|
// renderedBeforeTheFold is route-proxy's acme.env as the binding to public-acme rendered it on every
|
||||||
|
// machine running the proxy, read from the controller's plan on 2026-10-06.
|
||||||
|
const renderedBeforeTheFold = "ACME_DIRECTORY=https://acme-v02.api.letsencrypt.org:443/directory\n" +
|
||||||
|
"ACME_ROOTS=https://acme-v02.api.letsencrypt.org:443\n" +
|
||||||
|
"ACME_ROOTS_PATH=\n"
|
||||||
|
|
||||||
|
// trustImage is the image whose system bundle becomes the public root the account directory is
|
||||||
|
// named after. Moving it renames that directory.
|
||||||
|
const trustImage = "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"
|
||||||
|
|
||||||
|
func TestRouteProxyKeepsItsPublicAccountDirectory(t *testing.T) {
|
||||||
|
m := catalogueManifest(t, "route-proxy")
|
||||||
|
for _, r := range m.Requires {
|
||||||
|
if r == "acme-ca" {
|
||||||
|
t.Fatal("route-proxy still asks for acme-ca; the public issuer is its own fact (ADR 0226)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a build would leave it: each artifact a container names resolved to an image. Which image
|
||||||
|
// does not matter to the file checked here.
|
||||||
|
for _, res := range m.Resources {
|
||||||
|
if artifact, ok := res["artifact"].(string); ok {
|
||||||
|
delete(res, "artifact")
|
||||||
|
res["image"] = "registry.invalid/route-proxy/" + artifact +
|
||||||
|
"@sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r := Resolution{
|
||||||
|
Node: "anchor",
|
||||||
|
Modules: []Manifest{m},
|
||||||
|
Needs: []Needed{{
|
||||||
|
Name: "internal-acme-ca", From: "home", At: "home.internal", For: "route-proxy",
|
||||||
|
Serves: map[string]any{
|
||||||
|
"port": float64(9000), "path": "/acme/acme/directory", "roots": "/roots.pem",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
// Its own broker credential, sealed as the mesh would; what it is does not matter here.
|
||||||
|
out, err := r.Declaration(Rendering{Needed: map[string]map[string]string{
|
||||||
|
"route-proxy": {"broker": "sealed"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("route-proxy could not be composed for a machine: %v", err)
|
||||||
|
}
|
||||||
|
env := fileNamed(out, "route-proxy.acme-env")
|
||||||
|
if env == nil {
|
||||||
|
t.Fatalf("nothing writes the public issuer's environment: %v", out)
|
||||||
|
}
|
||||||
|
if got, _ := env["content"].(string); got != renderedBeforeTheFold {
|
||||||
|
t.Fatalf("acme.env changed, which moves the proxy's account and reorders every certificate:\n"+
|
||||||
|
"got %q\nwant %q", got, renderedBeforeTheFold)
|
||||||
|
}
|
||||||
|
if fileNamed(out, "route-proxy.bound-acme-ca") != nil {
|
||||||
|
t.Error("a binding to acme-ca is still written")
|
||||||
|
}
|
||||||
|
|
||||||
|
var trust string
|
||||||
|
if m.Build != nil {
|
||||||
|
for _, a := range m.Build.Artifacts {
|
||||||
|
if a.Name == "trust" {
|
||||||
|
trust = a.From
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if trust != trustImage {
|
||||||
|
t.Errorf("the trust image is %q, not %q: its system bundle is the public root the account "+
|
||||||
|
"directory is named after, so moving it reorders every certificate. Move it only with a "+
|
||||||
|
"plan for the account (ADR 0226)", trust, trustImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The modules ADR 0226 retired stay retired, and nothing asks for what they provided.
|
||||||
|
func TestTheRetiredNetworkingModulesAreNotInTheCatalogue(t *testing.T) {
|
||||||
|
if _, err := os.Stat("../../../mesh-catalog/modules"); err != nil {
|
||||||
|
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
||||||
|
}
|
||||||
|
for _, gone := range []string{"public-acme", "dhcpcd", "cloudflare-dns"} {
|
||||||
|
if _, err := os.Stat("../../../mesh-catalog/modules/" + gone); err == nil {
|
||||||
|
t.Errorf("%s is in the catalogue again; ADR 0226 retired it", gone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir("../../../mesh-catalog/modules")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
raw, err := os.ReadFile("../../../mesh-catalog/modules/" + e.Name() + "/module.json")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m, err := ParseManifest(raw)
|
||||||
|
if err != nil {
|
||||||
|
continue // judged by the catalogue's own tests
|
||||||
|
}
|
||||||
|
for _, r := range m.Requires {
|
||||||
|
if r == "acme-ca" || r == "public-dns" {
|
||||||
|
t.Errorf("%s requires %q, which nothing in the catalogue provides since ADR 0226",
|
||||||
|
m.Module, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1019,8 +1019,10 @@ func FromAnywhere(names ...string) []Offer {
|
|||||||
//
|
//
|
||||||
// A string here rather than an import, because the private network is a module the control plane
|
// A string here rather than an import, because the private network is a module the control plane
|
||||||
// ships and this package must not depend on the thing it resolves. The name being wrong would
|
// ships and this package must not depend on the thing it resolves. The name being wrong would
|
||||||
// show up as a refusal naming a module nobody can assign, which a test checks.
|
// show up as a refusal naming a module nobody can assign, which a test checks
|
||||||
const meshNetwork = "networking"
|
// (provided_test.go). The private network's own module since novox/hq ADR 0226 retired the
|
||||||
|
// `networking` bundle that required it.
|
||||||
|
const meshNetwork = "mesh-wireguard"
|
||||||
|
|
||||||
// providersFirst orders a node's modules so that what answers a requirement comes before what
|
// providersFirst orders a node's modules so that what answers a requirement comes before what
|
||||||
// asked for it.
|
// asked for it.
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ var bothResolvers = []Held{
|
|||||||
|
|
||||||
// uplinks is every module in the catalogue holding node-uplink, and so writing the machine's resolver
|
// uplinks is every module in the catalogue holding node-uplink, and so writing the machine's resolver
|
||||||
// file (novox/hq ADR 0223 part 2): the program that would otherwise rewrite it is the one that writes it.
|
// file (novox/hq ADR 0223 part 2): the program that would otherwise rewrite it is the one that writes it.
|
||||||
var uplinks = []string{"dhcpcd", "networkmanager", "systemd-networkd"}
|
// dhcpcd's left the catalogue with ADR 0226, held by no machine.
|
||||||
|
var uplinks = []string{"networkmanager", "systemd-networkd"}
|
||||||
|
|
||||||
// managing is a machine as each uplink module needs it: able to install, run a service and run the
|
// managing is a machine as each uplink module needs it: able to install, run a service and run the
|
||||||
// manager that module is for.
|
// manager that module is for.
|
||||||
|
|||||||
@@ -284,6 +284,92 @@ func (i *Inventory) Provide(ctx context.Context, m catalogue.Manifest) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RetireUnshipped removes every module the control plane recorded as its own and no longer ships.
|
||||||
|
//
|
||||||
|
// **`module forget` refuses a provided module**, rightly: the next start would put it back. So a
|
||||||
|
// module the control plane stops shipping — `networking`, retired by novox/hq ADR 0226 — could be
|
||||||
|
// removed by nothing at all, and would sit in the catalogue for ever, assignable and pointing at a
|
||||||
|
// manifest no release carries. This is the other half of Provide: what this release ships is
|
||||||
|
// recorded, and what it does not is taken away.
|
||||||
|
//
|
||||||
|
// **Never from under a machine.** A retired module still assigned somewhere is kept, and named in
|
||||||
|
// `kept` with the machines it is on, so the caller can say "unassign it, and it goes at the next
|
||||||
|
// start". Taking it while assigned would drop whatever it pulled in — for `networking`, the
|
||||||
|
// private network itself — at the next push, with nobody having asked for that. Its settings,
|
||||||
|
// secrets and ports are kept the same way: a provided module that holds any is kept and named,
|
||||||
|
// because discarding them is a person's decision (novox/hq 04-ISSUES/017).
|
||||||
|
func (i *Inventory) RetireUnshipped(ctx context.Context, shipped []string) (retired []string, kept map[string]string, err error) {
|
||||||
|
keep := map[string]bool{}
|
||||||
|
for _, s := range shipped {
|
||||||
|
keep[s] = true
|
||||||
|
}
|
||||||
|
rows, err := i.store.Pool().Query(ctx,
|
||||||
|
`select name from module where source = 'the control plane' order by name`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var gone []string
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !keep[name] {
|
||||||
|
gone = append(gone, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kept = map[string]string{}
|
||||||
|
for _, name := range gone {
|
||||||
|
on, err := i.assignedOn(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(on) > 0 {
|
||||||
|
kept[name] = "still assigned on " + strings.Join(on, ", ") + "; unassign it and it goes at the next start"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
held, err := i.HeldFor(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if held.Any() {
|
||||||
|
kept[name] = "the mesh still holds things for it:\n" + strings.Join(held.Lines(), "\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := i.discard(ctx, name); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
retired = append(retired, name)
|
||||||
|
}
|
||||||
|
return retired, kept, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignedOn is the machines a module is assigned to, sorted.
|
||||||
|
func (i *Inventory) assignedOn(ctx context.Context, name string) ([]string, error) {
|
||||||
|
rows, err := i.store.Pool().Query(ctx,
|
||||||
|
`select n.name from assignment a join node n on n.id = a.node where a.module = $1
|
||||||
|
order by n.name`, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var on []string
|
||||||
|
for rows.Next() {
|
||||||
|
var node string
|
||||||
|
if err := rows.Scan(&node); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
on = append(on, node)
|
||||||
|
}
|
||||||
|
return on, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// Provided reports whether a module came with the control plane rather than from a repository.
|
// Provided reports whether a module came with the control plane rather than from a repository.
|
||||||
func (i *Inventory) Provided(ctx context.Context, name string) (bool, error) {
|
func (i *Inventory) Provided(ctx context.Context, name string) (bool, error) {
|
||||||
var source *string
|
var source *string
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// What a release stops shipping is retired at the next start, and never from under a machine
|
||||||
|
// (novox/hq ADR 0226). `module forget` refuses a provided module, so without this a module the
|
||||||
|
// control plane no longer carries could be removed by nothing.
|
||||||
|
|
||||||
|
func TestAModuleTheControlPlaneNoLongerShipsIsRetired(t *testing.T) {
|
||||||
|
inv := fresh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
for _, m := range []string{"mesh-wireguard", "networking"} {
|
||||||
|
if err := inv.Provide(ctx, manifest(m, nil, nil)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retired, kept, err := inv.RetireUnshipped(ctx, []string{"mesh-wireguard"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Join(retired, ",") != "networking" || len(kept) != 0 {
|
||||||
|
t.Fatalf("retired %v, kept %v", retired, kept)
|
||||||
|
}
|
||||||
|
if _, err := inv.Provided(ctx, "networking"); !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
t.Fatalf("networking is still in the catalogue: %v", err)
|
||||||
|
}
|
||||||
|
if provided, err := inv.Provided(ctx, "mesh-wireguard"); err != nil || !provided {
|
||||||
|
t.Fatalf("what this release ships went too: %v %v", provided, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARetiredModuleStillAssignedIsKeptAndSaidSo(t *testing.T) {
|
||||||
|
inv := fresh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := inv.Provide(ctx, manifest("networking", nil, nil)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := inv.Assign(ctx, "anchor", "networking"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
retired, kept, err := inv.RetireUnshipped(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(retired) != 0 {
|
||||||
|
// Taking it now would drop whatever it pulled in at the next push — for the bundle, the
|
||||||
|
// private network.
|
||||||
|
t.Fatalf("a module assigned on a machine was retired: %v", retired)
|
||||||
|
}
|
||||||
|
if !strings.Contains(kept["networking"], "anchor") {
|
||||||
|
t.Fatalf("keeping it does not say where it is still assigned: %v", kept)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unassigned, the next start takes it.
|
||||||
|
if err := inv.Unassign(ctx, "anchor", "networking"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
retired, _, err = inv.RetireUnshipped(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Join(retired, ",") != "networking" {
|
||||||
|
t.Fatalf("unassigned, it was still not retired: %v", retired)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAModuleFromARepositoryIsNeverRetiredByThis(t *testing.T) {
|
||||||
|
// Only what the control plane recorded as its own. A module somebody registered is theirs to
|
||||||
|
// forget.
|
||||||
|
inv := fresh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if err := inv.RegisterModule(ctx, manifest("public-acme", nil, nil), Source{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
retired, kept, err := inv.RetireUnshipped(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(retired) != 0 || len(kept) != 0 {
|
||||||
|
t.Fatalf("a registered module was considered: retired %v, kept %v", retired, kept)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,15 +61,12 @@ const TheNetwork = "the-private-network"
|
|||||||
// network. A requirement nothing provides is refused at resolution, so leaving the names here
|
// network. A requirement nothing provides is refused at resolution, so leaving the names here
|
||||||
// would only have documented a mechanism that does not exist.
|
// would only have documented a mechanism that does not exist.
|
||||||
|
|
||||||
// Domain is the module for people who want a network and do not want to choose one.
|
// **There is no bundle any more** (novox/hq ADR 0226). A `networking` module requiring this one and
|
||||||
//
|
// nothing else used to be what a machine was assigned, so that "get the network working" was one
|
||||||
// It has no files of its own — it is requirements and nothing else. Assigning it finds one
|
// word and a second VPN could be chosen by assigning it instead. The mesh has one private network,
|
||||||
// answer to each and takes them silently, so getting a mesh onto a private network is one word.
|
// every machine is on it, and the bundle was a second name for this module that every machine
|
||||||
// The day the catalogue holds a second VPN there are two answers, the resolver refuses and names
|
// carried. A machine is assigned Name directly; another VPN is still chosen by assigning it in its
|
||||||
// both, and choosing is assigning the one you want. **That is the whole mechanism**: picking an
|
// place, which is all the bundle ever did.
|
||||||
// implementation is assigning a module, and there is no flavor field, no configuration language,
|
|
||||||
// and nothing to learn.
|
|
||||||
const Domain = "networking"
|
|
||||||
|
|
||||||
// Generator answers what one node's network configuration is.
|
// Generator answers what one node's network configuration is.
|
||||||
type Generator struct {
|
type Generator struct {
|
||||||
@@ -147,19 +144,6 @@ func Manifest() map[string]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainManifest is the module that means "get the network working".
|
|
||||||
func DomainManifest() map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
"module": Domain,
|
|
||||||
"version": "1",
|
|
||||||
// **Only the network now.** It used to require name-resolution as well, answered by a
|
|
||||||
// module that wrote a hosts file and ran nothing. Names are not a provision — they are a
|
|
||||||
// fact the mesh computes, and whatever puts a machine on the private network writes them,
|
|
||||||
// because a mesh name IS an address on that network.
|
|
||||||
"requires": []string{Requirement},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty is a network nobody is on.
|
// Empty is a network nobody is on.
|
||||||
//
|
//
|
||||||
// A mesh where no machine was given the module. Legitimate rather than broken — every node still
|
// A mesh where no machine was given the module. Legitimate rather than broken — every node still
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
// has no module.json for a test reading the catalogue to find. Parsed with the real parser, so a
|
// has no module.json for a test reading the catalogue to find. Parsed with the real parser, so a
|
||||||
// set that forgot this seat refuses the control plane's own module here rather than on a machine.
|
// set that forgot this seat refuses the control plane's own module here rather than on a machine.
|
||||||
func TestThePrivateNetworksClaimIsASeatTheMeshDefines(t *testing.T) {
|
func TestThePrivateNetworksClaimIsASeatTheMeshDefines(t *testing.T) {
|
||||||
for _, composed := range []map[string]any{Manifest(), DomainManifest()} {
|
for _, composed := range []map[string]any{Manifest()} {
|
||||||
raw, err := json.Marshal(composed)
|
raw, err := json.Marshal(composed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
Reference in New Issue
Block a user