// 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"` } // 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"` } // 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"` // 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 + ")" }