153 lines
5.3 KiB
Go
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
|
|
}
|