stage 6 of 8 for issue 1389
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user