stage 6 of 8 for issue 1389

This commit is contained in:
2026-10-20 11:16:13 +00:00
parent 66daf0cc27
commit f77c130dcd
13 changed files with 23 additions and 443 deletions
-400
View File
@@ -262,134 +262,6 @@ func certificateFor(ctx context.Context, open *stores, node string) (string, str
return issued, authority.Certificate, nil return issued, authority.Certificate, nil
} }
// grantsFor is every credential this node must create, because something elsewhere uses it.
//
// The mirror of what a consumer is given, and the half that makes the credential real: a password
// nothing was told to create is a password that authenticates nowhere. Sealed to this node, so
// the mesh hands over something it cannot itself use.
//
// **One consumer's identity never refuses the provider's machine** (novox/hq ADR 0225, issue 263).
// A consumer whose identity overflows the provision's bound is left out of the grants and returned
// beside them, for push, plan and `status` to say; every other consumer is granted and the provider's
// declaration composes. Refusing here once made a whole machine unpushable for one module elsewhere.
//
// **A provider is granted exactly the consumers whose own resolution binds them to it** (novox/hq
// issue 274). The pair credentials on record say only whom this node was ever asked by: after a
// consumer of a provision that keeps its data was moved and pinned back (issue 273, ADR 0232), the
// credential from the provider it left was still on record, so that provider went on being asked for
// five databases nobody used and never retired them. A credential whose consumer is bound elsewhere
// is withdrawn here like one nobody asks for — the provider retires it and keeps its data (ADR 0230)
// — and returned beside the grants, for plan and push to say. It stays on record: it is the key to
// the login the provider keeps until `cleanup delete`, and to that data should a person pin it back.
func grantsFor(ctx context.Context, open *stores, node string) (
[]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, error) {
inv := open.inventory
issued, err := inv.SecretsFrom(ctx, node)
if err != nil {
return nil, nil, nil, err
}
// Where each consumer is, so a provider that must reach back to one does not have to know how
// the mesh names machines.
shelf, err := inv.Catalogue(ctx)
if err != nil {
return nil, nil, nil, err
}
onNetwork, err := whereEveryoneIs(ctx, inv, shelf)
if err != nil {
return nil, nil, nil, err
}
// What each consumer actually asked for, taken from that machine's own resolution rather than
// from a record beside it. A provider told to create a password and not what to create it for
// can do nothing with it, and the name a consumer wants is the consumer's to say.
out := make([]catalogue.Grant, 0, len(issued))
var withheld []catalogue.Overflow
var unbound []catalogue.Unbound
for _, s := range issued {
plan, settings, err := planFor(ctx, open, s.Consumer)
switch {
case unresolvable(err):
// Their set does not resolve. Skipped rather than fatal: this node is not the place
// to report another machine's problem, and a grant for something that is not going to
// run would have the provider create a user nothing uses.
continue
case err != nil:
// The mesh could not be asked what they wanted, which is not the same as their wanting
// nothing — and withholding a grant on that reading takes a consumer's access away
// (novox/hq 04-ISSUES/152).
return nil, nil, nil, fmt.Errorf("what %s asked of %s cannot be read: %w", s.Consumer, s.Name, err)
}
values, asks, err := plan.ContributionsFrom(s.Name, s.ConsumerModule, settings)
if err != nil {
return nil, nil, nil, err
}
// A port in there is the consumer's software port until this. The consumer is on another
// machine, so the assignment that moved it is that machine's — fetched here rather than
// looked for in this one's, which is the whole reason the co-located fix could not reach
// this case (novox/hq 04-ISSUES/038, the cross-node half).
published, err := portsOn(ctx, inv, s.Consumer, s.ConsumerModule)
if err != nil {
return nil, nil, nil, err
}
values = catalogue.AtPublishedPort(values, s.ConsumerModule, published)
from := s.ConsumerModule
if !asks {
// That module no longer wants this. Left empty, which is what the declaration reads
// as "nobody asks for it any more" — and is how a login is withdrawn rather than kept
// working for ever after its consumer went away.
from = ""
}
if bound := plan.BindsFrom(s.Name, s.ConsumerModule, s.Local); from != "" && !slices.Contains(bound, node) {
// It still asks, and not of this node: its resolution — the same one read above, so an
// unreadable one is the error above and never an empty answer here (issue 152) — binds
// this credential to another provider, or under this local name to none. Withdrawn
// exactly as a credential nobody asks for, and said.
from = ""
unbound = append(unbound, catalogue.Unbound{Provision: s.Name, Provider: node,
Consumer: s.Consumer, Module: s.ConsumerModule, Local: s.Local, BoundTo: bound})
}
// The consumer's identity slug, from its own manifest, carried on the grant so the provider
// derives the same login the consumer does (novox/hq ADR 0049). Judged against the bound of
// this provision, as the consumer's resolution states it from the provider's offer (ADR
// 0225): a consumer it would not fit is left out of the grants and said, rather than a login a
// provider silently shortened — and rather than this whole machine refused for it.
slug := ""
for _, mm := range plan.Modules {
if mm.Module == s.ConsumerModule {
slug = mm.Slug
break
}
}
if from != "" {
bound := boundOfGrant(plan, s, node)
source := catalogue.IdentitySource(slug, s.ConsumerModule)
if catalogue.CheckIdentityWithin(s.Consumer, source, bound) != nil {
withheld = append(withheld, catalogue.Overflow{Provision: s.Name, Provider: node,
Consumer: s.Consumer, Module: s.ConsumerModule,
Identity: catalogue.ConsumerIdentity(s.Consumer, source), Bound: bound})
continue
}
}
out = append(out, catalogue.Grant{
Provision: s.Name, Consumer: s.Consumer, At: onNetwork[s.Consumer],
From: from, Values: values, Slug: slug, Sealed: s.ForProvider, Local: s.Local})
}
return out, withheld, unbound, nil
}
// boundOfGrant is the identity bound the consumer's own resolution states for the requirement this
// grant answers. A requirement not found there is held to the tightest bound the mesh knows rather
// than to none: what the provider keeps of it is not known here.
func boundOfGrant(consumer catalogue.Resolution, s inventory.Secret, provider string) catalogue.IdentityBound {
for _, n := range consumer.Needs {
if n.Name == s.Name && n.For == s.ConsumerModule && n.From == provider {
return n.Identity
}
}
return catalogue.DefaultIdentityBound
}
// listensLines is what a person is told about what this module would open, and why — the same // listensLines is what a person is told about what this module would open, and why — the same
// `why` every listens entry already carries for the firewall it also feeds (novox/hq ADR 0007), so // `why` every listens entry already carries for the firewall it also feeds (novox/hq ADR 0007), so
// deciding whether to assign a module can see what it would open before it opens it, not only // deciding whether to assign a module can see what it would open before it opens it, not only
@@ -596,91 +468,6 @@ func shownAs(r map[string]any) string {
return heading return heading
} }
// licencesFor is what this node can be answered with by record, and what it was put on.
//
// A mesh with no licences at all is the ordinary case and must not be an error: every existing
// mesh is one, and a control plane that refused to plan because nobody had bought an API key
// would be unusable for the thing it already does.
func licencesFor(ctx context.Context, open *stores, node string) (
map[string][]catalogue.Record, map[string]map[string]catalogue.Record, error) {
held, err := open.Licences(ctx)
if err != nil {
return nil, nil, err
}
all, err := held.All(ctx)
if err != nil {
return nil, nil, err
}
if len(all) == 0 {
return nil, nil, nil
}
offered := map[string][]catalogue.Record{}
byName := map[string]catalogue.Record{}
for _, one := range all {
record := catalogue.Record{Name: one.Name, Serves: one.Serves}
offered[licences.Provision] = append(offered[licences.Provision], record)
byName[one.Name] = record
}
using := map[string]map[string]catalogue.Record{}
for _, one := range all {
holders, err := held.HoldersOf(ctx, one.Name)
if err != nil {
return nil, nil, err
}
for _, h := range holders {
if h.Node != node {
continue
}
if using[h.Module] == nil {
using[h.Module] = map[string]catalogue.Record{}
}
using[h.Module][licences.Provision] = byName[one.Name]
}
}
return offered, using, nil
}
// keyFor is the licence key sealed to one machine, for one module.
//
// **Empty is not an error here.** The mesh discarded the plaintext when it was supplied, so a
// holder recorded afterwards genuinely has no key — and the declaration refuses that by name,
// where the module and the path are both in view, rather than here.
func keyFor(ctx context.Context, open *stores, licence, node, module string) (string, error) {
held, err := open.Licences(ctx)
if err != nil {
return "", err
}
return held.KeyFor(ctx, licence, node, module)
}
// managerPublicKeyFor is the manager node's public sealing key, but only when (node, module) is the
// licence's manager holder — empty otherwise.
//
// It is delivered to the manager module in its bound facts so it can re-seal a rotated refresh token
// to this node (novox/hq ADR 0050). Public, so it travels in the clear like any other bound fact; and
// scoped to the manager holder alone, so a consumer never receives it and nothing invites a consumer
// to seal anything.
func managerPublicKeyFor(
ctx context.Context, open *stores, inv *inventory.Inventory, licence, node, module string,
) (string, error) {
held, err := open.Licences(ctx)
if err != nil {
return "", err
}
managerNode, managerModule, err := held.ManagerOf(ctx, licence)
if err != nil {
return "", err
}
if managerNode == "" || node != managerNode || module != managerModule {
return "", nil
}
return inv.SealingKeyOf(ctx, node)
}
// servedOnNode is what a module on a node tells a consumer of one of its provisions, with THAT // servedOnNode is what a module on a node tells a consumer of one of its provisions, with THAT
// node's ports on it: the port the node was given (novox/hq ADR 0100) over the one the mesh // node's ports on it: the port the node was given (novox/hq ADR 0100) over the one the mesh
// assigned over the manifest's own, settled with the node's settings layers. // assigned over the manifest's own, settled with the node's settings layers.
@@ -725,76 +512,6 @@ func portsGivenOn(ctx context.Context, inv *inventory.Inventory, node string,
return ports, layers, nil return ports, layers, nil
} }
// seatsOn is where a node put the holder of each mesh-scoped seat, by seat and by the port the
// holder's software uses — what ${seat:…} answers with (novox/hq 04-ISSUES/102).
//
// Read for every module in the catalogue that claims a seat, in this node's set or not: the store
// and the broker are given their ports at genesis, as settings on a module that may be registered
// and not yet assigned (04-ISSUES/085), and the control plane must follow that setting from the
// first declaration it composes for itself. A holder in this node's set wins over one that is not.
func seatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
node string, inSet []catalogue.Manifest, adopted bool, taken map[string]bool) (map[string]map[int]int, error) {
assigned := map[string]bool{}
for _, m := range inSet {
assigned[m.Module] = true
}
names := make([]string, 0, len(shelf))
for name := range shelf {
names = append(names, name)
}
sort.Strings(names)
seats := map[string]map[int]int{}
for _, name := range names {
m := shelf[name]
var claims []string
for _, c := range m.Claims {
if c.At() == catalogue.ScopeMesh {
claims = append(claims, c.Name)
}
}
if len(claims) == 0 {
continue
}
// **Only a port the node was given or the mesh assigned — never the manifest's own
// number.** The sealed value the answer sits beside carries the port genesis wrote, which
// on a given-port node is the predecessor's; a manifest's long-form mapping is the
// catalogue's default, and answering with it would override the right number with one
// the mesh never checked (the contract in seat_into.go). And on an adopted node a holder
// assigned but not yet taken is the found container, on the ports it was found with, not
// the declaration's — so its mesh-assigned ports do not count there either; a given port
// does, because a given port is the found one by construction (ADR 0100).
ports, _, err := portsGivenOn(ctx, inv, node, m)
if err != nil {
return nil, err
}
if adopted && !taken[name] {
layers, err := inv.SettingsFor(ctx, node, m.Module)
if err != nil {
return nil, err
}
ports = map[int]int{}
if given, err := catalogue.GivenPorts(m, layers); err == nil {
ports = given
}
}
if len(ports) == 0 {
continue
}
for _, seat := range claims {
if seats[seat] == nil {
seats[seat] = map[int]int{}
}
for wanted, at := range ports {
if _, said := seats[seat][wanted]; said && !assigned[name] {
continue
}
seats[seat][wanted] = at
}
}
}
return seats, nil
}
// portsOn is one module's assignments on one machine, by the port the software uses. // portsOn is one module's assignments on one machine, by the port the software uses.
func portsOn( func portsOn(
ctx context.Context, inv *inventory.Inventory, node, module string, ctx context.Context, inv *inventory.Inventory, node, module string,
@@ -812,45 +529,6 @@ func portsOn(
return out, nil return out, nil
} }
// composeBusUsers is the bus's user list, for a push to the machine that runs the bus.
//
// Empty for every other machine, and for every machine while the mesh is on the bus it runs on
// today — where accounts are a management call and there is no file to write.
//
// **Composed on each push, never kept.** The list is a function of the mesh's records (who exists,
// what runs where, what each declares), and a stored copy would be a second account of who may reach
// the bus, able to disagree with the records while both looked internally consistent (ADR 0043).
//
// A user the mesh has never minted a password for is **left out and said**, not written as a user
// without one — the composer refuses that, because a user with no password is a user anybody is. That
// is an ordinary situation with an obvious remedy (`module issue`, or enrolling), so the push carries
// the rest rather than failing: a bus that is missing one module's user is a mesh where that module
// cannot connect, and a bus with no file at all is a mesh where nothing can.
func composeBusUsers(ctx context.Context, inv *inventory.Inventory,
onThisNode []catalogue.Manifest) (string, error) {
// **Not gated on which bus the controller is on, and that was a bug.** It read "compose this only
// once the mesh is on the new bus" — which cannot work, because the server needs its user list
// *before* anything moves onto it. Step 2 of the change is exactly that: the server stands in the
// mesh carrying nothing, on its own ports, while every node is still on the old bus (novox/hq
// ADR 0116). Under the old gating that step could not happen: the module would come up, find no
// accounts file, and its entrypoint would wait for one the controller had decided not to write.
//
// So the question is only whether this machine runs the module that asked for the file. A mesh
// that never moves has written a user list nothing reads, which costs a few hundred bytes on one
// node; the reverse cost a step that cannot be taken.
//
// Asked of what this push resolves to rather than of the seat's holder mesh-wide: the file is a
// resource of that module, so the question is whether it is here.
list, missing, err := busUserList(ctx, inv, onThisNode)
if len(missing) > 0 {
fmt.Printf("the bus's user list leaves out %d user(s) the mesh has minted no credential "+
"for: %s. Each is a user that cannot connect until one is issued\n",
len(missing), strings.Join(missing, ", "))
}
return list, err
}
// busUserList is composeBusUsers without saying anything: the list, and the users left out of it // busUserList is composeBusUsers without saying anything: the list, and the users left out of it
// for want of a credential. Asked on every send to decide whether the machine holding the bus must // for want of a credential. Asked on every send to decide whether the machine holding the bus must
// go first (novox/hq issue 249), where saying the same missing users each time would bury them. // go first (novox/hq issue 249), where saying the same missing users each time would bury them.
@@ -905,60 +583,6 @@ func issuedOutsideBy(shelf map[string]catalogue.Manifest, n catalogue.Needed) (s
return catalogue.OffersIssuedOutside(shelf, n.Name) return catalogue.OffersIssuedOutside(shelf, n.Name)
} }
// issuedToHolder is whether the vault keeps a need's credential for its holder alone (issues 749, 757 and 779): its
// providing module is marked moved, so the vault holds the credential in a row of its own for each holder
// (filledTheNewWay), and its offer says either that the provider's software issues the provision's credential, or
// that the provider gives each consumer a login of its own (form pair, ADR 0158 §11). A credential every holder
// shares, or one a person gives, is neither.
func issuedToHolder(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
if n.ByRecord || n.Module == "" {
return false
}
provider, known := shelf[n.Module]
if !known || !movedProvider(provider) {
return false
}
if _, issued := provider.IssuedCredentialOf(n.Name); issued {
return true
}
return provider.CredentialFormOf(n.Name) == catalogue.CredentialFormPair
}
// providerMoved is whether a need is answered by a module marked moved (issue 788), or by one whose offer of the
// provision says the provider helper answers it (issue 792).
func providerMoved(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
if n.ByRecord || n.Module == "" {
return false
}
provider, known := shelf[n.Module]
return known && (movedProvider(provider) || provider.AnsweredByHelperFor(n.Name))
}
// movedProvider is whether a provider's credentials are the vault's: the module is marked moved, or it holds the
// mesh-vault seat, whose pair rows have been in the vault since V3 (issue 793), so its consumers' credentials
// (gitea's internal token and admin password) are filled from those very rows with the values unchanged.
func movedProvider(m catalogue.Manifest) bool {
return m.Moved || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == "mesh-vault" })
}
func providerModuleOf(resolved catalogue.Resolution, open *stores, ctx context.Context, n catalogue.Needed) string {
for _, m := range resolved.Modules {
if _, shared := m.SharedCredentialOf(n.Name); shared {
return m.Module
}
}
shelf, err := open.inventory.Catalogue(ctx)
if err != nil {
return ""
}
for name, m := range shelf {
if _, shared := m.SharedCredentialOf(n.Name); shared {
return name
}
}
return ""
}
// unheldLogger remembers what was last logged about each node's unmet seat dependencies, so the // unheldLogger remembers what was last logged about each node's unmet seat dependencies, so the
// log says each change once (novox/hq ADR 0207), on stderr so `status --json` stays a document. // log says each change once (novox/hq ADR 0207), on stderr so `status --json` stays a document.
// A test gives its own writer and memory, and no longer shares the process's (novox/hq issue 486). // A test gives its own writer and memory, and no longer shares the process's (novox/hq issue 486).
@@ -1074,27 +698,3 @@ func sortedNames[V any](m map[string]V) []string {
sort.Strings(out) sort.Strings(out)
return out return out
} }
// wireBuilds is the build of each carried module that goes on the wire (novox/hq issue 1145): the build its assignment
// names when that assignment is of the commit carried, else the build made from that commit; a module with neither
// is not named.
func wireBuilds(carried map[string]string, runs map[string]chain.Ref, lookup func(module, commit string) (string, error)) (map[string]string, error) {
out := map[string]string{}
for module, commit := range carried {
if commit == "" {
continue
}
if ref, ok := runs[module]; ok && ref.Commit == commit && ref.Build != "" {
out[module] = ref.Build
continue
}
build, err := lookup(module, commit)
if err != nil {
return nil, err
}
if build != "" {
out[module] = build
}
}
return out, nil
}
@@ -8,6 +8,7 @@ import (
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
) )
@@ -21,8 +22,9 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
if _, err := assign(ctx, open, "laptop", m.Module); err != nil { if _, err := assign(ctx, open, "laptop", m.Module); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := busCredentialIssued(ctx, inv, "laptop", m.Module, broker.ProvisionerSecret); err == nil { // The composition refuses it in the send's words (mesh-sdk's go/compose: TestTheSendsRefusalsAreRefusedInTheSendsWords).
t.Fatal("a push would seal a placeholder for the client's account before it was issued") if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err == nil || !strings.Contains(err.Error(), "has no bus credential") {
t.Fatalf("a push would seal a placeholder for the client's account before it was issued: %v", err)
} }
if err := issueProvisionerClient(ctx, inv, m, "laptop", broker.Broker{Fingerprint: "ab"}, "10.0.0.1:4222"); err != nil { if err := issueProvisionerClient(ctx, inv, m, "laptop", broker.Broker{Fingerprint: "ab"}, "10.0.0.1:4222"); err != nil {
@@ -38,7 +40,7 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
if !slices.ContainsFunc(states, func(s inventory.SecretState) bool { return s.Name == broker.ProvisionerSecret }) { if !slices.ContainsFunc(states, func(s inventory.SecretState) bool { return s.Name == broker.ProvisionerSecret }) {
t.Fatalf("the client's account is not sealed to the machine for its service: %+v", states) t.Fatalf("the client's account is not sealed to the machine for its service: %+v", states)
} }
if err := busCredentialIssued(ctx, inv, "laptop", m.Module, broker.ProvisionerSecret); err != nil { if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil && strings.Contains(err.Error(), "has no bus credential") {
t.Fatalf("an issued client account is refused: %v", err) t.Fatalf("an issued client account is refused: %v", err)
} }
if !runsTheProvisionerClient(m) { if !runsTheProvisionerClient(m) {
+3 -3
View File
@@ -522,8 +522,8 @@ func pushCommand(ctx context.Context, args []string) error {
return nil return nil
} }
} }
gens, err := generatorsOf(ctx, open) // The private network every declaration of the push is composed with: not computable refuses the push up front.
if err != nil { if _, err := generatorsOf(ctx, open); err != nil {
return err return err
} }
@@ -724,7 +724,7 @@ func pushCommand(ctx context.Context, args []string) error {
keep := keeping(func(held context.Context, sending []readyNode) ([]readyNode, error) { keep := keeping(func(held context.Context, sending []readyNode) ([]readyNode, error) {
return holdingBack(held, inv, sending, movable, &heldBack) return holdingBack(held, inv, sending, movable, &heldBack)
}) })
refused, err := flushBehind(ctx, open, nodes, handled, composeForPush(open, gens), bus, holder, keep, os.Stdout) refused, err := flushBehind(ctx, open, nodes, handled, composeForPush(open), bus, holder, keep, os.Stdout)
refusals = append(refusals, refused...) refusals = append(refusals, refused...)
if err != nil { if err != nil {
return err return err
+1 -9
View File
@@ -48,15 +48,7 @@ func TestAPushSaysWhatItRecreates(t *testing.T) {
} }
// Composed as a person's push composes it. // Composed as a person's push composes it.
plan, settings, err := planFor(ctx, open, "anchor") declared, err := composeSend(ctx, open, "anchor")
if err != nil {
t.Fatal(err)
}
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
declared, err := declarationWith(ctx, open, "anchor", plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-10
View File
@@ -180,16 +180,6 @@ func recordedKept(ctx context.Context, open *stores, node string) (map[string]st
return out, nil return out, nil
} }
// keepRecorded puts, in a shelf about to be resolved for a machine, the build each recorded module there
// runs in place of the one the mesh holds; it answers what it kept, module → commit.
func keepRecorded(ctx context.Context, open *stores, node string, shelf map[string]catalogue.Manifest) (map[string]string, error) {
kept, err := recordedKept(ctx, open, node)
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 // 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 // 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. // a scratch shelf, to read the manifests it gives the node as its own Kept.
@@ -63,7 +63,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
// A plan's, a release plan's, a healer's send: the database is kept as it runs, mail moves. // A plan's, a release plan's, a healer's send: the database is kept as it runs, mail moves.
kept := keepingRecorded(ctx) kept := keepingRecorded(ctx)
plan, settings, err := planFor(kept, open, "anchor") plan, _, err := resolveNode(kept, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -76,11 +76,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
if imageOf(t, plan, "postgres", "server") != pgImage { if imageOf(t, plan, "postgres", "server") != pgImage {
t.Fatal("the recorded module's container lost its image") t.Fatal("the recorded module's container lost its image")
} }
gens, err := generators(kept, open) declared, err := composeSend(kept, open, "anchor")
if err != nil {
t.Fatal(err)
}
declared, err := declarationWith(kept, open, "anchor", plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -89,7 +85,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
} }
// A person's push: the recorded module's new build. // A person's push: the recorded module's new build.
plan, _, err = planFor(ctx, open, "anchor") plan, _, err = resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -98,7 +94,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
} }
// The bus step moves the bus alone: a recorded module it is told it may move moves, the others stay. // The bus step moves the bus alone: a recorded module it is told it may move moves, the others stay.
plan, _, err = planFor(keepingRecorded(ctx, "postgres"), open, "anchor") plan, _, err = resolveNode(keepingRecorded(ctx, "postgres"), open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -128,7 +124,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
map[string]string{"postgres": "c0000000", "mailu": "c2222222"}); err != nil { map[string]string{"postgres": "c0000000", "mailu": "c2222222"}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, _, err := planFor(kept, open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") { if _, _, err := resolveNode(kept, open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") {
t.Fatalf("a recorded build that cannot be kept did not refuse the send with its remedy: %v", err) t.Fatalf("a recorded build that cannot be kept did not refuse the send with its remedy: %v", err)
} }
+1 -1
View File
@@ -349,7 +349,7 @@ func TestARecordedMoveThatCannotBeJudgedWaits(t *testing.T) {
!strings.Contains(v.said, "not in the build records") { !strings.Contains(v.said, "not in the build records") {
t.Fatalf("an unjudgeable move: %+v", v) t.Fatalf("an unjudgeable move: %+v", v)
} }
if _, _, err := planFor(keepingRecorded(t.Context()), rm.open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") { if _, _, err := resolveNode(keepingRecorded(t.Context()), rm.open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") {
t.Fatalf("a send that cannot keep the unjudged build did not refuse with its remedy: %v", err) t.Fatalf("a send that cannot keep the unjudged build did not refuse with its remedy: %v", err)
} }
// laptop, judged from c1, goes; anchor waits. // laptop, judged from c1, goes; anchor waits.
+1 -1
View File
@@ -34,7 +34,7 @@ func TestReplay164(t *testing.T) {
} }
sent := func() []map[string]any { sent := func() []map[string]any {
t.Helper() t.Helper()
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err) t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err)
} }
+1 -1
View File
@@ -71,7 +71,7 @@ func TestReplay201ThePlansSend(t *testing.T) {
if keeping { if keeping {
c = keepingRecorded(ctx) c = keepingRecorded(ctx)
} }
plan, settings, err := planFor(c, open, "anchor") plan, settings, err := resolveNode(c, open, "anchor")
if err != nil { if err != nil {
t.Fatalf("%s to anchor did not compose: %v", sends, err) t.Fatalf("%s to anchor did not compose: %v", sends, err)
} }
+1 -1
View File
@@ -34,7 +34,7 @@ func TestReplay237(t *testing.T) {
said := answered(t, func() error { return assignCommand(ctx, "assign", []string{"laptop", "docker"}) }) said := answered(t, func() error { return assignCommand(ctx, "assign", []string{"laptop", "docker"}) })
if _, _, err := planFor(ctx, open, "laptop"); err != nil { if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
t.Fatalf("laptop does not resolve right after the act: %v\nthe answer was:\n%s", err, said) t.Fatalf("laptop does not resolve right after the act: %v\nthe answer was:\n%s", err, said)
} }
if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") { if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") {
+1 -1
View File
@@ -33,7 +33,7 @@ func TestReplay401(t *testing.T) {
} }
sent := func() []map[string]any { sent := func() []map[string]any {
t.Helper() t.Helper()
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -35,7 +35,7 @@ func TestReplay263AnIdentityTooLongNeverRefusesItsProvidersMachine(t *testing.T)
} }
// The consumer's machine composes first, as a push does: what it is sent is what its provider grants. // The consumer's machine composes first, as a push does: what it is sent is what its provider grants.
for _, node := range []string{"laptop", "anchor"} { for _, node := range []string{"laptop", "anchor"} {
plan, settings, err := planFor(ctx, open, node) plan, settings, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s does not resolve: %v", node, err) t.Fatalf("%s does not resolve: %v", node, err)
} }
@@ -84,7 +84,7 @@ func TestReplay273AConsumerBesideItsStoreStaysBoundToIt(t *testing.T) {
assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"}, assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"},
[2]string{"laptop", "board"}) [2]string{"laptop", "board"})
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -2
View File
@@ -22,7 +22,7 @@ func TestASetThatDoesNotComposeIsMarkedAsTheNodesOwnProblem(t *testing.T) {
} }
} }
_, _, err := planFor(t.Context(), open, "laptop") _, _, err := resolveNode(t.Context(), open, "laptop")
if err == nil { if err == nil {
t.Fatal("two modules claiming one seat composed anyway") t.Fatal("two modules claiming one seat composed anyway")
} }
@@ -38,7 +38,7 @@ func TestAStoreThatCannotBeReadIsNotANodeThatDoesNotCompose(t *testing.T) {
stopped, cancel := context.WithCancel(t.Context()) stopped, cancel := context.WithCancel(t.Context())
cancel() cancel()
_, _, err := planFor(stopped, open, "anchor") _, _, err := resolveNode(stopped, open, "anchor")
if err == nil { if err == nil {
t.Fatal("a plan composed against a store that could not be read") t.Fatal("a plan composed against a store that could not be read")
} }