From 5eaaeb71353721ab4f1c33bb06690697f6a0b609 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 08:38:52 +0000 Subject: [PATCH] Vendor mesh-sdk's go/compose and drop the node-engine's validator from vendor/ (issues 1381, 1382) go mod vendor writes go/compose (copied on the forge from mesh-sdk 14fd5cb68fd1, equal to its blobs) now that the adapter imports it, and no longer writes github.com/novox/mesh-host/validate and internal/declaration, which nothing imports since the validator comes from mesh-sdk (issue 1381; the swap commits left those five files behind). vendor/modules.txt follows with the adapter. --- .../novox/mesh-sdk/go/compose/credentials.go | 495 ++++ .../novox/mesh-sdk/go/compose/input.go | 324 ++ .../novox/mesh-sdk/go/compose/network.go | 262 ++ .../novox/mesh-sdk/go/compose/node.go | 335 +++ .../novox/mesh-sdk/go/compose/render.go | 128 + .../novox/mesh-sdk/go/compose/rendering.go | 532 ++++ .../novox/mesh-sdk/go/compose/resolve.go | 280 ++ .../novox/mesh-sdk/go/compose/rest.go | 233 ++ .../mesh-host/internal/declaration/busy.go | 87 - .../mesh-host/internal/declaration/cron.go | 182 -- .../internal/declaration/declaration.go | 2607 ----------------- .../mesh-host/internal/declaration/health.go | 188 -- .../novox/mesh-host/validate/validate.go | 38 - 13 files changed, 2589 insertions(+), 3102 deletions(-) create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/credentials.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/input.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/network.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/node.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/render.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/rendering.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/resolve.go create mode 100644 vendor/git.novox.be/novox/mesh-sdk/go/compose/rest.go delete mode 100644 vendor/github.com/novox/mesh-host/internal/declaration/busy.go delete mode 100644 vendor/github.com/novox/mesh-host/internal/declaration/cron.go delete mode 100644 vendor/github.com/novox/mesh-host/internal/declaration/declaration.go delete mode 100644 vendor/github.com/novox/mesh-host/internal/declaration/health.go delete mode 100644 vendor/github.com/novox/mesh-host/validate/validate.go diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/credentials.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/credentials.go new file mode 100644 index 00000000..97668876 --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/credentials.go @@ -0,0 +1,495 @@ +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 +) + +// 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 + if 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 +} + +// 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 +} + +// busCredentialIssued refuses an own secret called `broker` whose bus account nobody issued (novox/hq issue 203): the broker +// credential is never invented, an account on the bus minted by `module issue`. +func (c *composer) busCredentialIssued(node, module, name string) error { + var user string + switch name { + case "broker": + user = node + "." + module + case ProvisionerSecret: + user = "provisioner." + node + default: + return nil + } + if !slices.Contains(c.in.BusMinted, user) { + return fmt.Errorf( + "%s on %s has no bus credential: nothing was issued for %s, and a push "+ + "would seal a placeholder its process cannot read (novox/hq issue 203). "+ + "`module issue %s --node %s`, then push again", + module, node, user, module, node) + } + 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 == 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). + if err := c.busCredentialIssued(node.Name, m.Module, name); err != nil { + return nil, nil, err + } + 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 +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/input.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/input.go new file mode 100644 index 00000000..e98a7fee --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/input.go @@ -0,0 +1,324 @@ +// Package compose is the controller's composition of a machine, as a function over a plain input (novox/hq ADR 0453 +// clause 3, ADR 0446 clause 4): what the rest of the mesh offers and holds, the private network's generator, one +// node's resolution and, in the issues after, its rendering and its declaration. +// +// **It never allocates, makes, writes, logs or raises.** The controller composes with a store behind it; this +// package is its Reading and Foreseeing modes over data. Everything the controller reads from its store is a field of +// Input, and everything a send would make or record (a port, a credential, a seat passed on, a node left out) is +// returned for the caller to act on, never done. Input is never changed by a call. No field holds a secret value +// the composition needs: sealed values are optional inputs, and without them the composition uses stand-ins named +// foreseen. The logic is the controller's, ported from mesh-controller cmd/mesh-controller at commit +// 6c2a8d62100cb7d491f8eb9a623e1eeea5ec0de1 (plan.go, network.go, holdings.go, seat_pass.go), in the same order, with the +// same refusals in the same words, so that a comparison node for node is a fair one. +// +// It imports the standard library and mesh-sdk's own packages only. +// It reads no environment variable: the range the mesh allocates from and the suffix of its internal names are fields of Input. +package compose + +import ( + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Input is everything the composition reads. Each field is what one read of the controller's inventory answers, named +// beside it; the controller's adapter fills it from its stores and the assigner from the mesh's facts and buckets +// (ADR 0446 clause 2). +type Input struct { + // Catalogue is the shelf: every module's manifest by name (inv.Catalogue), as it is to be composed. A recorded + // module kept at the build a machine runs (keepRecorded) is that node's Kept, applied to its own resolution only (never to the rest of the mesh). + Catalogue map[string]catalogue.Manifest + // Nodes is every node the mesh has (inv.Nodes and inv.Overlays together, one row per node). + Nodes []Node + // Holdings is every seat whose holder is on record (inv.Holdings). + Holdings []catalogue.Held + // Licences is every licence the mesh holds, with the machines that use it (open.Licences); none for a mesh with no + // licence. + Licences []Licence + + // OverlayCIDR is the range the mesh allocates from when no tunnel was adopted, as genesis was told it (the + // controller's OverlayCIDRVar); empty means DefaultOverlayCIDR. The package reads no environment. + OverlayCIDR string + // InternalSuffix is what the mesh's internal names end in (the controller's MESH_INTERNAL_SUFFIX), with or without a leading + // dot; empty means overlay.DefaultSuffix. A fact of the whole mesh, so it is in the Input and never read from the environment. + InternalSuffix string + // Tunnels is, by node name, the found tunnel whose key is the node's overlay key (inv.Tunnels). + Tunnels map[string]FoundTunnel + // Adopted is the hub's found tunnel and the hub's name, whether or not the hub took it (inv.AdoptedTunnel); nil + // when no hub has one. + Adopted *AdoptedTunnel + // CarriedPeers is every peer of the adopted tunnel (inv.CarriedPeers). + CarriedPeers []CarriedPeer + // Joining is the nodes that hold a live enrolment token (inv.NodesWithALiveToken). + Joining []string + + // Credentials is how each pair credential a node takes from elsewhere stands, never its value (what inv.SecretFor, + // SharedSecretFor and GivenPairSecret would answer): held, to be made by the next send, only a person's to give, or + // refused. A need with no entry here is one the next send would make. + Credentials []Credential + + // Built is every repository the mesh has built (the keys of inv.Held): what tells a reference recorded with an address + // from an image a module runs straight from a public registry. + Built []string + // OperatorExport is every operator-sealed secret in the mesh, for a module that keeps them (inv.OperatorExport): sealed + // values the composition carries without opening. Nil when the mesh has no operator key. + OperatorExport *catalogue.KeptExport + + // Issued is the credentials made for the consumers of each provider (inv.SecretsFrom): one row for each pair credential + // that exists, by the provider node it is for. The grants a provider must create are read from these, so a credential not + // made yet has no grant until the send that makes it. + Issued []Issued + // OwnSecrets is the own secrets held on each machine (the module_secret rows): who made each, and the key it is sealed to. + // Their sealed values are optional. + OwnSecrets []OwnSecretRow + // KeptCoreBoxes is the vault's box last kept for each core credential on each machine (inv.KeptCoreBoxes). + KeptCoreBoxes []CoreBox + // BusUsers is the bus's user list as the controller composes it from the bus records and the hashes it holds (composeBusUsers), + // for the machine that runs the bus: names and hashes, never passwords. Empty when the Input does not carry it, and then + // the composition uses a stand-in, named foreseen. BusMissing are the users left out of it for want of a credential. + BusUsers string + BusMissing []string + // BusMinted are the bus users the mesh has minted a password hash for (inv.BusUserHash): `.` for a module and + // `provisioner.` for a machine's provisioner client. + BusMinted []string + // BusMemberships is, by node, the membership sealed to it for the bus the mesh is moving to (inv.BusMemberships). + BusMemberships map[string]string + // Authority is the mesh's own certificate (identity.EstablishAuthority), public. Optional. + Authority string + + // CurrentBuilds is, by module, the commit of the build the mesh holds for it (inv.CurrentBuilds, the commit it was built from); + // BuildIDs the build made from a commit of a module (inv.BuildOf), by module then commit. Read only to name the builds a + // declaration carries to a node-engine that reads them. + CurrentBuilds map[string]string + BuildIDs map[string]map[string]string +} + +// Issued is one pair credential made for a consumer of a provider (inv.Secret): ForProvider is its copy sealed to the +// provider, optional. +type Issued struct { + Provision string + Consumer string + ConsumerModule string + Local string + Provider string + ForProvider string +} + +// OwnSecretRow is one own secret held on a machine: Origin is `made` (the mesh generated it) or `accepted` (a person gave it), +// SealedTo the sealing key it is sealed to, and Sealed its value sealed to that key, optional. +type OwnSecretRow struct { + Node string + Module string + Name string + Origin string + SealedTo string + Sealed string +} + +// CoreBox is the vault's box for one core credential of one machine, as last kept (inv.CoreBox). +type CoreBox struct { + Node string + Module string + Name string + Generation int + SealedTo string + Box json.RawMessage +} + +// Node is one node of the mesh and what the composition reads about it. +type Node struct { + Name string + // Place is where it is on the private network (inv.Overlays). + Place Place + // Assigned is the modules assigned to it, in the order the controller reads them (inv.Assigned). + Assigned []string + // Capabilities are the ones present on it (inv.ProfileOf). + Capabilities map[string]bool + // PublicDomain is the domain it composes its routed names under (inv.PublicDomainOf). + PublicDomain string + // Account, AccountHome, AgentAccount and AgentAccountHome are its operator and agent logins (inv.NodeByName). + Account, AccountHome, AgentAccount, AgentAccountHome string + // Adopted is whether it is adopted rather than converged. + Adopted bool + // Pins is the provider each provision was pinned to for it (inv.PinsFor). + Pins map[string]catalogue.Chosen + // Bound is where each of its consumers of a provision that keeps data was last sent (inv.BindingsFor), by consumer + // module and provision. + Bound map[string]map[string]catalogue.Chosen + // Settings is its settings layers by module (inv.SettingsFor); a module with none is absent. + Settings map[string][]catalogue.Layer + // Ports is the machine port each module's software port was given on it (inv.PortsFor), by module then port. + Ports map[string]map[int]int + // SealingKey is the public key its secrets are sealed to (inv.SealingKeyOf); empty when it has none. A public key, so + // nothing secret. + SealingKey string + // Taken is the modules taken on it, for an adopted node (inv.Taken). + Taken []string + // OutwardLinks are the links it reported as facing outside it (inv.OutwardLinksOf). + OutwardLinks []string + // Networks are the networks it reported being attached to, as link and network (inv.MachineOf); NetworksSaid is whether + // its node-engine said any at all, which is not the same as saying none. + Networks []catalogue.AttachedNetwork + NetworksSaid bool + // ServingKey is the key the machine serves with, which a certificate is issued for (identity.ServingKeyOf); empty when it has + // never reported one. Certificate is the certificate the mesh issued for its internal name, public and optional. + ServingKey string + Certificate string + // Sequence, Epoch and Generation are what the declaration is sent under (inv.Sequence, SentEpoch, SentGeneration): the order + // against every other send to the node, the lease epoch and the assignment generation it was last sent. Zero is not sent. + Sequence int64 + Epoch uint64 + Generation int64 + // ReadsModules says its node-engine reads a module per resource and a build per module (inv.ReadsModules, issue 1145); only + // then are they sent. SentBuilds is the build of each module it was last sent, by module to commit, and SentKnown whether + // that is known at all (inv.SentBuilds); Runs is what each of its assignments runs (inv.AssignmentRuns), by module. + ReadsModules bool + SentBuilds map[string]string + SentKnown bool + Runs map[string]Run + // Kept is, by module, the manifest of a recorded or core module kept at the build this machine runs (plan.go keepRecorded: + // the caller reads it with recordedKept and ManifestAt), and KeptBuilds the commit of that build. They apply to this node's + // own resolution only, never to another node's, and a declaration carries the kept commit for the module. + Kept map[string]catalogue.Manifest + KeptBuilds map[string]string + // Contract is the contract of its node-engine's newest statement (inv.HealthOf); Stated is whether it ever stated one. + Contract int + Stated bool +} + +// Place is where a node is, as its placement says (inv.Overlay). +type Place struct { + Key string + Endpoint string + Site string + Hub bool + Address string +} + +// Reachable says whether the node can be dialled. +func (p Place) Reachable() bool { return strings.TrimSpace(p.Endpoint) != "" } + +// Tunnel is a tunnel found on a node (inv.Tunnel). +type Tunnel struct { + Interface string + Unit string + Config string + Port int + MTU int + // Address is the interface's own address with its prefix length, Range the network that prefix names. + Address string + Range string + PublicKey string + Peers []TunnelPeer +} + +// TunnelPeer is one peer of a found tunnel. +type TunnelPeer struct { + PublicKey string + Address string +} + +// FoundTunnel is a node's tunnel and whether the node is adopted (inv.FoundTunnel). +type FoundTunnel struct { + Tunnel + NodeAdopted bool +} + +// AdoptedTunnel is the hub's tunnel, the hub's name, and whether the hub's overlay key is the tunnel's, which is +// whether the private network took the tunnel over. +type AdoptedTunnel struct { + Tunnel Tunnel + Hub string + Adopted bool +} + +// CarriedPeer is a peer of the adopted tunnel (inv.CarriedPeer). +type CarriedPeer struct { + PublicKey string + Address string + EnrolledAs string + Named string +} + +// Licence is one licence the mesh holds: its name, what a consumer may know of it, and the machines and modules +// that use it. +type Licence struct { + Name string + Serves map[string]any + Holders []Holder + // Keys is, by holder, the licence's key sealed to that holder's machine (held.KeyFor), empty where the mesh holds none: + // the plaintext was discarded when it was supplied. Optional: nil means not given, and the composition uses a stand-in named + // foreseen; given (even empty), a holder with no entry is one the mesh holds no key for. + Keys map[Holder]string + // Manager is the holder of a refreshable-grant licence that manages it (held.ManagerOf); the zero Holder when none. + Manager Holder +} + +// Holder is a module on a node that uses a licence. +type Holder struct { + Node string + Module string +} + +// ErrNoSuchNode is what the controller's inventory answers for a node it does not have. +var ErrNoSuchNode = errors.New("no node of that name") + +// node is the node of that name, or the refusal the controller's inventory gives for one it does not have. +func (in Input) node(name string) (Node, error) { + for _, n := range in.Nodes { + if n.Name == name { + return n, nil + } + } + return Node{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name) +} + +// sorted is the nodes by name, the order the controller reads them in. +func (in Input) sorted() []Node { + out := append([]Node(nil), in.Nodes...) + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out +} + +// CredentialState is how one pair credential stands. +type CredentialState string + +const ( + // CredentialHeld is a credential held for both ends as they stand now. Sealed, when given, is the consumer's copy; a held + // credential with no value is composed with a stand-in, named foreseen. + CredentialHeld CredentialState = "held" + // CredentialWouldMake is a credential none is held for: the next send makes it, and the composition uses a stand-in, + // named foreseen. It is also what a need with no entry is. + CredentialWouldMake CredentialState = "would-make" + // CredentialNotGiven is a credential only a person gives (novox/hq ADR 0337 clause 4) that nobody has given, or that was given + // under a key one end no longer has: the consumer still resolves, without it. Why is the controller's words for it. + CredentialNotGiven CredentialState = "not-given" + // CredentialRefused is a credential the send would refuse to make, for the reason Why gives in the controller's words: a + // value a person gave sealed to a key the machine no longer has, a machine with no sealing key. + CredentialRefused CredentialState = "refused" +) + +// Credential is the state of one pair credential, by the ends it joins: the provision, the consumer's node and module, the +// provider's node, and the local name where a module keeps several (inv.Secret's key). +type Credential struct { + Provision string + Consumer string + ConsumerModule string + Provider string + Local string + State CredentialState + // Sealed is the consumer's copy as it is sealed to the consumer's key; optional, and only the controller's adapter passes it. + Sealed string + // Why is the words of a CredentialNotGiven or CredentialRefused. + Why string +} + +// Run is the build an assignment names: the commit it is of and the build made from it (chain.Ref). +type Run struct { + Commit string + Build string +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/network.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/network.go new file mode 100644 index 00000000..1e66c516 --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/network.go @@ -0,0 +1,262 @@ +package compose + +import ( + "errors" + "fmt" + "slices" + "sort" + "strconv" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" +) + +// The private network: who is on it, where, and what they are called. Ported from mesh-controller +// cmd/mesh-controller/network.go at 6c2a8d62. + +// DefaultOverlayCIDR is the range the mesh allocates from when nothing says another. +const DefaultOverlayCIDR = "10.42.0.0/16" + +// composer is one composition over an Input: it reads Input, changes nothing in it, and keeps what the controller +// said on its standard error as lines to return. +type composer struct { + in Input + said []string + seen map[string]bool +} + +func newComposer(in Input) *composer { return &composer{in: in, seen: map[string]bool{}} } + +// say keeps a line the controller would have written to its log, once. +func (c *composer) say(format string, args ...any) { + line := fmt.Sprintf(format, args...) + if c.seen[line] { + return + } + c.seen[line] = true + c.said = append(c.said, line) +} + +// suffix is what the mesh's internal names end in: the Input's, and overlay.DefaultSuffix when it gives none. The package never calls +// overlay.Suffix or overlay.InternalName, which read the environment, so the assigner and the controller compose the same names +// from the same Input. +func (c *composer) suffix() string { + if v := strings.TrimPrefix(strings.TrimSpace(c.in.InternalSuffix), "."); v != "" { + return v + } + return overlay.DefaultSuffix +} + +// internalName is a node's name inside the mesh. +func (c *composer) internalName(node string) string { return node + "." + c.suffix() } + +// saidBeside is the lines already said and then every line the composer kept, once each, in the order they were found. +func (c *composer) saidBeside(lines []string) []string { + for _, l := range c.said { + if !slices.Contains(lines, l) { + lines = append(lines, l) + } + } + return lines +} + +// overlayRange is the range the mesh allocates node addresses from: the adopted tunnel's first (novox/hq ADR 0105), +// then the range genesis was told, then the default. +func (c *composer) overlayRange() string { + if a := c.in.Adopted; a != nil && a.Adopted { + return a.Tunnel.Range + } + if v := strings.TrimSpace(c.in.OverlayCIDR); v != "" { + return v + } + return DefaultOverlayCIDR +} + +// network builds the private network over the machines that resolved the module for it. A machine is on it because it +// was given the module, or because it is joining with a key and an address (novox/hq ADR 0169). +func (c *composer) network(on map[string]bool, refused map[string]string) (*overlay.Generator, error) { + tunnels := c.in.Tunnels + carried := c.in.CarriedPeers + isJoining := map[string]bool{} + for _, name := range c.in.Joining { + isJoining[name] = true + } + var nodes []overlay.Node + for _, node := range c.in.sorted() { + p := node.Place + if !on[node.Name] && !(isJoining[node.Name] && p.Key != "" && p.Address != "") { + continue + } + n := overlay.Node{ + Name: node.Name, Key: p.Key, Endpoint: p.Endpoint, + Site: p.Site, Hub: p.Hub, Address: p.Address, + } + if t, takes := tunnels[node.Name]; takes && t.NodeAdopted { + // Only an adopted node is told to take the found unit over. A hub whose placement disagrees with the tunnel is + // refused, not composed. + if wrong := disagrees(p, t.Tunnel); wrong != "" { + return nil, fmt.Errorf("%s takes over the tunnel on %s and its placement disagrees with it: %s. "+ + "Re-place it — `overlay place %s --hub --endpoint :%d …` — and push again; "+ + "nothing was composed", node.Name, t.Interface, wrong, node.Name, t.Port) + } + n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU} + } + if p.Hub { + for _, cp := range carried { + n.Carried = append(n.Carried, overlay.Carried{Key: cp.PublicKey, Address: cp.Address}) + } + } + nodes = append(nodes, n) + } + if len(nodes) == 0 { + // Nobody was given it: an empty network is a legitimate mesh, not a broken one. + return overlay.Empty(), nil + } + g, err := overlay.From(nodes, c.overlayRange(), "") + if err != nil && len(refused) > 0 { + // The network is missing something, and some machines could not be resolved at all: almost always the same fact. + var who []string + for name, why := range refused { + who = append(who, fmt.Sprintf(" %s: %s", name, why)) + } + sort.Strings(who) + return nil, fmt.Errorf("%w\n\nand %d node(s) could not be resolved at all, which is "+ + "probably why:\n%s", err, len(refused), strings.Join(who, "\n")) + } + return g, err +} + +// Graph is the whole mesh's network, for showing it: the nodes on it and each one's peers. +func Graph(in Input) ([]overlay.Node, overlay.Graph, error) { + c := newComposer(in) + on, refused, err := c.whoResolves(overlay.Requirement) + if err != nil { + return nil, nil, err + } + g, err := c.network(on, refused) + if err != nil { + return nil, nil, err + } + return g.Nodes(), g.Graph(), nil +} + +// whoResolves is the machines whose resolution answers a requirement, and why the others did not: by what a module +// provides, not by its name, resolved rather than read from the assignments. +func (c *composer) whoResolves(requirement string) (map[string]bool, map[string]string, error) { + on := map[string]bool{} + // Why a node could not be resolved, kept rather than raised: one broken node must not stop the rest being described. + refused := map[string]string{} + for _, n := range c.in.sorted() { + resolved, err := c.planFor(n.Name) + switch { + case unresolvable(err): + refused[n.Name] = err.Error() + continue + case err != nil: + // Not a node that does not resolve: a question that went unanswered. + return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err) + } + for _, m := range resolved.Plan.Modules { + for _, offered := range m.Offers() { + if offered == requirement { + on[n.Name] = true + } + } + } + } + return on, refused, nil +} + +// Generators is what every node's declaration is composed with, over the whole mesh: the private network's generator +// (one generator now; the names and a resolver's zone file are facts a module asks for in its manifest). +func Generators(in Input) (map[string]catalogue.Generator, error) { + c := newComposer(in) + return c.generators() +} + +func (c *composer) generators() (map[string]catalogue.Generator, error) { + on, refused, err := c.whoResolves(overlay.Addressing) + if err != nil { + return nil, err + } + net, err := c.network(on, refused) + if err != nil { + return nil, err + } + return map[string]catalogue.Generator{overlay.Name: net}, nil +} + +// disagrees says how a node's placement differs from the tunnel it takes over — its address not the tunnel's, its +// endpoint not on the tunnel's port — or nothing when both agree. +func disagrees(p Place, t Tunnel) string { + var wrong []string + want := t.Address + if i := strings.Index(want, "/"); i >= 0 { + want = want[:i] + } + if p.Address != want { + wrong = append(wrong, fmt.Sprintf("its address is %s and the tunnel's is %s", orNothing(p.Address), want)) + } + if p.Reachable() && portOfEndpoint(p.Endpoint) != strconv.Itoa(t.Port) { + wrong = append(wrong, fmt.Sprintf("its endpoint %s is not on the tunnel's port %d", p.Endpoint, t.Port)) + } + return strings.Join(wrong, "; ") +} + +func orNothing(s string) string { + if s == "" { + return "unset" + } + return s +} + +// portOfEndpoint is the port in host:port, or empty. +func portOfEndpoint(endpoint string) string { + if i := strings.LastIndex(endpoint, ":"); i >= 0 { + return endpoint[i+1:] + } + return "" +} + +// whereEveryoneIs is each machine's name on the private network, for the ones on it. Resolved without consulting the +// rest of the mesh (it is called from that path), and refused without the catalogue. +func (c *composer) whereEveryoneIs() (map[string]string, error) { + if c.in.Catalogue == nil { + return nil, errors.New("asked where everyone is without the catalogue, which cannot be answered") + } + out := map[string]string{} + for _, p := range c.onTheNetwork() { + out[p.Name] = c.internalName(p.Name) + } + return out, nil +} + +// onTheNetwork is every placed machine that resolves the private network: has an address AND runs what puts it there +// (novox/hq issue 079). A machine that does not resolve is not counted, and said. +func (c *composer) onTheNetwork() []Node { + var out []Node + for _, n := range c.in.sorted() { + if n.Place.Address == "" { + continue + } + if len(n.Assigned) == 0 { + continue + } + got, err := catalogue.Resolve(c.in.Catalogue, n.Assigned, + catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities}, + catalogue.World{Unchecked: true, Holdings: c.in.Holdings}) + if err != nil { + c.say("%s is not counted as on the network: it does not resolve: %v", n.Name, err) + continue + } + for _, m := range got.Modules { + for _, offered := range m.Offers() { + if offered == overlay.Requirement { + out = append(out, n) + } + } + } + } + return out +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/node.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/node.go new file mode 100644 index 00000000..858f77da --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/node.go @@ -0,0 +1,335 @@ +package compose + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/validate" +) + +// Declared is a declaration as a machine is sent it (the controller's sendable, sendable.go): its resources and, for an adopted +// node, its mode and which modules were taken on it (novox/hq ADR 0100), with what the composition says beside it and never puts on +// the wire. +// +// **Body is the only place the envelope is marshalled**, so the digest the mesh compares and the body validated are one. +type Declared struct { + Resources []map[string]any + // Sequence, Epoch and Generation are what the Input gave the node: zero is not sent at all. + Sequence int64 + Epoch uint64 + Generation int64 + // ReadsModules says the machine's node-engine reads a module per resource and a build per module (novox/hq issue 1145): only + // then are they sent. ModuleOf is the composer's owner of each resource id and BuildIDs the build each module carries. + ReadsModules bool + ModuleOf map[string]string + BuildIDs map[string]string + // Adoption is nil for a converged node, and then the body is byte for byte what it was before adoption existed. + Adoption *Adoption + // Filled is the path of every file this declaration hands to the provisioner, and Provisioner the account its client runs as + // (novox/hq ADR 0349): on the wire as `filled_files` and `provisioner_account`, both absent when nothing is handed. + Filled []string + Provisioner string + // LeftOut is every module of the machine's set left out because a stored setting cannot compose with its definition (novox/hq + // ADR 0163, rule 6), sorted; LeftOutWhy is why each was. + LeftOut []string + LeftOutWhy map[string]string + + // Not on the wire: + Received map[string]map[string][]catalogue.Contribution + Mesh []string + BusUsers string + // Modules are the modules this declaration names, sorted. + Modules []string + // LocalNetwork is every port admitted from the local network and what it resolved to (novox/hq ADR 0340). + LocalNetwork []catalogue.LocalNetworkPort + Withheld []catalogue.Overflow + Unbound []catalogue.Unbound + Unplaced []string + HandedOnly []string + // RequiredBy is, for each handed module only pulled in by another, the module that requires it (issue 765). + RequiredBy map[string]string + // Foreseen is every own secret composed with a stand-in, as `module/name`; StoodIn is everything the composition stood in for + // (an own secret, a grant's sealed copy, the certificate, the bus's user list); ForeseenPorts the ports the mesh may assign that + // the Input holds none for. A declaration that has any is never sent: what a send would make, named, never invented. + Foreseen []string + StoodIn []string + ForeseenPorts []string + // CoreBoxesWanted are the core credentials whose box the send must ask the vault for and keep. + CoreBoxesWanted []CoreBoxWanted + // Passes are the seats the mesh would pass on, for the caller to record, and Said the lines found while composing. + Passes Passes + Said []string +} + +// Adoption is what an adopted node is told about its mode. Taken is every module taken on it that it runs; Untaken is, for every +// module it runs that is not taken, the ids of every one of that module's resources: what the host keeps as found until the module +// is taken (ADR 0103). +type Adoption struct { + Taken []string `json:"taken"` + Untaken map[string][]string `json:"untaken,omitempty"` +} + +// Body is the declaration's bytes, as sent and as digested. +func (d Declared) Body() ([]byte, error) { + envelope := map[string]any{"declaration": 1, "resources": d.Resources} + if d.ReadsModules { + // The module of each resource the composer owns, and the build of each module (novox/hq issue 1145): to a machine that + // said it reads them, and no other. + named := make([]map[string]any, len(d.Resources)) + for n, r := range d.Resources { + module, owned := d.ModuleOf[fmt.Sprint(r["id"])] + if !owned { + named[n] = r + continue + } + with := make(map[string]any, len(r)+1) + for k, v := range r { + with[k] = v + } + with["module"] = module + named[n] = with + } + envelope["resources"] = named + if len(d.BuildIDs) > 0 { + envelope["builds"] = d.BuildIDs + } + } + if d.Adoption != nil { + envelope["adoption"] = d.Adoption + } + if d.Sequence > 0 { + envelope["sequence"] = d.Sequence + } + if d.Epoch > 0 { + envelope["epoch"] = d.Epoch + } + if len(d.LeftOut) > 0 { + envelope["left_out"] = d.LeftOut + } + if d.Generation > 0 { + envelope["generation"] = d.Generation + } + if len(d.Filled) > 0 { + envelope["filled_files"] = d.Filled + envelope["provisioner_account"] = d.Provisioner + } + // An empty declaration is deliberate: the node owns nothing the mesh put there (novox/hq issue 127), and the host refuses an + // empty body unless it is told the emptiness is meant. + if len(d.Resources) == 0 { + envelope["owns_nothing"] = true + } + return json.Marshal(envelope) +} + +// Refusals are the findings a check refuses a change for, beside the validator's problems: a consumer left out of the grants +// because its identity overflows the provision's bound, and a credential bound to another consumer (novox/hq ADR 0446 clause 1). +// Each names the consumer and the provision. +func (d Declared) Refusals() []string { + var out []string + for _, o := range d.Withheld { + out = append(out, o.String()) + } + for _, u := range d.Unbound { + out = append(out, u.String()) + } + sort.Strings(out) + return out +} + +// Compose composes one node's declaration (the issue calls it Node; the Input already has a type of that name) over an Input -- as a push would, without the sending (the controller's +// composedAndValidated in the Foreseeing mode) -- and runs the node-engine's own validator over its body. An error is that the +// node did not compose (a set that does not compose satisfies IsUnresolvable); problems are what the validator refuses, in the +// node-engine's words. It allocates, makes, keeps and writes nothing, and names what a send would do as foreseen. +func Compose(in Input, node string) (Declared, []string, error) { + return ComposeIn(in, node, Foreseeing) +} + +// ComposeIn is Compose in a chosen mode. +func ComposeIn(in Input, node string, mode Mode) (Declared, []string, error) { + c := newComposer(in) + d, err := c.declaration(node, mode) + if err != nil { + return Declared{}, nil, err + } + body, err := d.Body() + if err != nil { + return Declared{}, nil, err + } + return d, validate.Declaration(body), nil +} + +// declaration is the controller's declarationWith over the plain Input. +func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) { + resolved, err := c.planFor(nodeName) + if err != nil { + return Declared{}, err + } + gens, err := c.generators() + if err != nil { + return Declared{}, err + } + rendered, err := c.render(nodeName, mode) + if err != nil { + return Declared{}, err + } + with := rendered.Rendering + with.Generators = gens + plan := resolved.Plan + composed, err := plan.Compose(with) + if err != nil { + return Declared{}, err + } + // Which ports the filter admits from the local network, and from which networks (novox/hq ADR 0340 clause 8): the same rules the + // composition rendered, never sent. + rules, err := plan.Rules(with) + if err != nil { + return Declared{}, err + } + node, err := c.in.node(nodeName) + if err != nil { + return Declared{}, err + } + out := Declared{Resources: composed.Resources, Adoption: adoptionFor(plan, node, composed), + Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers, + LeftOut: sortedKeysOf(composed.LeftOut), LeftOutWhy: composed.LeftOut, + Withheld: rendered.Withheld, Unbound: rendered.Unbound, + Foreseen: composed.Foreseen, StoodIn: rendered.Foreseen, ForeseenPorts: rendered.ForeseenPorts, + Unplaced: composed.Unplaced, HandedOnly: composed.HandedOnly, Modules: namedModules(plan, composed.LeftOut), + Filled: composed.Filled, LocalNetwork: catalogue.LocalNetworkPorts(rules, with.LocalNetworks), + CoreBoxesWanted: rendered.CoreBoxesWanted, Said: rendered.Said, + Sequence: node.Sequence, Epoch: node.Epoch, Generation: node.Generation} + if len(composed.Filled) > 0 { + out.Provisioner = with.Provisioner + } + // A handed module only pulled in by another keeps what requires it, for its value of its own in the assignments mirror (issue 765). + for module := range composed.Plans { + if by := plan.RequiredBy(module); by != "" { + if out.RequiredBy == nil { + out.RequiredBy = map[string]string{} + } + out.RequiredBy[module] = by + } + } + // And the module of each resource and the build of each module, to a machine whose node-engine said it reads them (novox/hq issue + // 1145) and to no other. + if node.ReadsModules { + carried := c.carriedOn(node, plan, composed.LeftOut) + out.ReadsModules, out.ModuleOf, out.BuildIDs = true, composed.Owner, c.wireBuilds(carried, node.Runs) + } + _, passes, left, err := c.theRestOfTheMesh("") + if err != nil { + return Declared{}, err + } + out.Passes = passes + for _, l := range left { + out.Said = append(out.Said, l.String()) + } + out.Said = c.saidBeside(out.Said) + return out, nil +} + +// carriedOn is the build of each module a declaration for the node carries (plan.go carriedOn, in a question that sends nothing): the +// module's current build for each module in it, and for a module left out of it the build it was last sent, since the machine keeps +// that one -- or nothing, when that is not known. +func (c *composer) carriedOn(node Node, plan catalogue.Resolution, leftOut map[string]string) map[string]string { + before := node.SentBuilds + if !node.SentKnown { + before = nil + } + out := map[string]string{} + for _, m := range plan.Modules { + if _, left := leftOut[m.Module]; left { + if was, kept := before[m.Module]; kept { + out[m.Module] = was + } + continue + } + out[m.Module] = c.in.CurrentBuilds[m.Module] + // A module kept at the build the machine runs is carried at that build (plan.go carriedOn). + if kept, ok := node.KeptBuilds[m.Module]; ok && kept != "" { + out[m.Module] = kept + } + } + 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 (c *composer) wireBuilds(carried map[string]string, runs map[string]Run) map[string]string { + 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 + } + if build := c.in.BuildIDs[module][commit]; build != "" { + out[module] = build + } + } + return out +} + +// adoptionFor is the envelope computed from what was taken and who owns each resource: nil for a converged node. Every module the +// node runs that is not taken is untaken -- including one pulled in by another rather than assigned. +func adoptionFor(plan catalogue.Resolution, node Node, composed catalogue.Composed) *Adoption { + if !node.Adopted { + return nil + } + isTaken := map[string]bool{} + for _, m := range node.Taken { + isTaken[m] = true + } + out := &Adoption{Taken: []string{}} + runs := map[string]bool{} + for _, m := range plan.Modules { + runs[m.Module] = true + if isTaken[m.Module] { + out.Taken = append(out.Taken, m.Module) + } + } + sort.Strings(out.Taken) + for _, r := range composed.Resources { + id, _ := r["id"].(string) + module, owned := composed.Owner[id] + // Every kind, not only files and containers (novox/hq ADR 0103). What the mesh declares of its own is never held. + if !owned || !runs[module] || isTaken[module] || strings.HasPrefix(id, catalogue.AdoptionPrefix) { + continue + } + if out.Untaken == nil { + out.Untaken = map[string][]string{} + } + out.Untaken[module] = append(out.Untaken[module], id) + } + return out +} + +// sortedKeysOf is a map's keys, sorted: so what a declaration says it left out does not move for a reordering nobody made. +func sortedKeysOf(m map[string]string) []string { + if len(m) == 0 { + return nil + } + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} + +// namedModules are the modules a declaration names: its machine's set, less what was left out of it. +func namedModules(plan catalogue.Resolution, leftOut map[string]string) []string { + out := make([]string, 0, len(plan.Modules)) + for _, m := range plan.Modules { + if _, left := leftOut[m.Module]; !left { + out = append(out, m.Module) + } + } + sort.Strings(out) + return out +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/render.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/render.go new file mode 100644 index 00000000..a50a65ea --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/render.go @@ -0,0 +1,128 @@ +package compose + +import ( + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// Rendered is everything a node's declaration is composed with (plan.go renderingFor), over an Input: the place half (Places) +// and the credential half. Nothing was allocated, made, kept or written; what a send would do is returned for the caller to do. +type Rendered struct { + Rendering catalogue.Rendering + // ForeseenPorts are the ports the mesh may assign that the Input holds none for (":"). + ForeseenPorts []string + // Foreseen are the stand-ins the composition used for what the Input does not hold: an own secret the next send makes + // ("/"), a grant's sealed copy, the certificate and the bus's user list. + Foreseen []string + // CoreBoxesWanted are the core credentials whose box the send must ask the vault for and keep. + CoreBoxesWanted []CoreBoxWanted + // Withheld are the consumers the grants leave out because their identity overflows the provision's bound (novox/hq ADR 0225), + // and Unbound those whose credential from this node is on record and whose own resolution binds them elsewhere (issue + // 274). The controller says them; a check refuses a change that adds one (ADR 0446 clause 1). + Withheld []catalogue.Overflow + Unbound []catalogue.Unbound + // Said are the lines the controller says while composing (the bus users left out for want of a credential). + Said []string +} + +// Render composes the rendering of a node: the grants it must create, where it puts its ports, its own secrets, the vault's boxes, +// its certificate, the names of the mesh, the bus's user list and its membership. The refusals are the controller's, in its words; +// where more than one fault stands, the first reported may differ from the controller's, which interleaves the halves. +func Render(in Input, node string, mode Mode) (Rendered, error) { + return newComposer(in).render(node, mode) +} + +func (c *composer) render(nodeName string, mode Mode) (Rendered, error) { + resolved, err := c.planFor(nodeName) + if err != nil { + return Rendered{}, err + } + node, err := c.in.node(nodeName) + if err != nil { + return Rendered{}, err + } + plan := resolved.Plan + var out Rendered + foreseen := func(s string) { out.Foreseen = append(out.Foreseen, s) } + + grants, withheld, unbound, grantsForeseen, err := c.grantsFor(nodeName) + if err != nil { + return Rendered{}, err + } + placed, err := c.places(nodeName, resolved) + if err != nil { + return Rendered{}, err + } + r := placed.Rendering + out.ForeseenPorts = placed.ForeseenPorts + r.Grants, r.Withheld, r.Unbound = grants, withheld, unbound + out.Withheld, out.Unbound = withheld, unbound + for _, g := range grantsForeseen { + foreseen(g) + } + + // And each module's own secrets: a module handed to the provisioner has its secrets from the vault through the client. + handing := catalogue.Rendering{Hands: r.Hands, Provisioner: r.Provisioner} + needed, foreseenOwn, err := c.ownSecretsFor(node, plan, handing, mode) + if err != nil { + return Rendered{}, err + } + r.Needed = needed + if mode == Foreseeing || len(foreseenOwn) > 0 { + r.Foreseen = foreseenOwn + } + var names []string + for module, secrets := range foreseenOwn { + for name := range secrets { + names = append(names, module+"/"+name) + } + } + sort.Strings(names) + for _, n := range names { + foreseen(n) + } + + // The core's own credentials as the vault sealed them, for an engine that opens the vault's box. + if node.Stated && node.Contract >= VaultBoxContract { + sealed, notPlaced, wanted := c.coreBoxesFor(node, plan.Modules, handing) + r.VaultSealed, r.NotPlaced = sealed, notPlaced + out.CoreBoxesWanted = wanted + } + + // And a certificate for this machine's name inside the mesh, when anything on it asks. + for _, m := range plan.Modules { + if m.Certificate == nil { + continue + } + certificate, authority, stood, err := c.certificateFor(node) + if err != nil { + return Rendered{}, err + } + r.Certificate, r.Authority = certificate, authority + if stood { + foreseen("the certificate and the authority of " + nodeName) + } + break + } + + // The bus's user list, for the machine that runs the bus: names and hashes the Input carries, never passwords. + if holdsTheBus(plan.Modules) { + r.BusUsers = c.in.BusUsers + if r.BusUsers == "" { + r.BusUsers = ForeseenBusUsers + foreseen("the bus's user list") + } + if len(c.in.BusMissing) > 0 { + out.Said = append(out.Said, fmt.Sprintf("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", len(c.in.BusMissing), strings.Join(c.in.BusMissing, ", "))) + } + } + r.BusMembership = c.in.BusMemberships[nodeName] + out.Rendering = r + // And what the composition found on the way, which the controller writes to its log: a machine not counted as on the network. + out.Said = c.saidBeside(out.Said) + return out, nil +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/rendering.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/rendering.go new file mode 100644 index 00000000..468683d3 --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/rendering.go @@ -0,0 +1,532 @@ +package compose + +import ( + "errors" + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" +) + +// The contracts of the node-engine's statement that gate what a machine is sent, as the controller's link package names them +// (mesh-controller internal/link/protocol.go at 6c2a8d62). An older engine parses strictly and refuses a field it does not +// know, whole. +const ( + ReadinessContract = 2 // the engine reads a resource's `health` (novox/hq ADR 0240) + RootContract = 3 // the engine judges a user's declared `root` (ADR 0266) + BusyContract = 8 // the engine reads a container's `busy` (ADR 0335) + HandedContract = 11 // the engine writes a filled file on the provisioner client's word alone + VaultBoxContract = 12 // the engine opens the vault's box +) + +// The provisioner client's seat and own secret (the controller's broker.NodeProvisionerSeat and ProvisionerSecret). +const ( + NodeProvisionerSeat = "node-provisioner" + ProvisionerSecret = "provisioner-broker" +) + +// ErrPortTaken is the controller's refusal for a machine port held twice on one machine. +var ErrPortTaken = errors.New("that port is already held on this machine") + +// Placed is the place half of a rendering (plan.go renderingFor): where a machine puts what its modules need reachable, the +// seats and who holds them, the names of the mesh, the networks, accounts and the operator's export. It is a catalogue.Rendering +// with those fields set, and the ports it could not read named foreseen. Nothing was allocated. +type Placed struct { + Rendering catalogue.Rendering + // ForeseenPorts are the ports the mesh may assign that the Input holds none for, as ":": composed with the + // manifest's own number as the Reading mode composes them, and never invented. The controller's adapter allocates and + // composes again. + ForeseenPorts []string +} + +// Places composes the place half of the rendering for a node (the controller's renderingFor in the Reading and Foreseeing modes, +// without the credentials): over an Input, allocating nothing and changing nothing in it. The refusals are the controller's, +// in its words. +func Places(in Input, node string) (Placed, error) { + c := newComposer(in) + resolved, err := c.planFor(node) + if err != nil { + return Placed{}, err + } + return c.places(node, resolved) +} + +func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) { + var out Placed + node, err := c.in.node(nodeName) + if err != nil { + return out, err + } + plan, settings := resolved.Plan, resolved.Settings + r := &out.Rendering + + // Where this machine puts what each module needs reachable (novox/hq ADR 0038): what it was already given, and the ports + // it was given over those (ADR 0100). + given, err := c.portsFor(node, plan, settings, r, &out.ForeseenPorts) + if err != nil { + return out, err + } + + // Hands: whether the engine writes a filled file on the provisioner client's word alone; engines older than a contract are + // not sent the field they would refuse. + r.Hands = takesHanded(node) + r.Provisioner = provisionerAccount(plan) + r.ReadsHealth = node.Stated && node.Contract >= ReadinessContract + r.JudgesRoot = node.Stated && node.Contract >= RootContract + r.ReadsBusy = node.Stated && node.Contract >= BusyContract + + // And who else is on the private network, which is what a rule saying "from the mesh" resolves to. + r.Mesh = c.onThePrivateNetwork() + // The private network's range, offered to a module as ${machine:mesh-range}. + r.MeshRange = c.overlayRange() + + // The artifact store as this node reaches it now, with what the mesh has built. + store, err := c.artifactStoreAddress(nodeName) + if err != nil { + return out, err + } + r.ArtifactStore = store + r.Built = make(map[string]bool, len(c.in.Built)) + for _, repository := range c.in.Built { + r.Built[repository] = true + } + + // And every machine's name, so a container can reach one. + r.Names = c.namesInTheMesh() + + // Each machine's operator account, so an ssh Host block can name the login for every node (novox/hq to-be 29). + r.Accounts = map[string]string{} + for _, n := range c.in.sorted() { + if n.Account != "" { + r.Accounts[n.Name] = n.Account + } + } + + // The roster is the machines and nothing else (novox/hq ADR 0191). + r.Machines = make(map[string]string, len(r.Names)) + for name, at := range r.Names { + r.Machines[name] = at + } + + // And every zone a module in the mesh answers itself (novox/hq ADR 0199). + zones, err := c.zonesInTheMesh() + if err != nil { + return out, err + } + r.Zones = zones + + // And who holds each replicated seat, where (novox/hq ADR 0223). + r.Holders = c.replicatedHolders() + + r.Suffix, r.TunnelInterface = c.suffix(), overlay.Interface + + // And, for a module that keeps them, every operator-sealed secret in the mesh (novox/hq ADR 0085, amended). + for _, m := range plan.Modules { + if m.Keeps == "" { + continue + } + r.Kept = c.in.OperatorExport + break + } + + // Whether this node is adopted (novox/hq ADR 0100), and which modules were taken there. + r.Adopted = node.Adopted + var taken map[string]bool + if node.Adopted { + taken = map[string]bool{} + for _, m := range node.Taken { + taken[m] = true + } + } + r.Taken = taken + + // Where this node put the foundation's servers (novox/hq 04-ISSUES/102). + seats, err := c.seatsOn(node, plan.Modules, node.Adopted, taken) + if err != nil { + return out, err + } + r.Seats = seats + + // Which of this machine's links face outside (novox/hq ADR 0140), and which of its networks count as its local network + // (ADR 0340). + r.OutwardLinks = node.OutwardLinks + r.LocalNetworks = catalogue.CountedLocalNetworks(localNetworksOf(node)) + + // Where this machine reaches each mesh seat's holder, for ${seat::reach} (novox/hq ADR 0222). + if store != "" { + r.SeatReach = map[string]string{"mesh-artifact-store": store} + } + r.Settings = settings + r.Given = given + return out, nil +} + +// takesHanded says whether a machine's newest statement is of an engine that writes a filled file on the provisioner client's +// word alone. +func takesHanded(n Node) bool { return n.Stated && n.Contract >= HandedContract } + +// provisionerAccount is the account the provisioner's client runs as on this machine (novox/hq ADR 0349 clause 4): the user +// the process of the module running it runs as, empty where no such module is on the machine. +func provisionerAccount(plan catalogue.Resolution) string { + for _, m := range plan.Modules { + if !runsTheProvisionerClient(m) { + continue + } + for _, r := range m.Resources { + if r["type"] == "process" { + if user, _ := r["user"].(string); user != "" && user != "root" { + return user + } + } + } + } + return "" +} + +// runsTheProvisionerClient says a module runs its machine's provisioner client: it claims the seat and declares the own +// secret that account is delivered in. +func runsTheProvisionerClient(m catalogue.Manifest) bool { + _, reads := m.OwnSecrets[ProvisionerSecret] + return reads && m.ClaimsSeat(NodeProvisionerSeat) +} + +// portsFor is where a node puts what each module needs reachable, in the Reading mode: the ports it was given win, then the +// ones the mesh assigned, and a port the Input holds none for is composed as the manifest says and named foreseen. One holder +// per machine port across the node's modules: settings written before this was refused are refused here. +func (c *composer) portsFor(node Node, plan catalogue.Resolution, settings catalogue.SettingsBy, r *catalogue.Rendering, + foreseen *[]string) (map[string]map[int]int, error) { + already := node.Ports + + given := map[string]map[int]int{} + for _, m := range plan.Modules { + g, err := catalogue.GivenPorts(m, settings[m.Module]) + if err != nil { + // A given port its definition no longer publishes: the module is left out of the declaration, by name, when it is + // composed (novox/hq ADR 0163, rule 6) -- never the machine refused here for it. + continue + } + if g != nil { + given[m.Module] = g + } + } + holders := map[int]string{} + for _, m := range plan.Modules { + for _, at := range given[m.Module] { + if other, twice := holders[at]; twice && other != m.Module { + return nil, fmt.Errorf("%w: %s and %s are both given machine port %d on %s", ErrPortTaken, other, m.Module, + at, node.Name) + } + holders[at] = m.Module + } + } + + ports := map[string]map[int]int{} + for _, m := range plan.Modules { + for _, l := range m.Listens { + if at, isGiven := given[m.Module][l.Port]; isGiven { + if ports[m.Module] == nil { + ports[m.Module] = map[int]int{} + } + ports[m.Module][l.Port] = at + continue + } + // Only a port the module actually publishes is the mesh's to move. Recorded either way, because this map means where + // this module's port is on this machine. + where, mayAssign := m.MachineSide(l.Port) + if mayAssign { + // Whatever was chosen last time, and nothing if there was no last time. + if at, known := already[m.Module][l.Port]; known { + where = at + } else { + *foreseen = append(*foreseen, fmt.Sprintf("%s:%d", m.Module, l.Port)) + } + } + if ports[m.Module] == nil { + ports[m.Module] = map[int]int{} + } + ports[m.Module][l.Port] = where + } + } + r.Ports = ports + return given, nil +} + +// onThePrivateNetwork is every node's address on the private network, sorted: the same set the names mean by it, so a rule +// saying "from the mesh" admits exactly the machines the mesh names. A node with no address is left out, not rendered as an +// empty source. +func (c *composer) onThePrivateNetwork() []string { + var out []string + for _, p := range c.onTheNetwork() { + if strings.TrimSpace(p.Place.Address) != "" { + out = append(out, p.Place.Address) + } + } + sort.Strings(out) + return out +} + +// namesInTheMesh is every machine's internal name and the address behind it, for every machine on the private network, and +// the carried peers the operator has named (novox/hq issue 112). +func (c *composer) namesInTheMesh() map[string]string { + out := map[string]string{} + for _, p := range c.onTheNetwork() { + out[c.internalName(p.Name)] = p.Place.Address + } + for _, p := range c.in.CarriedPeers { + if p.Named == "" || p.EnrolledAs != "" { + continue + } + if _, taken := out[c.internalName(p.Named)]; taken { + continue // a node of the mesh owns the name; the stale statement loses + } + out[c.internalName(p.Named)] = p.Address + } + return out +} + +// artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the port that machine put it on. +func (c *composer) artifactStoreOnNetwork(on map[string]bool) (node, port string, found bool, err error) { + providers := map[string]catalogue.Manifest{} + for name, m := range c.in.Catalogue { + if _, offers := m.Serves[catalogue.ArtifactStoreProvision]; offers { + providers[name] = m + } + } + if len(providers) == 0 { + return "", "", false, nil + } + // In a stated order, so two machines offering it would always answer the same one. + machines := make([]string, 0, len(on)) + for machine := range on { + machines = append(machines, machine) + } + sort.Strings(machines) + for _, machine := range machines { + n, err := c.in.node(machine) + if err != nil { + return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err) + } + for _, a := range n.Assigned { + m, offers := providers[a] + if !offers { + continue + } + serves, _, err := c.servedOnNode(machine, m, catalogue.ArtifactStoreProvision) + if err != nil { + return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err) + } + if p, ok := serves["port"]; ok { + return machine, fmt.Sprintf("%v", p), true, nil + } + } + } + return "", "", false, nil +} + +// artifactStoreAddress is the artifact store as `forNode` reaches it: `.internal:` over the private network, or +// `127.0.0.1:` for the node that holds the store itself when nothing is on the network yet, and empty for any other +// (novox/hq 04-ISSUES/102). +func (c *composer) artifactStoreAddress(forNode string) (string, error) { + onNetwork, err := c.whereEveryoneIs() + if err != nil { + return "", err + } + on := map[string]bool{} + for name := range onNetwork { + on[name] = true + } + node, port, found, err := c.artifactStoreOnNetwork(on) + if err != nil { + return "", err + } + if found { + return c.internalName(node) + ":" + port, nil + } + all := map[string]bool{} + for _, n := range c.in.Nodes { + all[n.Name] = true + } + holder, port, found, err := c.artifactStoreOnNetwork(all) + if err != nil || !found || holder != forNode { + return "", err + } + return "127.0.0.1:" + port, nil +} + +// zonesInTheMesh is every zone a module in the mesh declares, where the mesh placed it (novox/hq ADR 0199): the zone settled +// from that node's settings, the node's private address, the port the answering listen is published on there. A node whose set +// does not compose declares nothing and is passed over; what the mesh refuses about the zones together is refused, by name. +func (c *composer) zonesInTheMesh() ([]catalogue.ZoneAt, error) { + address := map[string]string{} + for _, n := range c.in.sorted() { + if strings.TrimSpace(n.Place.Address) != "" { + address[n.Name] = n.Place.Address + } + } + var zones []catalogue.ZoneAt + var public []string + for _, n := range c.in.sorted() { + resolved, err := c.planFor(n.Name) + switch { + case unresolvable(err): + continue + case err != nil: + return nil, fmt.Errorf("the zones %s answers cannot be read: %w", n.Name, err) + } + plan := resolved.Plan + if plan.PublicDomain != "" { + public = append(public, plan.PublicDomain) + } + for _, m := range plan.Modules { + if m.Zone == nil { + continue + } + at := address[n.Name] + if at == "" { + // Not on the private network yet: nothing could reach its answerer. + continue + } + published, layers, err := c.portsGivenOn(n.Name, m) + if err != nil { + return nil, fmt.Errorf("the zone %s declares on %s cannot be read: %w", m.Module, n.Name, err) + } + z, err := catalogue.ZoneOn(m, layers, published, n.Name, at) + if err != nil { + return nil, err + } + zones = append(zones, *z) + } + } + if problems := catalogue.ZonesProblems(zones, c.suffix(), public); len(problems) > 0 { + return nil, fmt.Errorf("the mesh's zones cannot be forwarded:\n - %s", strings.Join(problems, "\n - ")) + } + return zones, nil +} + +// replicatedHolders is, for each replicated mesh seat, every machine on the private network holding it, by internal name at its +// private address (novox/hq ADR 0223): the holders on record, and only the sole claimant when there are none. A holder off +// the private network is left out. +func (c *composer) replicatedHolders() map[string]map[string]string { + var replicated []catalogue.Seat + for _, s := range catalogue.Seats() { + if s.Replicated && s.Scope == catalogue.ScopeMesh { + replicated = append(replicated, s) + } + } + if len(replicated) == 0 { + return nil + } + address := map[string]string{} + for _, p := range c.onTheNetwork() { + address[p.Name] = p.Place.Address + } + // Every module of the catalogue with the nodes it is assigned on, sorted, as the controller's Catalogued answers. + on := map[string][]string{} + for _, n := range c.in.sorted() { + for _, module := range n.Assigned { + on[module] = append(on[module], n.Name) + } + } + names := make([]string, 0, len(c.in.Catalogue)) + for name := range c.in.Catalogue { + names = append(names, name) + } + sort.Strings(names) + out := map[string]map[string]string{} + for _, seat := range replicated { + var nodes []string + for _, h := range c.in.Holdings { + if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name && h.Scope == seat.Scope { + nodes = append(nodes, h.Node) + } + } + if len(nodes) == 0 { + var derived []string + for _, name := range names { + for _, cl := range c.in.Catalogue[name].Claims { + if cs, ok := catalogue.SeatNamed(cl.Name); ok && cs.Name == seat.Name && cl.At() == seat.Scope { + derived = append(derived, on[name]...) + } + } + } + if len(derived) == 1 { + nodes = derived + } + } + at := map[string]string{} + for _, n := range nodes { + if address[n] != "" { + at[c.internalName(n)] = address[n] + } + } + out[seat.Name] = at + } + return out +} + +// 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; a holder in this node's set wins over one that is not. +func (c *composer) seatsOn(node Node, 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(c.in.Catalogue)) + for name := range c.in.Catalogue { + names = append(names, name) + } + sort.Strings(names) + seats := map[string]map[int]int{} + for _, name := range names { + m := c.in.Catalogue[name] + var claims []string + for _, cl := range m.Claims { + if cl.At() == catalogue.ScopeMesh { + claims = append(claims, cl.Name) + } + } + if len(claims) == 0 { + continue + } + // Only a port the node was given or the mesh assigned, never the manifest's own number. And on an adopted node a holder + // assigned but not yet taken is the found container, so its mesh-assigned ports do not count there; a given port does. + ports, _, err := c.portsGivenOn(node.Name, m) + if err != nil { + return nil, err + } + if adopted && !taken[name] { + layers := node.Settings[m.Module] + 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 +} + +// localNetworksOf is a machine's attached networks judged (novox/hq ADR 0340): which count as its local network, from what it +// last said of itself and the links it said face outside it. A machine that has said none judges to nothing. +func localNetworksOf(n Node) []catalogue.LocalNetwork { + if !n.NetworksSaid { + return nil + } + return catalogue.JudgeLocalNetworks(n.Networks, n.OutwardLinks, overlay.Interface) +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/resolve.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/resolve.go new file mode 100644 index 00000000..d2fffd8a --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/resolve.go @@ -0,0 +1,280 @@ +package compose + +import ( + "fmt" + "slices" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// LicenceProvision is what a module requires in order to be given a licence (the controller's licences.Provision). +const LicenceProvision = "model-access" + +// Resolved is what one node should run, from what was assigned to it, with the settings of everything that resolved. +type Resolved struct { + Plan catalogue.Resolution + Settings catalogue.SettingsBy + // Foreseen are the needs composed with a stand-in for a credential the next send makes (or holds with no value given + // here), as " on needs from ". Nothing was made. + Foreseen []string + // NotGiven are the words of each credential only a person gives that nobody has: the consumer still resolves without it + // (novox/hq ADR 0337 clause 4). + NotGiven []string +} + +// Resolve works out everything a node should run, from what was assigned to it (the controller's planFor, plan.go): the +// resolution over the rest of the mesh, the credential of each thing the node takes from elsewhere, and the settings of +// every module that resolved. It makes and writes nothing: a credential not held is composed with a stand-in and named +// foreseen, one only a person gives and nobody has is left out and said, one the send would refuse is refused with the +// controller's words. A failure to compose the node's own modules is an error that satisfies IsUnresolvable, told apart +// from an unreadable Input (novox/hq 04-ISSUES/152). +func Resolve(in Input, node string) (Resolved, error) { + return newComposer(in).planFor(node) +} + +// IsUnresolvable reports whether err is a node's own set failing to compose, rather than the mesh being unable to answer. +func IsUnresolvable(err error) bool { return unresolvable(err) } + +// planFor is the controller's planFor. +func (c *composer) planFor(nodeName string) (Resolved, error) { + resolved, err := c.resolution(nodeName) + if err != nil { + return Resolved{}, err + } + node, err := c.in.node(nodeName) + if err != nil { + return Resolved{}, err + } + shelf := c.shelfFor(node) + var out Resolved + + // The credential for each thing this node takes from elsewhere. + for i, n := range resolved.Needs { + // Issued for this holder alone and kept by the vault (issue 757). + resolved.Needs[i].IssuedToHolder = issuedToHolder(shelf, n) + resolved.Needs[i].ProviderMoved = providerMoved(shelf, n) + if n.ByRecord { + // Answered by something the mesh holds, so there is no pair-wise secret between two machines. + licence, _ := c.licence(n.From) + if licence.Keys == nil { + // The keys are not given: composed with a stand-in and named, not failed for a value only a send reads. + resolved.Needs[i].Sealed = catalogue.ForeseenSealed + out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From)) + } else { + // Given, and none for this holder: the mesh holds none. + resolved.Needs[i].Sealed = licence.Keys[Holder{Node: nodeName, Module: n.For}] + } + // If this holder is the licence's manager, hand it the manager node's PUBLIC sealing key in its bound facts + // (novox/hq ADR 0050). + if pub := c.managerPublicKeyFor(licence, nodeName, n.For); pub != "" { + serves := map[string]any{} + for k, v := range resolved.Needs[i].Serves { + serves[k] = v + } + serves["manager_public_key"] = pub + resolved.Needs[i].Serves = serves + // The manager holder: its empty pre-adoption refresh token is a waiting state, not a missing consumer key. + resolved.Needs[i].Manager = true + } + continue + } + cred, held := c.credential(n, nodeName) + _, outside := issuedOutsideBy(shelf, n) + switch { + case cred.State == CredentialRefused: + // Said rather than skipped. A machine that resolves cleanly and receives no credential is one that will fail to + // authenticate at some later, less obvious moment. + return Resolved{}, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %s", + n.For, nodeName, n.Name, n.From, cred.Why) + case cred.State == CredentialNotGiven, outside && !(held && cred.State == CredentialHeld): + // Not given, and the rest of the consumer runs (ADR 0337 clause 4): no credential file, so the module can tell + // it was not given; nothing the mesh made stands in for it. + why := cred.Why + if why == "" { + why = "nobody gave it" + } + out.NotGiven = append(out.NotGiven, fmt.Sprintf("%s on %s holds no %s from %s: %s", n.For, nodeName, n.Name, n.From, why)) + continue + case cred.State == CredentialHeld && cred.Sealed != "": + resolved.Needs[i].Sealed = cred.Sealed + default: + // Held with no value here, or none held: the next send makes it (or reads it), and a stand-in is composed. + resolved.Needs[i].Sealed = catalogue.ForeseenSealed + out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From)) + } + } + + // Settings for everything that resolved, including modules nobody assigned directly: a requirement pulled in by + // something else is still configurable. + settings := catalogue.SettingsBy{} + for _, m := range resolved.Modules { + layers := node.Settings[m.Module] + if len(layers) == 0 { + continue + } + settings[m.Module] = append([]catalogue.Layer(nil), layers...) + } + out.Plan, out.Settings = resolved, settings + return out, nil +} + +// credential is how the pair credential of one need stands in the Input, and whether the Input names it at all. +func (c *composer) credential(n catalogue.Needed, consumer string) (Credential, bool) { + for _, cr := range c.in.Credentials { + if cr.Provision == n.Name && cr.Consumer == consumer && cr.ConsumerModule == n.For && cr.Provider == n.From && cr.Local == n.Local { + return cr, true + } + } + return Credential{State: CredentialWouldMake}, false +} + +// licence is the licence of that name. +func (c *composer) licence(name string) (Licence, bool) { + for _, l := range c.in.Licences { + if l.Name == name { + return l, true + } + } + return Licence{}, false +} + +// managerPublicKeyFor is the manager node's public sealing key, but only when (node, module) is the licence's manager holder +// and empty otherwise (plan.go managerPublicKeyFor): 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), and to no consumer. +func (c *composer) managerPublicKeyFor(l Licence, node, module string) string { + if l.Manager == (Holder{}) || node != l.Manager.Node || module != l.Manager.Module { + return "" + } + n, err := c.in.node(node) + if err != nil { + return "" + } + return n.SealingKey +} + +// issuedOutsideBy is whether a need's provision is offered by its provider with a credential issued outside the mesh +// (novox/hq ADR 0337), and that provider module: the one answering it when known, else any that offers it so. +func issuedOutsideBy(shelf map[string]catalogue.Manifest, n catalogue.Needed) (string, bool) { + if n.Module != "" { + if m, known := shelf[n.Module]; known { + return n.Module, m.IssuedOutsideOf(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). +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 (issue 793). +func movedProvider(m catalogue.Manifest) bool { + return m.Moved || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == "mesh-vault" }) +} + +// resolution works out what one node should run, from what was assigned to it: the controller's planFor up to and +// including the resolution (plan.go planFor), and no further. A failure to compose the node's own modules is wrapped as +// notResolvable; every other failure is returned as it is. +func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) { + node, err := c.in.node(nodeName) + if err != nil { + return catalogue.Resolution{}, err + } + shelf := c.shelfFor(node) + world, _, _, err := c.theRestOfTheMesh(nodeName) + if err != nil { + return catalogue.Resolution{}, err + } + world.Pinned = node.Pins + // Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232). + world.Bound = node.Bound + + onNetwork, err := c.whereEveryoneIs() + if err != nil { + return catalogue.Resolution{}, err + } + + // What this mesh can answer with a record rather than a machine, and which record each of this node's modules was + // put on. + world.Licences, world.Using = c.licencesFor(nodeName) + + resolved, err := catalogue.Resolve(shelf, node.Assigned, + catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities, + At: onNetwork[nodeName], PublicDomain: node.PublicDomain, + Account: node.Account, AccountHome: node.AccountHome, + AgentAccount: node.AgentAccount, AgentAccountHome: node.AgentAccountHome}, world) + if err != nil { + // The node's own set does not compose: the only failure a mesh-wide gatherer may pass over. + return catalogue.Resolution{}, notResolvable{err} + } + return resolved, nil +} + +// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the +// build it runs put in place of the current one (plan.go keepRecorded). A copy: the Input's catalogue is untouched. Only the node's own planFor and +// resolution use it; the rest of the mesh and who is on the network are read with the current manifests, as the controller reads them +// before it keeps a recorded module. +func (c *composer) shelfFor(node Node) map[string]catalogue.Manifest { + if len(node.Kept) == 0 { + return c.in.Catalogue + } + shelf := make(map[string]catalogue.Manifest, len(c.in.Catalogue)+len(node.Kept)) + for name, m := range c.in.Catalogue { + shelf[name] = m + } + for name, m := range node.Kept { + shelf[name] = m + } + return shelf +} + +// 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 not an error. +func (c *composer) licencesFor(node string) (map[string][]catalogue.Record, map[string]map[string]catalogue.Record) { + all := c.in.Licences + if len(all) == 0 { + return 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[LicenceProvision] = append(offered[LicenceProvision], record) + byName[one.Name] = record + } + using := map[string]map[string]catalogue.Record{} + for _, one := range all { + for _, h := range one.Holders { + if h.Node != node { + continue + } + if using[h.Module] == nil { + using[h.Module] = map[string]catalogue.Record{} + } + using[h.Module][LicenceProvision] = byName[one.Name] + } + } + return offered, using +} diff --git a/vendor/git.novox.be/novox/mesh-sdk/go/compose/rest.go b/vendor/git.novox.be/novox/mesh-sdk/go/compose/rest.go new file mode 100644 index 00000000..f7509e2f --- /dev/null +++ b/vendor/git.novox.be/novox/mesh-sdk/go/compose/rest.go @@ -0,0 +1,233 @@ +package compose + +import ( + "errors" + "fmt" + "sort" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// notResolvable marks the one failure in a node's resolution that is a statement about the node: its assigned modules +// do not compose. Every other failure means the mesh could not be asked. Whoever gathers across the mesh skips a node +// that cannot resolve and never a node that could not be read (novox/hq 04-ISSUES/152). +type notResolvable struct{ err error } + +func (n notResolvable) Error() string { return n.err.Error() } +func (n notResolvable) Unwrap() error { return n.err } + +// unresolvable reports whether err is a node's own set failing to compose. +func unresolvable(err error) bool { + var n notResolvable + return errors.As(err, &n) +} + +// Passes is what a composition found that the mesh must record, and did not: each seat held once whose holder on record +// is assigned nowhere that exactly one claimant takes over (novox/hq ADR 0449 clause 4), and the lines that say what was +// found. The caller records a pass and writes its hand act; this package does neither. +type Passes struct { + // Passes are the seats to pass, in the order they were found. + Passes []catalogue.SeatPass + // Said are the lines the finding says (a seat two claimants could take, one nothing can). + Said []string +} + +// LeftOut is a node that does not resolve in the view of the rest of the mesh, with why: it offers nothing and holds +// nothing as far as every other machine's plan can tell (novox/hq issue 188). +type LeftOut struct { + Node string + Err error +} + +// String is the line the controller says of it. +func (l LeftOut) String() string { + return fmt.Sprintf("%s is left out of the rest of the mesh: it does not resolve: %v", l.Node, l.Err) +} + +// TheRestOfTheMesh is what every other node holds and offers, as the controller works it out (plan.go theRestOfTheMesh): +// two passes over the others, because what a node offers needs it resolved and resolving it may need what the mesh +// offers. The first takes brokered requirements on trust and answers only what each node offers; the second answers +// everything with that in hand. Nothing is declared from the first. +// +// It writes nothing. A seat the mesh passes on is returned in Passes for the caller to record; the World carries the +// holdings as they stand after the passes, and Input's own are unchanged. A node left out is returned with its reason, in +// the order the controller said them (the first pass's, then the second's when no node is excluded). +func TheRestOfTheMesh(in Input, exclude string) (catalogue.World, Passes, []LeftOut, error) { + c := newComposer(in) + return c.theRestOfTheMesh(exclude) +} + +func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, []LeftOut, error) { + shelf := c.in.Catalogue + holdings := append([]catalogue.Held(nil), c.in.Holdings...) + nodes := c.in.sorted() + + onNetwork, err := c.whereEveryoneIs() + if err != nil { + return catalogue.World{}, Passes{}, nil, err + } + + type candidate struct { + node catalogue.Node + assigned []string + pins map[string]catalogue.Chosen + } + var others []candidate + // Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4): a seat + // held once whose holder on record is assigned on no machine passes to its only claimant here. + everyAssigned := map[string][]string{} + for _, n := range nodes { + if len(n.Assigned) > 0 { + everyAssigned[n.Name] = n.Assigned + } + } + var passes Passes + holdings, passes = passSeatsOn(holdings, assignmentsOf(shelf, everyAssigned)) + for _, n := range nodes { + if n.Name == exclude { + continue + } + theirs := everyAssigned[n.Name] + if len(theirs) == 0 { + continue + } + others = append(others, candidate{ + node: catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities, + At: onNetwork[n.Name], PublicDomain: n.PublicDomain}, + assigned: theirs, pins: n.Pins}) + } + + var left []LeftOut + offered := map[string][]catalogue.Provider{} + var firstHeld []catalogue.Held + for _, o := range others { + got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings}) + if err != nil { + // Said, not skipped: a machine dropped here offers nothing and holds nothing. + left = append(left, LeftOut{Node: o.node.Name, Err: err}) + continue + } + firstHeld = append(firstHeld, got.Claims...) + for _, m := range got.Modules { + for _, name := range m.OffersAt(catalogue.ScopeMesh) { + // What that module says a consumer needs to know, with that node's settings on it. + serves, layers, err := c.servedOnNode(o.node.Name, m, name) + if err != nil { + return catalogue.World{}, Passes{}, nil, err + } + // And its own composed names, as that node composes them (novox/hq ADR 0346). + serves, leftOut, err := got.ServedOwnNames(m, name, serves, catalogue.WithDefaults(m, layers)) + if err != nil { + return catalogue.World{}, Passes{}, nil, err + } + offered[name] = append(offered[name], catalogue.Provider{ + Node: o.node.Name, At: o.node.At, Serves: serves, Module: m.Module, LeftOut: leftOut}) + } + } + } + for k := range offered { + sort.Slice(offered[k], func(i, j int) bool { + return offered[k][i].Node < offered[k][j].Node + }) + } + + // The second pass is given the first pass's holdings (novox/hq ADR 0110). + world := catalogue.World{Offered: offered, Held: firstHeld, Holdings: holdings} + var held []catalogue.Held + for _, o := range others { + // Each machine is resolved with its own pins, as its plan is. + theirs := world + theirs.Pinned = o.pins + got, err := catalogue.Resolve(shelf, o.assigned, o.node, theirs) + if err != nil { + // Said only for the whole-mesh view: with one machine excluded, the view is partial by design. + if exclude == "" { + left = append(left, LeftOut{Node: o.node.Name, Err: err}) + } + continue + } + held = append(held, got.Claims...) + } + world.Held = held + return world, passes, left, nil +} + +// passSeatsOn finds, in the composition that finds it, the new holder of each seat held once whose holder on record is +// assigned nowhere and which has exactly one claimant able to hold it (novox/hq ADR 0449 clause 4), and answers the +// holdings as they are after it, with the passes. Nothing is recorded. +func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) ([]catalogue.Held, Passes) { + passes, said := catalogue.SeatsToPassOn(holdings, assignments) + out := Passes{Said: said} + for _, p := range passes { + out.Passes = append(out.Passes, p) + var next []catalogue.Held + for _, h := range holdings { + if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == p.Seat.Name { + continue + } + next = append(next, h) + } + p.To.Site = p.From.Site + holdings = append(next, p.To) + } + return holdings, out +} + +// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. A module assigned and +// missing from the shelf is still assigned: an assignment with no claims, so its holder is not taken for gone. +func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment { + var out []catalogue.Assignment + names := make([]string, 0, len(assigned)) + for node := range assigned { + names = append(names, node) + } + sort.Strings(names) + for _, node := range names { + for _, module := range assigned[node] { + m, ok := shelf[module] + if !ok { + m = catalogue.Manifest{Module: module} + } + out = append(out, catalogue.Assignment{Node: node, Manifest: m}) + } + } + return out +} + +// 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 over the one the mesh assigned over the manifest's own, settled with the node's settings +// layers. The one derivation for every reader of a provider's address. +func (c *composer) servedOnNode(node string, m catalogue.Manifest, provision string) (map[string]any, []catalogue.Layer, error) { + ports, layers, err := c.portsGivenOn(node, m) + if err != nil { + return nil, nil, err + } + serves := catalogue.ServedOn(m, provision, ports) + if len(serves) > 0 { + serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers)) + if err != nil { + return nil, nil, err + } + } + return serves, layers, nil +} + +// portsGivenOn is where a node puts a module's ports — assigned, then given over them — and the node's settings layers +// for the module. +func (c *composer) portsGivenOn(node string, m catalogue.Manifest) (map[int]int, []catalogue.Layer, error) { + n, err := c.in.node(node) + if err != nil { + return nil, nil, err + } + ports := map[int]int{} + for wanted, at := range n.Ports[m.Module] { + ports[wanted] = at + } + layers := n.Settings[m.Module] + if given, err := catalogue.GivenPorts(m, layers); err == nil { + for wanted, at := range given { + ports[wanted] = at + } + } + return ports, layers, nil +} diff --git a/vendor/github.com/novox/mesh-host/internal/declaration/busy.go b/vendor/github.com/novox/mesh-host/internal/declaration/busy.go deleted file mode 100644 index 3b734852..00000000 --- a/vendor/github.com/novox/mesh-host/internal/declaration/busy.go +++ /dev/null @@ -1,87 +0,0 @@ -package declaration - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "time" -) - -// Busy is how a long-running container says it holds work that would be lost if it were stopped now -// (novox/hq ADR 0335 §1): a command run inside it, by its shell, as an exec health check is, that exits 0 -// while it is busy and non-zero when it is idle. Being asked is also the container's notice that it is -// about to be replaced: from the first ask it takes no new work, so its answer ends once the work in hand -// ends. -// -// **Not part of what the container is.** Busy is not in the container's spec: a changed command changes -// when the container is replaced, never whether — a container is not recreated for its busy command alone. -type Busy struct { - Command string `json:"command"` - // Timeout is how long one ask may take; empty is BusyTimeoutDefault. - Timeout string `json:"timeout,omitempty"` -} - -// The bounds of an ask (novox/hq ADR 0335). -const ( - BusyTimeoutDefault = 10 * time.Second - // BusyTimeoutCeiling is the longest one ask may take: an ask runs inside an apply, and an apply waits - // for nothing longer than a look. - BusyTimeoutCeiling = time.Minute - // BusyBound is how long a container that says busy is left running after the first apply that would - // have replaced it (ADR 0335 §3). The node-engine's, not the module's: no module holds its own - // replacement for ever. - BusyBound = 15 * time.Minute -) - -// UnmarshalJSON reads a busy strictly, as everything a declaration carries is read. -func (b *Busy) UnmarshalJSON(raw []byte) error { - type plain Busy - var p plain - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&p); err != nil { - return fmt.Errorf("busy: %w", err) - } - *b = Busy(p) - return nil -} - -// Within is how long one ask may take. Validated on arrival, so an unreadable timeout is the default. -func (b *Busy) Within() time.Duration { - if d, err := time.ParseDuration(b.Timeout); err == nil && d > 0 { - return d - } - return BusyTimeoutDefault -} - -// problems holds a container's busy to its shape: a one-line command, a timeout within its bounds, and only -// on a container that stays up. -func (b *Busy) problems(where string, longRunning bool) []string { - if b == nil { - return nil - } - var problems []string - say := func(format string, args ...any) { - problems = append(problems, where+": "+fmt.Sprintf(format, args...)) - } - if !longRunning { - say("busy is asked of a container that stays up; a step or a scheduled run is never replaced mid-run") - } - if strings.TrimSpace(b.Command) == "" { - say("busy needs a command") - } - if strings.ContainsAny(b.Command, "\n\r") { - say("busy's command is one line") - } - if b.Timeout != "" { - d, err := time.ParseDuration(b.Timeout) - switch { - case err != nil: - say("busy's timeout is a duration, not %q", b.Timeout) - case d <= 0 || d > BusyTimeoutCeiling: - say("an ask of busy takes more than nothing and at most %s, not %s", BusyTimeoutCeiling, d) - } - } - return problems -} diff --git a/vendor/github.com/novox/mesh-host/internal/declaration/cron.go b/vendor/github.com/novox/mesh-host/internal/declaration/cron.go deleted file mode 100644 index 69b97df9..00000000 --- a/vendor/github.com/novox/mesh-host/internal/declaration/cron.go +++ /dev/null @@ -1,182 +0,0 @@ -package declaration - -// A minimal five-field cron, enough to say when a scheduled step is due (novox/hq ADR 0053). -// -// **Written rather than pulled in.** The host depends on nothing it does not have to -// (novox/hq ADR 0005), and a scheduled step needs exactly two questions answered — *is this minute -// a match* and *when is the next one* — over the ordinary five fields (minute, hour, day-of-month, -// month, day-of-week) with `*`, lists (`,`), ranges (`-`) and steps (`/`). That is small enough to -// keep in the vocabulary the host already owns, and a cron library would be a dependency carried -// for the parts of it nobody here uses. -// -// **The host both validates and evaluates.** The control plane refuses a malformed cron near its -// author (novox/hq ADR 0053), and the host refuses it again on arrival for the same near-versus-far -// reason every other field is checked here: a declaration the host does not fully understand is -// refused whole rather than half-applied. The evaluation half — `Next` — is what the scheduler -// fires on. - -import ( - "fmt" - "strconv" - "strings" - "time" -) - -// Cron is a parsed five-field expression, each field held as a bitset of the values it permits. -// -// The two day fields carry a flag for whether they were written as a bare `*`, because standard -// cron gives them a special rule: when day-of-month and day-of-week are *both* restricted, a day -// matches if *either* does; when one is `*`, only the other is consulted. Getting that wrong is the -// classic cron surprise, so the flag is kept rather than rediscovered. -type Cron struct { - minute uint64 - hour uint64 - dom uint64 - month uint64 - dow uint64 - domStar bool - dowStar bool -} - -// ParseCron reads a five-field cron expression, or says why it is not one. -func ParseCron(expr string) (*Cron, error) { - fields := strings.Fields(expr) - if len(fields) != 5 { - return nil, fmt.Errorf( - "a schedule is a five-field cron expression (minute hour day-of-month month "+ - "day-of-week), and %q has %d field(s)", expr, len(fields)) - } - - minute, _, err := parseCronField(fields[0], 0, 59) - if err != nil { - return nil, fmt.Errorf("schedule minute field: %w", err) - } - hour, _, err := parseCronField(fields[1], 0, 23) - if err != nil { - return nil, fmt.Errorf("schedule hour field: %w", err) - } - dom, domStar, err := parseCronField(fields[2], 1, 31) - if err != nil { - return nil, fmt.Errorf("schedule day-of-month field: %w", err) - } - month, _, err := parseCronField(fields[3], 1, 12) - if err != nil { - return nil, fmt.Errorf("schedule month field: %w", err) - } - // Day-of-week is 0-6 with Sunday at 0, and 7 is also accepted for Sunday — the convention every - // cron keeps, so a crontab copied from elsewhere is not refused for saying 7. - dow, dowStar, err := parseCronField(fields[4], 0, 7) - if err != nil { - return nil, fmt.Errorf("schedule day-of-week field: %w", err) - } - if dow&(1<<7) != 0 { - dow |= 1 << 0 - dow &^= 1 << 7 - } - - return &Cron{ - minute: minute, hour: hour, dom: dom, month: month, dow: dow, - domStar: domStar, dowStar: dowStar, - }, nil -} - -// parseCronField turns one field into the bitset of values it permits, and reports whether it was a -// bare `*` (which the day fields treat specially). -func parseCronField(spec string, min, max int) (uint64, bool, error) { - if spec == "" { - return 0, false, fmt.Errorf("is empty") - } - star := spec == "*" - - var bits uint64 - for _, part := range strings.Split(spec, ",") { - if part == "" { - return 0, false, fmt.Errorf("%q has an empty element between commas", spec) - } - - // A step may follow either `*` or a range: `*/15`, `0-30/5`. - step := 1 - rangePart := part - if slash := strings.IndexByte(part, '/'); slash >= 0 { - rangePart = part[:slash] - n, err := strconv.Atoi(part[slash+1:]) - if err != nil || n < 1 { - return 0, false, fmt.Errorf("step in %q is not a positive number", part) - } - step = n - } - - lo, hi := min, max - switch { - case rangePart == "*": - // Full range, already set. - case strings.IndexByte(rangePart, '-') >= 0: - dash := strings.IndexByte(rangePart, '-') - a, errA := strconv.Atoi(rangePart[:dash]) - b, errB := strconv.Atoi(rangePart[dash+1:]) - if errA != nil || errB != nil { - return 0, false, fmt.Errorf("range %q is not two numbers", rangePart) - } - lo, hi = a, b - default: - n, err := strconv.Atoi(rangePart) - if err != nil { - return 0, false, fmt.Errorf("%q is not a number", rangePart) - } - lo, hi = n, n - } - - if lo < min || hi > max || lo > hi { - return 0, false, fmt.Errorf( - "%q is outside the allowed range %d-%d", part, min, max) - } - for v := lo; v <= hi; v += step { - bits |= 1 << uint(v) - } - } - return bits, star, nil -} - -// Matches reports whether a scheduled step is due at this minute. -func (c *Cron) Matches(t time.Time) bool { - if c.minute&(1< 0 && unknown == "" { - problems = append(problems, where+": knows is given without unknown, and says nothing without it") - } - seen := map[string]bool{} - for _, k := range knows { - switch { - case k == "": - problems = append(problems, where+": knows holds an empty entry") - continue - case strings.HasPrefix(k, "/"): - problems = append(problems, fmt.Sprintf("%s: knows entry %q is absolute; it is relative to the path", where, k)) - continue - } - bad := false - for _, c := range strings.Split(strings.TrimSuffix(k, "/"), "/") { - if c == "" || c == "." || c == ".." { - bad = true - } - } - if bad { - problems = append(problems, fmt.Sprintf("%s: knows entry %q holds an empty, . or .. component", where, k)) - continue - } - if seen[k] { - problems = append(problems, fmt.Sprintf("%s: knows entry %q is repeated", where, k)) - } - seen[k] = true - } - return problems -} - -func (d *Directory) Identity() string { return d.ID } -func (d *Directory) Kind() Type { return TypeDirectory } -func (d *Directory) Target() string { return d.Path } - -func (d *Directory) validate(where string, _ bool) []string { - var problems []string - if d.Path == "" { - problems = append(problems, where+": a directory needs a path") - } - // A directory's mode may carry the setgid bit (2xxx), so what is made beneath it keeps its group (novox/hq ADR - // 0334 §1); never setuid or sticky, which mean nothing the mesh asks of a directory it owns. - mode := d.Mode - if len(mode) == 4 && mode[0] == '2' { - mode = "0" + mode[1:] - } - problems = append(problems, checkUnknown(where, "directory", d.Unknown, d.Knows)...) - return append(problems, checkMode(where, mode)...) -} - -// File is a file with literal content. The host renders nothing. -type File struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Path string `json:"path"` - Content string `json:"content"` - Mode string `json:"mode,omitempty"` - - // CreateOnce says the content is a seed: written when the file is absent, and left alone — - // content, mode and owner — whenever it is present. - // - // **Two intentions had one vocabulary** (novox/hq issue 035, ADR 0087). "This file has this - // content, for ever" is what an ordinary file says, and the host holds the machine to it. A - // module that needs a file to exist before a program first starts — an access list the - // program then persists into, a bootstrap configuration it rewrites — needs the other thing, - // and with only the first available, every reconcile restored the seed behind the running - // program and erased what had grown in it, reporting success. What grows in a seeded file - // is somebody else's work the mesh asked for; the mesh removes nothing it did not create - // (ADR 0030), and it does not overwrite that either. - CreateOnce bool `json:"create-once,omitempty"` - - // Into says the file is shared with software the mesh did not install, and the content is - // the mesh's part of it: written into what is there, never over it (novox/hq ADR 0102). Only - // "json" is spoken — the content is a JSON object whose keys the host sets in the file's - // object, keeping every other key as it found it and recording what each of its keys held - // before, so undeclaring the file gives those back. - // - // "block" is the same idea for a file that is not structured (novox/hq issue 128): the - // content is the mesh's lines, and the host owns only the region between `# BEGIN mesh ` - // and `# END mesh `, keeping every line outside it byte for byte. The machine's hosts file - // is the case that needed it — on a workstation the distribution, a local development tool and - // the operator all write into it, and the mesh writing it whole took their lines away at the - // next change to the mesh's names, silently. Marked blocks are the shape the other tools in - // that file already use, and `#` is the comment character of every file this serves. - // - // Written into, in either format, the file's mode and owner are the machine's: a declared mode - // and owner apply only to a file the host creates, and a file that was there keeps its own. - Into string `json:"into,omitempty"` - - // At is where a file written into a block has its region added when the file does not hold - // one yet: "end", the default, or "start". A region already there stays where it is, whatever - // this says — moving it would move the lines around it, and those are the machine's. - // - // **Some files give a line its meaning by what stands above it.** dhcpcd's configuration scopes - // every line after `interface X` to that interface, and a real one ends with exactly that — an - // interface and its static address. A region added at the end would make the mesh's global - // options (`nohook resolv.conf`, `denyinterfaces mesh0`) options of one interface, and dhcpcd - // would read them without complaint. At the start, nothing stands above them. - At string `json:"at,omitempty"` - - // Sealed is content encrypted to this node's sealing key, for a file the mesh must deliver - // without being able to read. - // - // The one thing here the host cannot simply write. Everything else in a declaration is - // visible to whatever carried it — the broker relays the message, and the message is signed - // so it cannot be forged, but signing does not make it unreadable. A password travelling in - // `content` would be a password the broker sees, which is the transitive trust the design - // refuses everywhere else (novox/hq ADR 0004). - // - // Exclusive with Content: a file is one or the other, so that "was this secret" is answerable - // by looking rather than by knowing which field won. - Sealed string `json:"sealed,omitempty"` - - // VaultSealed is content the vault sealed to this node's sealing key, in the mesh's sealed box (novox/hq ADR - // 0206): a core module's own credential, which the controller carries in the declaration and cannot open - // (ADR 0349 §16, issue 755). The same promise as Sealed, from the vault rather than the controller; sent only to - // an engine that states VaultBoxContract. Exclusive with Content, Sealed and Bytes. - VaultSealed *seal.Box `json:"vault-sealed,omitempty"` - - // Trusted is the catalogue's word on whether the settings this file's content took are trusted (novox/hq - // issue 339). It is the controller's to act on, and a controller takes it out before it sends a declaration. - // Accepted here, and nothing is done with it, so that a controller that passes it on — an older one, or one - // rolled back to — never costs a node its whole declaration. Not part of the file's digest. - Trusted *bool `json:"trusted,omitempty"` - - // Secrets are sealed values put into Content where it says `${secret:name}`. - // - // **The one place a secret and a configuration meet, and it happens on the machine.** A - // program that wants its token inside a JSON document cannot be given a file that is entirely - // a token, and the mesh cannot compose the document itself — it discarded the value - // (novox/hq ADR 0024). So the module supplies the document with a hole in it, the mesh - // delivers the value sealed, and the host is the only thing that ever sees both. - // - // **Substitution is textual and the host learns no formats.** That is deliberate: a mechanism - // that understood JSON would be asked to understand YAML next, and then INI, which is how the - // arrangement this replaces became something nobody could hold in their head. The module knows - // its own format, because it wrote the rest of the file. - // - // The sharp edge, stated rather than discovered: a value containing a quote or a backslash - // will not be escaped for whatever syntax surrounds it. - Secrets map[string]string `json:"secrets,omitempty"` - - // Bytes is content that is not text, base64-encoded — a wallpaper, a font, an icon. - // - // A third way of saying what is in a file, and the three are exclusive. It would have been - // tempting to let Content carry base64 and add a flag, and then "what is in this file" would - // depend on a field somewhere else. - Bytes string `json:"bytes,omitempty"` - - // Owner is the user this belongs to, by name. Absent means root. - Owner string `json:"owner,omitempty"` - - // OwnerAtRuntime says who owns the file once it is placed (novox/hq ADR 0349 §5): "owned", the mesh's - // whole, written back whenever it differs; "seed", written once and then the program's; or "managed", - // only its named keys or its marked block the mesh's, the program keeping the rest. - // - // **Three words for what the forms above already do.** Owned is an ordinary file, seed is create-once - // and managed is a file written into, so the word changes no write: a file that says it is applied - // exactly as its old form is, and one that does not say is owned by its old form (Ownership). The word - // is what the provisioner's files say of themselves, which the old forms only implied. Seed is enough - // alone; managed still needs `into`, which says the format. A word that contradicts the form is refused. - OwnerAtRuntime string `json:"owner-at-runtime,omitempty"` -} - -// The words a file says who owns it at runtime with (novox/hq ADR 0349 §5). -const ( - RuntimeOwned = "owned" - RuntimeSeed = "seed" - RuntimeManaged = "managed" -) - -// Ownership is who owns the file at runtime: the word it says, or the one its old form implies — a file -// written into is managed, one created once is a seed, and any other is owned. -func (f *File) Ownership() string { - switch { - case f.OwnerAtRuntime != "": - return f.OwnerAtRuntime - case f.Into != "": - return RuntimeManaged - case f.CreateOnce: - return RuntimeSeed - default: - return RuntimeOwned - } -} - -// Seeded reports whether the file is written only when absent: created once, by create-once or by the -// word seed. -func (f *File) Seeded() bool { return f.CreateOnce || f.OwnerAtRuntime == RuntimeSeed } - -// Secret reports whether this file arrived sealed, which is what decides both that it must be -// opened before writing and that its contents must never appear in a report. -func (f *File) Secret() bool { return f.Sealed != "" || f.VaultSealed != nil } - -func (f *File) Identity() string { return f.ID } -func (f *File) Kind() Type { return TypeFile } -func (f *File) Target() string { return f.Path } - -// placeholder is what Content says where a sealed value belongs: ${secret:name}. -var placeholder = regexp.MustCompile(`\$\{secret:([a-z0-9][a-z0-9-]*)\}`) - -// SecretsUsed are the names Content asks for, in the order they first appear. -func (f *File) SecretsUsed() []string { - var used []string - seen := map[string]bool{} - for _, m := range placeholder.FindAllStringSubmatch(f.Content, -1) { - if !seen[m[1]] { - seen[m[1]] = true - used = append(used, m[1]) - } - } - return used -} - -func (f *File) validate(where string, _ bool) []string { - var problems []string - if f.Path == "" { - problems = append(problems, where+": a file needs a path") - } - switch f.Into { - case "": - case IntoJSON: - var object map[string]json.RawMessage - if err := json.Unmarshal([]byte(f.Content), &object); err != nil || object == nil { - problems = append(problems, where+ - ": a file written into JSON carries a JSON object of the keys it sets") - } - case IntoBlock: - // The markers are how the host finds its region again. A marker in the content would be - // a second region, or the end of this one, the next time the file is read — and the host - // would then rewrite, or on undeclare take out, lines that were never the mesh's. - for _, line := range strings.Split(f.Content, "\n") { - if strings.HasPrefix(line, BlockBegin) || strings.HasPrefix(line, BlockEnd) { - problems = append(problems, fmt.Sprintf( - "%s: a file written into a block carries the mesh's lines, and %q is a marker the "+ - "host keeps for itself", where, strings.TrimRight(line, "\r"))) - break - } - } - // The id is written into the markers, so it has to stay on one line — and whitespace at - // either end of it is whitespace the host would have to match exactly in a line some - // editor may trim. - if strings.ContainsAny(f.ID, "\r\n") { - problems = append(problems, where+ - ": a file written into a block names its region by its id, and this id spans lines") - } else if strings.TrimSpace(f.ID) != f.ID { - problems = append(problems, where+ - ": a file written into a block names its region by its id, and this id begins or ends in whitespace") - } - default: - problems = append(problems, fmt.Sprintf( - "%s: into %q; a file is written into \"json\" or \"block\", or omits it to be written whole", - where, f.Into)) - } - switch { - case f.At == "": - case f.Into != IntoBlock: - problems = append(problems, fmt.Sprintf( - "%s: at %q; only a file written into a block has a place its region is added", where, f.At)) - case f.At != AtStart && f.At != AtEnd: - problems = append(problems, fmt.Sprintf( - "%s: at %q; a block is added at \"start\" or \"end\", or omits it to be added at the end", - where, f.At)) - } - if f.Into == IntoJSON || f.Into == IntoBlock { - if f.Secret() || f.Bytes != "" || len(f.Secrets) > 0 || f.CreateOnce { - problems = append(problems, where+ - ": a file written into says only its part, in content — not sealed, bytes, "+ - "secrets or create-once") - } - } - problems = append(problems, f.checkOwnership(where)...) - var said []string - for name, value := range map[string]string{ - "content": f.Content, "sealed": f.Sealed, "bytes": f.Bytes, - } { - if value != "" { - said = append(said, name) - } - } - if f.VaultSealed != nil { - said = append(said, "vault-sealed") - if f.VaultSealed.V != seal.Version { - problems = append(problems, fmt.Sprintf("%s: vault-sealed is a sealed box of version %d, and this "+ - "node-engine opens version %d", where, f.VaultSealed.V, seal.Version)) - } - if len(f.Secrets) > 0 { - problems = append(problems, where+": a file the vault sealed whole has no holes for secrets") - } - } - if len(said) > 1 { - sort.Strings(said) - problems = append(problems, where+ - ": a file says what is in it exactly once, and this says it as "+ - strings.Join(said, " and ")+ - " — otherwise nobody can tell by looking which one landed on the machine") - } - // A file whose content names a secret must be given exactly the secrets it names. - // - // **Both directions, and both are refusals rather than warnings.** A placeholder with nothing - // to fill it would write `${secret:x}` into a configuration file, which the program reads as - // a value and fails on somewhere unrelated. A secret nobody uses means whoever wrote this - // believes a credential is in a file where it is not. - if len(f.Secrets) > 0 && f.Content == "" { - problems = append(problems, where+ - ": secrets were given and there is no content to put them in") - } - used := f.SecretsUsed() - for _, name := range used { - if f.Secrets[name] == "" { - problems = append(problems, fmt.Sprintf( - "%s: the content asks for the secret %q and none was given", where, name)) - } - } - for name := range f.Secrets { - if !slices.Contains(used, name) { - problems = append(problems, fmt.Sprintf( - "%s: the secret %q was given and the content never asks for it", where, name)) - } - } - return append(problems, checkMode(where, f.Mode)...) -} - -// checkOwnership refuses a word that contradicts the file's form (novox/hq ADR 0349 §5): owned is a whole -// file written back, so not created once nor written into; a seed is a whole file created once, so not -// written into; managed is a file written into, which says its format. Managed and created once together -// is already refused by the rule that a file written into is not created once. -func (f *File) checkOwnership(where string) []string { - switch f.OwnerAtRuntime { - case "": - case RuntimeOwned: - var also []string - if f.CreateOnce { - also = append(also, "create-once") - } - if f.Into != "" { - also = append(also, "into") - } - if len(also) > 0 { - return []string{fmt.Sprintf("%s: owned at runtime is the mesh's whole file, written back whenever it "+ - "differs, and this also says %s", where, strings.Join(also, " and "))} - } - case RuntimeSeed: - if f.Into != "" { - return []string{where + ": a seed is a whole file written once, and this says into; a file the " + - "program shares with the mesh is managed"} - } - case RuntimeManaged: - if f.Into == "" { - return []string{where + ": a managed file names the format its keys or block are written into, " + - "with into \"json\" or \"block\""} - } - default: - return []string{fmt.Sprintf("%s: owner-at-runtime %q; a placed file is %q, %q or %q, or omits it to be "+ - "owned by its form", where, f.OwnerAtRuntime, RuntimeOwned, RuntimeSeed, RuntimeManaged)} - } - return nil -} - -// User is a login on the machine. -// -// The thing that makes a shell, a chat client or a desktop expressible at all: each is a package -// plus configuration in somebody's home, and until this the mesh could only own /etc. -// -// It also makes "zsh is my login shell" **declared state** rather than an action. `chsh` is a -// command, the link may not carry one (novox/hq ADR 0005), and a shell that could only be set by -// hand would be a shell the mesh cannot manage — which is most of the reason to manage a machine -// at all. -type User struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Name string `json:"name"` - - // Shell this user logs in with. Absent means the host asserts nothing and leaves whatever is - // there — the same rule Service.Boot follows, for the same reason: a field that always - // asserts cannot express "I do not care". - Shell string `json:"shell,omitempty"` - - // Groups this user must be in. Additive: the host puts the user in these and does not remove - // it from others, because a machine's own groups are not the mesh's to know about. - Groups []string `json:"groups,omitempty"` - - // Home directory. Absent means the system's default for a new user, and is not changed for - // one that exists — moving somebody's home is not something a declaration should do quietly. - Home string `json:"home,omitempty"` - - // Linger is whether the account's own service manager runs with nobody logged in (novox/hq ADR - // 0177). A user-scoped unit lives in that manager, and the manager runs only from the account's - // first login to its last logout — so a server's user unit, where nobody ever logs in, never - // runs without it, and a workstation's runs only while its person is there, which on a desktop - // is what is wanted. Absent asserts nothing, as Shell's does: true has the account linger, false - // has it not. On the account rather than on the unit, because it is the account's: two units of - // one account cannot disagree about it, and undeclaring one of them must not stop the other. - Linger *bool `json:"linger,omitempty"` - - // Root says whether this account may become root without a person (novox/hq ADR 0266). "never" - // is the agents' own account: the login an agent session runs as on a machine where it must not - // reach root by itself. Empty asserts nothing, as Shell's does. - // - // **A statement the engine judges, never one it acts on.** The apply gives an account it creates - // no password, no sudo rule and no group beyond those declared, as it always has, and takes none - // away from one it finds: a sudo rule or a group granted by hand is a person's to remove, and a - // declaration that silently stripped them would be the mesh deciding what a person's machine - // grants. What "never" adds is the look: on every look the engine reads whether the account can - // become root by itself — by its uid, a group that grants root, any sudo rule, or a secret the mesh - // placed that it can read — and says it unhealthy while it can (internal/accounts), so the - // controller can tell a machine where it holds from one where it does not. - Root string `json:"root,omitempty"` -} - -// RootNever is the one value Root takes besides empty: the account never becomes root without a person. -const RootNever = "never" - -// Network is a named network on this machine. -// -// **A name and nothing else.** Not a driver, a subnet or a gateway: each of those is something a -// module would have to know about the machine it lands on, and a module naming a subnet is a -// module that collides with whatever else chose the same one. The runtime picks; the mesh names -// (novox/hq ADR 0029). -type Network struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Name string `json:"name"` -} - -func (n *Network) Identity() string { return n.ID } -func (n *Network) Kind() Type { return TypeNetwork } -func (n *Network) Target() string { return n.Name } - -func (n *Network) validate(where string, _ bool) []string { - var problems []string - if n.Name == "" { - problems = append(problems, where+": a network needs a name") - } - // The runtimes accept more than this, and the mesh does not: a name with a slash or a colon - // in it reads as a reference to something else entirely wherever it is later printed. - for _, r := range n.Name { - if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') && - r != '-' && r != '_' && r != '.' { - problems = append(problems, where+ - ": a network name is letters, digits, dashes, underscores and dots, and "+ - n.Name+" is not") - break - } - } - return problems -} - -// Modes an access may be granted at. Plain words, not the octal a directory's mode is: an access -// is not a thing the host chmods, it is a statement of how this module reaches what the operator -// owns. -const ( - AccessRead = "read" - AccessReadWrite = "read-write" -) - -// Access is a pre-existing, operator-owned path this module is granted use of but does not own. -// -// **The distinction 04-ISSUES/036 and 026 turn on.** A `directory` resource is the mesh's own — -// it creates it, sets its owner and mode, and removes it when empty ([ADR 0030](novox/hq)). A -// media library, a download spool is the operator's: it existed before the mesh, several modules -// read and write it at once, and the mesh must not create, chown, chmod or remove it. The host -// confirms it is there and mounts it; nothing else. Library data is a library's (novox/hq ADR 0328), -// not an access. -type Access struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Path string `json:"path"` - // Mode is how this module reaches the path: read or read-write. Absent narrows to read. - Mode string `json:"mode,omitempty"` - - // Users and Taken are what a controller speaking ADR 0325 sends (the identities that use the folder, and - // that the operator's warrant was carried out): accepted, so its declaration is not refused whole, and not - // acted on since ADR 0328 superseded ADR 0325 — no ACL entry is kept. - Users []AccessUser `json:"users,omitempty"` - Taken bool `json:"taken,omitempty"` - - // Grant is what a controller older than ADR 0325 still sends (ADR 0312's group grant, superseded): accepted - // so its declaration is not refused whole, and never acted on — no account is put in a group for data (§5). - Grant json.RawMessage `json:"grant,omitempty"` -} - -// The uses an access's user has (ADR 0051 §3), and where it runs (ADR 0325 §4). -const ( - FromContainer = "container" - FromToolRunner = "tool-runner" -) - -// AccessUser is one identity that uses an accessed folder: its name and uid as the controller allocated them, its -// use, and where it runs. A tool runner's account may come with no uid (0 or absent): the engine reads it by name. -type AccessUser struct { - Identity string `json:"identity"` - UID int `json:"uid,omitempty"` - Use string `json:"use"` - From string `json:"from"` -} - -// UnmarshalJSON reads a user as strictly as a resource is read. -func (u *AccessUser) UnmarshalJSON(raw []byte) error { - type plain AccessUser - var p plain - if err := strictDecode(raw, &p); err != nil { - return fmt.Errorf("an access's user: %w", err) - } - *u = AccessUser(p) - return nil -} - -func (a *Access) Identity() string { return a.ID } -func (a *Access) Kind() Type { return TypeAccess } -func (a *Access) Target() string { return a.Path } - -func (a *Access) validate(where string, _ bool) []string { - var problems []string - if !strings.HasPrefix(a.Path, "/") { - problems = append(problems, where+": an access needs an absolute path, and "+ - a.Path+" is not one") - } - switch a.Mode { - case "", AccessRead, AccessReadWrite: - default: - problems = append(problems, fmt.Sprintf( - "%s: an access is %q or %q, not %q", where, AccessRead, AccessReadWrite, a.Mode)) - } - for _, u := range a.Users { - switch { - case u.Identity == "" || !accountName.MatchString(u.Identity): - problems = append(problems, fmt.Sprintf("%s: a user of an access is an account's name, and %q is not one", - where, u.Identity)) - case u.UID < 0 || (u.UID == 0 && u.From != FromToolRunner): - // Never root (novox/hq ADR 0325 §11): an entry for uid 0 would be a right root has anyway, and a - // number below it is no account. A tool runner's account alone may come without its number: the - // controller cannot know the operator's account's, and the engine reads it by name - // (apply.ResolveRunners), refusing one that is root's. - problems = append(problems, fmt.Sprintf("%s: the user %s has uid %d, and a module's identity is never "+ - "root or below it (novox/hq ADR 0325 §11)", where, u.Identity, u.UID)) - } - if u.Use != AccessRead && u.Use != AccessReadWrite { - problems = append(problems, fmt.Sprintf("%s: the user %s uses it %q, and a use is %q or %q", where, - u.Identity, u.Use, AccessRead, AccessReadWrite)) - } - if u.Use == AccessReadWrite && a.Mode != AccessReadWrite { - problems = append(problems, fmt.Sprintf("%s: the user %s writes an access declared %s", where, - u.Identity, orRead(a.Mode))) - } - if u.From != FromContainer && u.From != FromToolRunner { - problems = append(problems, fmt.Sprintf("%s: the user %s runs from %q, and that is %q or %q", where, - u.Identity, u.From, FromContainer, FromToolRunner)) - } - } - return problems -} - -// orRead is an access's mode as said: absent narrows to read. -func orRead(mode string) string { - if mode == "" { - return AccessRead - } - return mode -} - -func (u *User) Identity() string { return u.ID } -func (u *User) Kind() Type { return TypeUser } -func (u *User) Target() string { return u.Name } - -func (u *User) validate(where string, _ bool) []string { - var problems []string - if u.Name == "" { - problems = append(problems, where+": a user needs a name") - } - if u.Shell != "" && !strings.HasPrefix(u.Shell, "/") { - problems = append(problems, where+ - ": a login shell is an absolute path, and "+u.Shell+" is not one") - } - if u.Home != "" && !strings.HasPrefix(u.Home, "/") { - problems = append(problems, where+": a home directory is an absolute path") - } - if u.Root != "" && u.Root != RootNever { - problems = append(problems, fmt.Sprintf("%s: root is %q or absent, and %q is neither", where, RootNever, u.Root)) - } - return problems -} - -// Archive is a set of files, fetched by digest and unpacked. -// -// For the case inlining cannot serve: a theme, an icon set, a tree of configuration. Hundreds of -// files inlined would make every declaration enormous and rewrite all of it when one changed. -// -// **Pinned by digest, and the digest is checked before anything is unpacked.** The same discipline -// the bootstrap uses for images, and for the same reason — this is fetched over a network the -// mesh does not control, and a reference that can be made to point elsewhere is not a reference. -type Archive struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - // Source is where to fetch it from. - Source string `json:"source"` - // Digest is sha256 of the archive, as "sha256:". - Digest string `json:"digest"` - // Path is the directory it is unpacked into. - Path string `json:"path"` - // Owner is the user the unpacked files belong to. Absent means root. - Owner string `json:"owner,omitempty"` - // Unknown and Knows are as a directory's (issue 1024, ADR 0422). - Unknown string `json:"unknown,omitempty"` - Knows []string `json:"knows,omitempty"` -} - -func (a *Archive) Identity() string { return a.ID } -func (a *Archive) Kind() Type { return TypeArchive } -func (a *Archive) Target() string { return a.Path } - -func (a *Archive) validate(where string, _ bool) []string { - var problems []string - if a.Source == "" { - problems = append(problems, where+": an archive needs somewhere to fetch it from") - } - if a.Path == "" { - problems = append(problems, where+": an archive needs somewhere to unpack into") - } - if !strings.HasPrefix(a.Digest, "sha256:") || len(a.Digest) != len("sha256:")+64 { - // Refused rather than fetched and trusted. Everything else pinned in this vocabulary is - // pinned by digest, and an archive that was not would be the one way in. - problems = append(problems, where+ - ": an archive is pinned by digest, as sha256:<64 hex characters>") - } - problems = append(problems, checkUnknown(where, "archive", a.Unknown, a.Knows)...) - return problems -} - -// Process is the module's own code, run on the machine, in one of three modes. -// -// The difference from Service is who owns the unit: a Service puts an EXISTING unit into a state -// and deliberately does not install one, which is right for software that ships its own. This is -// the mesh's own code — a bundle it built — so there is no unit until the mesh writes it, and -// nothing else will. -// -// The difference from Container is the hosting, and a module should not have to choose: what this -// says is what to run, and the machine's own supervisor is how. Code that genuinely needs a -// container's isolation declares a container and says so. -// -// **Three modes, matching a container's**, because they are the same thing at different cadences: -// stays up, runs once, runs on a schedule. A module's scheduled task, its run-once migration, its -// health check and its tool host are all this — and each being its own resource kind would be four -// entries in a vocabulary where every entry widens what a compromised control plane can express. -type Process struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - // Name is what the unit is called, and what an operator will see in the process table. - Name string `json:"name"` - // Source is where to fetch the bundle from, and Digest is what it must hash to. The same - // discipline as an archive, for the same reason: this crosses a network the mesh does not - // control. - Source string `json:"source"` - Digest string `json:"digest"` - // Run is the command, relative to the unpacked bundle. The first element is the program. - // - // **Named by the module, never inferred.** Guessing an entrypoint from which files exist makes - // a daemon change what it runs when somebody adds a file. - Run []string `json:"run"` - // Env and EnvFile are what it runs with. A file rather than inline values is how a credential - // reaches a daemon without passing through the declaration. - Env map[string]string `json:"env,omitempty"` - EnvFile []string `json:"env-file,omitempty"` - // User is who it runs as. Absent means root, which is what the mesh's own modules need for - // the things they do to a machine. - User string `json:"user,omitempty"` - // RestartOn names resources whose change means this must be restarted — the same rule a - // service follows, and for the same reason: a running process does not re-read its - // configuration, so replacing a file and finding the process already up leaves the machine - // behaving the way it did before while every check passes. - RestartOn []string `json:"restart-on,omitempty"` - - // RunOnce marks code the host runs to completion rather than leaves running: a migration, a - // seed, a first-boot step. What follows it is gated on it finishing, because a step that did - // not make the machine ready must not be followed by the thing that needed it. - RunOnce bool `json:"run-once,omitempty"` - - // Schedule runs it on a cadence — a five-field cron expression (novox/hq ADR 0053). The - // recurring twin of RunOnce: the same code, run again rather than left running. - // - // Exclusive with RunOnce and with RestartOn, for the same reason a container's is: something - // that runs once does not run on a schedule, and something that is not running cannot be - // restarted when a file changes. - Schedule string `json:"schedule,omitempty"` - - // Replaces names resources this declaration no longer declares that this process takes the - // place of (novox/hq issue 213). Such a resource is not removed with the other orphans, before - // anything is applied: it is removed only once this process is applied and still running a - // moment later, and kept when it is not. So the thing being replaced answers until the thing - // replacing it does — the controller moving from its container to a process is the case: the - // container removed first left nothing answering the mesh's verbs for as long as fetching, - // unpacking and starting the process took, and for ever if the process did not start. - // - // For a process that stays up; a step or a scheduled run is not running a moment later by - // design, so there is nothing to hand over to. - Replaces []string `json:"replaces,omitempty"` - - // Witness is how the node-engine judges a new build of this process, and restores the build - // before it when the new one is not healthy in bound (novox/hq to-be 45 §8): "lease" — the - // controller this machine started holds the controller's lease; "ping" — this machine's runtime - // answers the services protocol's PING; "none". Absent is the default for the process's name: - // the mesh's two core processes are judged, nothing else is. For a process that stays up. - Witness string `json:"witness,omitempty"` - - // NotReversible says why this build may not be rolled back, when it may not: the build before it - // would run against what this one changes — a migration it runs that the older build cannot read - // (to-be 45 §8, rule 8). A build so declared that is not healthy in bound is left running and said - // as urgent; the build before it is never started against the newer data. - NotReversible string `json:"not-reversible,omitempty"` - - // Health is how this resource is ready (novox/hq ADR 0240 rule 2, Phase B): one kind and its - // timing, judged by the node-engine beside liveness. Absent: judged alive or not, and nothing more. - Health *Health `json:"health,omitempty"` -} - -func (d *Process) Identity() string { return d.ID } -func (d *Process) Kind() Type { return TypeProcess } -func (d *Process) Target() string { return d.Name } - -// ProcessNameProblem says what is wrong with a process name, or nothing. -// -// The name becomes a unit name, a file under the unit directory and a directory under the mesh's -// own — the one removing the process deletes, whole (novox/hq ADR 0118). So a name that is not one -// plain path element is refused: with a separator it writes somewhere nobody meant, and "." or -// ".." IS the mesh's directory or its parent — removing a process named ".." would delete every -// bundle the mesh has, and more. One with a leading dash is read by the service manager as an -// option, not a unit. Exported because the removal checks the recorded name again: a record is -// what the host wrote, and a host of an older version wrote it under looser rules. -func ProcessNameProblem(name string) string { - switch { - case name == "": - return "a process needs a name, which is what its unit is called" - case strings.ContainsAny(name, "/ \t"): - return "a process name becomes a unit name, so it cannot contain a path separator or a space" - case name == "." || name == "..": - return fmt.Sprintf("a process name becomes a directory under the mesh's own, and %q would be "+ - "that directory or its parent", name) - case strings.HasPrefix(name, "-"): - return "a process name cannot begin with a dash: the service manager would read it as an option" - } - return "" -} - -func (d *Process) validate(where string, _ bool) []string { - problems := d.Health.problems(where, false, !d.RunOnce && d.Schedule == "") - if problem := ProcessNameProblem(d.Name); problem != "" { - problems = append(problems, where+": "+problem) - } - if d.Source == "" { - problems = append(problems, where+": a process needs somewhere to fetch its bundle from") - } - if !strings.HasPrefix(d.Digest, "sha256:") || len(d.Digest) != len("sha256:")+64 { - // The same rule an archive follows, and for the same reason: this crosses a network the - // mesh does not control, and a reference that can be made to point elsewhere is not one. - problems = append(problems, where+ - ": a process bundle is pinned by digest, as sha256:<64 hex characters>") - } - if len(d.Run) == 0 { - problems = append(problems, where+": a process needs to say what to run") - } - switch d.Witness { - case "", "lease", "ping", "none": - default: - problems = append(problems, fmt.Sprintf("%s: witness %q is not one this host keeps: lease, ping or none", - where, d.Witness)) - } - if d.Witness != "" && d.Witness != "none" && (d.RunOnce || d.Schedule != "") { - problems = append(problems, where+": a witness judges a process that stays up; a step or a "+ - "scheduled run is not running between its runs") - } - if strings.ContainsAny(d.NotReversible, "\n\r") { - problems = append(problems, where+": not-reversible is one line") - } - for _, part := range d.Run { - if part == "" { - problems = append(problems, where+": a process command has an empty element") - break - } - } - // **A newline cannot be represented in a unit's environment, so it is refused rather than - // mangled.** Everything else a unit file reinterprets — a percent specifier, whitespace - // splitting assignments, a quote ending one early — can be escaped. A newline cannot: it ends - // the line, and what follows is read as a unit DIRECTIVE. A value carrying one could write - // ExecStart= and have the machine run something nobody declared. - // - // Refused here, near whoever wrote it, rather than at the far end of a declaration. - for key, value := range d.Env { - if strings.ContainsAny(value, "\n\r") { - problems = append(problems, fmt.Sprintf( - "%s: the value of %s contains a line break, which cannot be written into a unit's "+ - "environment — what followed it would be read as a unit directive", where, key)) - } - if key == "" { - problems = append(problems, where+": an environment value with no name") - } - } - - if d.Schedule != "" { - if d.RunOnce { - problems = append(problems, where+ - ": a process runs once or on a schedule, not both") - } - if len(d.RestartOn) > 0 { - problems = append(problems, where+ - ": a scheduled process is not running between its fires, so there is nothing to "+ - "restart when something it reads changes") - } - if _, err := ParseCron(d.Schedule); err != nil { - problems = append(problems, where+": "+err.Error()) - } - } - if len(d.Replaces) > 0 && (d.RunOnce || d.Schedule != "") { - problems = append(problems, where+ - ": only a process that stays up replaces something — a step or a scheduled run is not "+ - "running a moment later, so what it replaced would be removed with nothing in its place, "+ - "or never") - } - for _, id := range d.Replaces { - switch { - case strings.TrimSpace(id) == "": - problems = append(problems, where+": replaces names an empty id") - case id == d.ID: - problems = append(problems, where+": a process cannot replace itself") - } - } - return problems -} - -// Service is a unit the host puts into a state. It does not install the unit. -// -// Two states, and they are orthogonal rather than one scale. A unit can be enabled and stopped -// (it will come back at boot), or disabled and running (started by hand, gone after a reboot). -// Folding them into one field would make the second expressible only by accident. -type Service struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Unit string `json:"unit"` - // State is "running" or "stopped" — or absent, and then **the unit's lifecycle is the - // machine's; the mesh only reflects its triggers** (novox/hq ADR 0117). The uplink modules - // declare the machine's own network manager this way: the mesh writes into its configuration - // and needs it to read that again, and nothing more. Stated, the host would start the manager - // on a machine that uses another one — two managers fighting over the same links — and, when - // the module was unassigned, stop it: the machine's network, the channel the mesh itself - // arrives on, gone at the moment of a routine change. So a service without a state is never - // started, stopped, enabled or disabled, is reloaded or restarted only when a trigger changed - // and it is already running, and undeclared is simply forgotten. It says nothing unless it - // names a trigger, and it may not say boot or takes-over, which are both lifecycle. - State string `json:"state,omitempty"` - // Boot is "enabled" or "disabled" — whether the unit starts at boot. Optional: absent means - // the host asserts nothing about it and leaves whatever is there. - // - // Without this the host could start a unit and not make it survive a reboot, which is a - // declaration that reports success and stops being true at the next power cut. - Boot string `json:"boot,omitempty"` - - // Scope is whose service manager the unit belongs to: "system" (absent means system), or - // "user" — the operator account's own manager (novox/hq ADR 0177). A workstation's per-user - // daemons — a window manager's reload watcher, an audio mask, a memory guard — are units in - // that manager, and until this they had no form the mesh could send. A user-scoped unit names - // its User; the host talks to that account's manager and never starts a system unit by the - // same name. - Scope string `json:"scope,omitempty"` - // User is the account whose manager a user-scoped unit lives in. Required with scope "user", - // refused otherwise; a module names it ${machine:account} and never the person. - User string `json:"user,omitempty"` - // RestartOn names resources whose change means this service must be restarted. - // - // Because a running service does not re-read its configuration. Replace the file, find the - // service already running, do nothing, and the machine keeps behaving the way it did before — - // while every check passes, because the file is right and the service is up. That is not - // hypothetical: it is how a third node joining a mesh left the first two carrying a network - // that no longer existed, and every part of it reported success. - // - // This is declared state rather than a command. The declaration says the running service must - // reflect these files; the host works out that it does not and acts. A *command* to restart - // would be an action, and the link may not carry one (novox/hq ADR 0005) — so this is not a - // way around that rule, it is the shape the rule leaves. - RestartOn []string `json:"restart-on,omitempty"` - - // ReloadOn names resources whose change means this service must be reloaded — for a service - // that re-reads its configuration when told to, where a restart would stop what it runs: the - // container runtime, whose restart stops every container on the machine (novox/hq ADR 0102). - // A change that is also in RestartOn restarts it, which covers a reload. - ReloadOn []string `json:"reload-on,omitempty"` - - // TakesOver names the found tunnel this service replaces (novox/hq ADR 0105): before this unit - // is started, the named unit is stopped and disabled — never flushed — and its configuration - // file is kept like any held file — until the take is proven by a peer's handshake, and then - // retired, its original staying kept (novox/hq ADR 0119). Only on an adopted node, and only - // said by the controller, which knows the found tunnel's key is this node's own: without that, - // starting this unit on the found one's port would drop every peer's packets. - TakesOver *TakeOver `json:"takes-over,omitempty"` - - // Health is how this resource is ready (novox/hq ADR 0240 rule 2, Phase B): one kind and its - // timing, judged by the node-engine beside liveness. Absent: judged alive or not, and nothing more. - Health *Health `json:"health,omitempty"` -} - -// TakeOver is a found tunnel a service replaces: its interface, the unit that raised it, and its -// configuration file. -type TakeOver struct { - Interface string `json:"interface"` - Unit string `json:"unit"` - Config string `json:"config"` -} - -// Stateless reports whether the unit's lifecycle is the machine's, and the mesh only reflects the -// service's triggers (novox/hq ADR 0117). -func (s *Service) Stateless() bool { return s.State == "" } - -func (s *Service) Identity() string { return s.ID } -func (s *Service) Kind() Type { return TypeService } -func (s *Service) Target() string { return s.Unit } - -// ScopeSystem and ScopeUser are the two managers a unit may belong to (novox/hq ADR 0177). -const ( - ScopeSystem = "system" - ScopeUser = "user" -) - -// UserScoped is whether this unit lives in an account's own service manager. -func (s *Service) UserScoped() bool { return s.Scope == ScopeUser } - -func (s *Service) validate(where string, _ bool) []string { - problems := s.Health.problems(where, false, s.State == "running") - if s.Unit == "" { - problems = append(problems, where+": a service needs a unit") - } - switch s.Scope { - case "", ScopeSystem: - if s.User != "" { - problems = append(problems, where+": a system unit names no user; only a user-scoped unit does") - } - case ScopeUser: - if s.User == "" { - problems = append(problems, where+": a user-scoped unit names the account whose manager it lives in") - } - default: - problems = append(problems, fmt.Sprintf( - "%s: scope %q; a unit is in the \"system\" manager or the operator account's \"user\" one", where, s.Scope)) - } - switch { - case s.State == "running" || s.State == "stopped": - case s.State != "": - problems = append(problems, fmt.Sprintf( - "%s: state %q; a service is \"running\" or \"stopped\", or omits state to leave the "+ - "unit's lifecycle to the machine", where, s.State)) - case s.Boot != "" || s.TakesOver != nil: - problems = append(problems, where+": a service that omits state leaves the unit's lifecycle "+ - "to the machine, and boot and takes-over are both its lifecycle") - case len(s.RestartOn) == 0 && len(s.ReloadOn) == 0: - problems = append(problems, where+": a service that omits state leaves the unit's lifecycle "+ - "to the machine, and names no restart-on or reload-on — it declares nothing") - } - if s.Boot != "" && s.Boot != "enabled" && s.Boot != "disabled" { - problems = append(problems, fmt.Sprintf( - "%s: boot %q; a service is \"enabled\" or \"disabled\" at boot, or omits it to "+ - "leave the machine's own setting alone", where, s.Boot)) - } - if t := s.TakesOver; t != nil { - switch { - case t.Unit == "" || t.Config == "" || t.Interface == "": - problems = append(problems, where+": takes-over names the found tunnel's interface, unit "+ - "and config, and this leaves one out") - case t.Unit == s.Unit: - problems = append(problems, fmt.Sprintf("%s: takes-over names %s, which is this service's own unit", - where, t.Unit)) - case s.State != "running" && s.State != "": - problems = append(problems, where+": a service that takes over a tunnel is running — stopping "+ - "the found one for a service that will not run would leave the peers with nothing") - } - } - return problems -} - -// What a file is written into (novox/hq ADR 0102): a JSON object whose keys the mesh sets, or a -// text file in which the mesh owns one marked block of lines (novox/hq issue 128). -const ( - IntoJSON = "json" - IntoBlock = "block" -) - -// The lines that delimit the mesh's region in a file written into a block, each followed by the -// resource's id. Exact lines, never patterns: another tool's `# BEGIN …` block in the same file is -// that tool's, and a marker that merely resembled the mesh's must not be taken for it. -const ( - BlockBegin = "# BEGIN mesh " - BlockEnd = "# END mesh " -) - -// Where a file written into a block has its region added, when it has none yet. -const ( - AtStart = "start" - AtEnd = "end" -) - -// BlockMarkers are the two lines, without their line ends, that delimit a resource's region. -func BlockMarkers(id string) (begin, end string) { - return BlockBegin + id, BlockEnd + id -} - -// Opening is a port reachable on an adopted node, from where, and on which path. -// -// **From** is everywhere or mesh — the private network, by its interface. **Path** is incoming, -// for something listening on the machine, or forwarded, for a published container port: the found -// firewall sees a published port after the runtime has translated it, so a forwarded opening names -// the container's own port in To as well as the machine's in Port. -type Opening struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Port int `json:"port"` - Protocol string `json:"protocol"` - From string `json:"from"` - Path string `json:"path"` - To int `json:"to,omitempty"` -} - -// Where an opening admits from, and the path it is on. -const ( - FromEverywhere = "everywhere" - FromMesh = "mesh" - PathIncoming = "incoming" - PathForwarded = "forwarded" -) - -func (o *Opening) Identity() string { return o.ID } -func (o *Opening) Kind() Type { return TypeOpening } - -func (o *Opening) Target() string { - if o.Path == PathForwarded { - return fmt.Sprintf("%s/%d forwarded to %d from %s", o.Protocol, o.Port, o.To, o.From) - } - return fmt.Sprintf("%s/%d %s from %s", o.Protocol, o.Port, o.Path, o.From) -} - -func (o *Opening) validate(where string, _ bool) []string { - var problems []string - if o.Port < 1 || o.Port > 65535 { - problems = append(problems, fmt.Sprintf("%s: an opening's port is 1-65535, not %d", where, o.Port)) - } - if o.Protocol != "tcp" && o.Protocol != "udp" { - problems = append(problems, fmt.Sprintf("%s: an opening is tcp or udp, not %q", where, o.Protocol)) - } - if o.From != FromEverywhere && o.From != FromMesh { - problems = append(problems, fmt.Sprintf( - "%s: an opening is from %q or %q, not %q", where, FromEverywhere, FromMesh, o.From)) - } - switch o.Path { - case PathIncoming: - if o.To != 0 { - problems = append(problems, where+ - ": an incoming opening names no container port; only a forwarded one does") - } - case PathForwarded: - if o.To < 1 || o.To > 65535 { - problems = append(problems, where+ - ": a forwarded opening names the container's port it reaches, as to, 1-65535") - } - default: - problems = append(problems, fmt.Sprintf( - "%s: an opening's path is %q or %q, not %q", where, PathIncoming, PathForwarded, o.Path)) - } - if !strings.HasPrefix(o.ID, AdoptionPrefix) { - problems = append(problems, fmt.Sprintf( - "%s: an opening is the mesh's own, so its id starts %q", where, AdoptionPrefix)) - } - return problems -} - -// Package is a package that should be present. -// -// Present is the whole of what it asserts, never a version: version is the package manager's -// business and the mesh does not hold a second opinion about it. -type Package struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Package string `json:"package"` - // Absent declares that the package is NOT installed (novox/hq ADR 0180): the host removes it - // when it is, and leaves a machine that never had it alone. For the one case a module replaces - // software the machine was found with and the operator has decided it does not come back — the - // firewall front end a converged machine's filter module retired. Nothing to undo when the - // declaration drops it: the host does not install what a declaration stopped saying is absent. - Absent bool `json:"absent,omitempty"` -} - -func (p *Package) Identity() string { return p.ID } -func (p *Package) Kind() Type { return TypePackage } -func (p *Package) Target() string { return p.Package } - -func (p *Package) validate(where string, _ bool) []string { - if p.Package == "" { - return []string{where + ": a package needs a package name"} - } - return nil -} - -// Container is a container that should be running, from an image pinned by digest. -type Container struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Name string `json:"name"` - // Image is pinned by digest (novox/hq ADR 0006) — a tag moves and a digest does not. - Image string `json:"image"` - Env map[string]string `json:"env,omitempty"` - // EnvFile names files the runtime reads environment from, in order. - // - // **Because a secret may not travel in Env.** A declaration reaches a node over the broker, - // and `env` is plain text in it — so a password there is a password the broker sees, which is - // the transitive trust refused everywhere else (novox/hq ADR 0004). A sealed file reaches the - // machine unreadable, the host writes it, and the runtime reads it: the mesh never holds it - // and neither does anything between them. - // - // It is also simply how third-party software takes credentials. Nothing that ships in a - // container will read a path the mesh invented; every one of them reads its environment. - EnvFile []string `json:"env-file,omitempty"` - Ports []string `json:"ports,omitempty"` - Volumes []string `json:"volumes,omitempty"` - Args []string `json:"args,omitempty"` - // Names this container can reach, as `name:address`. - // - // **Because a container does not inherit the machine's names.** It gets its own `/etc/hosts` - // holding only its own hostname, so every internal name the mesh wrote for this machine is - // invisible to the thing the machine is running. That was hit for real: a database client on - // one node could not resolve another node, on a mesh where both names were correct and - // present on both machines. - // - // **A file rather than a resolver, which is the decision the mesh already made about names** - // and this extends rather than overturns: it works on every runtime, needs no package, and - // has no failure mode of its own. A resolver becomes necessary when names are wanted that are - // not one-per-node — service names, wildcards — and that is still not true. - // - // Set by the mesh, not by a module: which machines exist is a fact about the mesh, and a - // module that listed them would be a module that goes stale when one joins. - Hosts []string `json:"hosts,omitempty"` - - // Network is the container's network, passed to the runtime unchanged. - // - // Needed because the control plane must reach the store and the broker on the machine it was - // raised on, before there is any mesh to arrange that. The alternative was publishing ports - // and guessing an address that works from inside a container, which is the same thing with a - // worse failure mode. - Network string `json:"network,omitempty"` - - // Dns is the resolvers this container asks, passed to the runtime unchanged. - // - // **Because some software refuses to run behind the runtime's forwarding resolver.** A mail - // server's admin demands a DNSSEC-validating resolver, and the runtime's own (127.0.0.11) - // forwards to whatever the machine has — so a module that ships its own validating resolver - // must be able to point its other containers at it. Addresses, not names: the runtime's flag - // takes only addresses, which is also why IP below exists — the resolver has to be somewhere - // its siblings can name before any of them can resolve anything. - Dns []string `json:"dns,omitempty"` - - // Capabilities are the Linux capabilities this container is granted beyond the runtime's - // default set, by name (novox/hq ADR 0170): a holder's runtime that changes the machine's packet - // filter asks for NET_ADMIN. Exactly these, named in the spec so a change recreates the - // container; a privileged container stays undeclarable. - Capabilities []string `json:"capabilities,omitempty"` - - // Logging names where the runtime sends this container's output: "journald" sends it to the - // machine's journal, under the container's name, where what reads the machine's logs — its - // intrusion prevention first of all (novox/hq ADR 0179) — can read it the way it reads the - // machine's own services. Empty keeps the runtime's default, which is a file of the runtime's - // own that nothing but the runtime reads. Part of the spec: a container that logs elsewhere - // is a different container, and the runtime cannot change a running one's driver. - Logging string `json:"logging,omitempty"` - - // Networks are networks this container also joins once created, by name — a found network a - // per-machine setting keeps for a taken container (novox/hq ADR 0163, rule 4), so a - // neighbour that resolves it there keeps resolving it until the neighbour is taken too. - // Joined after creation, because a runtime starts a container on one network; part of the - // container's spec, so a network kept or let go recreates it. - Networks []string `json:"networks,omitempty"` - - // IP is this container's address on its network, passed to the runtime unchanged. - // - // Only meaningful on a user-defined network, and refused by the runtime elsewhere. Exists for - // exactly one shape: a container others must reach *before* name resolution works — a - // module's own DNS resolver being the case that forced it (see Dns). - IP string `json:"ip,omitempty"` - - // RestartOn names resources whose change means this container must be recreated — the same - // field a service has, for the same reason (novox/hq 04-ISSUES/009). A container reads a - // mounted file once at start; a changed file leaves the running process holding the old value, - // while every check passes because the file on disk is right. The host recreates the container - // when one of these resources changed this pass, even if the spec matches. - // - // What a running container reads at creation — its env-files, and a file mounted into it - // directly — is part of its spec by content since novox/hq 04-ISSUES/103, and needs no naming - // here. A directory mounted into it is NOT looked inside, not even for files the host wrote - // there: whether a service reads such a file once or watches it live is the service's, and - // RestartOn is how a module says "once, at start" — a config the host renders under the - // module's state directory, a step whose result it consumes. On a run-once step it means *run - // again*: a step that fetches a fact from a provider names the binding it reads, and is run - // again when the provider moved (novox/hq ADR 0099). - RestartOn []string `json:"restart-on,omitempty"` - - // RunOnce marks a container the host runs to completion rather than leaves running: a step, - // not a service (novox/hq ADR 0052). The host runs it, requires it to exit 0, and records that - // it did — and because the declaration is applied in order and a failed step halts the apply, - // whatever is declared after a run-once container starts only once the step has finished. It is - // how a module runs its own code at first boot — seed a store, migrate, health-gate — under its - // own account (ADR 0047), before the container that depends on it. The record that it ran is - // the digest of this declaration, so a re-apply does not re-run it unless the declaration - // changed. - RunOnce bool `json:"run-once,omitempty"` - - // Schedule marks a container the host runs on a recurring cadence — a five-field cron - // expression (novox/hq ADR 0053). It is the recurring twin of RunOnce: the same container, run - // to completion, but again and again on the clock rather than once. Installing it does not run - // it — the schedule is state that is present, like a running service, so the apply is current as - // soon as it is recorded and does NOT gate what follows. The host's scheduler fires the - // container when the cron is due, re-established from this declaration each apply because the - // declaration is the source of truth (ADR 0018). A run that exits non-zero is recorded and never - // fails the apply or flips the node's state; a run still going when the next is due is skipped - // rather than stacked. It is exclusive with RunOnce and with restart-on: a container runs once - // and gates, runs on a cadence, or stays up — never two of these. - Schedule string `json:"schedule,omitempty"` - - // WhileStopped names resources of the same module — containers — that must be held still for - // the duration of this step's run (novox/hq ADR 0189). The host stops each before the run and - // starts each again after it, **whatever the step did**: a step that failed must leave the - // service running, because the one real risk of this field is a window that never closes. - // - // **For the work a service cannot have done underneath it.** The artifact store's collector - // walks the storage and requires every writer stopped; a run-once step runs beside containers - // and a scheduled one is the same container again, so until this there was no way for a module - // to say it. The predecessor said it with a shell script, which is how the mesh inherited a - // store that has never collected anything. - // - // **Its own module's containers, and only on a schedule.** A module that could quiesce a - // neighbour could stop the mesh. And at apply time the host already has a window — the - // declaration is applied in order and a run-once step gates what follows — so a one-time - // offline job says *before*, not *instead of*; a recurring window is the case order cannot - // express, and the only one this serves. - WhileStopped []string `json:"while-stopped,omitempty"` - - // Health is how this resource is ready (novox/hq ADR 0240 rule 2, Phase B): one kind and its - // timing, judged by the node-engine beside liveness. Absent: judged alive or not, and nothing more. - Health *Health `json:"health,omitempty"` - - // Busy is how this container says it holds work a replacement would lose (novox/hq ADR 0335): asked - // before it is replaced, and while it says busy it is left running, within the bound. Absent: never - // busy, and replaced at once, as before. - Busy *Busy `json:"busy,omitempty"` - - // RunAs is who the container runs as, resolved by the controller from the module's identity and the - // image's declared interface (novox/hq ADR 0325 §2, §3): told by environment (the image reads PUID and - // PGID, say) or by the runtime's own user option. Part of the spec, so a change recreates it. Absent: the - // container runs as its image says. Its key is `identity`, the manifest's word; the field is not, because - // a resource's Identity is its id. - RunAs *ContainerIdentity `json:"identity,omitempty"` -} - -// The ways an image is told who to run as (novox/hq ADR 0325 §2). -const ( - IdentityByEnv = "env" - IdentityByRun = "run" -) - -// ContainerIdentity is who a container runs as, and how its image is told. -type ContainerIdentity struct { - // Mode is IdentityByEnv or IdentityByRun. - Mode string `json:"mode"` - // UserEnv and GroupEnv are the variables the image reads the user's and the group's number from, for - // IdentityByEnv: "PUID", "PGID". - UserEnv string `json:"user_env,omitempty"` - GroupEnv string `json:"group_env,omitempty"` - UID int `json:"uid"` - GID int `json:"gid"` -} - -// UnmarshalJSON reads an identity as strictly as a resource is read. -func (i *ContainerIdentity) UnmarshalJSON(raw []byte) error { - type plain ContainerIdentity - var p plain - if err := strictDecode(raw, &p); err != nil { - return fmt.Errorf("a container's identity: %w", err) - } - *i = ContainerIdentity(p) - return nil -} - -// envName is a variable's name as a runtime takes it. -var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) - -func (i *ContainerIdentity) problems(where string, env map[string]string) []string { - if i == nil { - return nil - } - var problems []string - switch i.Mode { - case IdentityByEnv: - if !envName.MatchString(i.UserEnv) || (i.GroupEnv != "" && !envName.MatchString(i.GroupEnv)) { - problems = append(problems, fmt.Sprintf("%s: an identity told by environment names the variables "+ - "it is told in, and %q and %q are not both names", where, i.UserEnv, i.GroupEnv)) - } - for _, name := range []string{i.UserEnv, i.GroupEnv} { - if _, set := env[name]; set && name != "" { - problems = append(problems, fmt.Sprintf("%s: env sets %s, which the identity sets; one value "+ - "for one variable", where, name)) - } - } - case IdentityByRun: - if i.UserEnv != "" || i.GroupEnv != "" { - problems = append(problems, where+": an identity told by the runtime's user option names no variable") - } - default: - problems = append(problems, fmt.Sprintf("%s: an identity is told by %q or %q, not %q", where, - IdentityByEnv, IdentityByRun, i.Mode)) - } - // Never root (novox/hq ADR 0325 §11): refused here, so nothing of the declaration is made or started. - if i.UID <= 0 || i.GID <= 0 { - problems = append(problems, fmt.Sprintf("%s: the identity is uid %d, gid %d, and a module's identity is "+ - "never root, nor a number below it (novox/hq ADR 0325 §11)", where, i.UID, i.GID)) - } - return problems -} - -func (c *Container) Identity() string { return c.ID } -func (c *Container) Kind() Type { return TypeContainer } -func (c *Container) Target() string { return c.Name } - -func (c *Container) validate(where string, _ bool) []string { - problems := c.Health.problems(where, true, !c.RunOnce && c.Schedule == "") - problems = append(problems, c.Busy.problems(where, !c.RunOnce && c.Schedule == "")...) - if c.Name == "" { - problems = append(problems, where+": a container needs a name") - } - // A run-once step may name what it reads under restart-on. For a step the word means *run - // again*: what a container reads is part of its digest, so a step whose named resource changed - // is a different step and runs again (novox/hq ADR 0099). Not refused. - // A container runs once and gates, on a cadence, or stays up — never two of these - // (novox/hq ADR 0053). run-once and schedule are the two "runs to completion" lifecycles and - // contradict each other, and a scheduled step does not stay running to be brought back by - // restart-on either. Refused here on arrival, as the control plane refuses it near its author. - if c.RunOnce && c.Schedule != "" { - problems = append(problems, where+": a container is run-once or scheduled, not both; "+ - "run-once runs once and gates what follows, a schedule runs it again on a cadence") - } - if c.Schedule != "" && len(c.RestartOn) > 0 { - problems = append(problems, where+": a scheduled container cannot also declare restart-on; "+ - "it runs to completion on its cadence rather than staying running to be restarted") - } - if c.Schedule != "" { - if _, err := ParseCron(c.Schedule); err != nil { - problems = append(problems, where+": "+err.Error()) - } - } - // A maintenance window belongs to a recurring step (novox/hq ADR 0189). Refused on anything - // else here, where the field is; that it names containers of the same module, and not itself, - // is judged against the whole declaration (see whileStoppedNames). - if len(c.WhileStopped) > 0 && c.Schedule == "" { - problems = append(problems, where+": while-stopped needs a schedule; at apply the host "+ - "already has a window — the declaration is applied in order and a run-once step gates "+ - "what follows — so a one-time offline job is declared before what it works on") - } - for _, id := range c.WhileStopped { - if id == c.ID { - problems = append(problems, where+": while-stopped names "+strconv.Quote(id)+ - ", which is this step itself") - } - } - // The runtime's flags take addresses, and a name here would be handed to it verbatim and - // refused at create — after the old container was already removed. Refused on arrival instead. - for _, d := range c.Dns { - if net.ParseIP(d) == nil { - problems = append(problems, where+": dns "+strconv.Quote(d)+" is not an address; "+ - "the runtime's resolver flag takes only addresses") - } - } - if c.IP != "" { - if net.ParseIP(c.IP) == nil { - problems = append(problems, where+": ip "+strconv.Quote(c.IP)+" is not an address") - } - if c.Network == "" { - problems = append(problems, where+": an ip needs a network; the runtime refuses a "+ - "static address anywhere but a user-defined one") - } - } - for _, cap := range c.Capabilities { - if !capabilityName.MatchString(cap) { - problems = append(problems, where+": capabilities names "+strconv.Quote(cap)+", which is not a "+ - "capability's name (CAP_NET_ADMIN or NET_ADMIN)") - } - } - if c.Logging != "" && c.Logging != "journald" { - problems = append(problems, where+": logging is "+strconv.Quote(c.Logging)+", and the only place a "+ - "container's output can be sent besides the runtime's own file is \"journald\"") - } - for _, n := range c.Networks { - problems = append(problems, (&Network{Name: n}).validate(where+": networks", false)...) - if n == c.Network { - problems = append(problems, where+": networks names "+n+", which is already the container's network") - } - } - if len(c.Networks) > 0 && (c.RunOnce || c.Schedule != "") { - problems = append(problems, where+": networks is for a container that keeps running; a step "+ - "runs and exits, and joins nothing afterwards") - } - problems = append(problems, c.RunAs.problems(where+": container "+c.Name, c.Env)...) - return append(problems, checkImage(where, c.Image)...) -} - -// Action runs something the bundle declared, and the host never learns what it means. -type Action struct { - ID string `json:"id"` - Type Type `json:"type"` - // Module is the module this resource belongs to, as the controller says it (novox/hq ADR 0429 §4, issue 1132). Absent - // is an older controller, and the module is then guessed from the id (ModuleGuess). - Module string `json:"module,omitempty"` - Command []string `json:"command"` - // Verify is not optional and is not a courtesy. It is the read-back AND the idempotency - // check: the host does not know what a database is, so "is it already there" is a question - // only the declaration can ask (novox/hq ADR 0005). - Verify []string `json:"verify"` - // In names a container to run inside. Empty means the machine itself. - In string `json:"in,omitempty"` -} - -func (a *Action) Identity() string { return a.ID } -func (a *Action) Kind() Type { return TypeAction } - -func (a *Action) Target() string { - target := strings.Join(a.Command, " ") - if a.In != "" { - return "in " + a.In + ": " + target - } - return target -} - -func (a *Action) validate(where string, allowActions bool) []string { - // The bound the whole security argument rests on (novox/hq ADR 0005). - if !allowActions { - return []string{where + - ": an action arrived over the link, and the link may not carry one. The host " + - "applies declarations of known shape; a command to run is not one. A bundle may " + - "carry an action because it arrives with the binary — anyone able to put a " + - "hostile action there could have put it in the host itself"} - } - var problems []string - if len(a.Command) == 0 { - problems = append(problems, where+": an action needs a command") - } - if len(a.Verify) == 0 { - problems = append(problems, where+ - ": an action needs a verify. An action that runs and reports success without "+ - "reading anything back is the fault this host exists to prevent, and verify is "+ - "also how the host knows whether the action is already done") - } - return problems -} - -// newOf returns an empty resource of a kind, or nil if the kind is unknown. -// -// This is the whole vocabulary, in one place. A kind that is not here cannot be declared. -func newOf(t Type) Resource { - switch t { - case TypeDirectory: - return &Directory{} - case TypeFile: - return &File{} - case TypeService: - return &Service{} - case TypePackage: - return &Package{} - case TypeContainer: - return &Container{} - case TypeAction: - return &Action{} - case TypeNetwork: - return &Network{} - case TypeUser: - return &User{} - case TypeArchive: - return &Archive{} - case TypeAccess: - return &Access{} - case TypeProcess: - return &Process{} - case TypeOpening: - return &Opening{} - } - return nil -} - -// Vocabulary is every kind this host speaks. -func Vocabulary() []Type { - return []Type{ - TypeAccess, TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile, - TypeNetwork, TypeOpening, TypePackage, TypeProcess, TypeService, TypeUser, - } -} - -// Declaration is what a machine should be, in the order it should be made so. -type Declaration struct { - Version int - // For names the node this is meant for. A host with an identity refuses one addressed - // elsewhere; a host without one — the first node, applying the bundle it carries — has - // nothing to check against. - For string - // Resources, in the order they are applied. The host does not sort them: ordering is a - // decision, and deciding is not what the host does (novox/hq ADR 0005). - Resources []Resource - - // Adoption says this node is adopted, and which of its modules have been taken. Nil is a - // converged node — which is every node the mesh raised before adoption existed, and so the - // only form an older controller ever sends (novox/hq ADR 0100). - Adoption *Adoption - - // Sequence orders this declaration against every other the mesh has sent this node: each - // send is one higher than the last, assigned under the control plane's hold on the node - // (novox/hq 04-ISSUES/107). Zero is a declaration that carries no order — every one an older - // controller sent, and the bundle genesis applies — and a host makes no ordering claim about - // one of those. - // - // **The one property a declaration needs that its signature does not give it.** A signature - // says the mesh sent this; it cannot say the mesh sent it AFTER the one the host is holding. - // Before this, "older" was inferred from arrival within a batch and a 750ms window, and a - // backlog longer than the batch, or a slow broker, applied a declaration the mesh had already - // superseded. - Sequence int64 - - // Epoch is the controller's lease epoch this declaration was sent under (novox/hq to-be 45 §6): - // the revision at which the sending controller took the lease. A controller that lost its lease - // and goes on sending sends an older epoch than the holder's, and the node-engine refuses what - // is older than what it applied. Zero is a declaration from a controller without a lease — every - // one sent before the lease existed — and carries no claim. - // - // Inside what is signed, beside the sequence, so a message cannot be given a newer epoch than - // the controller gave it. - Epoch int64 - - // LeftOut names the modules of this machine's set the mesh left out of this declaration, - // because a setting stored for one cannot compose with its definition (novox/hq ADR 0163, - // rule 6). A machine is told everything or nothing about what it IS told; this is what it is - // not told, said. The host keeps what it holds for a left-out module and touches none of - // what it wrote for it — its resources are absent from the declaration, and absence would - // otherwise read as removal. - LeftOut []string - - // Generation is the assignment generation this declaration was composed from: the controller's - // counter, raised in the same transaction as every change to what is assigned where (novox/hq - // issue 234). The sequence orders arrival and cannot tell a later send that carries an older view - // of the assignments; this can. A node-engine refuses a declaration composed from a generation - // older than the highest it applied — unless it is a put-back — because applying it would - // undeclare what the mesh still assigns. Zero is a declaration from a controller that claims none, - // and is applied as before. - Generation int64 - - // PutBack says this declaration is a gate putting a machine back (novox/hq issue 234): it carries - // the generation of what it puts back, which may be older than what the machine applied, and is - // not refused for it. - PutBack bool - - // Identities is the identity of every module assigned to this machine (novox/hq ADR 0328 §4): an account - // and a group `mesh-` with the numbers the controller allocated once for the module, the same on - // every machine. The engine makes each before any resource is applied, and removes what it made of one no - // longer declared. Absent is a controller older than ADR 0325. - Identities []Identity - - // Groups is the group of every library this machine is reached by (novox/hq ADR 0328 §3): `mesh-` - // with the number the controller allocated once, mesh-wide. The engine makes each, and removes what it made of - // one no longer declared. Absent is a controller older than ADR 0328. - Groups []Group - - // Libraries is every library this machine is reached by (ADR 0328 §1, §6, §9): its root here, its group, whether - // this is its home, whether the operator's warrant to take it was carried out, the sub-paths its modules ask for, - // and the tool runner's account to put in its group (§11). Absent is a controller older than ADR 0328. - Libraries []Library - - // Filled is the paths of the files the provisioner fills on this machine (novox/hq ADR 0349 §4, §5): each a - // file this declaration declares, which says who owns it at runtime. The provisioner's client has the - // node-engine write only these. Absent is a controller that hands no file to the provisioner, and every - // file is written from the declaration as before. - Filled []string - - // Provisioner is the account the provisioner's client runs as on this machine, by name (novox/hq ADR 0349 §4): - // the only account whose write of a filled file the node-engine's local write socket accepts. Absent is a - // controller that names none, and every such write is refused. - Provisioner string - - // Builds is the build id of every module this declaration names, by module (novox/hq ADR 0429 §4, issue 1132). - Builds map[string]string - - // declared is what each resource says of its module, by id; nil on a declaration built by hand. - declared map[string]string -} - -// ModuleGuess is the module a resource id names when the declaration does not say: everything before its last dot. -// -// The mesh composes a module's resource ids as `.`, and **a module's name may contain a dot** -// (`novox.be` is one on this mesh) while a resource's own id never does, so the owner is everything before the *last* -// dot. False for what the mesh declares in its own right: the foundation's resources carry no dot, and the -// adoption's are named for the mesh. This is the guess an older controller's declaration is read by; a declaration -// that says `module` is read by ModuleOfID, which never guesses for a resource that says (issue 1132). -func ModuleGuess(id string) (string, bool) { - if strings.HasPrefix(id, AdoptionPrefix) { - return "", false - } - at := strings.LastIndex(id, ".") - if at <= 0 { - return "", false - } - return id[:at], true -} - -// ModuleOfID is the module a resource of this declaration belongs to: the module it says (novox/hq ADR 0429 §4, -// issue 1132), and otherwise the guess from its id (ModuleGuess), so a declaration from an older controller reads as -// it always did. False for what belongs to no module. -func (d *Declaration) ModuleOfID(id string) (string, bool) { - if d != nil { - if d.declared == nil { - for _, r := range d.Resources { - if r.Identity() == id && r.DeclaredModule() != "" { - return r.DeclaredModule(), true - } - } - } else if m, ok := d.declared[id]; ok { - return m, true - } - } - return ModuleGuess(id) -} - -// indexModules keeps what each resource says of its module, so ModuleOfID is a lookup. -func (d *Declaration) indexModules() { - d.declared = map[string]string{} - for _, r := range d.Resources { - if m := r.DeclaredModule(); m != "" { - d.declared[r.Identity()] = m - } - } -} - -// checkProvisioner refuses a provisioner client's account that is no account name, or is root: the account whose -// writes the node-engine accepts must be one that could not write the file without it. -func checkProvisioner(name string) []string { - switch { - case name == "": - return nil - case !accountName.MatchString(name): - return []string{fmt.Sprintf("provisioner_account %q is not an account name", name)} - case name == "root": - return []string{"provisioner_account is root; the provisioner's client runs as an account of its own"} - } - return nil -} - -// Fills reports whether the provisioner fills the file at path on this machine. -func (d *Declaration) Fills(path string) bool { return slices.Contains(d.Filled, path) } - -// checkFilled refuses a filled path that is not a declared file saying who owns it at runtime, or that is named -// twice or empty. Every file declared at the path must say it: the provisioner writes the path, and a file there -// the mesh writes some other way would be a second writer of it (ADR 0349 §5). -func checkFilled(filled []string, resources []Resource) []string { - var problems []string - files := map[string][]*File{} - for _, r := range resources { - if f, ok := r.(*File); ok { - files[f.Path] = append(files[f.Path], f) - } - } - seen := map[string]bool{} - for _, path := range filled { - switch { - case path == "": - problems = append(problems, "filled_files names a file with no path") - continue - case seen[path]: - problems = append(problems, fmt.Sprintf("filled_files names %q twice", path)) - continue - } - seen[path] = true - if len(files[path]) == 0 { - problems = append(problems, fmt.Sprintf("filled_files names %q, and this declaration declares no "+ - "file there", path)) - continue - } - for _, f := range files[path] { - if f.OwnerAtRuntime == "" { - problems = append(problems, fmt.Sprintf("resource %q: the provisioner fills %q, and a file it "+ - "fills says who owns it at runtime with owner-at-runtime", f.ID, path)) - } - } - } - return problems -} - -// Identity is one module's identity, as the controller allocated it (ADR 0328 §4): its user `mesh-` with -// uid, its own group of the same name and number, its primary group gid (its own, or its first library's group), -// and the groups of its other libraries as added groups. -type Identity struct { - Module string `json:"module"` - Name string `json:"name"` - UID int `json:"uid"` - GID int `json:"gid"` - Groups []int `json:"groups,omitempty"` - // Runner is the tool runner's account, put in the module's own group (novox/hq ADR 0334 §2) when its tools run - // on this machine. Absent for a module that declares no tools here, and from a controller older than ADR 0334. - Runner string `json:"runner,omitempty"` -} - -// Group is one library's group, as the controller allocated it (ADR 0328 §3). -type Group struct { - Name string `json:"name"` - GID int `json:"gid"` -} - -// GroupPrefix starts every library group's name, as IdentityPrefix starts every module's. -const GroupPrefix = "mesh-" - -// LibraryAccessPrefix starts the access a library's wait and its take act name: `library:`. -const LibraryAccessPrefix = "library:" - -// Library is one library as this machine sees it (ADR 0328). -type Library struct { - Library string `json:"library"` - // Root is the library's root on this machine: on its home the data itself, elsewhere the mount point the mounts - // module mounts it at. - Root string `json:"root"` - Group string `json:"group"` - GID int `json:"gid"` - // Home is that the library's data lives on this machine: only there does the engine keep it. - Home bool `json:"home"` - // Taken is the controller saying the operator's warrant to take it was carried out (§7). - Taken bool `json:"taken,omitempty"` - // Paths are the sub-paths its modules ask for, relative to the root. - Paths []string `json:"paths,omitempty"` - // Runner is the tool runner's account, put in the library's group (§11). Absent when no module's tools use it. - Runner string `json:"runner,omitempty"` -} - -// checkLibraries judges the groups and libraries on arrival: every library's group declared, named for it, never -// root's number; paths that stay below the root; a runner that is an account's name; and no two roots that nest -// (§13). -func checkLibraries(groups []Group, libs []Library) []string { - var problems []string - byName := map[string]int{} - numbers := map[int]string{} - for _, g := range groups { - where := fmt.Sprintf("group %q", g.Name) - if !strings.HasPrefix(g.Name, GroupPrefix) || !accountName.MatchString(g.Name) { - problems = append(problems, fmt.Sprintf("%s: a library's group is named %s", where, GroupPrefix)) - } - if g.GID <= 0 { - problems = append(problems, fmt.Sprintf("%s: gid %d, and a library's group is never root, nor a number "+ - "below it (novox/hq ADR 0328 §15); nothing of this declaration was made", where, g.GID)) - } - if _, twice := byName[g.Name]; twice { - problems = append(problems, where+": declared twice") - } - if other, used := numbers[g.GID]; used { - problems = append(problems, fmt.Sprintf("%s: gid %d is %s's too", where, g.GID, other)) - } - byName[g.Name], numbers[g.GID] = g.GID, g.Name - } - seen := map[string]bool{} - for _, l := range libs { - where := fmt.Sprintf("library %q", l.Library) - switch { - case l.Library == "" || strings.ContainsAny(l.Library, "/ \t\n"): - problems = append(problems, where+": a library is named by one word") - case seen[l.Library]: - problems = append(problems, where+": declared twice") - } - seen[l.Library] = true - if !filepath.IsAbs(l.Root) || filepath.Clean(l.Root) != l.Root || l.Root == "/" { - problems = append(problems, fmt.Sprintf("%s: its root %q is not an absolute, clean path below /", where, - l.Root)) - } - if gid, ok := byName[l.Group]; !ok || gid != l.GID { - problems = append(problems, fmt.Sprintf("%s: its group %s (gid %d) is not one this declaration makes", - where, l.Group, l.GID)) - } - if l.Group != GroupPrefix+l.Library { - problems = append(problems, fmt.Sprintf("%s: its group is %s%s, and %s is not", where, GroupPrefix, - l.Library, l.Group)) - } - for _, p := range l.Paths { - if p == "" || filepath.IsAbs(p) || filepath.Clean(p) != p || p == ".." || strings.HasPrefix(p, "../") { - problems = append(problems, fmt.Sprintf("%s: the path %q is not a clean path below its root", where, p)) - } - } - if l.Runner != "" && !accountName.MatchString(l.Runner) { - problems = append(problems, fmt.Sprintf("%s: its runner %q is not an account's name", where, l.Runner)) - } - } - for i, a := range libs { - for _, b := range libs[i+1:] { - if a.Root != "" && b.Root != "" && (a.Root == b.Root || strings.HasPrefix(a.Root, b.Root+"/") || - strings.HasPrefix(b.Root, a.Root+"/")) { - problems = append(problems, fmt.Sprintf("library %q: its root and library %q's nest, and libraries "+ - "never overlap (novox/hq ADR 0328 §13)", a.Library, b.Library)) - } - } - } - return problems -} - -// IdentityPrefix starts every module identity's name. -const IdentityPrefix = "mesh-" - -// checkIdentities judges the identities on arrival: each named for its module, never root, and no name or number -// twice — two modules with one account would write as each other. -func checkIdentities(ids []Identity, groups []Group) []string { - var problems []string - names, uids := map[string]bool{}, map[int]string{} - libraryGroups := map[int]bool{} - for _, g := range groups { - libraryGroups[g.GID] = true - } - for _, id := range ids { - where := fmt.Sprintf("identity %q", id.Name) - if id.Module == "" || id.Name != IdentityPrefix+id.Module || !accountName.MatchString(id.Name) { - problems = append(problems, fmt.Sprintf("%s: a module's identity is named %s, and this one is "+ - "for the module %q", where, IdentityPrefix, id.Module)) - } - if id.UID <= 0 || id.GID <= 0 { - problems = append(problems, fmt.Sprintf("%s: uid %d, gid %d, and a module's identity is never root, "+ - "nor a number below it (novox/hq ADR 0328 §15); nothing of this declaration was made", where, - id.UID, id.GID)) - } - if names[id.Name] { - problems = append(problems, where+": declared twice") - } - if other, used := uids[id.UID]; used { - problems = append(problems, fmt.Sprintf("%s: uid %d is %s's too", where, id.UID, other)) - } - // Its primary group is its own (gid == uid) or a library's this declaration makes (ADR 0328 §4); so is - // every added group. A module's own number is never a library group's. - if id.GID != id.UID && !libraryGroups[id.GID] { - problems = append(problems, fmt.Sprintf("%s: its primary group gid %d is neither its own (%d) nor a "+ - "library's this declaration makes", where, id.GID, id.UID)) - } - if libraryGroups[id.UID] { - problems = append(problems, fmt.Sprintf("%s: uid %d is a library group's number too", where, id.UID)) - } - for _, g := range id.Groups { - if !libraryGroups[g] { - problems = append(problems, fmt.Sprintf("%s: its added group gid %d is not a library's this "+ - "declaration makes", where, g)) - } - } - if id.Runner != "" && !accountName.MatchString(id.Runner) { - problems = append(problems, fmt.Sprintf("%s: its runner %q is not an account's name", where, id.Runner)) - } - names[id.Name], uids[id.UID] = true, id.Name - } - return problems -} - -// LeftOutModuleOf says which left-out module a recorded resource belongs to, if any: its id is the -// module's name, a dot, and the module's own id for it. A module's name may contain a dot, so the -// longest left-out name that prefixes the id wins; a false match keeps a thing an apply would -// otherwise remove, which is the conservative mistake. -func (d *Declaration) LeftOutModuleOf(id string) (string, bool) { - best := "" - for _, m := range d.LeftOut { - if strings.HasPrefix(id, m+".") && len(m) > len(best) { - best = m - } - } - return best, best != "" -} - -// Adoption is a node's mode, as the controller records it: the node is adopted, and these are -// the modules taken on it so far (novox/hq ADR 0100). -// -// **Authoritative, and only ever stated by the controller.** A host does not work out whether it -// is adopted; it is told, in every declaration, so a host restarted from the declaration it kept -// is in the same mode it was in before. -// -// Untaken names, per module assigned here and not yet taken, the ids of its resources. Any kind -// may be listed: a file, a directory, a service's unit or a container can already be on the -// machine, and an action run inside a held container reaches what was found (novox/hq ADR 0103); -// the host decides per kind what can be held. The host cannot split a resource id into its -// module, because module names may contain dots, so the controller says which ids belong to which -// module rather than leaving the host to guess. -type Adoption struct { - Taken []string `json:"taken"` - Untaken map[string][]string `json:"untaken,omitempty"` -} - -// capabilityName is what a Linux capability is called: upper case, underscores, an optional CAP_ -// prefix. The runtime accepts either spelling. -var capabilityName = regexp.MustCompile(`^(CAP_)?[A-Z][A-Z0-9_]*$`) - -// AdoptionPrefix is the id prefix of what the mesh itself declares because a node is adopted — -// its openings and its guard. Nothing under it belongs to a module, so none of it is ever held. -const AdoptionPrefix = "adoption." - -// UntakenModuleOf says which untaken module declares a resource, if any. -func (a *Adoption) UntakenModuleOf(id string) (string, bool) { - if a == nil { - return "", false - } - for module, ids := range a.Untaken { - if slices.Contains(ids, id) { - return module, true - } - } - return "", false -} - -// checkAdoption holds what an adoption says against the resources beside it. Every problem is a -// refusal: a host that misread which module is untaken would replace a predecessor's service the -// operator never took. -func checkAdoption(a *Adoption, resources []Resource, allowActions bool) []string { - if a == nil { - return nil - } - if allowActions { - // The bundle is carried with the binary and raises a foundation before any mesh exists. - // Whether a node is adopted is the controller's record, and a bundle that claimed it would - // be the host deciding its own mode (novox/hq ADR 0100). - return []string{"a carried bundle says the node is adopted, and only the mesh can say " + - "that: a node's mode is the controller's record, sent in every declaration"} - } - kinds := map[string]Type{} - for _, r := range resources { - kinds[r.Identity()] = r.Kind() - } - var problems []string - for _, module := range a.Taken { - if _, both := a.Untaken[module]; both { - problems = append(problems, fmt.Sprintf( - "adoption: the module %q is said to be both taken and untaken", module)) - } - } - owner := map[string]string{} - modules := make([]string, 0, len(a.Untaken)) - for module := range a.Untaken { - modules = append(modules, module) - } - sort.Strings(modules) - for _, module := range modules { - for _, id := range a.Untaken[module] { - if strings.HasPrefix(id, AdoptionPrefix) { - problems = append(problems, fmt.Sprintf( - "adoption: %q is the mesh's own and belongs to no module, so it cannot be untaken", id)) - continue - } - if first, twice := owner[id]; twice { - problems = append(problems, fmt.Sprintf( - "adoption: %q is said to belong to both %q and %q", id, first, module)) - continue - } - owner[id] = module - if _, declared := kinds[id]; !declared { - problems = append(problems, fmt.Sprintf( - "adoption: %q of the untaken module %q is not in this declaration", id, module)) - } - } - } - return problems -} - -// RefusalError refuses a whole declaration, naming every problem at once. -// -// Every problem rather than the first: a caller fixing one at a time learns the next only by -// running again, and a declaration is generated, so a person reading this is debugging the -// generator. -type RefusalError struct { - Problems []string -} - -func (e *RefusalError) Error() string { - return fmt.Sprintf( - "this declaration is refused, and none of it was applied:\n - %s\n\n"+ - "A host that applied the parts it understood would leave a machine that looks "+ - "configured and is not.", - strings.Join(e.Problems, "\n - ")) -} - -// Parse reads a declaration that arrived over the link, and refuses anything it does not fully -// understand — including any action, which the link may not carry (novox/hq ADR 0005). -func Parse(raw []byte) (*Declaration, error) { return parse(raw, false) } - -// ParseTrusted reads a declaration from a source already as privileged as the host itself: the -// bundle it carries, or a file handed to it by someone who is running it as root. -// -// Actions are permitted here and nowhere else. The asymmetry is deliberate and is the entire -// content of ADR 0005: refusing actions from the bundle buys nothing, because whoever built the -// bundle built the binary; refusing them from the link buys the bound on what a compromised -// control plane can express. -func ParseTrusted(raw []byte) (*Declaration, error) { return parse(raw, true) } - -// envelope is the declaration with its resources still unread. -// -// Two passes, because which fields are legal depends on the "type" inside each resource. The -// first pass takes the envelope and each resource's bytes; the second decodes each one into -// the struct for its kind, strictly. -type envelope struct { - Version int `json:"declaration"` - For string `json:"for,omitempty"` - Adoption *Adoption `json:"adoption,omitempty"` - // OwnsNothing is the control plane saying, explicitly, that this node's declaration is empty - // on purpose — it owns nothing the mesh put there (novox/hq issue 127). Without it an empty - // resources list is refused as a likely mistake; with it the node applies the empty - // declaration and drops what it last held. The two are distinguished because a truncated or - // mis-composed body arrives as empty too, and a host that could not tell them apart would let - // a bug quietly strip a machine. - OwnsNothing bool `json:"owns_nothing,omitempty"` - Resources []json.RawMessage `json:"resources"` - // Sequence is optional on the wire, so a controller that does not send one is still - // understood: absent reads as zero, which is "no ordering claimed" rather than "first". - Sequence int64 `json:"sequence,omitempty"` - // Epoch is optional on the wire as the sequence is: absent is a controller without a lease. - // **An older host refuses this key**, decoding strictly; a controller sends it only to a host - // whose reports carry a report sequence, which a host that reads it does. - Epoch int64 `json:"epoch,omitempty"` - // LeftOut is optional on the wire too, and absent when nothing was left out (ADR 0163). - LeftOut []string `json:"left_out,omitempty"` - // Generation and PutBack are optional on the wire too (novox/hq issue 234): absent is a controller - // that claims no generation. **An older host refuses these keys**, decoding strictly; a controller - // sends them only to a host whose reports say `reads_generation`. - Generation int64 `json:"generation,omitempty"` - PutBack bool `json:"put_back,omitempty"` - // Identities is optional on the wire too (novox/hq ADR 0325 §3, ADR 0328 §4). **An older host refuses this key**; a - // controller sends it only to a host whose health says link.IdentityContract. - Identities []Identity `json:"identities,omitempty"` - // Groups and Libraries are optional too (novox/hq ADR 0328). **An older host refuses these keys**; a controller - // sends them only to a host whose health says link.LibraryContract. - Groups []Group `json:"groups,omitempty"` - Libraries []Library `json:"libraries,omitempty"` - // Filled is optional too (novox/hq ADR 0349 §5), as is a file's `owner-at-runtime`. **An older host refuses - // these keys**; a controller sends them only to a host whose health says link.OwnershipContract. - Filled []string `json:"filled_files,omitempty"` - // Provisioner is optional too (novox/hq ADR 0349 §4). **An older host refuses this key**; a controller sends it - // only to a host whose health says link.ProvisionerContract. - Provisioner string `json:"provisioner_account,omitempty"` - // Builds is optional too (novox/hq ADR 0429 §4, issue 1132): the build id of each module the declaration names, and - // every resource's `module`. **An older host refuses these keys**; a controller sends them only to a host whose - // reports say `reads_modules`. - Builds map[string]string `json:"builds,omitempty"` -} - -func parse(raw []byte, allowActions bool) (*Declaration, error) { - var env envelope - if err := strictDecode(raw, &env); err != nil { - return nil, &RefusalError{Problems: []string{"not a declaration: " + err.Error()}} - } - - if env.Version != Version { - // Everything below assumes the vocabulary, so there is nothing further to say. - return nil, &RefusalError{Problems: []string{fmt.Sprintf( - "declaration version %d; this host speaks version %d. Refused whole rather than "+ - "partly, so a newer vocabulary is never half-applied by an older host", - env.Version, Version)}} - } - - d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption, Sequence: env.Sequence, - Epoch: env.Epoch, LeftOut: env.LeftOut, Generation: env.Generation, PutBack: env.PutBack, - Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries, Filled: env.Filled, - Provisioner: env.Provisioner, Builds: env.Builds} - problems := checkIdentities(env.Identities, env.Groups) - problems = append(problems, checkProvisioner(env.Provisioner)...) - problems = append(problems, checkLibraries(env.Groups, env.Libraries)...) - if env.Generation < 0 { - // Below zero would read as "none claimed" and pass every refusal of an older generation. - problems = append(problems, fmt.Sprintf("an assignment generation below zero (%d) is not one the "+ - "mesh assigns", env.Generation)) - } - if env.PutBack && allowActions { - // A put-back is a gate's, sent by the mesh; a carried bundle puts nothing back. - problems = append(problems, "a carried bundle says it is a put-back, and only the mesh's gate can say that") - } - if env.Sequence < 0 || env.Epoch < 0 { - // Below zero is no order any controller assigns, and read as "none claimed" it would let the - // declaration past every refusal of what is older. - problems = append(problems, fmt.Sprintf("an order below zero (epoch %d, sequence %d) is not "+ - "one the mesh assigns", env.Epoch, env.Sequence)) - } - if len(env.LeftOut) > 0 && allowActions { - // The bundle is carried with the binary and leaves nothing out: which module a setting - // stopped composing for is the mesh's record (ADR 0163). - problems = append(problems, "a carried bundle says modules were left out, and only the "+ - "mesh can say that") - } - for _, m := range env.LeftOut { - if strings.TrimSpace(m) == "" { - problems = append(problems, "left_out names a module with no name") - } - } - - if len(env.Resources) == 0 && !env.OwnsNothing { - problems = append(problems, "no resources. An empty declaration is a mistake, not a "+ - "machine with nothing on it — say so with an explicit empty list if that is meant") - } - - seen := map[string]int{} - for i, rawResource := range env.Resources { - // Peek, leniently. This pass only needs to know which struct to decode into; reading - // strictly here would report an unknown field before knowing which fields are known. - var head struct { - ID string `json:"id"` - Type Type `json:"type"` - } - _ = json.Unmarshal(rawResource, &head) - - where := fmt.Sprintf("resource %d", i) - if head.ID != "" { - where = fmt.Sprintf("resource %q", head.ID) - } - - if head.ID == "" { - problems = append(problems, where+": no id. Identity is what lets the host know "+ - "this is the same resource it applied last time") - } else if first, ok := seen[head.ID]; ok { - problems = append(problems, fmt.Sprintf( - "%s: id already used by resource %d. Two resources with one identity cannot "+ - "both be tracked", where, first)) - } else { - seen[head.ID] = i - } - - resource := newOf(head.Type) - if resource == nil { - problems = append(problems, fmt.Sprintf( - "%s: unknown type %q. This host understands %s", where, head.Type, vocabulary())) - continue - } - - // A field the kind does not have is refused, and the struct is what says so — there - // is no list of exclusions for anyone to keep current. - // - // Asked separately rather than taken from the decoder's error, because the decoder - // stops at the first unknown field and this record promises every problem at once. A - // caller fixing one field at a time learns the next only by running again. - if unknown := unknownFields(rawResource, resource); len(unknown) > 0 { - for _, field := range unknown { - problems = append(problems, fmt.Sprintf( - "%s: a %s does not use %q, and it is set. Refused rather than ignored", - where, head.Type, field)) - } - continue - } - if err := json.Unmarshal(rawResource, resource); err != nil { - problems = append(problems, fmt.Sprintf("%s: %s", where, err)) - continue - } - - problems = append(problems, resource.validate(where, allowActions)...) - d.Resources = append(d.Resources, resource) - } - problems = append(problems, checkWhileStopped(d.Resources)...) - problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...) - problems = append(problems, checkReplaces(d.Resources)...) - problems = append(problems, checkFilled(env.Filled, d.Resources)...) - if env.Adoption == nil { - for _, r := range d.Resources { - if r.Kind() == TypeOpening { - // On a converged node the mesh's own filter admits what is declared, and the - // found firewall is retired; an opening there would be a rule in a firewall the - // mesh has disabled (novox/hq ADR 0100). - problems = append(problems, fmt.Sprintf( - "resource %q: an opening is for an adopted node, and this declaration does not "+ - "say the node is adopted", r.Identity())) - } - if svc, ok := r.(*Service); ok && svc.TakesOver != nil { - // A tunnel is taken over on an adopted node, where what is found is kept: on a - // converged one there is nothing found to take over, and stopping a unit the - // mesh did not declare would be the host deciding (novox/hq ADR 0105). - problems = append(problems, fmt.Sprintf( - "resource %q: taking over a tunnel is for an adopted node, and this declaration "+ - "does not say the node is adopted", r.Identity())) - } - } - } - - if len(problems) > 0 { - return nil, &RefusalError{Problems: problems} - } - d.indexModules() - return d, nil -} - -// checkWhileStopped judges a maintenance window against the whole declaration (novox/hq ADR 0189). -// -// A step may hold still only a container that is **here** — in this same declaration, which is to -// say on this machine and placed by the mesh. That is what makes it the module's own: a node's -// declaration carries one module's resources beside another's, so the id must also be a container -// and not a file or a directory, which there would be nothing to stop. -// -// Refused on arrival rather than discovered at the first fire. A window that names something the -// host cannot stop is a window that opens at 03:00 and reports nothing until somebody reads a log. -func checkWhileStopped(resources []Resource) []string { - containers := map[string]bool{} - for _, r := range resources { - if r.Kind() == TypeContainer { - containers[r.Identity()] = true - } - } - var problems []string - for _, r := range resources { - c, ok := r.(*Container) - if !ok { - continue - } - for _, id := range c.WhileStopped { - if containers[id] { - continue - } - problems = append(problems, fmt.Sprintf( - "resource %q: while-stopped names %q, and this declaration has no container by "+ - "that id. A step may hold still only a container placed on this machine "+ - "beside it", c.ID, id)) - } - } - return problems -} - -// accountName is what an identity's or an access user's name may be: the portable account name the user tools -// take, optionally ending in `$`, never starting with a dash — it is handed to them as an argument. -var accountName = regexp.MustCompile(`^[A-Za-z0-9_.][A-Za-z0-9_.-]*\$?$`) - -func strictDecode(raw []byte, into any) error { - // DisallowUnknownFields is the whole point rather than strictness for its own sake: a - // field the host does not know is a thing the control plane believes it asked for. - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(into); err != nil { - return err - } - // And **nothing after it**. A decoder reads one value and stops, so a file holding a - // declaration followed by anything at all — a truncated rewrite, two declarations - // concatenated, a stray line from whatever wrote the file — parses as the first value and the - // rest is never looked at. - // - // That is the same fault this host refuses everywhere else, in its quietest form: the machine - // applies something, reports success, and what it applied is not what the file says. Found - // when a test harness appended a line to a bundle by accident and every apply kept working. - if _, err := dec.Token(); err != io.EOF { - return fmt.Errorf( - "there is more in this file after the declaration ends. Refused whole: a file with " + - "something after it may be a truncated rewrite or two declarations run together, " + - "and applying the first would be applying something nobody wrote") - } - return nil -} - -// unknownFields names every JSON key the kind's struct has no field for. -// -// The struct's own tags are the list of what is legal, so adding a field to a kind is the -// whole of adding it — there is nowhere else that has to agree. -func unknownFields(raw []byte, into Resource) []string { - var got map[string]json.RawMessage - if err := json.Unmarshal(raw, &got); err != nil { - return nil // not an object; the decode below will say so properly - } - - known := map[string]bool{} - t := reflect.TypeOf(into).Elem() - for i := 0; i < t.NumField(); i++ { - name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",") - if name != "" && name != "-" { - known[name] = true - } - } - - var unknown []string - for field := range got { - if !known[field] { - unknown = append(unknown, field) - } - } - sort.Strings(unknown) - return unknown -} - -func checkMode(where, mode string) []string { - if mode == "" { - return nil - } - if len(mode) != 4 || mode[0] != '0' { - return []string{fmt.Sprintf( - "%s: mode %q; write it as four octal digits such as \"0644\", so it means the "+ - "same thing here as it does in the manifest it came from", where, mode)} - } - for _, c := range mode[1:] { - if c < '0' || c > '7' { - return []string{fmt.Sprintf("%s: mode %q is not octal", where, mode)} - } - } - return nil -} - -// checkImage insists on content, not on a name. -// -// A tag moves and a digest does not. The bundle's whole claim is that what it names is exact -// (novox/hq ADR 0006), and a bundle pinning `postgres:17` pins nothing — it names whatever -// that tag points at on the day the host happens to run. -// -// **Two forms say something exact, and only one of them needs a registry.** `name@sha256:…` is a -// manifest digest, which a registry assigns on push. A bare `sha256:…` is an image the machine -// already holds, addressed by the digest of its own configuration — equally immutable, equally -// unforgeable, and requiring nothing to have served it. -// -// That second form is what a first machine needs. The mesh's own control plane exists in no public -// registry and never will: it is built from source, and until this mesh has a registry of its own -// there is nowhere to push it to and therefore no manifest digest to name it by. Insisting on one -// would mean a registry has to exist before the thing that lets a mesh have a registry can start — -// which is not a pin, it is a dependency the rule accidentally created. A machine that built an -// image, or was handed one, can name it by what it is. -func checkImage(where, image string) []string { - if image == "" { - return []string{where + ": a container needs an image"} - } - // An image this machine holds, named by the digest of its own configuration. - if strings.HasPrefix(image, "sha256:") { - if len(image) != len("sha256:")+64 { - return []string{fmt.Sprintf( - "%s: image id %q is not a sha256 digest", where, image)} - } - return nil - } - name, digest, found := strings.Cut(image, "@") - if !found || name == "" { - return []string{fmt.Sprintf( - "%s: image %q is not pinned. Write it as name@sha256:… — or as sha256:… for an image "+ - "this machine already holds. A tag moves, and a bundle that pinned a tag would "+ - "not be pinned", where, image)} - } - if !strings.HasPrefix(digest, "sha256:") || len(digest) != len("sha256:")+64 { - return []string{fmt.Sprintf( - "%s: image digest %q is not a sha256 digest", where, digest)} - } - return nil -} - -func vocabulary() string { - kinds := Vocabulary() - names := make([]string, 0, len(kinds)) - for _, t := range kinds { - names = append(names, string(t)) - } - sort.Strings(names) - return strings.Join(names, ", ") -} - -// ParseFileTrusted reads a declaration from a file somebody handed this host. -// -// The same as ParseTrusted, and it allows whole-line `//` comments first. A pinned, hand-authored -// artefact that nobody can annotate is one nobody can review — the foundation bundle is mostly -// explanation of why each digest is what it is. -// -// **Only for a file, never for the link.** Over the link the format stays exactly JSON, because -// a wire format with a second thing to strip is a wire format with a second thing to disagree -// about. -// -// It exists because there were two readers for one file: the bundle stripped comments and `apply` -// did not, so the example bundle in this repository could be built into a binary and not applied -// from disk. The failure was `invalid character '/'`, which names the symptom and not the cause. -func ParseFileTrusted(raw []byte) (*Declaration, error) { - return ParseTrusted(stripComments(raw)) -} - -// stripComments removes whole lines beginning with `//`. -// -// Only whole lines: anything cleverer would need to know where strings begin and end, and a -// parser that half-understands its input is worse than one that does not try. A `//` inside a -// value — every image reference has one — is untouched. -func stripComments(raw []byte) []byte { - var kept []string - for _, line := range strings.Split(string(raw), "\n") { - if strings.HasPrefix(strings.TrimSpace(line), "//") { - continue - } - kept = append(kept, line) - } - return []byte(strings.Join(kept, "\n")) -} - -// checkReplaces refuses a `replaces` naming something this same declaration still declares, or -// named by two processes (novox/hq issue 213). What is replaced is what the declaration no longer -// says — a resource still declared is applied, not handed over, and one handed to two replacements -// would go when the first of them came up, whatever became of the second. -func checkReplaces(resources []Resource) []string { - declared := map[string]bool{} - for _, r := range resources { - declared[r.Identity()] = true - } - var problems []string - by := map[string]string{} - for _, r := range resources { - p, ok := r.(*Process) - if !ok { - continue - } - for _, id := range p.Replaces { - if declared[id] { - problems = append(problems, fmt.Sprintf( - "resource %q: replaces %q, which this declaration still declares — a process "+ - "replaces what the declaration no longer says", p.ID, id)) - } - if other, taken := by[id]; taken && other != p.ID { - problems = append(problems, fmt.Sprintf( - "resource %q: replaces %q, which %q replaces too — one thing has one replacement", - p.ID, id, other)) - } - by[id] = p.ID - } - } - return problems -} diff --git a/vendor/github.com/novox/mesh-host/internal/declaration/health.go b/vendor/github.com/novox/mesh-host/internal/declaration/health.go deleted file mode 100644 index d6395bf7..00000000 --- a/vendor/github.com/novox/mesh-host/internal/declaration/health.go +++ /dev/null @@ -1,188 +0,0 @@ -package declaration - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "time" -) - -// Health is how a long-running resource is ready, as the controller composed it from the module's -// `health` (novox/hq ADR 0240 rule 2, to-be 48 §2–§3, Phase B): one kind and its timing, the endpoint -// already the port this machine published it on. -// -// **The node-engine runs every kind and owns every verdict.** http and tcp it makes itself, from the -// machine to the port; unit it reads from the service manager it already reads; exec and runtime it hands -// to the container runtime as the container's own check, with this timing, and reads the state; tool it -// asks of its own node tools. Nothing else on the machine sets a container's check. -// -// Refused here as the controller refuses it near the author, in the same bounds: an engine that took a -// check it could not judge would say a module ready that nothing looked at. -type Health struct { - Kind string `json:"kind"` - // Endpoint is the module's name for what Port is: for the words a verdict is said in. - Endpoint string `json:"endpoint,omitempty"` - Port int `json:"port,omitempty"` - Path string `json:"path,omitempty"` - Status int `json:"status,omitempty"` - Body string `json:"body,omitempty"` - Scheme string `json:"scheme,omitempty"` - Command string `json:"command,omitempty"` - Tool string `json:"tool,omitempty"` - Interval string `json:"interval"` - Timeout string `json:"timeout"` - Looks int `json:"looks"` - Grace string `json:"grace"` - // Needs is the provision the check exercises (to-be 48 §6): said with every verdict, so the - // controller can hold what it finds under the provider's own condition. - Needs string `json:"needs,omitempty"` -} - -// UnmarshalJSON reads a health strictly, as everything a declaration carries is read: a field this host -// does not know is a part of the check the controller believes it asked for, and nothing would look at it. -func (h *Health) UnmarshalJSON(raw []byte) error { - type plain Health - var p plain - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&p); err != nil { - return fmt.Errorf("health: %w", err) - } - *h = Health(p) - return nil -} - -// The kinds. -const ( - HealthRuntime = "runtime" - HealthHTTP = "http" - HealthTCP = "tcp" - HealthExec = "exec" - HealthUnit = "unit" - HealthTool = "tool" -) - -// The bounds (ADR 0240 rule 2) — the controller's, held again here. -const ( - HealthIntervalFloor = 10 * time.Second - HealthLooksFloor = 2 - HealthWithin = 5 * time.Minute -) - -// Every, Within and GraceOf are the timing, read. Validated on arrival, so a parse error here is -// impossible on a declaration that was accepted; it reads as zero. -func (h *Health) Every() time.Duration { d, _ := time.ParseDuration(h.Interval); return d } -func (h *Health) Within() time.Duration { d, _ := time.ParseDuration(h.Timeout); return d } -func (h *Health) GraceOf() time.Duration { d, _ := time.ParseDuration(h.Grace); return d } - -// RunByRuntime says the container runtime runs this check as the container's own: exec and runtime. -func (h *Health) RunByRuntime() bool { - return h != nil && (h.Kind == HealthExec || h.Kind == HealthRuntime) -} - -// Words is the check in a few words, as a verdict is said: "http /healthz on web". -func (h *Health) Words() string { - switch h.Kind { - case HealthHTTP: - return "http " + h.Path + " on " + orPort(h.Endpoint, h.Port) - case HealthTCP: - return "tcp on " + orPort(h.Endpoint, h.Port) - case HealthTool: - return "its tool " + h.Tool - case HealthRuntime: - return "its image's own check" - case HealthExec: - return "its command" - case HealthUnit: - return "its unit" - } - return h.Kind -} - -func orPort(endpoint string, port int) string { - if endpoint != "" { - return endpoint - } - return fmt.Sprint(port) -} - -// problems holds a resource's health to its kind and bounds. container says whether the resource is a -// container; longRunning whether it stays up. -func (h *Health) problems(where string, container, longRunning bool) []string { - if h == nil { - return nil - } - var problems []string - say := func(format string, args ...any) { - problems = append(problems, where+": "+fmt.Sprintf(format, args...)) - } - if !longRunning { - say("health is judged on what stays up; a step or a scheduled run is judged by its own outcome") - } - switch h.Kind { - case HealthHTTP, HealthTCP: - if h.Port < 1 || h.Port > 65535 { - say("a %s check needs the port it looks at", h.Kind) - } - case HealthExec: - if !container { - say("an exec check runs inside a container") - } - if strings.TrimSpace(h.Command) == "" { - say("an exec check needs a command") - } - case HealthRuntime: - if !container { - say("a runtime check is a container image's own") - } - case HealthUnit: - if container { - say("a unit check is a service's or a process's own") - } - case HealthTool: - if strings.TrimSpace(h.Tool) == "" { - say("a tool check names the tool") - } - default: - say("health of kind %q; it is runtime, http, tcp, exec, unit or tool", h.Kind) - } - if h.Kind == HealthHTTP { - if !strings.HasPrefix(h.Path, "/") { - say("an http check asks a path starting with /") - } - if h.Status != 0 && (h.Status < 100 || h.Status > 599) { - say("an http check expects status %d, which is not one", h.Status) - } - if h.Scheme != "" && h.Scheme != "http" && h.Scheme != "https" { - say("an http check is over http or https, not %q", h.Scheme) - } - } - if strings.ContainsAny(h.Command, "\n\r") { - say("an exec check's command is one line") - } - every, everyErr := time.ParseDuration(h.Interval) - within, withinErr := time.ParseDuration(h.Timeout) - grace, graceErr := time.ParseDuration(h.Grace) - switch { - case everyErr != nil || withinErr != nil || graceErr != nil: - say("health's interval, timeout and grace are durations") - default: - if every < HealthIntervalFloor { - say("a check looks no more often than every %s, not every %s", HealthIntervalFloor, every) - } - if within <= 0 || within >= every { - say("a look takes more than nothing and less than its interval") - } - if grace < 0 { - say("a grace is not negative") - } - if h.Looks >= HealthLooksFloor && grace+time.Duration(h.Looks)*every > HealthWithin { - say("a grace and the failing looks take at most %s", HealthWithin) - } - } - if h.Looks < HealthLooksFloor { - say("a check is unhealthy after at least %d failing looks, not %d", HealthLooksFloor, h.Looks) - } - return problems -} diff --git a/vendor/github.com/novox/mesh-host/validate/validate.go b/vendor/github.com/novox/mesh-host/validate/validate.go deleted file mode 100644 index aa140890..00000000 --- a/vendor/github.com/novox/mesh-host/validate/validate.go +++ /dev/null @@ -1,38 +0,0 @@ -// Package validate is the node-engine's own judgement of a declaration, for whoever must know before -// a declaration is sent whether a machine would take it (novox/hq to-be 45 §4, D1; §9, the merge gate). -// -// **One validator.** The controller composed declarations the host then refused whole — a manifest -// the catalogue check passed (novox/hq issue 236), a consumer's identity too long for the machine's -// names (issue 263) — because the only validator was the one the host runs as it applies. A second, -// written in the controller from the host's rules, would drift from them the first time either -// changed. So the host's own parsing is exported here, unchanged: what the controller's self-check and -// the merge gate run is what every machine runs. -// -// It judges a declaration as it arrives over the link: actions are refused, as the host refuses them -// from the link (novox/hq ADR 0005). Nothing here touches a machine. -package validate - -import ( - "errors" - - "github.com/novox/mesh-host/internal/declaration" -) - -// Declaration is every problem the node-engine would refuse a declaration for, each in its own words; -// nil when it would take it. The body is the declaration as the controller composes it — the bytes a -// signature is made over — not the signed envelope. -func Declaration(raw []byte) []string { - _, err := declaration.Parse(raw) - if err == nil { - return nil - } - var refused *declaration.RefusalError - if errors.As(err, &refused) { - return refused.Problems - } - return []string{err.Error()} -} - -// Version is the declaration vocabulary this validator speaks: a declaration of another version is -// refused whole by Declaration, as by the host. -const Version = declaration.Version