The seed's shadow answers named the first refusal only (ace: baserow's archive, novox: de-spiegel), so each round showed one more. A build that cannot be read or taken in, and a module the composition refuses, are now each composed as a stub and named. A module folded into the node's own composition whose manifest names a build (de-spiegel, nats, photos, systemd-resolved: haves naming a build-... id) runs a build only the controller holds: it is composed as a stub and its resources are carried as the node runs them.
696 lines
29 KiB
Go
696 lines
29 KiB
Go
// Package frombus fills mesh-sdk's composition input from what is on the bus, so a node agent, the composition check
|
|
// and the grants part compose with one composer over the same inputs (novox/hq ADR 0547 §2, ADR 0549 §3; issue 1126).
|
|
//
|
|
// **One composer, three callers.** compose.Compositions is the composer; Input is how each caller fills its
|
|
// compose.Input from the bus: the node agent for its own node (ADR 0547 §2), the assigner's composition check for every
|
|
// node (ADR 0539 §6's check, ADR 0547 §4), and the grants part (ADR 0547 §5). Each reads the bus with its own credential
|
|
// through a Reader, so this package holds no bus client, no store and no timer: what it is given is all it reads.
|
|
//
|
|
// **What it reads, and from whom.**
|
|
// - which modules run on each node, with their pins, holdings, host ports, bindings and settings placement: the
|
|
// indexer's state `placement`, one key per node (ADR 0549 §2);
|
|
// - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and
|
|
// accounts): the same placement key, Placement.Decided;
|
|
// - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the
|
|
// node's own have key, `<node>.node` (HaveNode.Record); the tunnel found at a node's adoption and the names of the hub's peers are decided,
|
|
// in its placement, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both;
|
|
// - every module's manifest: the indexer's `modules.<module>` and its manifests by digest; for each module placed on
|
|
// the composing node, its built manifest instead: the builder's done key at the trunk's FP2, or at the fingerprint
|
|
// the node's want names where it runs another build (Scope.KeptAt), resolved by the controller's own take-in rule
|
|
// (takein.Manifest), a moved module's templates archive read in the artifact store (issue 1126);
|
|
// - each module's config values on a node: mesh-config's resolved state, the composing node's slice alone unless the
|
|
// caller composes every node (ADR 0547 §2, the prepare step's L4); of another node, only the keys its modules'
|
|
// served values fill (catalogue.ServedSettings), which every consumer is told (issue 1126).
|
|
//
|
|
// **Own secrets and pair credentials** come from the vault sealed to the composing node's key (OwnSecrets, Credentials),
|
|
// for that node alone: as a consumer and as a provider, exactly the keys compose.CredentialsNeeded names. **Not yet
|
|
// read** (each a later subtask of issue 1126): licences (none is placed today) and core boxes. Without one
|
|
// the composition stands in for it, and Compose refuses a composition that stands in for anything (compose.ErrStoodIn):
|
|
// a node never applies a stand-in. No node is joining before ADR 0547's third milestone (ADR 0550 §6), so Joining is
|
|
// always empty.
|
|
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/netip"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
"git.novox.be/novox/mesh-sdk/go/takein"
|
|
)
|
|
|
|
// Reader is the bus as a caller reads it with its own credential. Each method reads one part's state; an error is a
|
|
// read that failed, never an absent key (an absent key is an empty answer).
|
|
type Reader interface {
|
|
// Placements is every node's placement, by node (delivery.PlacementBucket).
|
|
Placements(ctx context.Context) (map[string]delivery.Placement, error)
|
|
// Records is every node's record, by node: HaveNode.Record of each node's own have key, `<node>.node` of
|
|
// delivery.HaveBucket (ADR 0555 §3).
|
|
Records(ctx context.Context) (map[string]delivery.NodeRecord, error)
|
|
// Modules is every module the indexer holds, by module (delivery.ModulesBucket).
|
|
Modules(ctx context.Context) (map[string]delivery.Module, error)
|
|
// Manifest is the raw manifest of a digest (delivery.ManifestsBucket).
|
|
Manifest(ctx context.Context, digest string) ([]byte, error)
|
|
// Values is one module's config values on one node as mesh-config resolved them (mesh-config's state `resolved`,
|
|
// keys `<node>.<module>.<key>`), decoded; nil when it has none.
|
|
Values(ctx context.Context, node, module string) (map[string]any, error)
|
|
// OwnSecrets is the own secrets of the composing node's modules, each sealed to that node's key, as the vault answers
|
|
// the node's engine (the vault's verb `own`, ADR 0550 §5); asked only for the node composing itself.
|
|
OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecretRow, error)
|
|
// Credentials is, for the composing node, each pair credential it reads as a consumer, held and sealed to its key,
|
|
// and each it grants as a provider, with the copy sealed to it (the vault's `own` with the keys, ADR 0550 §5). A key
|
|
// the vault holds no value for is left out; the composition then stands in for it, and Compose refuses it.
|
|
Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error)
|
|
// Build is a module's build at a fingerprint, the builder's key (delivery.BuilderBuildsBucket, BuilderBuildKey); nil
|
|
// when the builder holds none. The composing node composes each of its modules from its build (issue 1126).
|
|
Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error)
|
|
// Archive is the archive at url, a kept reference routed to the artifact store as the composing node reaches it
|
|
// (catalogue.Routed), checked against digest: a moved module's templates archive, read back for its texts as the
|
|
// controller's take-in reads it (takein.Built).
|
|
Archive(ctx context.Context, url, digest string) ([]byte, error)
|
|
// Served is, of one module's config values on another node than the composing one (mesh-config's state `resolved`,
|
|
// keys `<node>.<module>.<key>`), the keys named, decoded: the keys its served values and zone name fill
|
|
// (catalogue.ServedSettings), which every consumer is told and so every node may read (issue 1126). Nil when none
|
|
// is set.
|
|
Served(ctx context.Context, node, module string, keys []string) (map[string]any, error)
|
|
}
|
|
|
|
// Mesh is what is true of the whole mesh and is no part's state on the bus yet: the private network's range and the
|
|
// internal names' suffix as genesis was told them, and the mesh's authority certificate (public). Empty fields take
|
|
// compose's defaults.
|
|
type Mesh struct {
|
|
OverlayCIDR string
|
|
InternalSuffix string
|
|
Authority string
|
|
// Predefined is the list of predefined keys mesh-config published, which a manifest's values falling back to a
|
|
// predefined key are judged against; nil reads each manifest as the indexer does (catalogue.ParseManifestForInputs).
|
|
Predefined catalogue.PredefinedList
|
|
}
|
|
|
|
// Scope says who composes. Node is the node composing; only its config values are read unless Every is set, as the
|
|
// composition check sets it to compose every node (of another node's modules, only the values their served values fill
|
|
// are read, Reader.Served). KeptAt is, by module, the fingerprint the composing node's want names where it differs from
|
|
// the indexer's trunk: that node composes the module from its build at that fingerprint, as it runs it.
|
|
//
|
|
// **Composed from its build** (issue 1126): each module placed on the composing node, and each in KeptAt, composes from
|
|
// its built manifest (takein.Manifest over the builder's done key at the trunk's FP2 or the kept fingerprint), as the
|
|
// controller composes from the manifest its take-in resolved: so a moved module is handed to the provisioner, a resource
|
|
// naming an artifact names what was built, and a templated file has its text. A placed module the builder holds no done
|
|
// build of at the trunk's FP2 composes from the indexer's manifest, as before. Every leaves the catalogue as the indexer
|
|
// read it: the check of every node judges placement, and reads no archive.
|
|
//
|
|
// Placements, when set, stands in for the indexer's placement of the nodes it names: a node's candidate placement
|
|
// (delivery.PlacementCandidate), so a node composes in shadow with a catalogue pull request's placement and compares
|
|
// the result with what it runs (issue 1126); a nil value removes the node's placement. The rest is read as always.
|
|
//
|
|
// InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again
|
|
// (compose.Node.InOwn) rather than cut out as modules no build is given for (issue 1126).
|
|
//
|
|
// Running is what the composing node runs; of it, its own composition (`<node>.node`) is given to the composition
|
|
// (compose.Node.RunsOwn), so a module folded there that names a build of the old form is left to the node, carried
|
|
// as it runs (issue 1126). Nil composes every folded module from its manifest.
|
|
type Scope struct {
|
|
Node string
|
|
Every bool
|
|
KeptAt map[string]string
|
|
Placements map[string]*delivery.Placement
|
|
InOwn []string
|
|
Running []delivery.Composed
|
|
}
|
|
|
|
// InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs
|
|
// (`<node>.node` in running) carries, by the module their ids name (declaration.ModuleGuess), sorted: the modules the
|
|
// controller's composed writer folded there because no fingerprint of the new form names them (mesh-wireguard, a module
|
|
// run at a build of the old form; issue 1126). A node composing itself passes it as Scope.InOwn, so its own
|
|
// composition keeps them as it runs them until a want names a build of the new form for one.
|
|
func InOwn(running []delivery.Composed, p delivery.Placement) []string {
|
|
placed := map[string]bool{}
|
|
for _, m := range p.Modules {
|
|
placed[m.Module] = true
|
|
}
|
|
in := map[string]bool{}
|
|
for _, c := range running {
|
|
if c.Module != "" {
|
|
continue
|
|
}
|
|
for _, raw := range c.Resources {
|
|
var head struct {
|
|
ID string `json:"id"`
|
|
Module string `json:"module"`
|
|
}
|
|
if json.Unmarshal(raw, &head) != nil || head.Module != "" {
|
|
continue
|
|
}
|
|
if m, ok := declaration.ModuleGuess(head.ID); ok && placed[m] {
|
|
in[m] = true
|
|
}
|
|
}
|
|
}
|
|
return sortedKeys(in)
|
|
}
|
|
|
|
// Input is the composition input for s, read through r.
|
|
func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) {
|
|
return input(ctx, r, mesh, s, nil)
|
|
}
|
|
|
|
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in is
|
|
// handed to refuse and composed as a stub (compose.Stub, kept for that node alone) rather than ending the read, so a
|
|
// node's answer names every module it cannot compose (issue 1126).
|
|
func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error)) (compose.Input, error) {
|
|
placements, err := r.Placements(ctx)
|
|
if err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: placement: %w", err)
|
|
}
|
|
if len(s.Placements) > 0 {
|
|
given := make(map[string]delivery.Placement, len(placements)+len(s.Placements))
|
|
for node, p := range placements {
|
|
given[node] = p
|
|
}
|
|
for node, p := range s.Placements {
|
|
if p == nil {
|
|
delete(given, node)
|
|
continue
|
|
}
|
|
if p.Node != node {
|
|
return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
|
|
}
|
|
given[node] = *p
|
|
}
|
|
placements = given
|
|
}
|
|
records, err := r.Records(ctx)
|
|
if err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: node records: %w", err)
|
|
}
|
|
modules, err := r.Modules(ctx)
|
|
if err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: the indexer's modules: %w", err)
|
|
}
|
|
if _, ok := placements[s.Node]; !ok && !s.Every {
|
|
return compose.Input{}, fmt.Errorf("frombus: %s has no placement in the catalogue", s.Node)
|
|
}
|
|
|
|
in := compose.Input{OverlayCIDR: mesh.OverlayCIDR, InternalSuffix: mesh.InternalSuffix, Authority: mesh.Authority,
|
|
Catalogue: map[string]catalogue.Manifest{}}
|
|
if in.Authority == "" {
|
|
// The mesh's own certificate, public, as the composing node holds it; for the check of every node, as the first
|
|
// node holding one does.
|
|
if rec, ok := records[s.Node]; ok && rec.Authority != "" {
|
|
in.Authority = rec.Authority
|
|
} else {
|
|
for _, node := range sortedKeys(records) {
|
|
if a := records[node].Authority; a != "" {
|
|
in.Authority = a
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
parse := func(raw []byte) (catalogue.Manifest, error) {
|
|
if mesh.Predefined != nil {
|
|
return catalogue.ParseManifestWith(raw, mesh.Predefined)
|
|
}
|
|
return catalogue.ParseManifestForInputs(raw)
|
|
}
|
|
read := func(module, digest string) (catalogue.Manifest, error) {
|
|
raw, err := r.Manifest(ctx, digest)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, fmt.Errorf("frombus: the manifest of %s (digest %s): %w", module, digest, err)
|
|
}
|
|
return parse(raw)
|
|
}
|
|
for _, name := range sortedKeys(modules) {
|
|
m := modules[name]
|
|
if m.Manifest == "" {
|
|
continue // a registered module the indexer has not read: nothing to compose it from
|
|
}
|
|
man, err := read(name, m.Manifest)
|
|
if err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
in.Catalogue[name] = man
|
|
}
|
|
|
|
// The module the mesh provides itself (the private network) has no repository the indexer reads: its manifest comes
|
|
// with mesh-sdk and always wins, as the controller's providedModules and the grants part have it.
|
|
provided, err := json.Marshal(overlay.Manifest())
|
|
if err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
var private catalogue.Manifest
|
|
if err := json.Unmarshal(provided, &private); err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: the private network's manifest mesh-sdk provides: %w", err)
|
|
}
|
|
in.Catalogue[overlay.Name] = private
|
|
|
|
var missing []string
|
|
for _, node := range nodesOf(placements, records) {
|
|
rec, hasRecord := records[node]
|
|
p := placements[node]
|
|
if !hasRecord {
|
|
if len(p.Modules) > 0 {
|
|
missing = append(missing, node)
|
|
}
|
|
continue
|
|
}
|
|
n := nodeOf(rec, p)
|
|
withValues := s.Every || node == s.Node
|
|
for _, m := range p.Modules {
|
|
var values map[string]any
|
|
if withValues {
|
|
if values, err = r.Values(ctx, node, m.Module); err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: %s's config values on %s: %w", m.Module, node, err)
|
|
}
|
|
} else if keys := catalogue.ServedSettings(in.Catalogue[m.Module]); len(keys) > 0 {
|
|
// What a provider elsewhere serves from its settings is told to every consumer: the composing node reads
|
|
// those keys of it, and no other (issue 1126: mailu's smtp domain, a consumer's on shanks).
|
|
if values, err = r.Served(ctx, node, m.Module, keys); err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: what %s serves on %s (%s): %w", m.Module, node,
|
|
strings.Join(keys, ", "), err)
|
|
}
|
|
}
|
|
if layers := m.Layers(node, values); layers != nil {
|
|
if n.Settings == nil {
|
|
n.Settings = map[string][]catalogue.Layer{}
|
|
}
|
|
n.Settings[m.Module] = layers
|
|
}
|
|
}
|
|
if node == s.Node {
|
|
for _, module := range s.InOwn {
|
|
if n.InOwn == nil {
|
|
n.InOwn = map[string]bool{}
|
|
}
|
|
n.InOwn[module] = true
|
|
}
|
|
for i := range s.Running {
|
|
if s.Running[i].Module == "" {
|
|
own := s.Running[i]
|
|
n.RunsOwn = &own
|
|
}
|
|
}
|
|
}
|
|
in.Nodes = append(in.Nodes, n)
|
|
in.Holdings = append(in.Holdings, p.Held()...)
|
|
// A tunnel found at adoption is the node's private network's when the node's tunnel key is the tunnel's (the
|
|
// controller's inv.Tunnels); the hub's found tunnel is the mesh's adopted one whether or not the hub took it over
|
|
// (inv.AdoptedTunnel), and its peers are carried once it did (inv.CarriedPeers).
|
|
if t := p.Decided.Tunnel; t != nil {
|
|
if rec.TunnelKey == t.PublicKey {
|
|
if in.Tunnels == nil {
|
|
in.Tunnels = map[string]compose.FoundTunnel{}
|
|
}
|
|
in.Tunnels[node] = compose.FoundTunnel{Tunnel: tunnelOf(*t), NodeAdopted: p.Decided.Adopted}
|
|
}
|
|
if p.Decided.Hub {
|
|
in.Adopted = &compose.AdoptedTunnel{Tunnel: tunnelOf(*t), Hub: node, Adopted: rec.TunnelKey == t.PublicKey}
|
|
}
|
|
}
|
|
}
|
|
if in.Adopted != nil && in.Adopted.Adopted {
|
|
in.CarriedPeers = carriedPeers(in.Adopted.Tunnel, placements[in.Adopted.Hub].Decided.PeerNames, records)
|
|
}
|
|
if len(missing) > 0 {
|
|
return compose.Input{}, fmt.Errorf("frombus: modules are placed on %s, whose agent has written no record yet",
|
|
strings.Join(missing, ", "))
|
|
}
|
|
if !s.Every {
|
|
if _, ok := records[s.Node]; !ok {
|
|
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
|
|
}
|
|
if err := fromBuilds(ctx, r, &in, s, placements[s.Node], modules, read, parse, refuse); err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
// Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5).
|
|
// Asked only when a module placed on it declares one: a node none of whose modules has a secret composes
|
|
// whether or not the vault answers.
|
|
if declaresOwnSecrets(in, s.Node) {
|
|
if in.OwnSecrets, err = r.OwnSecrets(ctx, s.Node); err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: the own secrets of %s's modules: %w", s.Node, err)
|
|
}
|
|
}
|
|
// The core's credentials travel as the vault's box sealed to the machine (ADR 0349 §16): the same values, which
|
|
// the composition reads for a core module as its kept boxes.
|
|
for _, o := range in.OwnSecrets {
|
|
if json.Valid([]byte(o.Sealed)) {
|
|
in.KeptCoreBoxes = append(in.KeptCoreBoxes, compose.CoreBox{Node: o.Node, Module: o.Module, Name: o.Name,
|
|
SealedTo: o.SealedTo, Box: json.RawMessage(o.Sealed)})
|
|
}
|
|
}
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.CompositionsApplied at the
|
|
// fingerprints fps gives each module: composed in the Reading mode as the controller composes them, and refused when it
|
|
// stands in for anything the input does not carry (compose.ErrStoodIn), since the node applies what it composed.
|
|
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (map[string]delivery.Composed, []string, error) {
|
|
in, err := Input(ctx, r, mesh, s)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// The pair credentials its composition reads, asked of the vault sealed to it (ADR 0550 §5).
|
|
asConsumer, asProvider, err := compose.CredentialsNeeded(in, s.Node)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(asConsumer)+len(asProvider) > 0 {
|
|
if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil {
|
|
return nil, nil, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err)
|
|
}
|
|
}
|
|
return compose.CompositionsApplied(in, s.Node, fps)
|
|
}
|
|
|
|
// fromBuilds puts in the built manifest of each module placed on the composing node, at the trunk's FP2, and of each
|
|
// module it keeps at another build (Scope.KeptAt, into its node's Kept), read from the builder's done key and resolved
|
|
// by the one take-in rule (takein.Manifest), a templates archive read in the artifact store as the node reaches it
|
|
// (issue 1126).
|
|
func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module,
|
|
read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error),
|
|
refuse func(module string, err error)) error {
|
|
// stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides
|
|
// and requires still shapes the rest.
|
|
stub := func(module string, err error) {
|
|
refuse(module, err)
|
|
for i := range in.Nodes {
|
|
if in.Nodes[i].Name != s.Node {
|
|
continue
|
|
}
|
|
man, ok := in.Nodes[i].Kept[module]
|
|
if !ok {
|
|
man, ok = in.Catalogue[module]
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
if in.Nodes[i].Kept == nil {
|
|
in.Nodes[i].Kept = map[string]catalogue.Manifest{}
|
|
}
|
|
in.Nodes[i].Kept[module] = compose.Stub(man)
|
|
}
|
|
}
|
|
address, addressErr := "", error(nil)
|
|
addressed := false
|
|
archive := func(ctx context.Context, reference, digest string) ([]byte, error) {
|
|
if !addressed {
|
|
address, addressErr = compose.ArtifactStoreAddress(*in, s.Node)
|
|
addressed = true
|
|
}
|
|
switch {
|
|
case addressErr != nil:
|
|
return nil, addressErr
|
|
case address == "":
|
|
return nil, fmt.Errorf("%s reaches no artifact store (%s)", s.Node, storeSought(*in))
|
|
}
|
|
return r.Archive(ctx, catalogue.Routed(reference, address), digest)
|
|
}
|
|
built := func(module, fp string, b *delivery.BuilderBuild) (catalogue.Manifest, error) {
|
|
if b.Manifest == "" {
|
|
return catalogue.Manifest{}, fmt.Errorf("frombus: the build of %s at %s names no manifest", module, fp)
|
|
}
|
|
src, err := read(module, b.Manifest)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, err
|
|
}
|
|
resolved, err := takein.Manifest(ctx, src, *b, archive)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, fmt.Errorf("frombus: %s at %s: %w", module, fp, err)
|
|
}
|
|
// Read back as the mesh holds a built manifest (the controller keeps the resolved manifest's bytes and reads them
|
|
// as any manifest): a template's text is then in the manifest's own words, its ${config:…} in the settings form.
|
|
raw, err := json.Marshal(resolved)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, err
|
|
}
|
|
if resolved, err = parse(raw); err != nil {
|
|
return catalogue.Manifest{}, fmt.Errorf("frombus: the built manifest of %s at %s: %w", module, fp, err)
|
|
}
|
|
return resolved, nil
|
|
}
|
|
for _, pm := range p.Modules {
|
|
m, ok := modules[pm.Module]
|
|
if !ok || m.FP2 == "" || s.KeptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) {
|
|
// A module it runs inside its own composition is composed there as before: no build of the new form is what
|
|
// it runs (Scope.InOwn).
|
|
continue
|
|
}
|
|
if man, ok := in.Catalogue[pm.Module]; !ok || (man.Build == nil && !man.Templated()) {
|
|
continue // nothing is built of it: its manifest is what it composes from
|
|
}
|
|
b, err := r.Build(ctx, pm.Module, m.FP2)
|
|
if err != nil {
|
|
err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.FP2, err)
|
|
if refuse == nil {
|
|
return err
|
|
}
|
|
stub(pm.Module, err)
|
|
continue
|
|
}
|
|
if b == nil || b.State != delivery.BuildDone {
|
|
continue // no done build at the trunk's: composed from the indexer's manifest, as before
|
|
}
|
|
resolved, err := built(pm.Module, m.FP2, b)
|
|
if err != nil {
|
|
if refuse == nil {
|
|
return err
|
|
}
|
|
stub(pm.Module, err)
|
|
continue
|
|
}
|
|
in.Catalogue[pm.Module] = resolved
|
|
}
|
|
for i := range in.Nodes {
|
|
if in.Nodes[i].Name != s.Node {
|
|
continue
|
|
}
|
|
for _, module := range sortedKeys(s.KeptAt) {
|
|
fp := s.KeptAt[module]
|
|
b, err := r.Build(ctx, module, fp)
|
|
switch {
|
|
case err != nil:
|
|
err = fmt.Errorf("frombus: the build of %s at %s: %w", module, fp, err)
|
|
case b == nil || b.State != delivery.BuildDone:
|
|
err = fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build", module, fp)
|
|
}
|
|
var resolved catalogue.Manifest
|
|
if err == nil {
|
|
resolved, err = built(module, fp, b)
|
|
}
|
|
if err != nil {
|
|
if refuse == nil {
|
|
return err
|
|
}
|
|
stub(module, err)
|
|
continue
|
|
}
|
|
if in.Nodes[i].Kept == nil {
|
|
in.Nodes[i].Kept = map[string]catalogue.Manifest{}
|
|
}
|
|
in.Nodes[i].Kept[module] = resolved
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// declaresOwnSecrets says a module assigned to node declares an own secret, in the manifest the node composes it from.
|
|
func declaresOwnSecrets(in compose.Input, node string) bool {
|
|
for _, n := range in.Nodes {
|
|
if n.Name != node {
|
|
continue
|
|
}
|
|
for _, module := range n.Assigned {
|
|
m, ok := n.Kept[module]
|
|
if !ok {
|
|
m = in.Catalogue[module]
|
|
}
|
|
if len(m.OwnSecrets) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
|
|
// decided of it, and where its modules run).
|
|
func nodeOf(rec delivery.NodeRecord, p delivery.Placement) compose.Node {
|
|
n := compose.Node{
|
|
Name: rec.Node,
|
|
Place: compose.Place{Key: rec.TunnelKey},
|
|
SealingKey: rec.SealingKey,
|
|
OutwardLinks: rec.OutwardLinks, Networks: rec.Networks, NetworksSaid: rec.NetworksSaid,
|
|
ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: rec.Contract, Stated: rec.Contract > 0,
|
|
}
|
|
placeNode(&n, p)
|
|
if len(rec.Capabilities) > 0 {
|
|
n.Capabilities = make(map[string]bool, len(rec.Capabilities))
|
|
for _, c := range rec.Capabilities {
|
|
n.Capabilities[c] = true
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// placeNode sets what a person decided of n and where its modules run, from its placement; what the machine observes
|
|
// (its keys, links, networks, certificate) and its settings layers are left as they are.
|
|
func placeNode(n *compose.Node, p delivery.Placement) {
|
|
d := p.Decided
|
|
n.Place = compose.Place{Key: n.Place.Key, Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address}
|
|
n.Assigned, n.PublicDomain = p.Assigned(), d.PublicDomain
|
|
n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentHome
|
|
n.Adopted, n.Taken = d.Adopted, d.Taken
|
|
n.Pins, n.Bound, n.Ports = p.ChosenPins(), p.Bound(), p.HostPorts()
|
|
}
|
|
|
|
// Placed is in with the placements given standing in for those of the nodes they name (issue 1126): each such node's
|
|
// decided parts, modules, pins, bindings, host ports, settings placement and holdings are the given placement's, its
|
|
// config values those in already carries for it (the layer of the node's own name), and what the machine observes is
|
|
// kept. A nil placement leaves the node in the mesh with nothing placed on it. A node in does not hold is refused: it has
|
|
// no record to compose it from. The found tunnel and the hub's peers are read from in as they are, not from the given
|
|
// placement. It is how a check composes a change's own placement over an input already filled (composer.Case's
|
|
// Placements).
|
|
func Placed(in compose.Input, placements map[string]*delivery.Placement) (compose.Input, error) {
|
|
if len(placements) == 0 {
|
|
return in, nil
|
|
}
|
|
out := in
|
|
out.Nodes = append([]compose.Node(nil), in.Nodes...)
|
|
index := map[string]int{}
|
|
for i, n := range out.Nodes {
|
|
index[n.Name] = i
|
|
}
|
|
for _, node := range sortedKeys(placements) {
|
|
p := placements[node]
|
|
i, ok := index[node]
|
|
if !ok {
|
|
return compose.Input{}, fmt.Errorf("frombus: a placement is given for %s, which the input holds no node for", node)
|
|
}
|
|
if p == nil {
|
|
p = &delivery.Placement{Node: node}
|
|
}
|
|
if p.Node != node {
|
|
return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
|
|
}
|
|
n := out.Nodes[i]
|
|
placeNode(&n, *p)
|
|
settings := map[string][]catalogue.Layer{}
|
|
for _, m := range p.Modules {
|
|
var values map[string]any
|
|
for _, l := range n.Settings[m.Module] {
|
|
if l.From == node {
|
|
values = l.Values
|
|
}
|
|
}
|
|
if layers := m.Layers(node, values); layers != nil {
|
|
settings[m.Module] = layers
|
|
}
|
|
}
|
|
n.Settings = nil
|
|
if len(settings) > 0 {
|
|
n.Settings = settings
|
|
}
|
|
out.Nodes[i] = n
|
|
}
|
|
out.Holdings = nil
|
|
for _, h := range in.Holdings {
|
|
if _, given := placements[h.Node]; !given {
|
|
out.Holdings = append(out.Holdings, h)
|
|
}
|
|
}
|
|
for _, node := range sortedKeys(placements) {
|
|
if p := placements[node]; p != nil {
|
|
out.Holdings = append(out.Holdings, p.Held()...)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func tunnelOf(t delivery.RecordTunnel) compose.Tunnel {
|
|
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
|
|
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
|
|
for _, p := range t.Peers {
|
|
out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// carriedPeers is every peer of the hub's adopted tunnel, by address, each with the node that enrolled under its key
|
|
// where one has (a node whose tunnel key is the peer's) and the name a person gave it (inv.CarriedPeers).
|
|
func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]delivery.NodeRecord) []compose.CarriedPeer {
|
|
byKey := map[string]string{}
|
|
for node, rec := range records {
|
|
if rec.TunnelKey != "" {
|
|
byKey[rec.TunnelKey] = node
|
|
}
|
|
}
|
|
var out []compose.CarriedPeer
|
|
for _, p := range t.Peers {
|
|
address, _, _ := strings.Cut(p.Address, "/")
|
|
out = append(out, compose.CarriedPeer{PublicKey: p.PublicKey, Address: address, EnrolledAs: byKey[p.PublicKey], Named: names[p.PublicKey]})
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
a, errA := netip.ParseAddr(out[i].Address)
|
|
b, errB := netip.ParseAddr(out[j].Address)
|
|
if errA != nil || errB != nil {
|
|
return out[i].Address < out[j].Address
|
|
}
|
|
return a.Less(b)
|
|
})
|
|
return out
|
|
}
|
|
|
|
// nodesOf is every node named by a placement or a record, by name: the order the controller reads them in.
|
|
func nodesOf(placements map[string]delivery.Placement, records map[string]delivery.NodeRecord) []string {
|
|
seen := map[string]bool{}
|
|
for n := range placements {
|
|
seen[n] = true
|
|
}
|
|
for n := range records {
|
|
seen[n] = true
|
|
}
|
|
return sortedKeys(seen)
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// storeSought says, for a node that reaches no artifact store, what the composition holds of one: the nodes it holds and
|
|
// where each module serving the artifact store is placed, so the refusal names why (issue 1126).
|
|
func storeSought(in compose.Input) string {
|
|
var nodes, where []string
|
|
for _, n := range in.Nodes {
|
|
nodes = append(nodes, n.Name)
|
|
for _, a := range n.Assigned {
|
|
if m, ok := in.Catalogue[a]; ok {
|
|
if _, serves := m.Serves[catalogue.ArtifactStoreProvision]; serves {
|
|
where = append(where, a+" on "+n.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(nodes)
|
|
sort.Strings(where)
|
|
said := "the composition holds " + strings.Join(nodes, ", ")
|
|
if len(where) == 0 {
|
|
return said + "; no module placed there serves " + catalogue.ArtifactStoreProvision
|
|
}
|
|
return said + "; " + catalogue.ArtifactStoreProvision + " is served by " + strings.Join(where, ", ") +
|
|
", none of it on the private network with a port"
|
|
}
|