diff --git a/cmd/mesh-controller/compose_input.go b/cmd/mesh-controller/compose_input.go new file mode 100644 index 00000000..41ca4bf7 --- /dev/null +++ b/cmd/mesh-controller/compose_input.go @@ -0,0 +1,392 @@ +package main + +import ( + "context" + "os" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/overlay" + "github.com/novox/mesh-controller/internal/inventory" +) + +// The controller's adapter from its stores to mesh-sdk's composition input (novox/hq ADR 0453 clause 3, design 47 R4b, +// issues 1382 and 1383). +// +// **The adapter reads, and perform writes.** readComposeInput asks the stores once for everything the composition reads +// and returns compose.Input, which the composition never changes; it makes, seals, allocates and records nothing. A +// pair credential, a port, an own secret or a core box that the next send would make is not made here: the composition +// names it foreseen, and compose_perform.go makes it for a send, then reads the stores again and composes again. A +// question (status, facts, probes, the merge gate's reading) never performs. +// +// **The Input is built once per operation.** A verb that composes every node reads the stores once and hands the Input to +// each node's composition of it. + +// composeInputBuilt is called once for each Input the adapter builds: the seam a test counts the reads of an operation by +// (issue 1382, criterion 5). +var composeInputBuilt = func() {} + +// readComposeInput reads the stores once and returns what the composition needs. ctx carries what a send keeps (the +// recorded modules kept at the build their machine runs, recorded_kept.go): the same contexts every send composes under. +func readComposeInput(ctx context.Context, open *stores) (*meshInput, error) { + in, shape, err := readMeshAndNodes(ctx, open) + if err != nil { + return nil, err + } + // What each node resolves to, with no credential and no settings given: the modules whose settings are read, and the + // needs whose pair credentials are asked of the stores. Nothing is written by it. + plans := map[string]compose.Resolved{} + for _, n := range in.Nodes { + resolved, err := compose.Resolve(in, n.Name) + switch { + case compose.IsUnresolvable(err): + // Their set does not compose: nothing of theirs is read further, as the controller never read a thing for a + // node that did not resolve. + continue + case err != nil: + return nil, err + } + plans[n.Name] = resolved + } + if err := readSettings(ctx, open, &in, plans); err != nil { + return nil, err + } + if err := readCredentialsAndBuilds(ctx, open, &in, plans, shape); err != nil { + return nil, err + } + composeInputBuilt() + return &meshInput{Input: in}, nil +} + +// readCredentialsAndBuilds is the credential and build half of the Input, which issue 1383 fills. +func readCredentialsAndBuilds(context.Context, *stores, *compose.Input, map[string]compose.Resolved, inputShape) error { + return nil +} + +// meshInput is the Input an operation composes over. +type meshInput struct { + compose.Input +} + +// nodeOf is the node of that name in an Input. +func nodeOf(in compose.Input, name string) (compose.Node, bool) { + for _, n := range in.Nodes { + if n.Name == name { + return n, true + } + } + return compose.Node{}, false +} + +// inputShape is what the first half of the reading found that the second half needs. +type inputShape struct { + sent map[string]map[string]string // by node: module to the commit last sent + kept map[string]map[string]string // by node: module to the commit kept + current map[string]inventory.CurrentBuild +} + +// readMeshAndNodes is the mesh-wide and per-node half of the Input (issue 1382). +func readMeshAndNodes(ctx context.Context, open *stores) (compose.Input, inputShape, error) { + inv := open.inventory + var in compose.Input + shape := inputShape{sent: map[string]map[string]string{}, kept: map[string]map[string]string{}} + + shelf, err := inv.Catalogue(ctx) + if err != nil { + return in, shape, err + } + in.Catalogue = shelf + if in.Holdings, err = inv.Holdings(ctx); err != nil { + return in, shape, err + } + if in.Licences, err = readLicences(ctx, open); err != nil { + return in, shape, err + } + + nodes, err := inv.Nodes(ctx) + if err != nil { + return in, shape, err + } + places, err := inv.Overlays(ctx) + if err != nil { + return in, shape, err + } + placed := map[string]inventory.Overlay{} + for _, p := range places { + placed[p.Name] = p + } + if shape.current, err = inv.CurrentBuilds(ctx); err != nil { + return in, shape, err + } + + // The range the mesh allocates from and what its internal names end in, read from the environment here and nowhere else. + in.OverlayCIDR = strings.TrimSpace(os.Getenv(OverlayCIDRVar)) + in.InternalSuffix = overlay.Suffix() + if in.Tunnels, err = readTunnels(ctx, inv); err != nil { + return in, shape, err + } + if in.Adopted, err = readAdopted(ctx, inv); err != nil { + return in, shape, err + } + carried, err := inv.CarriedPeers(ctx) + if err != nil { + return in, shape, err + } + for _, c := range carried { + in.CarriedPeers = append(in.CarriedPeers, compose.CarriedPeer{ + PublicKey: c.PublicKey, Address: c.Address, EnrolledAs: c.EnrolledAs, Named: c.Named}) + } + if in.Joining, err = inv.NodesWithALiveToken(ctx); err != nil { + return in, shape, err + } + + keepsSecrets := false + for _, m := range shelf { + keepsSecrets = keepsSecrets || m.Keeps != "" + } + + for _, n := range nodes { + node, err := readNode(ctx, open, n, placed[n.Name]) + if err != nil { + return in, shape, err + } + shape.sent[n.Name], shape.kept[n.Name] = node.SentBuilds, node.KeptBuilds + in.Nodes = append(in.Nodes, node) + } + if keepsSecrets { + if in.OperatorExport, err = inv.OperatorExport(ctx); err != nil { + return in, shape, err + } + } + return in, shape, nil +} + +// readNode is one node of the Input: everything the composition reads about it, in the order the controller read it. +func readNode(ctx context.Context, open *stores, n inventory.Node, place inventory.Overlay) (compose.Node, error) { + inv := open.inventory + out := compose.Node{ + Name: n.Name, + Place: compose.Place{Key: place.Key, Endpoint: place.Endpoint, Site: place.Site, Hub: place.Hub, + Address: place.Address}, + Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount, AgentAccountHome: n.AgentAccountHome, + Adopted: n.Adopted, + } + var err error + if out.Assigned, err = inv.Assigned(ctx, n.Name); err != nil { + return out, err + } + if out.Capabilities, err = inv.ProfileOf(ctx, n.Name); err != nil { + return out, err + } + if out.PublicDomain, err = inv.PublicDomainOf(ctx, n.Name); err != nil { + return out, err + } + if out.Pins, err = inv.PinsFor(ctx, n.Name); err != nil { + return out, err + } + if out.Bound, err = inv.BindingsFor(ctx, n.Name); err != nil { + return out, err + } + given, err := inv.PortsFor(ctx, n.Name) + if err != nil { + return out, err + } + for _, a := range given { + if out.Ports == nil { + out.Ports = map[string]map[int]int{} + } + if out.Ports[a.Module] == nil { + out.Ports[a.Module] = map[int]int{} + } + out.Ports[a.Module][a.Wanted] = a.Machine + } + if out.SealingKey, err = inv.SealingKeyOf(ctx, n.Name); err != nil { + return out, err + } + if n.Adopted { + if out.Taken, err = inv.Taken(ctx, n.Name); err != nil { + return out, err + } + } + if out.OutwardLinks, err = inv.OutwardLinksOf(ctx, n.Name); err != nil { + return out, err + } + account, had, err := inv.MachineOf(ctx, n.Name) + if err != nil { + return out, err + } + if had && account.Machine != nil { + out.NetworksSaid = true + for _, a := range account.Machine.Networks { + out.Networks = append(out.Networks, catalogue.AttachedNetwork{Link: a.Link, Network: a.Network}) + } + } + if out.Sequence, err = inv.Sequence(ctx, n.ID); err != nil { + return out, err + } + if out.Epoch, err = inv.SentEpoch(ctx, n.ID); err != nil { + return out, err + } + if out.Generation, err = inv.SentGeneration(ctx, n.ID); err != nil { + return out, err + } + if out.ReadsModules, err = inv.ReadsModules(ctx, n.ID); err != nil { + return out, err + } + if out.SentBuilds, out.SentKnown, err = inv.SentBuilds(ctx, n.Name); err != nil { + return out, err + } + runs, err := inv.AssignmentRuns(ctx, n.Name) + if err != nil { + return out, err + } + if len(runs) > 0 { + out.Runs = make(map[string]compose.Run, len(runs)) + for module, ref := range runs { + out.Runs[module] = compose.Run{Commit: ref.Commit, Build: ref.Build} + } + } + // A recorded module kept at the build this machine runs, in the context a send composes under (recorded_kept.go): its + // manifest and the commit, applied to this node's own resolution only. + kept, err := recordedKept(ctx, open, n.Name) + if err != nil { + return out, err + } + if len(kept) > 0 { + out.KeptBuilds = kept + if out.Kept, err = keptManifests(ctx, open, n.Name, kept); err != nil { + return out, err + } + } + stated, has, err := inv.HealthOf(ctx, n.Name) + if err != nil { + return out, err + } + out.Stated = has + if has { + out.Contract = stated.Contract + } + return out, nil +} + +// keptManifests is the manifest of each module kept at the build a machine runs: keepRecorded's reads, and its refusals in its +// words, without putting any of it into a shelf (that is the node's own Kept, applied by the composition). +func keptManifests(ctx context.Context, open *stores, node string, kept map[string]string) (map[string]catalogue.Manifest, error) { + // applyKept puts each manifest into the shelf it is given and refuses a build that cannot be read; the shelf here is a + // scratch one, whose entries are the kept manifests. + scratch := map[string]catalogue.Manifest{} + if err := applyKept(ctx, open, node, kept, scratch); err != nil { + return nil, err + } + return scratch, nil +} + +func readTunnels(ctx context.Context, inv *inventory.Inventory) (map[string]compose.FoundTunnel, error) { + found, err := inv.Tunnels(ctx) + if err != nil { + return nil, err + } + if len(found) == 0 { + return nil, nil + } + out := make(map[string]compose.FoundTunnel, len(found)) + for name, t := range found { + out[name] = compose.FoundTunnel{Tunnel: tunnelOf(t.Tunnel), NodeAdopted: t.NodeAdopted} + } + return out, nil +} + +func readAdopted(ctx context.Context, inv *inventory.Inventory) (*compose.AdoptedTunnel, error) { + tunnel, hub, adopted, err := inv.AdoptedTunnel(ctx) + if err != nil { + return nil, err + } + if hub == "" { + return nil, nil + } + return &compose.AdoptedTunnel{Tunnel: tunnelOf(tunnel), Hub: hub, Adopted: adopted}, nil +} + +func tunnelOf(t inventory.Tunnel) compose.Tunnel { + out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU, + Address: t.Address, Range: t.Range, PublicKey: t.PublicKey} + for _, p := range t.Peers { + out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address}) + } + return out +} + +// readLicences is every licence the mesh holds, with who uses it and the key sealed to each holder: given even where empty, +// so a holder the mesh holds no key for composes as that, and not as a key that was not asked for. +func readLicences(ctx context.Context, open *stores) ([]compose.Licence, error) { + held, err := open.Licences(ctx) + if err != nil { + return nil, err + } + all, err := held.All(ctx) + if err != nil { + return nil, err + } + var out []compose.Licence + for _, one := range all { + l := compose.Licence{Name: one.Name, Serves: one.Serves, Keys: map[compose.Holder]string{}} + holders, err := held.HoldersOf(ctx, one.Name) + if err != nil { + return nil, err + } + for _, h := range holders { + holder := compose.Holder{Node: h.Node, Module: h.Module} + l.Holders = append(l.Holders, holder) + key, err := held.KeyFor(ctx, one.Name, h.Node, h.Module) + if err != nil { + return nil, err + } + l.Keys[holder] = key + } + node, module, err := held.ManagerOf(ctx, one.Name) + if err != nil { + return nil, err + } + l.Manager = compose.Holder{Node: node, Module: module} + out = append(out, l) + } + return out, nil +} + +// readSettings is each node's settings layers for the modules it resolves to: the ones the composition reads for it and for +// the rest of the mesh, and no module nothing resolves (a module's settings may fail to read where nothing runs it). +func readSettings(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved) error { + modules := map[string]map[string]bool{} + for node, resolved := range plans { + modules[node] = map[string]bool{} + for _, m := range resolved.Plan.Modules { + modules[node][m.Module] = true + } + } + // The rest of the mesh reads the settings of the modules that offer something on another node (servedOnNode), which are + // among the modules that node resolves to. + for i := range in.Nodes { + n := &in.Nodes[i] + names := make([]string, 0, len(modules[n.Name])) + for m := range modules[n.Name] { + names = append(names, m) + } + sort.Strings(names) + for _, module := range names { + layers, err := open.inventory.SettingsFor(ctx, n.Name, module) + if err != nil { + return err + } + if len(layers) == 0 { + continue + } + if n.Settings == nil { + n.Settings = map[string][]catalogue.Layer{} + } + n.Settings[module] = layers + } + } + return nil +} diff --git a/cmd/mesh-controller/compose_input_integration_test.go b/cmd/mesh-controller/compose_input_integration_test.go new file mode 100644 index 00000000..6b19fd8f --- /dev/null +++ b/cmd/mesh-controller/compose_input_integration_test.go @@ -0,0 +1,251 @@ +//go:build integration + +package main + +import ( + "reflect" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/link" +) + +// 1382: the adapter's Input names, for every node, what the inventory answers for it, each compared with the read named +// beside the field in go/compose/input.go (novox/hq ADR 0453 clause 3). +func TestTheAdaptersInputNamesWhatTheInventoryAnswersForEachNode(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + if err := inv.SetPublicDomain(ctx, "anchor", "example.test"); err != nil { + t.Fatal(err) + } + if _, err := inv.PortFor(ctx, "laptop", "files", 8080, false); err != nil { + t.Fatal(err) + } + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + + shelf, err := inv.Catalogue(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(in.Catalogue, shelf) { + t.Error("the Input's Catalogue is not the shelf the inventory holds") + } + nodes, err := inv.Nodes(ctx) + if err != nil { + t.Fatal(err) + } + if len(in.Nodes) != len(nodes) { + t.Fatalf("the Input names %d node(s), the inventory has %d", len(in.Nodes), len(nodes)) + } + places, err := inv.Overlays(ctx) + if err != nil { + t.Fatal(err) + } + for _, record := range nodes { + n, ok := nodeOf(in.Input, record.Name) + if !ok { + t.Fatalf("%s is not in the Input", record.Name) + } + for _, p := range places { + if p.Name == record.Name { + if want := (compose.Place{Key: p.Key, Endpoint: p.Endpoint, Site: p.Site, Hub: p.Hub, Address: p.Address}); n.Place != want { + t.Errorf("%s: Place is %+v, the inventory's %+v", record.Name, n.Place, want) + } + } + } + assigned, _ := inv.Assigned(ctx, record.Name) + if !reflect.DeepEqual(n.Assigned, assigned) { + t.Errorf("%s: Assigned is %v, the inventory's %v", record.Name, n.Assigned, assigned) + } + caps, _ := inv.ProfileOf(ctx, record.Name) + if !reflect.DeepEqual(n.Capabilities, caps) { + t.Errorf("%s: Capabilities are %v, the inventory's %v", record.Name, n.Capabilities, caps) + } + domain, _ := inv.PublicDomainOf(ctx, record.Name) + if n.PublicDomain != domain { + t.Errorf("%s: PublicDomain is %q, the inventory's %q", record.Name, n.PublicDomain, domain) + } + if n.Account != record.Account || n.AccountHome != record.AccountHome || n.AgentAccount != record.AgentAccount || + n.AgentAccountHome != record.AgentAccountHome || n.Adopted != record.Adopted { + t.Errorf("%s: the accounts or the mode differ from the inventory's: %+v", record.Name, n) + } + pins, _ := inv.PinsFor(ctx, record.Name) + if len(pins) != len(n.Pins) || (len(pins) > 0 && !reflect.DeepEqual(n.Pins, pins)) { + t.Errorf("%s: Pins are %v, the inventory's %v", record.Name, n.Pins, pins) + } + bound, _ := inv.BindingsFor(ctx, record.Name) + if len(bound) != len(n.Bound) || (len(bound) > 0 && !reflect.DeepEqual(n.Bound, bound)) { + t.Errorf("%s: Bound is %v, the inventory's %v", record.Name, n.Bound, bound) + } + key, _ := inv.SealingKeyOf(ctx, record.Name) + if n.SealingKey != key { + t.Errorf("%s: SealingKey is %q, the inventory's %q", record.Name, n.SealingKey, key) + } + ports, _ := inv.PortsFor(ctx, record.Name) + want := map[string]map[int]int{} + for _, a := range ports { + if want[a.Module] == nil { + want[a.Module] = map[int]int{} + } + want[a.Module][a.Wanted] = a.Machine + } + if len(want) != len(n.Ports) || (len(want) > 0 && !reflect.DeepEqual(n.Ports, want)) { + t.Errorf("%s: Ports are %v, the inventory's %v", record.Name, n.Ports, want) + } + // The settings of every module it resolves to, layer for layer. + resolved, err := compose.Resolve(in.Input, record.Name) + if err != nil { + continue + } + for _, m := range resolved.Plan.Modules { + layers, err := inv.SettingsFor(ctx, record.Name, m.Module) + if err != nil { + t.Fatal(err) + } + if len(layers) != len(n.Settings[m.Module]) || (len(layers) > 0 && !reflect.DeepEqual(n.Settings[m.Module], layers)) { + t.Errorf("%s: the settings of %s are %v, the inventory's %v", record.Name, m.Module, n.Settings[m.Module], layers) + } + } + } + if domain := nodeDomain(in.Input, "anchor"); domain != "example.test" { + t.Errorf("anchor's PublicDomain is %q", domain) + } + if got := nodePorts(in.Input, "laptop"); got["files"][8080] == 0 { + t.Errorf("laptop's port for files:8080 is not in the Input: %v", got) + } +} + +func nodeDomain(in compose.Input, name string) string { + n, _ := nodeOf(in, name) + return n.PublicDomain +} + +func nodePorts(in compose.Input, name string) map[string]map[int]int { + n, _ := nodeOf(in, name) + return n.Ports +} + +// 1382/AC2: a node keeping a recorded module at the build it runs has that module in Kept and KeptBuilds, and the Input's +// Catalogue is the current shelf, not the kept manifest (R4a review round 1, finding 1). +func TestANodeKeepingARecordedBuildHasItInKeptAndTheCatalogueStaysCurrent(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + start := time.Now().Add(-time.Hour) + image := "registry.invalid:5000/postgres/server@sha256:" + strings.Repeat("a", 64) + for _, b := range []link.BuildResult{ + aContainerBuild(t, "postgres", "c1111111", catalogue.PolicyRecord, start, map[string][2]string{"server": {image, ""}}), + } { + if _, _, err := takeIn(ctx, inv, asTheOperator(t, inv, b)); err != nil { + t.Fatal(err) + } + } + if _, err := inv.Assign(ctx, "anchor", "postgres"); err != nil { + t.Fatal(err) + } + if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"postgres": "c1111111"}); err != nil { + t.Fatal(err) + } + if _, _, err := takeIn(ctx, inv, aContainerBuild(t, "postgres", "c2222222", catalogue.PolicyRecord, start.Add(time.Minute), + map[string][2]string{"server": {image, "runtime"}})); err != nil { + t.Fatal(err) + } + + // A send that is not a person's push keeps the build the machine runs. + in, err := readComposeInput(keepingRecorded(ctx), open) + if err != nil { + t.Fatal(err) + } + anchor, _ := nodeOf(in.Input, "anchor") + if anchor.KeptBuilds["postgres"] != "c1111111" { + t.Fatalf("anchor keeps %v, want postgres at c1111111", anchor.KeptBuilds) + } + kept, found := anchor.Kept["postgres"] + if !found { + t.Fatalf("the kept manifest of postgres is not in anchor's Kept: %v", anchor.Kept) + } + current, err := inv.Catalogue(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(in.Catalogue["postgres"], current["postgres"]) { + t.Error("the Input's Catalogue is not the current shelf") + } + if reflect.DeepEqual(kept, current["postgres"]) { + t.Error("the kept manifest is the current one: nothing is kept") + } + if laptop, _ := nodeOf(in.Input, "laptop"); len(laptop.Kept) != 0 || len(laptop.KeptBuilds) != 0 { + t.Errorf("laptop keeps what only anchor runs: %v", laptop.KeptBuilds) + } + + // A person's push keeps nothing. + pushed, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + if a, _ := nodeOf(pushed.Input, "anchor"); len(a.Kept) != 0 || len(a.KeptBuilds) != 0 { + t.Errorf("a person's push keeps %v", a.KeptBuilds) + } +} + +// 1382/AC3: the seats on record, the tunnels, the adopted tunnel, its carried peers and the nodes joining equal the +// inventory's answers; the range and the suffix are read from the environment once, here. +func TestTheAdaptersInputCarriesTheNetworkAsTheInventoryHoldsIt(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + t.Setenv(OverlayCIDRVar, "10.99.0.0/16") + t.Setenv("MESH_INTERNAL_SUFFIX", "mesh.test") + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + holdings, err := inv.Holdings(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(in.Holdings, holdings) { + t.Errorf("Holdings are %v, the inventory's %v", in.Holdings, holdings) + } + tunnels, err := inv.Tunnels(ctx) + if err != nil { + t.Fatal(err) + } + if len(tunnels) != len(in.Tunnels) { + t.Errorf("Tunnels are %v, the inventory's %v", in.Tunnels, tunnels) + } + if _, hub, _, err := inv.AdoptedTunnel(ctx); err != nil { + t.Fatal(err) + } else if (hub == "") != (in.Adopted == nil) { + t.Errorf("Adopted is %v, the inventory's hub is %q", in.Adopted, hub) + } + carried, err := inv.CarriedPeers(ctx) + if err != nil { + t.Fatal(err) + } + if len(carried) != len(in.CarriedPeers) { + t.Errorf("CarriedPeers are %v, the inventory's %v", in.CarriedPeers, carried) + } + joining, err := inv.NodesWithALiveToken(ctx) + if err != nil { + t.Fatal(err) + } + if len(joining) != len(in.Joining) { + t.Errorf("Joining is %v, the inventory's %v", in.Joining, joining) + } + if in.OverlayCIDR != "10.99.0.0/16" || in.InternalSuffix != "mesh.test" { + t.Errorf("the range and the suffix are %q and %q", in.OverlayCIDR, in.InternalSuffix) + } + // Read once: what the environment says afterwards moves nothing already read. + t.Setenv(OverlayCIDRVar, "10.1.0.0/16") + if in.OverlayCIDR != "10.99.0.0/16" { + t.Error("the range moved with the environment") + } +} diff --git a/cmd/mesh-controller/recorded_kept.go b/cmd/mesh-controller/recorded_kept.go index 127e1a8b..1ecd9864 100644 --- a/cmd/mesh-controller/recorded_kept.go +++ b/cmd/mesh-controller/recorded_kept.go @@ -187,6 +187,13 @@ func keepRecorded(ctx context.Context, open *stores, node string, shelf map[stri if err != nil || len(kept) == 0 { return nil, err } + return kept, applyKept(ctx, open, node, kept, shelf) +} + +// applyKept puts the manifest of each module kept at the build its machine runs into a shelf, and refuses, in the words of +// keepRecorded, a build the records no longer hold. The adapter to the composition's input (compose_input.go) calls it with +// a scratch shelf, to read the manifests it gives the node as its own Kept. +func applyKept(ctx context.Context, open *stores, node string, kept map[string]string, shelf map[string]catalogue.Manifest) error { names := make([]string, 0, len(kept)) for m := range kept { names = append(names, m) @@ -195,28 +202,28 @@ func keepRecorded(ctx context.Context, open *stores, node string, shelf map[stri for _, m := range names { ran, found, err := open.inventory.ManifestAt(ctx, m, kept[m]) if err != nil { - return nil, err + return err } if !found && settingsKeptOn(ctx, node) { - return nil, fmt.Errorf("%w: %s runs %s's build %s, which the build records no longer hold (novox/hq ADR 0344)", + return fmt.Errorf("%w: %s runs %s's build %s, which the build records no longer hold (novox/hq ADR 0344)", errSettingsCannotKeep, node, m, short(kept[m])) } if !found && everyBuildKept(ctx, node) { - return nil, fmt.Errorf("%s runs %s's build %s, which the build records no longer hold: the bus's user "+ + return fmt.Errorf("%s runs %s's build %s, which the build records no longer hold: the bus's user "+ "list cannot be sent there alone with it kept (novox/hq issue 490)", node, m, short(kept[m])) } if !found && guarded(coreComponent(m)) { - return nil, fmt.Errorf("%s was last sent %s's build %s, newer than the one the mesh holds, and the build records "+ + return fmt.Errorf("%s was last sent %s's build %s, newer than the one the mesh holds, and the build records "+ "no longer hold it: this send cannot keep it and does not move it back — `push %s` sends the older build on "+ "a person's word, and a newer merge or `rebuild %s` moves the record forward (novox/hq issue 201)", node, m, short(kept[m]), node, m) } if !found { - return nil, fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+ + return fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+ "records no longer hold: this send cannot keep it and does not move it — `push %s` sends the new "+ "one on a person's word (novox/hq ADR 0245)", m, node, short(kept[m]), node) } shelf[m] = ran } - return kept, nil + return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index ce761ef2..2399e4fd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,6 +2,7 @@ ## explicit; go 1.26.0 git.novox.be/novox/mesh-sdk/go/asks git.novox.be/novox/mesh-sdk/go/catalogue +git.novox.be/novox/mesh-sdk/go/compose git.novox.be/novox/mesh-sdk/go/declaration git.novox.be/novox/mesh-sdk/go/delivery git.novox.be/novox/mesh-sdk/go/overlay @@ -86,9 +87,7 @@ github.com/nats-io/nkeys github.com/nats-io/nuid # github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 ## explicit; go 1.26.0 -github.com/novox/mesh-host/internal/declaration github.com/novox/mesh-host/rootsearch -github.com/novox/mesh-host/validate # github.com/therootcompany/xz v1.0.1 ## explicit; go 1.15 github.com/therootcompany/xz