mesh/delivery merged: source.main moved to novox/mesh-sdk@d6c87ee1a7ae
mesh/delivery-group group fix/1126-provision-credentials merged: every member is merged
mesh/check pass c459f757308a on 036266daff85 | unit 6028 run, 142 kept, 16 skipped | integration 6091 run, 174 kept, 25 skipped | over bound: unit, in…
After the controller, nothing makes a pair row for a new consumer, so a need whose provider offers no credential (form none) was composed with a stand-in on both ends and the node-engine held the consumer (issue 1126: mesh-cli on ace, "stands in for the grant of mcp-endpoint"). The vault never mints for form none, so such a need now composes with no credential: the consumer's need carries none, the provider's grant carries the consumer and nothing sealed, neither is named foreseen, CredentialsNeeded asks the vault for neither, and no empty grant file is written. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
499 lines
21 KiB
Go
499 lines
21 KiB
Go
package compose
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// Mode says what a composition does about what a send would make. Both modes read what the Input holds and make nothing; they
|
|
// differ as the controller's Reading and Foreseeing modes do.
|
|
type Mode int
|
|
|
|
const (
|
|
// Reading composes what is held and leaves out an own secret the Input holds none for: that machine cannot be running it.
|
|
Reading Mode = iota
|
|
// Foreseeing composes an own secret the next send would make with a stand-in and names it foreseen, while one the send would
|
|
// refuse (a bus credential nobody issued, a given secret under an old key) is refused here, in the same words.
|
|
Foreseeing
|
|
// Planning composes for a provisioning plan alone (novox/hq ADR 0570 clause 2): every own secret the Input holds none
|
|
// for stands in, named foreseen, and nothing is refused for a value, since a plan names secrets and never carries one.
|
|
// The seat mesh-provisioner's holder composes so, reading no secret; a rendering in this mode is never applied.
|
|
Planning
|
|
)
|
|
|
|
// ForeseenCertificate and ForeseenBusUsers stand for what the Input does not carry, in a composition asked ahead of the send.
|
|
// Never sent.
|
|
const (
|
|
ForeseenCertificate = "foreseen: issued by the next send"
|
|
ForeseenBusUsers = "foreseen: composed from the bus records by the next send"
|
|
)
|
|
|
|
// grantsFor is every credential this node must create, because something elsewhere uses it (plan.go grantsFor): the mirror of
|
|
// what a consumer is given. A consumer whose identity overflows the provision's bound is left out of the grants and returned
|
|
// beside them (novox/hq ADR 0225); one whose resolution binds it to another provider is withdrawn and returned beside them
|
|
// (issue 274). Every other is granted.
|
|
func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, []string, error) {
|
|
issued, err := c.rowsFor(node)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
onNetwork, err := c.whereEveryoneIs()
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
out := make([]catalogue.Grant, 0, len(issued))
|
|
var withheld []catalogue.Overflow
|
|
var unbound []catalogue.Unbound
|
|
var foreseen []string
|
|
for _, s := range issued {
|
|
resolved, err := c.planFor(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.
|
|
continue
|
|
case err != nil:
|
|
// The mesh could not be asked what they wanted, which is not the same as their wanting nothing.
|
|
return nil, nil, nil, nil, fmt.Errorf("what %s asked of %s cannot be read: %w", s.Consumer, s.Provision, err)
|
|
}
|
|
plan := resolved.Plan
|
|
values, asks, err := plan.ContributionsFrom(s.Provision, s.ConsumerModule, resolved.Settings)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
// A port in there is the consumer's software port until this: the assignment that moved it is that machine's.
|
|
consumer, err := c.in.node(s.Consumer)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
values = catalogue.AtPublishedPort(values, s.ConsumerModule, consumer.Ports[s.ConsumerModule])
|
|
from := s.ConsumerModule
|
|
if !asks {
|
|
// That module no longer wants this: left empty, which is how a login is withdrawn rather than kept working for ever.
|
|
from = ""
|
|
}
|
|
if bound := plan.BindsFrom(s.Provision, s.ConsumerModule, s.Local); from != "" && !slices.Contains(bound, node) {
|
|
// It still asks, and not of this node: withdrawn exactly as a credential nobody asks for, and said.
|
|
from = ""
|
|
unbound = append(unbound, catalogue.Unbound{Provision: s.Provision, 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).
|
|
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.Provision, Provider: node,
|
|
Consumer: s.Consumer, Module: s.ConsumerModule,
|
|
Identity: catalogue.ConsumerIdentity(s.Consumer, source), Bound: bound})
|
|
continue
|
|
}
|
|
}
|
|
sealed := s.ForProvider
|
|
switch {
|
|
case credentiallessRow(plan, s, node):
|
|
// Its offer presents no credential (form none): the grant carries the consumer and nothing sealed, and
|
|
// nothing stands in for it, so neither end waits for a value nobody makes (issue 1126).
|
|
sealed = ""
|
|
case sealed == "":
|
|
sealed = catalogue.ForeseenSealed
|
|
foreseen = append(foreseen, fmt.Sprintf("the grant of %s for %s on %s", s.Provision, s.ConsumerModule, s.Consumer))
|
|
}
|
|
out = append(out, catalogue.Grant{
|
|
Provision: s.Provision, Consumer: s.Consumer, At: onNetwork[s.Consumer],
|
|
From: from, Values: values, Slug: slug, Sealed: sealed, Local: s.Local})
|
|
}
|
|
return out, withheld, unbound, foreseen, nil
|
|
}
|
|
|
|
// credentialless is whether a need's provider offers it with no credential (catalogue.CredentialFormNone): the vault
|
|
// mints nothing for it, so nothing is asked of the vault, sealed into a grant or a file, or stood in for (issue 1126).
|
|
func credentialless(n catalogue.Needed) bool { return n.CredentialForm == catalogue.CredentialFormNone }
|
|
|
|
// credentiallessRow is whether the consumer's resolution answers row's need from provider with an offer of form none.
|
|
func credentiallessRow(consumer catalogue.Resolution, row Issued, provider string) bool {
|
|
return slices.ContainsFunc(consumer.Needs, func(n catalogue.Needed) bool {
|
|
return n.Name == row.Provision && n.For == row.ConsumerModule && n.Local == row.Local && n.From == provider &&
|
|
credentialless(n)
|
|
})
|
|
}
|
|
|
|
// rowsFor is every pair credential this node's declaration must carry a grant for, in the order the controller reads them
|
|
// (SecretsFrom: by provision, consumer, consumer module and local name): the ones the Input says were made, and the ones the same send
|
|
// would make before it composes this node -- every need of a node that resolves (this one too), whose provider is this node, that is not
|
|
// answered by a record and not a credential only a person gives and nobody gave. Those have no sealed copy (ForProvider empty), so the
|
|
// grant is composed with a stand-in and named foreseen (plan.go: planFor makes the secret in every mode before grantsFor reads it).
|
|
func (c *composer) rowsFor(node string) ([]Issued, error) {
|
|
var rows []Issued
|
|
for _, row := range c.in.Issued {
|
|
if row.Provider == node {
|
|
rows = append(rows, row)
|
|
}
|
|
}
|
|
for _, other := range c.in.sorted() {
|
|
resolved, err := c.planFor(other.Name)
|
|
switch {
|
|
case unresolvable(err):
|
|
continue
|
|
case err != nil:
|
|
return nil, fmt.Errorf("what %s asked of %s cannot be read: %w", other.Name, node, err)
|
|
}
|
|
shelf := c.shelfFor(other)
|
|
for _, n := range resolved.Plan.Needs {
|
|
if n.From != node || n.ByRecord {
|
|
continue
|
|
}
|
|
cred, held := c.credential(n, other.Name)
|
|
if _, outside := issuedOutsideBy(shelf, n); cred.State == CredentialNotGiven || (outside && !(held && cred.State == CredentialHeld)) {
|
|
continue
|
|
}
|
|
if slices.ContainsFunc(rows, func(r Issued) bool {
|
|
return r.Provision == n.Name && r.Consumer == other.Name && r.ConsumerModule == n.For && r.Local == n.Local
|
|
}) {
|
|
continue
|
|
}
|
|
rows = append(rows, Issued{Provision: n.Name, Consumer: other.Name, ConsumerModule: n.For, Local: n.Local, Provider: node})
|
|
}
|
|
}
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
a, b := rows[i], rows[j]
|
|
switch {
|
|
case a.Provision != b.Provision:
|
|
return a.Provision < b.Provision
|
|
case a.Consumer != b.Consumer:
|
|
return a.Consumer < b.Consumer
|
|
case a.ConsumerModule != b.ConsumerModule:
|
|
return a.ConsumerModule < b.ConsumerModule
|
|
}
|
|
return a.Local < b.Local
|
|
})
|
|
return rows, 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.
|
|
func boundOfGrant(consumer catalogue.Resolution, s Issued, provider string) catalogue.IdentityBound {
|
|
for _, n := range consumer.Needs {
|
|
if n.Name == s.Provision && n.For == s.ConsumerModule && n.From == provider {
|
|
return n.Identity
|
|
}
|
|
}
|
|
return catalogue.DefaultIdentityBound
|
|
}
|
|
|
|
// ownSecretRow is the row for one own secret of a module on a node.
|
|
func (c *composer) ownSecretRow(node, module, name string) (OwnSecretRow, bool) {
|
|
for _, r := range c.in.OwnSecrets {
|
|
if r.Node == node && r.Module == module && r.Name == name {
|
|
return r, true
|
|
}
|
|
}
|
|
return OwnSecretRow{}, false
|
|
}
|
|
|
|
// givenMember is one own secret a person gave on a machine and whether it is sealed to the key the machine holds now.
|
|
type givenMember struct {
|
|
Name string
|
|
Sealed string
|
|
Current bool
|
|
}
|
|
|
|
// givenMembers is every member of a module's secret family given on a machine, by name (inv.GivenMembers): the mesh never
|
|
// makes a member, so only a value a person gave is one; a machine with no sealing key holds none.
|
|
func (c *composer) givenMembers(node Node, module, family string) []givenMember {
|
|
if node.SealingKey == "" {
|
|
return nil
|
|
}
|
|
prefix := catalogue.FamilyPrefix(family)
|
|
var out []givenMember
|
|
for _, r := range c.in.OwnSecrets {
|
|
if r.Node == node.Name && r.Module == module && r.Origin == "accepted" && strings.HasPrefix(r.Name, prefix) {
|
|
out = append(out, givenMember{Name: r.Name, Sealed: r.Sealed, Current: r.SealedTo == node.SealingKey})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// givenOwnSecret is an own secret issued outside the mesh as a person gave it on a machine (inv.GivenOwnSecret): not given when
|
|
// the value held was made by the mesh or the machine has no sealing key.
|
|
func (c *composer) givenOwnSecret(node Node, module, name string) (givenMember, bool) {
|
|
if node.SealingKey == "" {
|
|
return givenMember{}, false
|
|
}
|
|
r, ok := c.ownSecretRow(node.Name, module, name)
|
|
if !ok || r.Origin != "accepted" {
|
|
return givenMember{}, false
|
|
}
|
|
return givenMember{Name: name, Sealed: r.Sealed, Current: r.SealedTo == node.SealingKey}, true
|
|
}
|
|
|
|
// moduleSecretIfIssued is the read half of the controller's SecretForModule (inv.ModuleSecretIfIssued): the secret when one is
|
|
// held and sealed to the key the machine holds now, and otherwise not held. A key it was sealed to that the machine no longer
|
|
// has is reported as absent rather than as an error.
|
|
func (c *composer) moduleSecretIfIssued(node Node, module, name string) (sealed string, held bool) {
|
|
if node.SealingKey == "" {
|
|
return "", false
|
|
}
|
|
r, ok := c.ownSecretRow(node.Name, module, name)
|
|
if !ok || r.SealedTo != node.SealingKey {
|
|
return "", false
|
|
}
|
|
return r.Sealed, true
|
|
}
|
|
|
|
// wouldMakeSecretForModule says what the send would do about an own secret without doing it: nil when it would make one (or one
|
|
// is held), and otherwise the very refusal it would meet (inv.WouldMakeSecretForModule).
|
|
func (c *composer) wouldMakeSecretForModule(node Node, module, name string) error {
|
|
if node.SealingKey == "" {
|
|
return fmt.Errorf("%s needs a secret and %s has no sealing key, so nothing can be sealed to it", module, node.Name)
|
|
}
|
|
r, ok := c.ownSecretRow(node.Name, module, name)
|
|
switch {
|
|
case !ok:
|
|
return nil
|
|
case r.SealedTo != node.SealingKey && r.Origin == "accepted":
|
|
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
|
"since generated a new sealing key. The mesh cannot make another; issue it again", module, node.Name, name, node.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// givenUnderAnOldKey is the refusal for a value a person gave sealed to a key the machine no longer holds.
|
|
func givenUnderAnOldKey(module, node, name string) error {
|
|
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
|
"since generated a new sealing key. The mesh cannot make another; give it again", module, node, name, node)
|
|
}
|
|
|
|
// ownSecretsFor is each module's own secrets as this node composes them (plan.go renderingFor): the needed ones sealed to the
|
|
// node, and, in the Foreseeing mode, the ones the next send would make named foreseen. A module handed to the provisioner has its
|
|
// secrets from the vault through the client, so none of them is composed here.
|
|
func (c *composer) ownSecretsFor(node Node, plan catalogue.Resolution, handing catalogue.Rendering, mode Mode) (
|
|
needed map[string]map[string]string, foreseen map[string]map[string]bool, err error) {
|
|
needed = map[string]map[string]string{}
|
|
foreseen = map[string]map[string]bool{}
|
|
mark := func(module, name string) {
|
|
if foreseen[module] == nil {
|
|
foreseen[module] = map[string]bool{}
|
|
}
|
|
foreseen[module][name] = true
|
|
}
|
|
for _, m := range plan.Modules {
|
|
if handing.Handing(m) {
|
|
continue
|
|
}
|
|
// A secret family is never made (novox/hq ADR 0283): each member a person gave on this machine is placed, and one not
|
|
// given is nothing -- the module says it waits for it.
|
|
for _, family := range m.OwnSecrets.Families() {
|
|
for _, g := range c.givenMembers(node, m.Module, family) {
|
|
if _, fam, ok := m.OwnSecrets.Lookup(g.Name); !ok || fam != family {
|
|
continue // a longer family's member, or a name no longer of this family
|
|
}
|
|
if !g.Current {
|
|
continue
|
|
}
|
|
if needed[m.Module] == nil {
|
|
needed[m.Module] = map[string]string{}
|
|
}
|
|
needed[m.Module][g.Name] = g.Sealed
|
|
if g.Sealed == "" {
|
|
// Given, and its value is not in the Input: placed with a stand-in, named.
|
|
needed[m.Module][g.Name] = catalogue.ForeseenSealed
|
|
mark(m.Module, g.Name)
|
|
}
|
|
}
|
|
}
|
|
names := make([]string, 0)
|
|
for name := range m.OwnSecrets.Plain() {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
own := m.OwnSecrets.Plain()[name]
|
|
// An own secret issued outside the mesh is never made (novox/hq issue 401, ADR 0228; ADR 0283 for a family): only a
|
|
// value a person gave is placed. One not given is nothing, and a value the mesh made for it before is not one either.
|
|
if own.IssuedBy == catalogue.IssuedOutside {
|
|
g, given := c.givenOwnSecret(node, m.Module, name)
|
|
if !given || !g.Current {
|
|
continue
|
|
}
|
|
if g.Sealed == "" {
|
|
mark(m.Module, name)
|
|
continue
|
|
}
|
|
if needed[m.Module] == nil {
|
|
needed[m.Module] = map[string]string{}
|
|
}
|
|
needed[m.Module][name] = g.Sealed
|
|
continue
|
|
}
|
|
sealed, held := c.moduleSecretIfIssued(node, m.Module, name)
|
|
if !held && mode == Planning {
|
|
mark(m.Module, name)
|
|
continue
|
|
}
|
|
if !held && mode == Foreseeing {
|
|
// Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in,
|
|
// named; refused by it -- refused here, the same words (novox/hq issue 275). A bus account not yet issued is
|
|
// foreseen, never refused: the vault mints it from the grants part's wanted list once the placement is written
|
|
// on the operator's Approve (ADR 0561 §1, ADR 0567 §5), so a composition asked ahead of that cannot hold it
|
|
// (issue 774, 2026-10-30: every proposal placing mesh-conditions, mesh-self-check or mesh-healers refused).
|
|
if err := c.wouldMakeSecretForModule(node, m.Module, name); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mark(m.Module, name)
|
|
continue
|
|
}
|
|
if !held {
|
|
// Never issued, so this machine cannot be running it: left out rather than invented.
|
|
continue
|
|
}
|
|
if sealed == "" {
|
|
// Held, and its value is not in the Input: composed with a stand-in, named.
|
|
mark(m.Module, name)
|
|
continue
|
|
}
|
|
if needed[m.Module] == nil {
|
|
needed[m.Module] = map[string]string{}
|
|
}
|
|
needed[m.Module][name] = sealed
|
|
}
|
|
}
|
|
return needed, foreseen, nil
|
|
}
|
|
|
|
// CoreBoxWanted is a core credential whose box the Input does not hold for a machine: the next send asks the vault for it and
|
|
// keeps the answer. It is returned for the caller to ask and keep; this package asks and keeps nothing.
|
|
type CoreBoxWanted struct {
|
|
Node string
|
|
Module string
|
|
Name string
|
|
}
|
|
|
|
// coreModulesOf is every module of the plan on the core's list whose own secrets this machine's declaration carries, with their
|
|
// plain own secret names, sorted: one handed to the provisioner has its secrets from the vault through the client, and one issued
|
|
// outside the mesh is never the vault's to seal here.
|
|
func coreModulesOf(modules []catalogue.Manifest, handed func(catalogue.Manifest) bool) map[string][]string {
|
|
out := map[string][]string{}
|
|
for _, m := range modules {
|
|
if !catalogue.CarriesVaultSealed(m.Module) || (handed != nil && handed(m)) {
|
|
continue
|
|
}
|
|
var names []string
|
|
for name, own := range m.OwnSecrets.Plain() {
|
|
if own.IssuedBy != catalogue.IssuedOutside {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
if len(names) > 0 {
|
|
sort.Strings(names)
|
|
out[m.Module] = names
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sealingKeyValid says a machine's sealing key, recorded raw in base64, is a raw X25519 key.
|
|
func sealingKeyValid(raw string) error {
|
|
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(raw))
|
|
if err != nil || len(b) != 32 {
|
|
return errors.New("the machine's sealing key is not a raw X25519 key")
|
|
}
|
|
_, err = ecdh.X25519().NewPublicKey(b)
|
|
return err
|
|
}
|
|
|
|
// coreBoxesFor decides each core credential of a machine in a composition that sends nothing and asks no vault (core_box.go
|
|
// coreBoxesOf, in the modes that are not a send): the kept box while it is sealed to the key the machine holds now, and otherwise a
|
|
// stand-in that the next send replaces by asking the vault -- returned in wanted for the caller to ask and keep. A machine with no
|
|
// sealing key, or one whose key is not a key, leaves its module out, said.
|
|
func (c *composer) coreBoxesFor(node Node, modules []catalogue.Manifest, handing catalogue.Rendering) (
|
|
sealed map[string]map[string]json.RawMessage, notPlaced map[string]string, wanted []CoreBoxWanted) {
|
|
core := coreModulesOf(modules, func(m catalogue.Manifest) bool { return handing.Handing(m) })
|
|
sealed, notPlaced = map[string]map[string]json.RawMessage{}, map[string]string{}
|
|
mods := make([]string, 0, len(core))
|
|
for m := range core {
|
|
mods = append(mods, m)
|
|
}
|
|
sort.Strings(mods)
|
|
for _, module := range mods {
|
|
if node.SealingKey == "" {
|
|
notPlaced[module] = fmt.Sprintf("%s has no sealing key, so the vault can seal %s's credentials to "+
|
|
"nothing there (novox/hq ADR 0349 §16)", node.Name, module)
|
|
continue
|
|
}
|
|
if err := sealingKeyValid(node.SealingKey); err != nil {
|
|
notPlaced[module] = fmt.Sprintf("%s's credentials cannot be asked of the vault: %v", module, err)
|
|
continue
|
|
}
|
|
boxes := map[string]json.RawMessage{}
|
|
for _, name := range core[module] {
|
|
var kept *CoreBox
|
|
for i := range c.in.KeptCoreBoxes {
|
|
k := &c.in.KeptCoreBoxes[i]
|
|
if k.Node == node.Name && k.Module == module && k.Name == name {
|
|
kept = k
|
|
}
|
|
}
|
|
if kept != nil && kept.SealedTo == node.SealingKey && len(kept.Box) > 0 {
|
|
boxes[name] = kept.Box
|
|
continue
|
|
}
|
|
boxes[name] = catalogue.ForeseenVaultBox
|
|
wanted = append(wanted, CoreBoxWanted{Node: node.Name, Module: module, Name: name})
|
|
}
|
|
sealed[module] = boxes
|
|
}
|
|
return sealed, notPlaced, wanted
|
|
}
|
|
|
|
// certificateFor is what the mesh certifies about one machine's internal name (plan.go certificateFor): refused for a machine that
|
|
// never told the mesh what key it serves with, and for one not on the private network, which has no name to be certified for.
|
|
// The certificate and the authority are public values the Input may carry; without them stand-ins are composed, and named.
|
|
func (c *composer) certificateFor(node Node) (certificate, authority string, foreseen bool, err error) {
|
|
if node.ServingKey == "" {
|
|
return "", "", false, fmt.Errorf(
|
|
"%s wants a certificate and has never told the mesh what key it serves with; it "+
|
|
"joins again to report one", node.Name)
|
|
}
|
|
where, err := c.whereEveryoneIs()
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
if where[node.Name] == "" {
|
|
return "", "", false, fmt.Errorf(
|
|
"%s wants a certificate and is not on the private network, so it has no name inside "+
|
|
"the mesh to be certified for", node.Name)
|
|
}
|
|
certificate, authority = node.Certificate, c.in.Authority
|
|
if certificate == "" || authority == "" {
|
|
return ForeseenCertificate, ForeseenCertificate, true, nil
|
|
}
|
|
return certificate, authority, false, nil
|
|
}
|
|
|
|
// holdsTheBus says whether a node's set includes the module that asks for the bus's user list.
|
|
func holdsTheBus(modules []catalogue.Manifest) bool {
|
|
for _, m := range modules {
|
|
if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|