An unplaceable line of a left-out module, such as an access nobody placed, failed the whole machine's declaration. Say it among what could not be placed instead, never copy the definition's path past a placement that does not read, and accept a removal only when the decoder is past it.
207 lines
6.8 KiB
Go
207 lines
6.8 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. Its data is still backed up as this controller reads it, " +
|
|
"which may not be what its newer manifest asks, 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, bestEffort: true}
|
|
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)
|
|
// Accepted only when the decoder is past it: nothing left, or an unknown key said
|
|
// elsewhere. A different kind of error means the removal broke the entry; the same
|
|
// words mean this was not the occurrence it refused.
|
|
err := decodesAlone(k, v)
|
|
if err == nil || (asUnknownField(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
|
|
}
|