stage 1 of 8 for issue 1389 (a staging branch; the tree is copied by one commit)

This commit is contained in:
2026-10-20 11:09:29 +00:00
parent fb556d6b74
commit f70b0c5077
12 changed files with 130 additions and 72 deletions
+1 -10
View File
@@ -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
@@ -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, &notMade) || 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)
}
+3 -4
View File
@@ -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)
@@ -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)
}
+17 -21
View File
@@ -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)
}
}
@@ -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)
}
@@ -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"))
@@ -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)
@@ -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
}
+2 -20
View File
@@ -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
}
@@ -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)
}
+18
View File
@@ -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).