Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
151 lines
4.7 KiB
Go
151 lines
4.7 KiB
Go
package delivery
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strconv"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// Seals is, by slot, each value one composition carries sealed to its node's key (issue 1126, C1): a file's `sealed`
|
|
// and a vault box (catalogue.VaultSealedField), as their JSON text (a box is carried as its JSON, a sealed value as
|
|
// the string it is). A slot is one field of one resource of the composition's module (SealSlot): a box is reused only
|
|
// at the slot that holds it, never for another resource whose value opens to the same (a provider's grant and its
|
|
// consumer's secret on one node hold one value).
|
|
type Seals map[string]string
|
|
|
|
// SealedFields are the fields of a resource that carry a value sealed to the node's key.
|
|
var SealedFields = []string{"sealed", catalogue.VaultSealedField}
|
|
|
|
// SealSlot is the slot of one sealed field of one resource, by the resource's id (its place, `#<i>`, when it has none).
|
|
// The module is the composition's: a slot is never compared across compositions.
|
|
func SealSlot(resource, field string) string { return resource + "#" + field }
|
|
|
|
// SealsIn is every sealed value c carries, by slot; nil when it carries none.
|
|
func SealsIn(c Composed) Seals {
|
|
var out Seals
|
|
for i, raw := range c.Resources {
|
|
id, fields, ok := sealedFieldsOf(raw, i)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, field := range SealedFields {
|
|
f, has := fields[field]
|
|
if !has {
|
|
continue
|
|
}
|
|
if text := sealedText(f); text != "" {
|
|
if out == nil {
|
|
out = Seals{}
|
|
}
|
|
out[SealSlot(id, field)] = text
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// KeepSeals is c with each sealed value replaced by the one kept at the same slot, where same says the two open to the
|
|
// same value (known and same): a value the vault sealed again is then the box that runs, so the composition is the
|
|
// one that runs (ADR 0547 §3, issue 1126). A slot kept holds nothing for, or whose value differs or cannot be opened,
|
|
// keeps c's. Every other byte of a resource is kept as it is; c itself is not changed.
|
|
func KeepSeals(c Composed, kept Seals, same func(a, b string) (same, known bool)) Composed {
|
|
if len(kept) == 0 || same == nil {
|
|
return c
|
|
}
|
|
out := c
|
|
out.Resources = nil
|
|
for i, raw := range c.Resources {
|
|
id, fields, ok := sealedFieldsOf(raw, i)
|
|
if ok {
|
|
for _, field := range SealedFields {
|
|
f, has := fields[field]
|
|
if !has {
|
|
continue
|
|
}
|
|
ours := sealedText(f)
|
|
theirs, held := kept[SealSlot(id, field)]
|
|
if !held || ours == "" || theirs == ours {
|
|
continue
|
|
}
|
|
if s, known := same(ours, theirs); !known || !s {
|
|
continue
|
|
}
|
|
value := json.RawMessage(theirs)
|
|
if f[0] == '"' {
|
|
value, _ = json.Marshal(theirs)
|
|
}
|
|
if swapped, ok := withField(raw, field, value); ok {
|
|
raw = swapped
|
|
}
|
|
}
|
|
}
|
|
out.Resources = append(out.Resources, raw)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sealedFieldsOf is a resource's id (`#<i>` when it has none) and its top-level fields, false when it is no object.
|
|
func sealedFieldsOf(raw json.RawMessage, i int) (string, map[string]json.RawMessage, bool) {
|
|
var fields map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &fields) != nil {
|
|
return "", nil, false
|
|
}
|
|
var id string
|
|
if f, ok := fields["id"]; !ok || json.Unmarshal(f, &id) != nil || id == "" {
|
|
id = "#" + strconv.Itoa(i)
|
|
}
|
|
return id, fields, true
|
|
}
|
|
|
|
// sealedText is a sealed field as its text: a string as it is, a box as its JSON.
|
|
func sealedText(f json.RawMessage) string {
|
|
var text string
|
|
if json.Unmarshal(f, &text) == nil {
|
|
return text
|
|
}
|
|
var compact bytes.Buffer
|
|
if json.Compact(&compact, f) != nil {
|
|
return string(f)
|
|
}
|
|
return compact.String()
|
|
}
|
|
|
|
// withField is the object raw with its top-level field replaced by value, every other byte as it was (so a composition
|
|
// whose sealed values are those that run is the one that runs, byte for byte); false when raw is no object holding it.
|
|
func withField(raw json.RawMessage, field string, value json.RawMessage) (json.RawMessage, bool) {
|
|
d := json.NewDecoder(bytes.NewReader(raw))
|
|
if t, err := d.Token(); err != nil || t != json.Delim('{') {
|
|
return nil, false
|
|
}
|
|
for d.More() {
|
|
t, err := d.Token()
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
key, _ := t.(string)
|
|
before := d.InputOffset()
|
|
var skip json.RawMessage
|
|
if err := d.Decode(&skip); err != nil {
|
|
return nil, false
|
|
}
|
|
if key != field {
|
|
continue
|
|
}
|
|
after := d.InputOffset()
|
|
// The span holds the colon, any space and the value: kept up to the colon, the value replaced.
|
|
span := raw[before:after]
|
|
colon := bytes.IndexByte(span, ':')
|
|
if colon < 0 {
|
|
return nil, false
|
|
}
|
|
out := make([]byte, 0, len(raw)-len(span)+colon+1+len(value))
|
|
out = append(out, raw[:int(before)+colon+1]...)
|
|
out = append(out, value...)
|
|
out = append(out, raw[after:]...)
|
|
return out, true
|
|
}
|
|
return nil, false
|
|
}
|