diff --git a/cmd/mesh-controller/adoption.go b/cmd/mesh-controller/adoption.go index 96390916..0ff12403 100644 --- a/cmd/mesh-controller/adoption.go +++ b/cmd/mesh-controller/adoption.go @@ -13,7 +13,6 @@ import ( "time" "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" ) @@ -708,18 +707,10 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s } // What the filter is derived from: the rendering of this machine as a question reads it, with the private network. - input, err := inputFor(ctx, open) + with, err := renderingOf(ctx, open, node) if err != nil { return "", err } - rendered, err := compose.Render(input.Input, node, compose.Reading) - if err != nil { - return "", err - } - with := rendered.Rendering - if with.Generators, err = compose.Generators(input.Input); err != nil { - return "", err - } rules, err := plan.Rules(with) if err != nil { return "", err diff --git a/cmd/mesh-controller/awaiting_push_integration_test.go b/cmd/mesh-controller/awaiting_push_integration_test.go index 3cf81782..b18d766f 100644 --- a/cmd/mesh-controller/awaiting_push_integration_test.go +++ b/cmd/mesh-controller/awaiting_push_integration_test.go @@ -9,6 +9,7 @@ import ( "time" "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/conditions" "github.com/novox/mesh-controller/internal/inventory" @@ -25,21 +26,13 @@ func TestAModuleAssignedAndNotPushedIsAwaitingAPushNotUncomposable(t *testing.T) // The read the rest of the mesh asks still refuses it, with the composer's typed error: nothing // can be compared about a secret that does not exist (status), and nothing here string-matches. - plan, settings, err := planFor(ctx, open, "laptop") - if err != nil { - t.Fatal(err) - } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } - _, err = declarationWith(ctx, open, "laptop", plan, settings, gens, Reading) + _, _, err := composeQuestion(ctx, open, "laptop", compose.Reading, false) var notMade *catalogue.NotMadeError if !errors.As(err, ¬Made) || notMade.Module != "keeper" || notMade.Name != "repository" { t.Fatalf("a read composition did not say which secret is not made, typed: %v", err) } // Foreseen, it composes, names what the push makes, and made nothing. - foreseen, err := declarationWith(ctx, open, "laptop", plan, settings, gens, Foreseeing) + foreseen, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false) if err != nil || len(foreseen.foreseen) != 1 || foreseen.foreseen[0] != "keeper/repository" { t.Fatalf("foreseen: %v %v", foreseen.foreseen, err) } diff --git a/cmd/mesh-controller/awaiting_push_test.go b/cmd/mesh-controller/awaiting_push_test.go index 8c947f3d..74b7097b 100644 --- a/cmd/mesh-controller/awaiting_push_test.go +++ b/cmd/mesh-controller/awaiting_push_test.go @@ -29,7 +29,7 @@ func aKeeper() catalogue.Manifest { func pushedBy(t *testing.T, open *stores, node string) string { t.Helper() ctx := t.Context() - plan, settings, err := planFor(ctx, open, node) + plan, settings, err := resolveNode(ctx, open, node) if err != nil { t.Fatal(err) } @@ -54,11 +54,10 @@ func pushedBy(t *testing.T, open *stores, node string) string { if err != nil { t.Fatal(err) } - names := make([]string, 0, len(plan.Modules)) + builds = map[string]string{} for _, m := range plan.Modules { - names = append(names, m.Module) + builds[m.Module] = current[m.Module].Commit } - builds = carriedBuilds(names, nil, current, nil) } if err := open.inventory.RecordSent(ctx, record.ID, digest, builds); err != nil { t.Fatal(err) diff --git a/cmd/mesh-controller/bindings_integration_test.go b/cmd/mesh-controller/bindings_integration_test.go index f7571e26..da08a10a 100644 --- a/cmd/mesh-controller/bindings_integration_test.go +++ b/cmd/mesh-controller/bindings_integration_test.go @@ -37,7 +37,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"}, [2]string{"laptop", "board"}) - plan, _, err := planFor(ctx, open, "laptop") + plan, _, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } @@ -64,7 +64,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) if err := inv.Unassign(ctx, "laptop", "store"); err != nil { t.Fatal(err) } - if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) || + if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || !unresolvable(err) || !strings.Contains(err.Error(), "board on laptop is bound to laptop/store") || !strings.Contains(err.Error(), "pin laptop postgres-database anchor store") { t.Fatalf("the consumer's store went and it was answered elsewhere: %v", err) @@ -74,7 +74,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil { t.Fatal(err) } - plan, _, err = planFor(ctx, open, "laptop") + plan, _, err = resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/compose_callers_test.go b/cmd/mesh-controller/compose_callers_test.go index 4cd076c7..626860dd 100644 --- a/cmd/mesh-controller/compose_callers_test.go +++ b/cmd/mesh-controller/compose_callers_test.go @@ -10,21 +10,13 @@ import ( "testing" ) -// 1388: no caller composes through the controller's own composition any more. The only functions left that call it are the -// composition's own, which issue 1389 deletes with it; this fails the moment any other function reaches for it again. -func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { - legacy := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true, - "renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true} - // The composition's own pairs, until 1389: "file: caller calls callee". - allowed := map[string]bool{ - "facts.go: composedAndValidated calls declarationWith": true, - "facts.go: composedAndValidated calls planFor": true, - "network.go: generators calls whoResolves": true, - "network.go: whoResolves calls planFor": true, - "plan.go: declarationWith calls renderingFor": true, - "plan.go: grantsFor calls planFor": true, - "plan.go: planFor calls theRestOfTheMesh": true, - } +// 1389: the controller's own composition is gone. No function of the package is named for it any more, and no function +// calls one by that name: a name reappearing is a second copy of what mesh-sdk's go/compose holds (novox/hq ADR 0453). +func TestTheControllersOwnCompositionIsGone(t *testing.T) { + gone := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true, + "renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true, "grantsFor": true, "namesInTheMesh": true, + "onThePrivateNetwork": true, "passSeatsOn": true, "theMeshPassesSeatsOn": true, "assignmentsOf": true, + "licencesFor": true, "seatsOn": true, "composeBusUsers": true, "adoptionFor": true, "localNetworksOf": true} files, err := filepath.Glob("*.go") if err != nil { t.Fatal(err) @@ -41,12 +33,18 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { } for _, d := range file.Decls { fn, ok := d.(*ast.FuncDecl) - if !ok || fn.Body == nil { + if !ok { + continue + } + if gone[fn.Name.Name] { + found = append(found, f+": "+fn.Name.Name+" is declared") + } + if fn.Body == nil { continue } ast.Inspect(fn.Body, func(n ast.Node) bool { if c, ok := n.(*ast.CallExpr); ok { - if id, ok := c.Fun.(*ast.Ident); ok && legacy[id.Name] { + if id, ok := c.Fun.(*ast.Ident); ok && gone[id.Name] { found = append(found, f+": "+fn.Name.Name+" calls "+id.Name) } } @@ -55,9 +53,7 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { } } sort.Strings(found) - for _, pair := range found { - if !allowed[pair] { - t.Errorf("%s: it composes through the controller's own composition, not mesh-sdk's", pair) - } + for _, one := range found { + t.Errorf("%s: the controller's own composition is back; compose through mesh-sdk (compose_sdk.go)", one) } } diff --git a/cmd/mesh-controller/compose_helpers_test.go b/cmd/mesh-controller/compose_helpers_test.go new file mode 100644 index 00000000..918f2dfa --- /dev/null +++ b/cmd/mesh-controller/compose_helpers_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" +) + +// grantsOf is the grants a provider creates for its consumers, the consumers whose identity overflows the provision's bound +// and the credentials bound to another consumer, as mesh-sdk's rendering gives them for the machine (it replaced the +// controller's grantsFor, issue 1389). +func grantsOf(ctx context.Context, open *stores, node string) ([]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, nil, nil, err + } + rendered, err := compose.Render(in.Input, node, compose.Reading) + if err != nil { + return nil, nil, nil, err + } + return rendered.Rendering.Grants, rendered.Withheld, rendered.Unbound, nil +} + +// madeAndRendered sends to the machine (so everything a send makes is made) and answers its rendering as a question reads it +// afterwards: what the controller's Allocating rendering was. +func madeAndRendered(ctx context.Context, open *stores, node string) (catalogue.Rendering, error) { + if _, err := composeSend(ctx, open, node); err != nil { + return catalogue.Rendering{}, err + } + forgetInput(ctx) + return renderingOf(ctx, open, node) +} diff --git a/cmd/mesh-controller/compose_input_credentials_integration_test.go b/cmd/mesh-controller/compose_input_credentials_integration_test.go index c0d43de9..c9bcd5d9 100644 --- a/cmd/mesh-controller/compose_input_credentials_integration_test.go +++ b/cmd/mesh-controller/compose_input_credentials_integration_test.go @@ -83,7 +83,7 @@ func TestAPairCredentialNoRowHoldsIsAWouldMakeAndNoRowIsInserted(t *testing.T) { t.Fatal(err) } // The controller's own planning makes the credentials of files and reports. - if _, _, err := planFor(ctx, open, "laptop"); err != nil { + if _, _, err := resolveNode(ctx, open, "laptop"); err != nil { t.Fatal(err) } register(t, open, aConsumerOfTheBucket("metrics")) diff --git a/cmd/mesh-controller/compose_input_integration_test.go b/cmd/mesh-controller/compose_input_integration_test.go index 6fea7665..6e4f190c 100644 --- a/cmd/mesh-controller/compose_input_integration_test.go +++ b/cmd/mesh-controller/compose_input_integration_test.go @@ -280,7 +280,7 @@ func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testin if anchor, _ := nodeOf(in.Input, "anchor"); len(anchor.Settings["step-ca"]) == 0 { t.Fatalf("the Input does not carry the setting of the provider: %v", anchor.Settings) } - oldPlan, _, err := planFor(ctx, open, "laptop") + oldPlan, _, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } @@ -317,7 +317,7 @@ func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *t if err := open.inventory.ClearSettings(ctx, "", "step-ca"); err != nil { t.Fatal(err) } - _, _, oldErr := planFor(ctx, open, "laptop") + _, _, oldErr := resolveNode(ctx, open, "laptop") _, newErr := readComposeInput(ctx, open) if (oldErr == nil) != (newErr == nil) { t.Fatalf("the controller's composition says %v, the adapter says %v", oldErr, newErr) diff --git a/cmd/mesh-controller/compose_made_integration_test.go b/cmd/mesh-controller/compose_made_integration_test.go new file mode 100644 index 00000000..2af13fa4 --- /dev/null +++ b/cmd/mesh-controller/compose_made_integration_test.go @@ -0,0 +1,46 @@ +//go:build integration + +package main + +import ( + "context" + "fmt" + "sort" +) + +// madeIn is what a store holds that a send makes, by key and never by value. +func madeIn(ctx context.Context, open *stores) (made, error) { + var out made + inv := open.inventory + nodes, err := inv.Nodes(ctx) + if err != nil { + return out, err + } + for _, n := range nodes { + ports, err := inv.PortsFor(ctx, n.Name) + if err != nil { + return out, err + } + for _, p := range ports { + out.Ports = append(out.Ports, fmt.Sprintf("%s %s:%d->%d", n.Name, p.Module, p.Wanted, p.Machine)) + } + rows, err := inv.SecretsFrom(ctx, n.Name) + if err != nil { + return out, err + } + for _, s := range rows { + out.Pairs = append(out.Pairs, fmt.Sprintf("%s %s on %s from %s local=%s", s.Name, s.ConsumerModule, s.Consumer, n.Name, s.Local)) + } + } + own, err := inv.OwnSecretRows(ctx) + if err != nil { + return out, err + } + for _, r := range own { + out.OwnSecrets = append(out.OwnSecrets, fmt.Sprintf("%s %s/%s (%s)", r.Node, r.Module, r.Name, r.Origin)) + } + sort.Strings(out.Ports) + sort.Strings(out.Pairs) + sort.Strings(out.OwnSecrets) + return out, nil +} diff --git a/cmd/mesh-controller/compose_perform.go b/cmd/mesh-controller/compose_perform.go index 3305106c..930d6634 100644 --- a/cmd/mesh-controller/compose_perform.go +++ b/cmd/mesh-controller/compose_perform.go @@ -46,26 +46,8 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co for _, line := range d.Passes.Said { fmt.Fprintln(w, line) } - sp := seatPassingOn(inv, w) - for _, p := range d.Passes.Passes { - moved, err := inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To) - if err != nil { - // Said, and the record stays as it was: a composition is never refused for it. - fmt.Fprintf(w, "%s could not be passed from %s on %s to %s on %s: %v\n", p.Seat.Name, - p.From.Module, p.From.Node, p.To.Module, p.To.Node, err) - continue - } - if !moved { - continue // another composition passed it first, and wrote the act - } + if performSeatPasses(ctx, seatPassingOn(inv, w), d.Passes.Passes) { did = true - if inv.Throwaway() { - continue - } - if err := sp.act(ctx, p); err != nil { - fmt.Fprintf(w, "%s passed from %s on %s to %s on %s, and the hand act could NOT be recorded: %v\n", - p.Seat.Name, p.From.Module, p.From.Node, p.To.Module, p.To.Node, err) - } } } @@ -260,7 +242,7 @@ func prepareSend(ctx context.Context, open *stores, node string) ([]string, erro if n.Stated && n.Contract >= compose.VaultBoxContract { boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool { return handing.Handing(m) - }, Allocating) + }, true) if err != nil { return nil, err } diff --git a/cmd/mesh-controller/compose_perform_integration_test.go b/cmd/mesh-controller/compose_perform_integration_test.go index 1d9ccbeb..d9ace743 100644 --- a/cmd/mesh-controller/compose_perform_integration_test.go +++ b/cmd/mesh-controller/compose_perform_integration_test.go @@ -59,7 +59,7 @@ func TestPerformAllocatesAForeseenPortAndTheNumberIsTheControllers(t *testing.T) t.Fatalf("a second composition names %v foreseen: %v", again.ForeseenPorts, err) } // The number is the one the controller allocates in the same store. - plan, settings, err := planFor(ctx, open, "laptop") + plan, settings, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go index 280fdcdf..56653cf5 100644 --- a/cmd/mesh-controller/compose_sdk.go +++ b/cmd/mesh-controller/compose_sdk.go @@ -119,6 +119,24 @@ func generatorsOf(ctx context.Context, open *stores) (map[string]catalogue.Gener return compose.Generators(in.Input) } +// renderingOf is what a machine is rendered with as a question reads it (nothing made), with the private network: what the +// filter's rules, the roster and the like are derived from. +func renderingOf(ctx context.Context, open *stores, node string) (catalogue.Rendering, error) { + in, err := inputFor(ctx, open) + if err != nil { + return catalogue.Rendering{}, err + } + rendered, err := compose.Render(in.Input, node, compose.Reading) + if err != nil { + return catalogue.Rendering{}, markUnresolvable(err) + } + with := rendered.Rendering + if with.Generators, err = compose.Generators(in.Input); err != nil { + return catalogue.Rendering{}, err + } + return with, nil +} + // whoResolvesOn is the machines whose resolution answers a requirement, and why the others did not (whoResolves over the // Input): by what a module provides, not by its name. A machine whose set does not compose is in refused, said; one the mesh // could not be asked about is an error, never a refusal (novox/hq 04-ISSUES/152).