mesh/delivery closed: the pull request closed unmerged
mesh/check pass 96a37f58ca76 on 030f9ee4e780 | unit 1173 run, 3272 kept, 13 skipped | integration 1212 run, 3274 kept, 22 skipped | over bound: unit, …
The controller's declared-config mirror never held de-spiegel's keys, and Resolve skipped an assigned module it held no declarations of, silently: de-spiegel stayed held on novox with its values set. Declarations now come from the indexer's trunk manifest with the wanted build's laid over it per node; which module runs where from the seat mesh-placement alone. A placed module with no readable manifest is listed as unresolved with why, logged, and keeps its last resolved values.
260 lines
8.4 KiB
Go
260 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What mesh-config resolves from (novox/hq ADR 0348 §2, §4, §7): each module's declarations as its manifests declare
|
|
// them, the list of predefined keys, and where each module is placed (issue 1953).
|
|
|
|
// The types a config value may have (ADR 0348 §4).
|
|
const (
|
|
TypeNumber = "number"
|
|
TypeString = "string"
|
|
TypeBoolean = "boolean"
|
|
TypeObject = "object"
|
|
TypeList = "list"
|
|
)
|
|
|
|
// Declaration is one value a module declares, as the declared-config mirror holds it: the config form's fields,
|
|
// or the settings form's kind, default and why for a module not yet moved (ADR 0348 §1). The mirror writes kind,
|
|
// default and why for every key, so a config-form key comes as kind "", why "" and default null: an empty kind or
|
|
// why and a null default are read as not given, never as an empty value or a null default.
|
|
type Declaration struct {
|
|
Kind string `json:"kind,omitempty"`
|
|
Default json.RawMessage `json:"default,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Enum []any `json:"enum,omitempty"`
|
|
Minimum *float64 `json:"minimum,omitempty"`
|
|
Maximum *float64 `json:"maximum,omitempty"`
|
|
Pattern string `json:"pattern,omitempty"`
|
|
Trusted bool `json:"trusted,omitempty"`
|
|
Predefined string `json:"predefined,omitempty"`
|
|
Required bool `json:"required,omitempty"`
|
|
Runtime bool `json:"runtime,omitempty"`
|
|
}
|
|
|
|
// HasDefault is whether the declaration gives a default: present and not null.
|
|
func (d Declaration) HasDefault() bool {
|
|
return len(bytes.TrimSpace(d.Default)) > 0 && string(bytes.TrimSpace(d.Default)) != "null"
|
|
}
|
|
|
|
// TypeOf is the value's type: the config form's, or for the settings form the type of its default.
|
|
func (d Declaration) TypeOf() string {
|
|
if d.Type != "" {
|
|
return d.Type
|
|
}
|
|
if !d.HasDefault() {
|
|
return ""
|
|
}
|
|
var v any
|
|
if json.Unmarshal(d.Default, &v) != nil {
|
|
return ""
|
|
}
|
|
return typeOfValue(v)
|
|
}
|
|
|
|
// ModuleDeclarations is one module's value of the declared-config mirror.
|
|
type ModuleDeclarations struct {
|
|
Module string `json:"module"`
|
|
Config map[string]Declaration `json:"settings"`
|
|
}
|
|
|
|
// ManifestDeclarations is the config a manifest declares (ADR 0348 §1, §4): its `config`, or for a module not yet
|
|
// moved its `settings` (kind, default and why), the two never both. Only these two fields are read, so a manifest
|
|
// field this module's SDK does not know yet never leaves a module's config unresolved.
|
|
func ManifestDeclarations(raw []byte) (ModuleDeclarations, error) {
|
|
var m struct {
|
|
Module string `json:"module"`
|
|
Config map[string]Declaration `json:"config"`
|
|
Settings map[string]Declaration `json:"settings"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return ModuleDeclarations{}, fmt.Errorf("not a manifest: %w", err)
|
|
}
|
|
if m.Config != nil && m.Settings != nil {
|
|
return ModuleDeclarations{}, fmt.Errorf("%s declares both config and settings, and a module uses one form "+
|
|
"(novox/hq ADR 0348 §1)", m.Module)
|
|
}
|
|
decl := m.Config
|
|
if decl == nil {
|
|
decl = m.Settings
|
|
}
|
|
raw, err := json.Marshal(ModuleDeclarations{Module: m.Module, Config: decl})
|
|
if err != nil {
|
|
return ModuleDeclarations{}, err
|
|
}
|
|
return ParseModuleDeclarations(raw)
|
|
}
|
|
|
|
// ParseModuleDeclarations reads one module's declarations in the layout ModuleDeclarations writes.
|
|
func ParseModuleDeclarations(raw []byte) (ModuleDeclarations, error) {
|
|
var m ModuleDeclarations
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return m, fmt.Errorf("not a module's declarations: %w", err)
|
|
}
|
|
if m.Module == "" {
|
|
return m, fmt.Errorf("a module's declarations that name no module")
|
|
}
|
|
if m.Config == nil {
|
|
m.Config = map[string]Declaration{}
|
|
}
|
|
for key, d := range m.Config {
|
|
if !d.HasDefault() {
|
|
d.Default = nil
|
|
}
|
|
m.Config[key] = d
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// PredefinedKey is one key of the list of predefined keys (ADR 0348 §7): its type, description, validation and
|
|
// whether it is trusted. It has no default.
|
|
type PredefinedKey struct {
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Enum []any `json:"enum,omitempty"`
|
|
Minimum *float64 `json:"minimum,omitempty"`
|
|
Maximum *float64 `json:"maximum,omitempty"`
|
|
Pattern string `json:"pattern,omitempty"`
|
|
Trusted bool `json:"trusted,omitempty"`
|
|
}
|
|
|
|
// PredefinedList is the list the controller kept, as its predefined mirror holds it under PredefinedMirrorKey.
|
|
type PredefinedList struct {
|
|
Keys map[string]PredefinedKey `json:"keys"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
Generation int `json:"generation"`
|
|
Build string `json:"build"`
|
|
}
|
|
|
|
// PredefinedMirrorKey is the controller's predefined mirror's one key.
|
|
const PredefinedMirrorKey = "list"
|
|
|
|
// Published is one key of mesh-config_predefined: the key's declaration, and the build of the list it came from.
|
|
type Published struct {
|
|
PredefinedKey
|
|
Generation int `json:"generation"`
|
|
Commit string `json:"commit"`
|
|
}
|
|
|
|
// Assignment is a module placed on a node, as the seat mesh-placement's key `placement.<node>` names it.
|
|
type Assignment struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
}
|
|
|
|
// rule is the validation a value is held to, from a module's declaration or a predefined key.
|
|
type rule struct {
|
|
Type string
|
|
Enum []any
|
|
Minimum *float64
|
|
Maximum *float64
|
|
Pattern string
|
|
}
|
|
|
|
func (d Declaration) rule() rule {
|
|
return rule{Type: d.TypeOf(), Enum: d.Enum, Minimum: d.Minimum, Maximum: d.Maximum, Pattern: d.Pattern}
|
|
}
|
|
|
|
func (k PredefinedKey) rule() rule {
|
|
return rule{Type: k.Type, Enum: k.Enum, Minimum: k.Minimum, Maximum: k.Maximum, Pattern: k.Pattern}
|
|
}
|
|
|
|
// check is every way value breaks the rule, said in a sentence each; none for a value that holds.
|
|
func (r rule) check(value any) []string {
|
|
var problems []string
|
|
if r.Type != "" && typeOfValue(value) != r.Type {
|
|
return []string{fmt.Sprintf("%s is not a %s", show(value), r.Type)}
|
|
}
|
|
if len(r.Enum) > 0 && !slices.ContainsFunc(r.Enum, func(e any) bool { return sameValue(e, value) }) {
|
|
problems = append(problems, fmt.Sprintf("%s is not one of %s", show(value), show(r.Enum)))
|
|
}
|
|
if n, ok := value.(float64); ok {
|
|
if r.Minimum != nil && n < *r.Minimum {
|
|
problems = append(problems, fmt.Sprintf("%s is below the minimum %s", show(n), show(*r.Minimum)))
|
|
}
|
|
if r.Maximum != nil && n > *r.Maximum {
|
|
problems = append(problems, fmt.Sprintf("%s is above the maximum %s", show(n), show(*r.Maximum)))
|
|
}
|
|
if math.IsNaN(n) || math.IsInf(n, 0) {
|
|
problems = append(problems, "a number is finite")
|
|
}
|
|
}
|
|
if s, ok := value.(string); ok && r.Pattern != "" {
|
|
pattern, err := regexp.Compile(`^(?:` + r.Pattern + `)$`)
|
|
switch {
|
|
case err != nil:
|
|
problems = append(problems, fmt.Sprintf("its pattern %q cannot be read: %v", r.Pattern, err))
|
|
case !pattern.MatchString(s):
|
|
problems = append(problems, fmt.Sprintf("%q does not match the pattern %q", s, r.Pattern))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// typeOfValue is the config type of a value read from JSON.
|
|
func typeOfValue(v any) string {
|
|
switch v.(type) {
|
|
case float64:
|
|
return TypeNumber
|
|
case string:
|
|
return TypeString
|
|
case bool:
|
|
return TypeBoolean
|
|
case map[string]any:
|
|
return TypeObject
|
|
case []any:
|
|
return TypeList
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func sameValue(a, b any) bool {
|
|
ra, _ := json.Marshal(a)
|
|
rb, _ := json.Marshal(b)
|
|
return bytes.Equal(ra, rb)
|
|
}
|
|
|
|
func show(v any) string {
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprint(v)
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
// keyName is what a config key may be (the catalogue's rule): lower-case letters, digits, dots, dashes and
|
|
// underscores, starting with a letter or digit.
|
|
var keyName = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]*$`)
|
|
|
|
// nodeOrModule is what a node or a module name may be: one token of a bucket's key.
|
|
var nodeOrModule = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func joinKeys[V any](m map[string]V) string {
|
|
keys := sortedKeys(m)
|
|
if len(keys) == 0 {
|
|
return "none"
|
|
}
|
|
return strings.Join(keys, ", ")
|
|
}
|