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

153 lines
5.3 KiB
Go

package delivery
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// CompareComposed compares the compositions a node composed (by key, as compose.Compositions answers them) with the ones
// it runs, as its agent would (issue 1126: 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, when it is composed and not run, or 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) (same []string, differs []ModuleDiffers) {
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
}
for _, m := range sortedKeys(modules) {
a, composedHere := ours[m]
b, runs := theirs[m]
switch {
case !runs:
differs = append(differs, ModuleDiffers{Module: m, Why: "composed, and the node does not run it"})
case !composedHere:
differs = append(differs, ModuleDiffers{Module: m, Why: "the node runs it, and it is not composed"})
default:
if d, ok := compareOne(m, a, b); ok {
same = append(same, m)
} else {
differs = append(differs, d)
}
}
}
return same, differs
}
// 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)
if errC != nil {
return ModuleDiffers{Module: module, Why: "the composition has no digest: " + errC.Error()}, false
}
dr, errR := ComposedDigestOf(running)
if errR == nil && dc == dr {
return ModuleDiffers{}, true
}
// 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 ModuleDiffers{Module: module, Why: "the compositions differ and cannot be read resource by resource"}, false
}
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 ModuleDiffers{Module: module, Resource: id, Why: "composed, and not in what runs"}, false
case !inA:
return ModuleDiffers{Module: module, Resource: id, Why: "in what runs, and not composed"}, false
case !bytes.Equal(x, y):
return ModuleDiffers{Module: module, Resource: id, Why: "the resource differs"}, false
}
}
switch {
case !jsonEqual(a.Filled, b.Filled):
return ModuleDiffers{Module: module, Why: "the files the provisioner fills differ"}, false
case a.Data != b.Data:
return ModuleDiffers{Module: module, Why: "whether it declares data differs"}, false
case !jsonEqual(a.Own, b.Own):
return ModuleDiffers{Module: module, Why: "the node's own parts differ"}, false
case a.FP != b.FP:
return ModuleDiffers{Module: module, Why: "the fingerprint differs"}, false
}
if dA, err := ComposedDigestOf(a); err == nil {
if dB, err := ComposedDigestOf(b); err == nil && dA == dB {
return ModuleDiffers{Module: module, Why: "the same content, a large file carried whole on one side and by digest on the other"}, false
}
}
return ModuleDiffers{Module: module, Why: "the same resources in another order"}, false
}
// 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
}