Files
mesh-sdk/go/delivery/composed_compare.go
T

168 lines
5.4 KiB
Go

package delivery
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Comparison is what CompareComposed finds, module by module: the same, differing, added by what was composed (not run
// now) and removed by it (run now, not composed). Each list is sorted by module.
type Comparison struct {
Same []string
Differs []ModuleDiffers
Added []ModuleAdded
Removed []string
}
// CompareComposed compares the compositions a node composed (by key, as compose.Compositions answers them) with the ones
// it runs, as its agent would (ADR 0559 §4: a node's answer to a placement candidate). A module is the same when its
// composition's digest is the one it runs (ComposedDigestOf, the signature left out), so the agent would apply nothing
// of it; it differs when the digests differ (both named); it is added when composed and not run, removed when run and
// not composed. The node's own composition is named HaveNodeWord. The bus's user list (catalogue.BusUsersID) is left out
// on both sides: the bus node follows it alone and never applies it from a composition it made (ADR 0547 §2, §8). A
// difference names the first resource, by its id, whose content differs or that one side alone holds; when every
// resource is equal it says what else differs (their order, the files filled, the data, the node's own parts, a file
// carried by digest).
func CompareComposed(composed map[string]Composed, running []Composed) Comparison {
name := func(c Composed) string {
if c.Module == "" {
return HaveNodeWord
}
return c.Module
}
ours := map[string]Composed{}
for _, c := range composed {
ours[name(c)] = c
}
theirs := map[string]Composed{}
for _, c := range running {
theirs[name(c)] = c
}
modules := map[string]bool{}
for m := range ours {
modules[m] = true
}
for m := range theirs {
modules[m] = true
}
var out Comparison
for _, m := range sortedKeys(modules) {
a, composedHere := ours[m]
b, runs := theirs[m]
switch {
case !runs:
dig, _ := ComposedDigestOf(withoutBusUsers(a))
out.Added = append(out.Added, ModuleAdded{Module: m, Composed: dig})
case !composedHere:
out.Removed = append(out.Removed, m)
default:
if d, ok := compareOne(m, a, b); ok {
out.Same = append(out.Same, m)
} else {
out.Differs = append(out.Differs, d)
}
}
}
return out
}
// compareOne is one module's comparison: true when the digests agree, else what differs first.
func compareOne(module string, composed, running Composed) (ModuleDiffers, bool) {
composed, running = withoutBusUsers(composed), withoutBusUsers(running)
dc, errC := ComposedDigestOf(composed)
dr, errR := ComposedDigestOf(running)
if errC != nil {
return ModuleDiffers{Module: module, Running: dr, Why: "the composition has no digest: " + errC.Error()}, false
}
if errR == nil && dc == dr {
return ModuleDiffers{}, true
}
differs := func(resource, why string) (ModuleDiffers, bool) {
return ModuleDiffers{Module: module, Composed: dc, Running: dr, Resource: resource, Why: why}, false
}
// Words for the difference: resource by resource, each side with its large files by digest, so a file carried whole
// and the same file carried by digest read as one.
a, _, errA := composed.ByDigest()
b, _, errB := running.ByDigest()
if errA != nil || errB != nil {
return differs("", "the compositions differ and cannot be read resource by resource")
}
ra, rb := resourcesByID(a.Resources), resourcesByID(b.Resources)
ids := map[string]bool{}
for id := range ra {
ids[id] = true
}
for id := range rb {
ids[id] = true
}
for _, id := range sortedKeys(ids) {
x, inA := ra[id]
y, inB := rb[id]
switch {
case !inB:
return differs(id, "composed, and not in what runs")
case !inA:
return differs(id, "in what runs, and not composed")
case !bytes.Equal(x, y):
return differs(id, "the resource differs")
}
}
switch {
case !jsonEqual(a.Filled, b.Filled):
return differs("", "the files the provisioner fills differ")
case a.Data != b.Data:
return differs("", "whether it declares data differs")
case !jsonEqual(a.Own, b.Own):
return differs("", "the node's own parts differ")
case a.FP != b.FP:
return differs("", "the fingerprint differs")
}
if dA, err := ComposedDigestOf(a); err == nil {
if dB, err := ComposedDigestOf(b); err == nil && dA == dB {
return differs("", "the same content, a large file carried whole on one side and by digest on the other")
}
}
return differs("", "the same resources in another order")
}
// withoutBusUsers is c without the bus's user list resource (its id `bus-users`, or `<module>.bus-users`).
func withoutBusUsers(c Composed) Composed {
out := c
out.Resources = nil
for _, raw := range c.Resources {
var head struct {
ID string `json:"id"`
}
if json.Unmarshal(raw, &head) == nil && (head.ID == catalogue.BusUsersID() || strings.HasSuffix(head.ID, "."+catalogue.BusUsersID())) {
continue
}
out.Resources = append(out.Resources, raw)
}
return out
}
// resourcesByID is each resource canonical, by its id (a resource without one by its place).
func resourcesByID(resources []json.RawMessage) map[string][]byte {
out := make(map[string][]byte, len(resources))
for i, raw := range resources {
var head struct {
ID string `json:"id"`
}
_ = json.Unmarshal(raw, &head)
id := head.ID
if id == "" {
id = "#" + strconv.Itoa(i)
}
canon, err := canonicalJSON(raw)
if err != nil {
canon = raw
}
out[id] = canon
}
return out
}