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 `.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 }