diff --git a/go/compose/composed.go b/go/compose/composed.go index f89dec1..afa9a77 100644 --- a/go/compose/composed.go +++ b/go/compose/composed.go @@ -55,11 +55,19 @@ func CompositionsShadow(in Input, node string, fps map[string]string) (out map[s return compositions(in, node, fps, Foreseeing, false) } +// CompositionsIn is the node's compositions in mode with what they stand in for answered (stood, sorted, the bus's user +// list left out) rather than refused: CompositionsShadow in Foreseeing, and in Reading what CompositionsApplied refuses +// before it is refused, so a caller that applies refuses each stand-in to the module it is for alone (issue 1126). +func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped, stood []string, err error) { + return compositions(in, node, fps, mode, false) +} + func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]delivery.Composed, skipped, stood []string, err error) { reading := in reading.Nodes = append([]Node(nil), in.Nodes...) found := false inOwn := map[string]bool{} + held := map[string]bool{} for i := range reading.Nodes { if reading.Nodes[i].Name == node { reading.Nodes[i].ReadsModules = true @@ -67,6 +75,9 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli for m, ok := range reading.Nodes[i].InOwn { inOwn[m] = ok } + for m, ok := range reading.Nodes[i].Held { + held[m] = ok + } } } // The private network's module is built by nobody: no fingerprint of the new form ever names it. @@ -88,15 +99,26 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli continue } runsOwn = n.RunsOwn + candidates := map[string]bool{} for m := range inOwn { - if !folds(m) { + candidates[m] = true + } + for m := range held { + candidates[m] = true + } + for m := range candidates { + // A held module is carried as it runs wherever the node's own runs it, folded or leaving the fold for a + // composition of its own (its want new, its names still unset): its stub is never what the node's own applies, + // and what the node's own applies never drops what runs of it (issue 1126, review of the apply; issue 1945). + heldHere := held[m] && RunsAnyOf(runsOwn, m) + if !folds(m) && !heldHere { continue } man, ok := n.Kept[m] if !ok { man, ok = reading.Catalogue[m] } - if !ok || !NamesBuild(man) { + if !ok || (!NamesBuild(man) && !held[m]) { continue } carried[m] = true @@ -231,6 +253,10 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli out[own] = c } for key, c := range out { + if held[c.Module] { + delete(out, key) // its stub's: the node keeps what it last applied of it (Node.Held) + continue + } if m, ok := in.Catalogue[c.Module]; ok && c.Module != "" { c.Data = len(m.DataItems()) > 0 out[key] = c @@ -243,6 +269,30 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli return out, skipped, stood, nil } +// RunsAnyOf says runs (a composition as it runs) holds a resource of module, by its module or the module its id names. +func RunsAnyOf(runs *delivery.Composed, module string) bool { + if runs == nil { + return false + } + for _, raw := range runs.Resources { + var head struct { + ID string `json:"id"` + Module string `json:"module"` + } + if json.Unmarshal(raw, &head) != nil { + continue + } + owner := head.Module + if owner == "" { + owner, _ = declaration.ModuleGuess(head.ID) + } + if owner == module { + return true + } + } + return false +} + // NamesBuild says a manifest names what a build makes: its build, or a resource naming an artifact. A node cannot // compose such a module folded into its own composition: the build of the old form it runs is held by the controller // alone (issue 1126). diff --git a/go/compose/frombus/answer.go b/go/compose/frombus/answer.go index 0f0bfe9..c871d6b 100644 --- a/go/compose/frombus/answer.go +++ b/go/compose/frombus/answer.go @@ -2,6 +2,7 @@ package frombus import ( "context" + "encoding/json" "fmt" "regexp" "slices" @@ -10,6 +11,7 @@ import ( "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" ) @@ -70,21 +72,14 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st return refuse(fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err)) } } - composed, skipped, stood, err := compose.CompositionsShadow(in, s.Node, fps) - for err != nil { - module, bare := culprit(in, s.Node, fps, failed, err) - if module == "" { - // Not one module's: the node's own (a value served elsewhere, the mesh's network), said once as the node's. - if len(failed) == 0 { - return refuse(err) - } - failed[delivery.HaveNodeWord] = err.Error() - composed, skipped, stood = nil, nil, nil - break + composed, skipped, stood, err := composeEach(in, s.Node, fps, false, s.Running, failed) + if err != nil { + // Not one module's: the node's own (a value served elsewhere, the mesh's network), said once as the node's. + if len(failed) == 0 { + return refuse(err) } - failed[module] = err.Error() - in = stubbed(in, s.Node, module, bare) - composed, skipped, stood, err = compose.CompositionsShadow(in, s.Node, fps) + failed[delivery.HaveNodeWord] = err.Error() + composed, skipped, stood = nil, nil, nil } // The node's own composition is judged only where the node runs one: one that runs none applies it only once a want // names it (the node agent's own composer hands it on that want alone), so a candidate does not add it. @@ -244,12 +239,259 @@ func standsInFor(x, node string) (string, string) { return node, "" } +// composeEach is the one rule, for the node that applies and for its answer to a candidate alike, by which a module's +// own refusal refuses that module and never the node (issue 1126: seed-8 fixed it in the answer alone, and the +// applying path still refused all of novox for de-spiegel's unset route names). The node is composed; a refusal is laid +// on the module whose stub (compose.Stub) changes it (culprit), that module is held (compose.Node.Held) and said in +// failed with the refusal's words, and the rest is composed again, until it composes or the refusal is no single +// module's, which is answered as the node's (err). Applied composes as the node applies (compose.Reading) and holds the +// module placed here each stand-in is for; one for no module placed here (the node's own, a value this node holds for a +// module elsewhere) refuses the node (compose.ErrStoodIn). Not applied, it composes in compose.Foreseeing and answers +// every stand-in for the caller to judge (Answer). +// +// **A held module is what runs of it** (review of the apply, round 1): composed from what running holds of it +// (compose.Carried of its own composition there; the node's own carries it wherever it runs there), so what the others +// read of it (the machine ports its containers publish, the routes reaching it) is what runs, and none of its own +// composition is in out: the node keeps what it last applied. **What depends on how it is held is held too:** a module +// whose composition differs when the held ones are composed as their stubs instead is held with them, and a node's own +// composition that differs (but for the held modules' own resources) refuses the node. And since a held module is +// composed from its new manifest, which may not be the build's that runs, what reads that manifest is held with it +// (dependents): the providers of what it contributes to (the proxy its routes), and, when it listens, the filter. +func composeEach(in compose.Input, node string, fps map[string]string, applied bool, running []delivery.Composed, failed map[string]string) (out map[string]delivery.Composed, skipped, stood []string, err error) { + mode := compose.Foreseeing + if applied { + mode = compose.Reading + } + runs := map[string]*delivery.Composed{} + for i := range running { + if m := running[i].Module; m != "" { + runs[m] = &running[i] + } + } + src := map[string]catalogue.Manifest{} // each held module's manifest before it was held + bareOf := map[string]bool{} + var hold func(m, why string, bare bool) + hold = func(m, why string, bare bool) { + if _, done := failed[m]; !done { + failed[m] = why // a held module tried bare keeps the words it was first held for + } + first := false + if _, ok := src[m]; !ok { + man, ok := manifestOn(in, node, m) + if !ok { + return + } + src[m], first = man, true + } + bareOf[m] = bareOf[m] || bare + in = heldAs(in, node, m, src[m], bareOf[m], runs[m]) + if !first { + return + } + // What the others read of its manifest (what it contributes, what it listens on) is its new manifest's, not + // necessarily the build's that runs: what reads it is held with it (review of the apply, round 2). + for _, r := range dependents(in, node, src[m], failed) { + hold(r, "it reads what "+m+" contributes or listens on, and "+m+" is held", false) + } + } + for _, m := range sortedKeys(failed) { + hold(m, failed[m], false) + } + for { + out, skipped, stood, err = compose.CompositionsIn(in, node, fps, mode) + if err != nil { + module, bare := culprit(in, node, fps, mode, failed, err) + if module == "" { + return nil, nil, nil, err + } + hold(module, err.Error(), bare) + continue + } + again := false + if applied && len(stood) > 0 { + placed := map[string]bool{} + for _, m := range assignedOn(in, node) { + placed[m] = true + } + of := map[string][]string{} + var nodes []string + for _, x := range stood { + if n, m := standsInFor(x, node); n == node && placed[m] { + of[m] = append(of[m], x) + } else { + nodes = append(nodes, x) + } + } + for _, m := range sortedKeys(of) { + if _, done := failed[m]; !done { + hold(m, "stands in for "+strings.Join(of[m], ", "), false) + again = true + } + } + if !again && len(nodes) > 0 { + return nil, nil, nil, fmt.Errorf("%w on %s: %s", compose.ErrStoodIn, node, strings.Join(nodes, ", ")) + } + } + if !again && len(failed) > 0 { + dependent, own := dependsOnHeld(in, node, fps, mode, out, src, bareOf, failed) + if own { + return nil, nil, nil, fmt.Errorf("frombus: %s's own composition depends on how %s, held, is composed, and is not applied", + node, strings.Join(sortedKeys(failed), ", ")) + } + for _, m := range dependent { + hold(m, "what it composes of "+strings.Join(sortedKeys(failed), ", ")+", held, is not what runs of them", false) + again = true + } + } + if !again { + break + } + } + skipped = slices.DeleteFunc(skipped, func(m string) bool { _, held := failed[m]; return held }) + return out, skipped, stood, nil +} + +// dependsOnHeld composes in again with each held module as its stub rather than as what runs of it, and answers the +// modules, not held, whose composition differs between the two (sorted), and own when the node's own composition does +// but for the held modules' own resources. A composition of stubs that fails says nothing more: what runs composed. +func dependsOnHeld(in compose.Input, node string, fps map[string]string, mode compose.Mode, asRuns map[string]delivery.Composed, + src map[string]catalogue.Manifest, bareOf map[string]bool, failed map[string]string) (dependent []string, own bool) { + stubs := in + for _, m := range sortedKeys(failed) { + if man, ok := src[m]; ok { + stubs = heldAs(stubs, node, m, man, bareOf[m], nil) + } + } + asStubs, _, _, err := compose.CompositionsIn(stubs, node, fps, mode) + if err != nil { + return nil, false + } + isHeld := func(m string) bool { _, ok := failed[m]; return ok } + keys := map[string]bool{} + for k := range asRuns { + keys[k] = true + } + for k := range asStubs { + keys[k] = true + } + for _, k := range sortedKeys(keys) { + a, inA := asRuns[k] + b, inB := asStubs[k] + m := a.Module + if !inA { + m = b.Module + } + if m != "" && isHeld(m) { + continue + } + if m == "" { + a, b = withoutHeld(a, isHeld), withoutHeld(b, isHeld) + } + da, errA := delivery.ComposedDigestOf(a) + db, errB := delivery.ComposedDigestOf(b) + if inA == inB && errA == nil && errB == nil && da == db { + continue + } + if m == "" { + own = true + continue + } + dependent = append(dependent, m) + } + return dependent, own +} + +// withoutHeld is the node's own composition c without the resources of a held module (its id naming it) and without +// its filled paths, for comparing the rest. +func withoutHeld(c delivery.Composed, isHeld func(string) bool) delivery.Composed { + out := c + out.Filled = nil + out.Resources = nil + for _, raw := range c.Resources { + var head struct { + ID string `json:"id"` + Module string `json:"module"` + } + _ = json.Unmarshal(raw, &head) + owner := head.Module + if owner == "" { + owner, _ = declaration.ModuleGuess(head.ID) + } + if owner != "" && isHeld(owner) { + continue + } + out.Resources = append(out.Resources, raw) + } + return out +} + +// dependents is, of the modules placed on node and not held, those that read what man says to them: each that provides +// what man contributes to (the proxy its routes), and, when man listens on a port, each that holds the filter (the +// openings). +func dependents(in compose.Input, node string, man catalogue.Manifest, failed map[string]string) []string { + to := map[string]bool{} + for p := range man.Contributes { + to[p] = true + } + for p := range man.ContributesMany { + to[p] = true + } + listens := len(man.Listens) > 0 + if len(to) == 0 && !listens { + return nil + } + var out []string + for _, m := range assignedOn(in, node) { + if _, held := failed[m]; held || m == man.Module { + continue + } + pm, ok := manifestOn(in, node, m) + if !ok { + continue + } + if listens && pm.Filtering != nil { + out = append(out, m) + continue + } + for _, o := range pm.Provides { + if to[o.Name] { + out = append(out, m) + break + } + } + } + return out +} + +// assignedOn is the modules placed on node, in placement order. +func assignedOn(in compose.Input, node string) []string { + for _, n := range in.Nodes { + if n.Name == node { + return n.Assigned + } + } + return nil +} + +// manifestOn is the manifest node composes module from: its Kept, else the catalogue's. +func manifestOn(in compose.Input, node, module string) (catalogue.Manifest, bool) { + for _, n := range in.Nodes { + if n.Name == node { + if man, ok := n.Kept[module]; ok { + return man, true + } + } + } + man, ok := in.Catalogue[module] + return man, ok +} + // culprit is the module placed on node whose composition the error is of: the first, in placement order, whose stub -// (compose.Stub) makes the composition succeed or fail otherwise; empty when none does (the error is not one module's). -// Modules already refused are passed over. A module whose stub still fails the same is tried bare (its stub contributing -// nothing, bare true): a refusal of what it contributes is its own, which the stub keeps (issue 1126, seed-8: de-spiegel's -// route names on novox, an unset required value, refused the whole node). -func culprit(in compose.Input, node string, fps map[string]string, failed map[string]string, err error) (string, bool) { +// (compose.Stub) makes the composition in mode succeed or fail otherwise; empty when none does (the error is not one +// module's). Modules already refused are passed over. A module whose stub still fails the same is tried bare (its stub +// contributing nothing, bare true): a refusal of what it contributes is its own, which the stub keeps (issue 1126, +// seed-8: de-spiegel's route names on novox, an unset required value, refused the whole node). +func culprit(in compose.Input, node string, fps map[string]string, mode compose.Mode, failed map[string]string, err error) (string, bool) { var assigned []string for _, n := range in.Nodes { if n.Name == node { @@ -266,7 +508,7 @@ func culprit(in compose.Input, node string, fps map[string]string, failed map[st } } changes := func(m string, bare bool) bool { - _, _, _, again := compose.CompositionsShadow(stubbed(in, node, m, bare), node, fps) + _, _, _, again := compose.CompositionsIn(stubbed(in, node, m, bare), node, fps, mode) return again == nil || again.Error() != err.Error() } for _, m := range order { @@ -277,8 +519,16 @@ func culprit(in compose.Input, node string, fps map[string]string, failed map[st return m, false } } + var kept map[string]catalogue.Manifest + for _, n := range in.Nodes { + if n.Name == node { + kept = n.Kept + } + } for _, m := range order { - if _, done := failed[m]; done { + // One already held is tried bare too, once: held for its build, its stub may still refuse (its own secret + // never issued, issue 1126). + if _, done := failed[m]; done && isBare(kept[m]) { continue } if changes(m, true) { @@ -288,31 +538,47 @@ func culprit(in compose.Input, node string, fps map[string]string, failed map[st return "", false } -// stubbed is in with module composed as a stub on node alone (compose.Stub, as node's Kept); bare, a stub that also -// contributes nothing to what it requires. +// isBare says a stub contributes nothing and declares no own secret (stubbed, bare). +func isBare(m catalogue.Manifest) bool { + return len(m.Contributes) == 0 && len(m.ContributesMany) == 0 && len(m.OwnSecrets) == 0 +} + +// stubbed is in with module held on node as its stub alone (heldAs, nothing of it running), from the manifest node +// composes it from: a culprit's try. func stubbed(in compose.Input, node, module string, bare bool) compose.Input { + man, ok := manifestOn(in, node, module) + if !ok { + return in + } + return heldAs(in, node, module, man, bare, nil) +} + +// heldAs is in with module held on node (compose.Node.Held): composed from man as what runs of it (compose.Carried of +// runs; its stub, compose.Stub, when runs is nil or holds nothing of it), as node's Kept; bare, contributing nothing to +// what it requires and declaring no own secret: none of a held module's own composition is applied, so what it has not +// been issued (restic's repository on a node it never ran on) is no refusal of the rest. +func heldAs(in compose.Input, node, module string, man catalogue.Manifest, bare bool, runs *delivery.Composed) compose.Input { out := in out.Nodes = append([]compose.Node(nil), in.Nodes...) for i := range out.Nodes { if out.Nodes[i].Name != node { continue } - man, ok := out.Nodes[i].Kept[module] - if !ok { - if man, ok = in.Catalogue[module]; !ok { - return in - } + if bare { + man.Contributes, man.ContributesMany, man.OwnSecrets = nil, nil, nil } kept := make(map[string]catalogue.Manifest, len(out.Nodes[i].Kept)+1) for k, v := range out.Nodes[i].Kept { kept[k] = v } - stub := compose.Stub(man) - if bare { - stub.Contributes, stub.ContributesMany = nil, nil - } - kept[module] = stub + kept[module] = compose.Carried(man, module, runs) out.Nodes[i].Kept = kept + held := make(map[string]bool, len(out.Nodes[i].Held)+1) + for k, v := range out.Nodes[i].Held { + held[k] = v + } + held[module] = true + out.Nodes[i].Held = held } return out } diff --git a/go/compose/frombus/answer_rows_test.go b/go/compose/frombus/answer_rows_test.go index b90e2a1..71ea788 100644 --- a/go/compose/frombus/answer_rows_test.go +++ b/go/compose/frombus/answer_rows_test.go @@ -111,7 +111,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { } // A refusal naming no module placed here is no module's: no compose is spent stubbing each in turn (issue 1126: ace's // answer took 7m46s, one compose per module), though here a stub would compose. - if m, _ := culprit(in, "n1", fps, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" { + if m, _ := culprit(in, "n1", fps, compose.Foreseeing, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" { t.Errorf("a refusal naming no placed module was laid on %s", m) } ownKey, _ := delivery.ComposedNodeKey("n1") @@ -129,7 +129,10 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { running = append(running, c) } scope := Scope{Node: "n1", InOwn: []string{"spiegel"}, Running: running} - got, _, err := Compose(ctx, b, Mesh{}, scope, fps) + got, _, held, err := Compose(ctx, b, Mesh{}, scope, fps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatalf("the node does not compose itself with spiegel folded at a build of the old form: %v", err) } @@ -162,9 +165,9 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("the node's own with its bus membership is answered %+v, want the same", a) } // Without what runs, the fold composes spiegel from its manifest, as before, and the composition refuses it. - if _, _, err := Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, fps); err == nil || - !strings.Contains(err.Error(), "spiegel has not been built for this mesh") { - t.Errorf("composed spiegel without what runs: %v", err) + if _, _, held, err := Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, fps); err != nil || + !strings.Contains(held["spiegel"], "spiegel has not been built for this mesh") { + t.Errorf("composed spiegel without what runs: %v %v, want spiegel held alone", err, held) } // (C) Issue 1126, C5: what the others read of a module carried as it runs is what runs, never its stub's: the filter @@ -217,7 +220,10 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { } running = append(running, c) } - got, _, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}, Running: running}, fps) + got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}, Running: running}, fps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatalf("the node does not compose itself with spiegel carried: %v", err) } @@ -278,7 +284,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { // de-spiegel folded, and its manifest at the indexer's commit (e25e1beb, captured) asks four required config values // for its route names that nothing sets. novox answered the whole candidate refused ("de-spiegel contributing // praktijk to route says ${setting:praktijk-name} ...": its stub still contributed the route). Only de-spiegel is - // refused, in the config form's words, and the rest is judged. + // refused, in the config form's words, and proxy, which receives its routes (what it would compose lacks them: review + // of the apply, round 1); the rest is judged. spiegel, err := os.ReadFile("testdata/de-spiegel-e25e1beb.module.json") if err != nil { t.Fatal(err) @@ -302,11 +309,143 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("de-spiegel is refused in other words than its config form's: %s", r.Why) } } - if a.Refused != "" || !reflect.DeepEqual(refused, []string{"de-spiegel"}) { - t.Errorf("a module's own refusal: %+v, want de-spiegel refused alone, the node not", a) + if a.Refused != "" || !reflect.DeepEqual(refused, []string{"de-spiegel", "proxy"}) { + t.Errorf("a module's own refusal: %+v, want de-spiegel refused with proxy, which receives its routes, the node not", a) } - if !slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "proxy" }) { - t.Errorf("proxy, which composes, is not judged beside de-spiegel's refusal: %+v", a) + if !slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "lemurs" }) { + t.Errorf("lemurs, which composes, is not judged beside de-spiegel's refusal: %+v", a) + } + + // (E) Issue 1126, the applying path (review of the seed): the composition novox applies follows the same rule. It + // composed with no culprit, so de-spiegel's unset route names refused the whole node and every module it runs was + // marked not composed. Now de-spiegel is held, its refusal said in its config form's words, with proxy, which + // receives its routes; lemurs composes, and the node's own composition carries de-spiegel as it runs, so applying it + // removes nothing of it. + spiegelRuns := []json.RawMessage{ + json.RawMessage(`{"id":"de-spiegel.state","type":"directory","path":"/var/lib/de-spiegel","mode":"0755"}`), + json.RawMessage(`{"id":"de-spiegel.server-env","type":"file","path":"/var/lib/de-spiegel/server.env","mode":"0600","content":"A=1\n"}`), + json.RawMessage(`{"id":"de-spiegel.server","type":"container","name":"de-spiegel","image":"registry.example/de-spiegel@sha256:` + strings.Repeat("c", 64) + `"}`)} + if in, err = Input(ctx, b, Mesh{}, stub); err != nil { + t.Fatal(err) + } + in.Nodes[0].Kept = map[string]catalogue.Manifest{"de-spiegel": compose.Stub(in.Catalogue["de-spiegel"])} + in.Nodes[0].Kept["de-spiegel"] = func(m catalogue.Manifest) catalogue.Manifest { + m.Contributes, m.ContributesMany = nil, nil + return m + }(in.Nodes[0].Kept["de-spiegel"]) + if base, _, err = compose.Compositions(in, "n1", fps, compose.Reading); err != nil { + t.Fatal(err) + } + running = nil + for _, c := range base { + if c.Module == "" { + c.Resources = append(append([]json.RawMessage{}, c.Resources...), spiegelRuns...) + } + running = append(running, c) + } + got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"de-spiegel"}, Running: running}, fps) + if err != nil { + t.Fatalf("de-spiegel's own refusal refuses the node's whole composition: %v", err) + } + if len(held) != 2 || !strings.Contains(held["de-spiegel"], "${config:praktijk-name}") || !strings.Contains(held["proxy"], "de-spiegel") { + t.Errorf("held %v, want de-spiegel in its config form's words, and proxy for its routes", held) + } + lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fps["lemurs"]) + proxyKey, _ := delivery.ComposedKey("n1", "proxy", fps["proxy"]) + if _, ok := got[lemursKey]; !ok { + t.Errorf("lemurs, which composes, is not composed beside de-spiegel's refusal: %v", sortedKeys(got)) + } + if _, ok := got[proxyKey]; ok { + t.Errorf("proxy is composed without the routes of held de-spiegel: %v", sortedKeys(got)) + } + carries := func(c delivery.Composed, what string) { + t.Helper() + ownRes, _ := json.Marshal(c.Resources) + for _, raw := range spiegelRuns { + var r struct{ ID string } + _ = json.Unmarshal(raw, &r) + if !strings.Contains(string(ownRes), `"id":"`+r.ID+`"`) { + t.Errorf("%s: the node's own composition drops %s, which de-spiegel runs: %s", what, r.ID, ownRes) + } + } + } + carries(got[ownKey], "folded") + // (F) Leaving the fold (review of the apply, round 1, finding 1): de-spiegel's want names a build of the new form, + // so it is no longer folded, while its names are still unset and the node's own still runs it. Held, it is carried + // in the node's own as it runs until a composition of its own is handed: else the node's own apply removes its + // container, files and units (issue 1945 by a new path). + fps["de-spiegel"] = fp("7") + got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: running}, fps) + if err != nil || !strings.Contains(held["de-spiegel"], "${config:praktijk-name}") { + t.Fatalf("de-spiegel leaving the fold: %v, held %v", err, held) + } + spiegelKey, _ := delivery.ComposedKey("n1", "de-spiegel", fps["de-spiegel"]) + if _, ok := got[spiegelKey]; ok { + t.Errorf("held de-spiegel's own composition is handed") + } + carries(got[ownKey], "leaving the fold") + + // (G) A held module that runs a composition of its own (review of the apply, round 1, finding 2): what the others + // compose of it is what runs, never its stub's. site listens on 8080, placed at the machine's 20005, and its own + // secret was never issued here: held, the filter must not open 8080 and drop the 20005 that runs. A module whose + // composition depends on how site is held (here the filter) is held with it. + site := []byte(`{"module":"site","version":"1","capabilities":["container-runtime"],` + + `"listens":[{"port":8080,"from":"mesh","why":"the site"}],"own-secrets":{"key":"${dir:state}/key"},` + + `"resources":[{"id":"state","type":"directory","path":"/var/lib/site","mode":"0700"},` + + `{"id":"server","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}]}`) + b = mk("lemurs", overlay.Name, "site", "nft") + for m, raw := range map[string][]byte{"site": site, "nft": nft} { + b.manifests[delivery.ManifestDigest(raw)] = raw + b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)} + } + p = b.placements["n1"] + p.Decided.Endpoint = "n1.example:51820" + for i := range p.Modules { + if p.Modules[i].Module == "site" { + p.Modules[i].Ports = map[string]int{"8080": 20005} + } + } + b.placements["n1"] = p + rec = b.records["n1"] + rec.OutwardLinks = []string{"eth0"} + b.records["n1"] = rec + fps = map[string]string{"lemurs": fp("e"), "nft": fp("f"), "site": fp("8")} + siteRuns := delivery.Composed{Node: "n1", Module: "site", FP: fps["site"], Resources: []json.RawMessage{ + json.RawMessage(`{"id":"site.state","module":"site","type":"directory","path":"/var/lib/site","mode":"0700"}`), + json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["10.77.0.1:20005:8080"]}`)}} + got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps) + if err != nil || held["site"] == "" || held["nft"] == "" { + t.Fatalf("site, its own secret never issued: %v, held %v; want site held, and the filter, which depends on it", err, held) + } + for key, c := range got { + for _, raw := range c.Resources { + if strings.Contains(string(raw), "dport 8080") || (c.Module == "nft" && !strings.Contains(string(raw), "dport 20005") && strings.Contains(string(raw), "# site")) { + t.Errorf("%s composes held site from its stub, not from what runs: %s", key, raw) + } + } + } + // (H) Review of the apply, round 2: a held module is composed from its new manifest, so what the others read of its + // manifest (its listens, what it contributes) may not be what runs. site's new manifest moves it to 9090 while the + // build that runs listens on 8080: the filter, the holder of what site listens on, is held with it, and nothing + // applied opens 9090. + moved := []byte(strings.Replace(string(site), `"port":8080`, `"port":9090`, 1)) + b.manifests[delivery.ManifestDigest(moved)] = moved + b.modules["site"] = delivery.Module{Manifest: delivery.ManifestDigest(moved)} + for i := range p.Modules { + p.Modules[i].Ports = nil + } + b.placements["n1"] = p + siteRuns.Resources[1] = json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}`) + got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps) + if err != nil || held["site"] == "" || held["nft"] == "" { + t.Errorf("site moved to 9090 in its new manifest: %v, held %v; want site held, and the filter with it", err, held) + } + for key, c := range got { + for _, raw := range c.Resources { + if strings.Contains(string(raw), "dport 9090") { + t.Errorf("%s opens 9090, which held site's new manifest names and nothing runs: %s", key, raw) + } + } } } diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index 7101fa0..7632246 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -25,8 +25,8 @@ // **Own secrets and pair credentials** come from the vault sealed to the composing node's key (OwnSecrets, Credentials), // for that node alone: as a consumer and as a provider, exactly the keys compose.CredentialsNeeded names. **Not yet // read** (each a later subtask of issue 1126): licences (none is placed today) and core boxes. Without one -// the composition stands in for it, and Compose refuses a composition that stands in for anything (compose.ErrStoodIn): -// a node never applies a stand-in. No node is joining before ADR 0547's third milestone (ADR 0550 §6), so Joining is +// the composition stands in for it, and Compose holds the module it stands in for (or refuses the node, for a stand-in +// of no module placed there; compose.ErrStoodIn): a node never applies a stand-in. No node is joining before ADR 0547's third milestone (ADR 0550 §6), so Joining is // always empty. package frombus @@ -379,25 +379,31 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module return in, nil } -// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.CompositionsApplied at the -// fingerprints fps gives each module: composed in the Reading mode as the controller composes them, and refused when it -// stands in for anything the input does not carry (compose.ErrStoodIn), since the node applies what it composed. -func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (map[string]delivery.Composed, []string, error) { - in, err := Input(ctx, r, mesh, s) +// Compose is the node's compositions over the bus (ADR 0547 §2): input for s, then composed at the fingerprints fps +// gives each module in the Reading mode, as the controller composes them, by the one rule its answer to a candidate +// follows too (composeEach, issue 1126): a module refused alone (its build unread or not taken in, its composition +// refused, a stand-in for a value of its own the input does not carry, compose.ErrStoodIn) is held, named in held with +// why, and none of its own composition is in out, so the node keeps what it last applied of it and applies the rest; a +// node never applies a stand-in. Only a refusal that is no single module's (the node's own resources, a value served +// elsewhere, a stand-in for the node's own) refuses the node, err. +func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) { + held = map[string]string{} + in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }) if err != nil { - return nil, nil, err + return nil, nil, held, err } // The pair credentials its composition reads, asked of the vault sealed to it (ADR 0550 §5). asConsumer, asProvider, err := compose.CredentialsNeeded(in, s.Node) if err != nil { - return nil, nil, err + return nil, nil, held, err } if len(asConsumer)+len(asProvider) > 0 { if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil { - return nil, nil, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err) + return nil, nil, held, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err) } } - return compose.CompositionsApplied(in, s.Node, fps) + out, skipped, _, err = composeEach(in, s.Node, fps, true, s.Running, held) + return out, skipped, held, err } // fromBuilds puts in the built manifest of each module placed on the composing node, at the trunk's FP2, and of each diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index e02fa56..d15dbed 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -223,7 +223,10 @@ func TestInput(t *testing.T) { // The same composer over both: the compositions from the bus are the store's, byte for byte. fps := map[string]string{overlay.Name: "src2:" + strings.Repeat("a", 64), "xterm": "src2:" + strings.Repeat("b", 64), "media": "src2:" + strings.Repeat("c", 64)} - fromBus, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop"}, fps) + fromBus, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop"}, fps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatal(err) } @@ -243,7 +246,10 @@ func TestInput(t *testing.T) { running = append(running, c) } laptop := mk().placements["laptop"] - asIs, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop}}, fps) + asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop}}, fps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatal(err) } @@ -252,7 +258,10 @@ func TestInput(t *testing.T) { } moved := mk().placements["anchor"] moved.Decided.Endpoint = "anchor.example:51821" - changed, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop, "anchor": &moved}}, fps) + changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop, "anchor": &moved}}, fps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatal(err) } @@ -289,7 +298,10 @@ func TestInput(t *testing.T) { } oneFps := map[string]string{"keeper": "src2:" + strings.Repeat("d", 64), "lemurs": "src2:" + strings.Repeat("e", 64), "objects": "src2:" + strings.Repeat("f", 64), "files": "src2:" + strings.Repeat("0", 64)} - runs, _, err := Compose(ctx, one("keeper", "lemurs"), Mesh{}, Scope{Node: "n1"}, oneFps) + runs, _, held, err := Compose(ctx, one("keeper", "lemurs"), Mesh{}, Scope{Node: "n1"}, oneFps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatal(err) } @@ -297,7 +309,10 @@ func TestInput(t *testing.T) { for _, c := range runs { runsNow = append(runsNow, c) } - objectsRun, _, err := Compose(ctx, one("objects"), Mesh{}, Scope{Node: "n1"}, oneFps) + objectsRun, _, held, err := Compose(ctx, one("objects"), Mesh{}, Scope{Node: "n1"}, oneFps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatal(err) } @@ -579,11 +594,21 @@ func TestInput(t *testing.T) { } return b } - if _, _, err := Compose(ctx, movedMesh(false), Mesh{}, Scope{Node: "n1"}, movedFps); err == nil || !strings.Contains(err.Error(), `restic needs a secret called "repository"`) { - t.Errorf("restic composed from the indexer's manifest: %v, want its own secret refused", err) + if out, _, held, err := Compose(ctx, movedMesh(false), Mesh{}, Scope{Node: "n1"}, movedFps); err != nil || !strings.Contains(held["restic"], `restic needs a secret called "repository"`) { + t.Errorf("restic composed from the indexer's manifest: %v %v, want its own secret refusing restic alone", err, held) + } else { + for key, c := range out { + if c.Module == "restic" { + // Its stub's composition, applied, would remove what restic runs: a held module keeps its last one. + t.Errorf("held restic is composed as %s, want none of its own composition handed", key) + } + } } builtBus := movedMesh(true) - fromBuild, _, err := Compose(ctx, builtBus, Mesh{}, Scope{Node: "n1"}, movedFps) + fromBuild, _, held, err := Compose(ctx, builtBus, Mesh{}, Scope{Node: "n1"}, movedFps) + if err == nil && len(held) > 0 { + err = fmt.Errorf("modules held: %v", held) + } if err != nil { t.Fatalf("restic composed from its build: %v", err) } @@ -597,8 +622,8 @@ func TestInput(t *testing.T) { // A done key an artifact of which names no reference is waited for, in the controller's words (ADR 0498 §8). unreferenced := movedMesh(true) unreferenced.builds["restic "+movedFps["restic"]].Artifacts = []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Digest: digestOf("6")}, resticArtifacts[1]} - if _, _, err := Compose(ctx, unreferenced, Mesh{}, Scope{Node: "n1"}, movedFps); err == nil || !strings.Contains(err.Error(), "the build of restic is done and waits for the reference of tools") { - t.Errorf("a done key with an unreferenced artifact: %v", err) + if _, _, held, err := Compose(ctx, unreferenced, Mesh{}, Scope{Node: "n1"}, movedFps); err != nil || !strings.Contains(held["restic"], "the build of restic is done and waits for the reference of tools") { + t.Errorf("a done key with an unreferenced artifact: %v %v, want restic held alone", err, held) } // A module the node runs inside its own composition (the fold) composes from the indexer's manifest though a done // build of it is at the trunk's FP2: its build is not read. @@ -662,12 +687,12 @@ func TestInput(t *testing.T) { } return b } - if _, _, err := Compose(ctx, withMailer(false), Mesh{}, Scope{Node: "laptop"}, fps); err == nil || !strings.Contains(err.Error(), `nothing sets "domain"`) { + if _, _, _, err := Compose(ctx, withMailer(false), Mesh{}, Scope{Node: "laptop"}, fps); err == nil || !strings.Contains(err.Error(), `nothing sets "domain"`) { t.Errorf("a provider's served setting nobody sets: %v", err) } served := withMailer(true) - if _, _, err := Compose(ctx, served, Mesh{}, Scope{Node: "laptop"}, fps); err != nil { - t.Errorf("laptop with what anchor's mailer serves: %v", err) + if _, _, held, err := Compose(ctx, served, Mesh{}, Scope{Node: "laptop"}, fps); err != nil || len(held) > 0 { + t.Errorf("laptop with what anchor's mailer serves: %v %v", err, held) } if !reflect.DeepEqual(served.servedRead, []string{"anchor.mailer:domain"}) || slices.Contains(served.valuesRead, "anchor.mailer") { t.Errorf("laptop read %v of other nodes (and %v of config values): the served keys of anchor's mailer alone", served.servedRead, served.valuesRead) diff --git a/go/compose/input.go b/go/compose/input.go index 5b54123..5b9cba0 100644 --- a/go/compose/input.go +++ b/go/compose/input.go @@ -204,6 +204,12 @@ type Node struct { // so it is left to the node: Compositions composes it as a stub (Stub) and carries its resources as they run, each // after the resource it runs after (issue 1126). Nil composes every folded module from its manifest, as before. RunsOwn *delivery.Composed + // Held is the modules refused alone on this node, each composed as what runs of it (Carried, in Kept; its Stub when + // nothing of it runs) so the rest is composed (novox/hq issue 1126): none of a held module's own composition is in what + // Compositions answers, so the node keeps what it last applied of it; one whose resources the node's own runs + // (RunsOwn), folded or leaving the fold, is carried there as it runs, whatever its manifest names, so the node's own + // never drops it. + Held map[string]bool } // Place is where a node is, as its placement says (inv.Overlay).