A module left out for an unknown key inside an entry lost its whole manifest, so every consumer of what it provides was refused and its data stopped being copied. Read past only the unknown key, keep its backup lines, and say in the condition what stops.
202 lines
6.4 KiB
Go
202 lines
6.4 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
// A key this controller does not know, in a manifest (novox/hq ADR 0262).
|
|
//
|
|
// Registration refuses it, as it always has. A manifest the store already holds was registered by a newer
|
|
// controller, and is read by this one after a rollback: refusing it there failed the whole catalogue, and
|
|
// with it every plan and every send. Dropping the key silently would be worse — a module running without
|
|
// something its manifest says. So the manifest is read, the module is left out of every machine's
|
|
// declaration by name, and the controller raises a condition until it is updated.
|
|
|
|
// UnknownFieldError is a key a manifest has that this controller does not know, wherever it is: at the
|
|
// top of the manifest or inside a block (a state, an offer, a data item's backup, an own secret, a verb).
|
|
// Typed, because the blocks' decoders wrap the decoder's words in their own.
|
|
type UnknownFieldError struct {
|
|
Field string
|
|
err error
|
|
}
|
|
|
|
func (e *UnknownFieldError) Error() string { return e.err.Error() }
|
|
func (e *UnknownFieldError) Unwrap() error { return e.err }
|
|
|
|
// unknownFieldText is how the JSON decoder words a key its target does not have.
|
|
var unknownFieldText = regexp.MustCompile(`^json: unknown field "([^"]*)"$`)
|
|
|
|
// typedUnknown is err as an UnknownFieldError when it is the decoder refusing an unknown key, else err.
|
|
func typedUnknown(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if m := unknownFieldText.FindStringSubmatch(err.Error()); m != nil {
|
|
return &UnknownFieldError{Field: m[1], err: err}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// asUnknownField is the unknown key err is about, at any depth, or nil.
|
|
func asUnknownField(err error) *UnknownFieldError {
|
|
var u *UnknownFieldError
|
|
if errors.As(typedUnknown(err), &u) {
|
|
return u
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnknownFieldReason is why a module whose stored manifest has a key this controller does not know is
|
|
// left out of a machine's declaration, or "" when it has none.
|
|
func UnknownFieldReason(m Manifest) string {
|
|
if m.unknown == "" {
|
|
return ""
|
|
}
|
|
return m.Module + " uses a field this controller does not know (" + m.unknown + "); it is left out " +
|
|
"until the controller is updated: nothing of it is changed on its machines and its contributions to " +
|
|
"other modules and its open ports stop, while its data is still backed up and it still provides " +
|
|
"what it provides (novox/hq ADR 0262)"
|
|
}
|
|
|
|
// backupView is what of a left-out module still reaches its machine: its data, so the backup holder
|
|
// keeps copying it, and the directories and accesses its data items name. No contribution, shell code
|
|
// or environment of its own: those are what leaving it out stops.
|
|
func backupView(m Manifest) Manifest {
|
|
view := Manifest{Module: m.Module, Data: m.Data, Accesses: m.Accesses}
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["type"]) == "directory" {
|
|
view.Resources = append(view.Resources, r)
|
|
}
|
|
}
|
|
return view
|
|
}
|
|
|
|
// prunedFields is a stored manifest's fields with every key this controller does not know taken out,
|
|
// and the keys taken out (novox/hq ADR 0262). A second pass, after the strict one refused: each
|
|
// top-level field is decoded alone, and where an entry inside it has an unknown key, the one
|
|
// occurrence whose removal moves the decoder past it is removed — never a key of the same name that
|
|
// the entry around it knows. So a left-out module still provides what it provides, and its data and
|
|
// directories are still read, which its backup lines are made from.
|
|
func prunedFields(keys map[string]json.RawMessage) (manifestFields, []string, error) {
|
|
var removed []string
|
|
tree := map[string]any{}
|
|
for k, raw := range keys {
|
|
var v any
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.UseNumber()
|
|
if err := dec.Decode(&v); err != nil {
|
|
return manifestFields{}, nil, err
|
|
}
|
|
tree[k] = v
|
|
}
|
|
for _, k := range sortedAnyKeys(tree) {
|
|
for tries := 0; ; tries++ {
|
|
err := decodesAlone(k, tree[k])
|
|
if err == nil {
|
|
break
|
|
}
|
|
unknown := asUnknownField(err)
|
|
if unknown == nil || tries > 64 {
|
|
return manifestFields{}, nil, err
|
|
}
|
|
if unknown.Field == k {
|
|
delete(tree, k)
|
|
removed = append(removed, k)
|
|
break
|
|
}
|
|
path, ok := removalThatHelps(k, tree[k], unknown.Field, err.Error())
|
|
if !ok {
|
|
// The same key unknown in two entries alike: no one removal changes the words.
|
|
// Every occurrence goes, and the field is judged again.
|
|
if removeEvery(tree[k], unknown.Field) == 0 {
|
|
return manifestFields{}, nil, err
|
|
}
|
|
path = "…." + unknown.Field
|
|
}
|
|
removed = append(removed, k+path)
|
|
}
|
|
}
|
|
raw, err := json.Marshal(tree)
|
|
if err != nil {
|
|
return manifestFields{}, nil, err
|
|
}
|
|
var fields manifestFields
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&fields); err != nil {
|
|
return manifestFields{}, nil, err
|
|
}
|
|
return fields, removed, nil
|
|
}
|
|
|
|
// decodesAlone is whether one top-level field decodes strictly on its own.
|
|
func decodesAlone(k string, v any) error {
|
|
raw, err := json.Marshal(map[string]any{k: v})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var fields manifestFields
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
return dec.Decode(&fields)
|
|
}
|
|
|
|
// removalThatHelps removes, from v, the one occurrence of key whose removal changes what the strict
|
|
// decoder says of field k, and says where it was. Every other occurrence is left as it was.
|
|
func removalThatHelps(k string, v any, key, said string) (string, bool) {
|
|
var found bool
|
|
var where string
|
|
var walk func(node any, at string) bool
|
|
walk = func(node any, at string) bool {
|
|
switch n := node.(type) {
|
|
case map[string]any:
|
|
if value, has := n[key]; has {
|
|
delete(n, key)
|
|
if err := decodesAlone(k, v); err == nil || err.Error() != said {
|
|
found, where = true, at+"."+key
|
|
return true
|
|
}
|
|
n[key] = value
|
|
}
|
|
for _, sub := range sortedAnyKeys(n) {
|
|
if walk(n[sub], at+"."+sub) {
|
|
return true
|
|
}
|
|
}
|
|
case []any:
|
|
for i, item := range n {
|
|
if walk(item, fmt.Sprintf("%s[%d]", at, i)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
walk(v, "")
|
|
return where, found
|
|
}
|
|
|
|
// removeEvery removes key from every object in v, and says how many it removed.
|
|
func removeEvery(v any, key string) int {
|
|
n := 0
|
|
switch node := v.(type) {
|
|
case map[string]any:
|
|
if _, has := node[key]; has {
|
|
delete(node, key)
|
|
n++
|
|
}
|
|
for _, sub := range node {
|
|
n += removeEvery(sub, key)
|
|
}
|
|
case []any:
|
|
for _, item := range node {
|
|
n += removeEvery(item, key)
|
|
}
|
|
}
|
|
return n
|
|
}
|