348 lines
15 KiB
Go
348 lines
15 KiB
Go
// Package facts is the mesh's facts snapshot: what a check needs to judge a change against the mesh
|
|
// that runs, and nothing it could leak (novox/hq to-be 45 §9, ADR 0227 rule 9).
|
|
//
|
|
// **Why it exists.** Every check the mesh had was right about the world it was given, and none was
|
|
// given the mesh's world: a module passed every test and refused the anchor's whole declaration because
|
|
// a real machine's name made its identity 23 characters (issue 263); a manifest passed the catalogue
|
|
// check and was refused by the node-engine (issue 236); a resolver answer that glibc forgave was final
|
|
// to musl (issue 262). The controller holds those facts. It writes them here, the build seat reads them,
|
|
// and a merge check composes every machine of the snapshot with the change applied.
|
|
//
|
|
// **What it holds, and what it never holds.** Every machine — under a stable pseudonym of the same length
|
|
// as its name, because the length is what a name limit meets — with its roles, system, C library,
|
|
// architecture, builds, what it reported it can do, what is assigned there, its pins and settings;
|
|
// every seat and its holders; every module the mesh holds, as its manifest; the sources the mesh built
|
|
// them from; the versions of the bus, the store and the node-engine it runs; and how each machine's
|
|
// declaration composes today. **No secret and no address**: a setting whose key or value reads as a
|
|
// secret is withheld, an address is replaced by one from a documentation range, and a machine's name, a
|
|
// site, an account and a public domain are replaced wherever they appear. So a snapshot can be copied
|
|
// into a test, a replay or a pull request without carrying anything of the installation it came from.
|
|
package facts
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// Format is the snapshot's version. A reader refuses one newer than it knows: a field it cannot read
|
|
// is a fact it would judge without.
|
|
const Format = 1
|
|
|
|
// Repository and Tag are where the controller keeps the newest snapshot in the artifact store, and
|
|
// MediaType what it is kept as.
|
|
const (
|
|
Repository = "facts"
|
|
Tag = "latest"
|
|
MediaType = "application/vnd.novox.mesh.facts.v1+json"
|
|
)
|
|
|
|
// Facts is one snapshot.
|
|
type Facts struct {
|
|
Format int `json:"facts"`
|
|
// Taken is when the controller composed it.
|
|
Taken time.Time `json:"taken"`
|
|
// Controller is the controller build that composed it — the one the mesh runs, which is the one a
|
|
// change to the catalogue must be readable by (version skew).
|
|
Controller Build `json:"controller"`
|
|
// Versions are what the mesh runs of the things its tests stand in for.
|
|
Versions Versions `json:"versions"`
|
|
// Sources are the repositories the mesh builds modules from, each with the newest commit it built.
|
|
Sources []Source `json:"sources"`
|
|
// Machines, in name order of their pseudonyms.
|
|
Machines []Machine `json:"machines"`
|
|
// Seats are every seat held, with its holders.
|
|
Seats []Seat `json:"seats"`
|
|
// Modules are every module the mesh holds, as it holds them.
|
|
Modules []Module `json:"modules"`
|
|
// Settings are the mesh-wide layer of every module's settings, scrubbed.
|
|
Settings []Settings `json:"settings,omitempty"`
|
|
// Edges are the build dependencies between modules, as the mesh recorded them — what a merge's
|
|
// rebuild width is computed from.
|
|
Edges []Edge `json:"edges,omitempty"`
|
|
// Beside is the ref each repository is cloned at beside a merge check, by the directory it is found
|
|
// under — the commits the mesh runs, the catalogue's main — so a check run by hand reads the siblings
|
|
// the build seat reads (novox/hq issue 286).
|
|
Beside map[string]string `json:"beside,omitempty"`
|
|
}
|
|
|
|
// Build names one build of a core component.
|
|
type Build struct {
|
|
Version string `json:"version,omitempty"`
|
|
Commit string `json:"commit,omitempty"`
|
|
}
|
|
|
|
// Versions are what the mesh runs.
|
|
type Versions struct {
|
|
// Bus is the bus server's release, as the server tells a client that connects.
|
|
Bus string `json:"bus,omitempty"`
|
|
// Store is the store's server version, as the store answers it.
|
|
Store string `json:"store,omitempty"`
|
|
// NodeEngines are the node-engine builds the machines report, each once.
|
|
NodeEngines []string `json:"node-engines,omitempty"`
|
|
// Toolchains are the toolchain images a merge check runs in, by language, as the mesh holds them —
|
|
// with no address: the artifact store the snapshot is read from is where they are pulled from. What
|
|
// a repository's merge-check.sh runs in on the build seat, and so what it must run in anywhere else
|
|
// (novox/hq issue 286): two releases of one compiler disagree, down to how gofmt lays out a file.
|
|
Toolchains map[string]string `json:"toolchains,omitempty"`
|
|
}
|
|
|
|
// Source is a repository the mesh builds from, and the newest commit it built a module of.
|
|
type Source struct {
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit,omitempty"`
|
|
Modules int `json:"modules"`
|
|
}
|
|
|
|
// Machine is one machine, under its pseudonym.
|
|
type Machine struct {
|
|
Name string `json:"name"`
|
|
// Length is the length of its real name, which the pseudonym keeps.
|
|
Length int `json:"length"`
|
|
// Roles are what it is to the mesh, in words: the control node, the hub, the bus's machine, a
|
|
// holder of a mesh seat. What a check names when it fails one.
|
|
Roles []string `json:"roles,omitempty"`
|
|
// System is the node-engine's system (arch, alpine, …), Libc its C library, as far as the mesh knows.
|
|
System string `json:"system,omitempty"`
|
|
Libc string `json:"libc,omitempty"`
|
|
Architecture string `json:"architecture,omitempty"`
|
|
Kernel string `json:"kernel,omitempty"`
|
|
// NodeEngine and NodeTools are the builds it runs.
|
|
NodeEngine string `json:"node-engine,omitempty"`
|
|
NodeTools string `json:"node-tools,omitempty"`
|
|
// Capabilities are what it reported it can do; the detail kept only where it is a version.
|
|
Capabilities []Capability `json:"capabilities,omitempty"`
|
|
// Site, Hub, Public: where it is on the private network — its site (a pseudonym), whether it is the
|
|
// hub, whether others can dial it. No address.
|
|
Site string `json:"site,omitempty"`
|
|
Hub bool `json:"hub,omitempty"`
|
|
Public bool `json:"public,omitempty"`
|
|
// OnNetwork is whether it has a place on the private network at all.
|
|
OnNetwork bool `json:"on-network,omitempty"`
|
|
// OutwardLinks are the links it reported as facing outside it, scrubbed: a machine that reported none
|
|
// is sent no filter, so a check that raised it without them would compose a machine the mesh does
|
|
// not (novox/hq ADR 0140).
|
|
OutwardLinks []string `json:"outward-links,omitempty"`
|
|
// Adopted is whether the mesh adopted it rather than converged it.
|
|
Adopted bool `json:"adopted,omitempty"`
|
|
// Account is the operator's login there (a pseudonym of the same length), and AccountHome where its
|
|
// home is when that is not the derived one.
|
|
Account string `json:"account,omitempty"`
|
|
AccountHome string `json:"account-home,omitempty"`
|
|
// PublicDomain is the domain it answers for, its labels replaced.
|
|
PublicDomain string `json:"public-domain,omitempty"`
|
|
// Assigned is every module assigned there.
|
|
Assigned []string `json:"assigned,omitempty"`
|
|
// Pins are where it was told a provision comes from.
|
|
Pins []Pin `json:"pins,omitempty"`
|
|
// Settings are its own layer of each module's settings, scrubbed.
|
|
Settings []Settings `json:"settings,omitempty"`
|
|
// Accepted are the secrets a person gave the mesh for modules here, by name only: the mesh cannot
|
|
// make them, so a check composing this machine gives each a stand-in instead of refusing it.
|
|
Accepted []Accepted `json:"accepted,omitempty"`
|
|
// Declaration is how its declaration composes today, as the controller that took this saw it.
|
|
Declaration Declaration `json:"declaration"`
|
|
}
|
|
|
|
// Capability is one thing a machine reported it can or cannot do.
|
|
type Capability struct {
|
|
Name string `json:"name"`
|
|
Present bool `json:"present"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// Pin is one provision a machine was told where to take from.
|
|
type Pin struct {
|
|
Provision string `json:"provision"`
|
|
Machine string `json:"machine"`
|
|
Module string `json:"module,omitempty"`
|
|
}
|
|
|
|
// Accepted is one secret a person gave: the module's own when Provider is empty, else the credential it
|
|
// takes from that machine's provider under Local.
|
|
type Accepted struct {
|
|
Module string `json:"module"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider,omitempty"`
|
|
Local string `json:"local,omitempty"`
|
|
}
|
|
|
|
// Settings is one layer of one module's settings.
|
|
type Settings struct {
|
|
Module string `json:"module"`
|
|
Values map[string]any `json:"values"`
|
|
}
|
|
|
|
// Declaration is how one machine's declaration composed when the snapshot was taken.
|
|
type Declaration struct {
|
|
// Composes is whether it composed and the node-engine's validator took it.
|
|
Composes bool `json:"composes"`
|
|
// Digest is its body's digest — composed as the next push would, so it moves with what the mesh
|
|
// would send, and is not the digest of what was last sent.
|
|
Digest string `json:"digest,omitempty"`
|
|
// Resources are what it declares, as `type:id`, sorted.
|
|
Resources []string `json:"resources,omitempty"`
|
|
// Problems are why it does not compose or validate, scrubbed.
|
|
Problems []string `json:"problems,omitempty"`
|
|
// LeftOut are the modules assigned there and left out of it, each with why.
|
|
LeftOut map[string]string `json:"left-out,omitempty"`
|
|
// Withheld are the consumers its grants leave out because an identity overflows the provision's
|
|
// bound (ADR 0225), and Unbound the credentials on record for consumers bound elsewhere (issue 274),
|
|
// each said in words.
|
|
Withheld []string `json:"withheld,omitempty"`
|
|
Unbound []string `json:"unbound,omitempty"`
|
|
}
|
|
|
|
// Seat is one seat and the machines holding it.
|
|
type Seat struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Holders []Holder `json:"holders"`
|
|
}
|
|
|
|
// Holder is one module on one machine holding a seat.
|
|
type Holder struct {
|
|
Machine string `json:"machine"`
|
|
Module string `json:"module"`
|
|
}
|
|
|
|
// Module is one module the mesh holds.
|
|
type Module struct {
|
|
Name string `json:"name"`
|
|
// Repository and Path are where it is built from; empty for one that came with the controller.
|
|
Repository string `json:"repository,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
// Commit is the commit the manifest the mesh holds was read at.
|
|
Commit string `json:"commit,omitempty"`
|
|
// Provided is a module that came with the controller rather than from a repository.
|
|
Provided bool `json:"provided,omitempty"`
|
|
// RollOut is its upgrade policy: rolled out when built, or recorded.
|
|
RollOut bool `json:"roll-out,omitempty"`
|
|
// Reads are the other repositories its build read source from.
|
|
Reads []string `json:"reads,omitempty"`
|
|
// Manifest is the module as the mesh holds it: artifacts resolved to the builds it runs.
|
|
Manifest json.RawMessage `json:"manifest"`
|
|
}
|
|
|
|
// Edge is one build dependency: From is built standing on To.
|
|
type Edge struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Kind string `json:"kind,omitempty"`
|
|
}
|
|
|
|
// Sorted puts every list in a fixed order, so two snapshots of one mesh are the same bytes apart from
|
|
// when they were taken.
|
|
func (f *Facts) Sorted() {
|
|
sort.Slice(f.Machines, func(i, j int) bool { return f.Machines[i].Name < f.Machines[j].Name })
|
|
for i := range f.Machines {
|
|
m := &f.Machines[i]
|
|
sort.Strings(m.Roles)
|
|
sort.Strings(m.Assigned)
|
|
sort.Slice(m.Capabilities, func(a, b int) bool { return m.Capabilities[a].Name < m.Capabilities[b].Name })
|
|
sort.Slice(m.Pins, func(a, b int) bool { return m.Pins[a].Provision < m.Pins[b].Provision })
|
|
sort.Slice(m.Settings, func(a, b int) bool { return m.Settings[a].Module < m.Settings[b].Module })
|
|
sort.Slice(m.Accepted, func(a, b int) bool {
|
|
x, y := m.Accepted[a], m.Accepted[b]
|
|
return x.Module+"\x00"+x.Name+"\x00"+x.Provider+"\x00"+x.Local < y.Module+"\x00"+y.Name+"\x00"+y.Provider+"\x00"+y.Local
|
|
})
|
|
sort.Strings(m.Declaration.Resources)
|
|
}
|
|
sort.Slice(f.Seats, func(i, j int) bool {
|
|
if f.Seats[i].Name != f.Seats[j].Name {
|
|
return f.Seats[i].Name < f.Seats[j].Name
|
|
}
|
|
return f.Seats[i].Scope < f.Seats[j].Scope
|
|
})
|
|
for i := range f.Seats {
|
|
h := f.Seats[i].Holders
|
|
sort.Slice(h, func(a, b int) bool {
|
|
if h[a].Machine != h[b].Machine {
|
|
return h[a].Machine < h[b].Machine
|
|
}
|
|
return h[a].Module < h[b].Module
|
|
})
|
|
}
|
|
sort.Slice(f.Modules, func(i, j int) bool { return f.Modules[i].Name < f.Modules[j].Name })
|
|
sort.Slice(f.Sources, func(i, j int) bool { return f.Sources[i].Repository < f.Sources[j].Repository })
|
|
sort.Slice(f.Settings, func(i, j int) bool { return f.Settings[i].Module < f.Settings[j].Module })
|
|
sort.Slice(f.Edges, func(i, j int) bool {
|
|
if f.Edges[i].From != f.Edges[j].From {
|
|
return f.Edges[i].From < f.Edges[j].From
|
|
}
|
|
return f.Edges[i].To < f.Edges[j].To
|
|
})
|
|
sort.Strings(f.Versions.NodeEngines)
|
|
}
|
|
|
|
// Encode is the snapshot as it is kept: sorted, indented, so a person can read the one the build seat
|
|
// read and a diff of two says what moved.
|
|
func (f Facts) Encode() ([]byte, error) {
|
|
f.Sorted()
|
|
return json.MarshalIndent(f, "", " ")
|
|
}
|
|
|
|
// Content is the digest of everything but when it was taken: two snapshots of an unchanged mesh have
|
|
// the same content, which is how the controller tells a change from another day.
|
|
func (f Facts) Content() (string, error) {
|
|
f.Taken = time.Time{}
|
|
body, err := f.Encode()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sum := sha256.Sum256(body)
|
|
return fmt.Sprintf("sha256:%x", sum), nil
|
|
}
|
|
|
|
// Decode reads a snapshot, refusing one newer than this reader knows and one that is not a snapshot.
|
|
func Decode(body []byte) (Facts, error) {
|
|
var f Facts
|
|
if err := json.Unmarshal(body, &f); err != nil {
|
|
return Facts{}, fmt.Errorf("not a facts snapshot: %w", err)
|
|
}
|
|
switch {
|
|
case f.Format == 0:
|
|
return Facts{}, fmt.Errorf("not a facts snapshot: it says no format")
|
|
case f.Format > Format:
|
|
return Facts{}, fmt.Errorf("a facts snapshot of format %d, and this reads format %d: a newer controller "+
|
|
"took it, and what it says beyond %d would be judged without", f.Format, Format, Format)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// Longest is the length of the longest machine name in the snapshot — what a consumer's identity is
|
|
// judged on (novox/hq ADR 0225).
|
|
func (f Facts) Longest() int {
|
|
longest := 0
|
|
for _, m := range f.Machines {
|
|
if m.Length > longest {
|
|
longest = m.Length
|
|
}
|
|
}
|
|
return longest
|
|
}
|
|
|
|
// Machine is the machine of that pseudonym.
|
|
func (f Facts) Machine(name string) (Machine, bool) {
|
|
for _, m := range f.Machines {
|
|
if m.Name == name {
|
|
return m, true
|
|
}
|
|
}
|
|
return Machine{}, false
|
|
}
|
|
|
|
// Described is how a check names a machine: its roles, then its pseudonym.
|
|
func (m Machine) Described() string {
|
|
if len(m.Roles) == 0 {
|
|
return "a machine (" + m.Name + ")"
|
|
}
|
|
out := m.Roles[0]
|
|
for _, r := range m.Roles[1:] {
|
|
out += ", " + r
|
|
}
|
|
return out + " (" + m.Name + ")"
|
|
}
|