A strict read of the stored catalogue fails every plan and send once a manifest uses a field an older controller lacks; registration stays strict. A node named default lost its layer to the name check. Judge the operator's keys by whole words, and scan a default under any key.
443 lines
17 KiB
Go
443 lines
17 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Settings are how somebody changes a config file a module owns, without editing it.
|
|
//
|
|
// Managed files are generated and never edited (novox/hq ADR 0011), so a person's intention has
|
|
// to be expressed somewhere the generator can see it. Then the file is produced from the module's
|
|
// defaults and that intention together, and upstream can rewrite its half freely.
|
|
//
|
|
// **An override beats a default, so there is nothing to resolve.** When the module changes a key
|
|
// somebody has set, the setting wins — it was a statement about that key, made deliberately, and
|
|
// the default was only ever what to do in the absence of one. This is the same shape as the rest
|
|
// of the mesh refusing to guess: there is no guess to make.
|
|
|
|
// Merge is how a file's content is combined with settings. A module names one per resource.
|
|
const (
|
|
// MergeJSON treats the content as a JSON document and merges settings into it by key.
|
|
MergeJSON = "json"
|
|
)
|
|
|
|
// Layer is one source of settings, in the order they are applied.
|
|
type Layer struct {
|
|
// Where these came from, for saying which layer set a value.
|
|
From string
|
|
Values map[string]any
|
|
// Default marks the layer a module's own defaults make (novox/hq ADR 0262). Marked rather than
|
|
// recognised by From, which is a node's name for a node's layer, and a node may be called anything.
|
|
Default bool
|
|
}
|
|
|
|
// ApplySettings produces a resource's final content from the module's own and the layers over it.
|
|
//
|
|
// Layers are applied in order, so a later one beats an earlier one — the mesh's settings for a
|
|
// module first, then this node's. A node that differs from the rest is expressed by differing,
|
|
// rather than by repeating everything the rest already say.
|
|
func ApplySettings(resource map[string]any, layers []Layer) (map[string]any, error) {
|
|
how, _ := resource["merge"].(string)
|
|
if how == "" {
|
|
// Not a mergeable file. Settings for it are ignored rather than silently doing nothing
|
|
// somewhere else — see UnusedSettings, which is what says so.
|
|
return resource, nil
|
|
}
|
|
if how != MergeJSON {
|
|
return nil, fmt.Errorf(
|
|
"%v says it merges as %q, and this control plane knows how to merge %q",
|
|
resource["id"], how, MergeJSON)
|
|
}
|
|
|
|
content, _ := resource["content"].(string)
|
|
var base map[string]any
|
|
if strings.TrimSpace(content) == "" {
|
|
base = map[string]any{}
|
|
} else if err := json.Unmarshal([]byte(content), &base); err != nil {
|
|
return nil, fmt.Errorf("%v says it merges as JSON and its content is not JSON: %w",
|
|
resource["id"], err)
|
|
}
|
|
|
|
protected := map[string]bool{}
|
|
for _, k := range stringsOf(resource["protected"]) {
|
|
protected[k] = true
|
|
}
|
|
|
|
merged, err := settle(base, layers, protected, fmt.Sprint(resource["id"]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := map[string]any{}
|
|
for k, v := range resource {
|
|
out[k] = v
|
|
}
|
|
// Keys sorted by the encoder, so the same settings always produce the same bytes. A file
|
|
// whose lines move for no reason makes every reconcile look like a change, and anything
|
|
// reflecting it would restart for ever.
|
|
rendered, err := json.MarshalIndent(merged, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out["content"] = string(rendered) + "\n"
|
|
delete(out, "merge")
|
|
delete(out, "protected")
|
|
return out, nil
|
|
}
|
|
|
|
// Settle lays settings over what a provider serves. Exported because a served fact is settled where
|
|
// the mesh is walked rather than where a node is declared.
|
|
//
|
|
// **A setting overrides a served key; it never adds one** (novox/hq 04-ISSUES/173). What a consumer
|
|
// is told is the provider's contract, and a setting made for one of the provider's files — a site
|
|
// name, a public address — is not part of it. Before this, every setting of a module reached every
|
|
// consumer of every provision it served.
|
|
func Settle(base map[string]any, layers []Layer) (map[string]any, error) {
|
|
return overridden(base, layers, "what is served")
|
|
}
|
|
|
|
// overridden lays settings over a map whose keys are its contract: a contribution, a served fact.
|
|
// Only the keys the map already declares are touched; the rest of a layer is somebody else's
|
|
// business (a file's, another destination's) and is left to reach it there.
|
|
//
|
|
// A declared value may itself be the operator's, `${setting:<key>}` (ADR 0155): a mail provider
|
|
// serves its domain, an identity provider its issuer, and neither is the definition's to state.
|
|
// Filled from the layers after the overrides, and refused by name when nothing sets it — a literal
|
|
// placeholder handed to a consumer is a service configured against a string nobody meant.
|
|
func overridden(base map[string]any, layers []Layer, what string) (map[string]any, error) {
|
|
kept := make([]Layer, 0, len(layers))
|
|
for _, layer := range layers {
|
|
values := map[string]any{}
|
|
for key, value := range layer.Values {
|
|
if _, declared := base[key]; declared {
|
|
values[key] = value
|
|
}
|
|
}
|
|
kept = append(kept, Layer{From: layer.From, Values: values, Default: layer.Default})
|
|
}
|
|
merged, err := settle(base, kept, nil, what)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for key, value := range merged {
|
|
s, ok := value.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, asked := range settingsUsed(s) {
|
|
v, set := settingValue(layers, asked)
|
|
if !set {
|
|
return nil, fmt.Errorf(
|
|
"%s says ${setting:%s} for %q, and nothing sets %q — an operator's value is the "+
|
|
"assignment's, never the definition's (novox/hq ADR 0112)%s",
|
|
what, asked, key, asked, orNoSettings(layers))
|
|
}
|
|
s = strings.ReplaceAll(s, "${setting:"+asked+"}", plainly(v))
|
|
}
|
|
merged[key] = s
|
|
}
|
|
return merged, nil
|
|
}
|
|
|
|
// settle lays the layers over a module's own values, in order.
|
|
//
|
|
// Shared by a file's content and a module's contributions, because they are the same act: the
|
|
// module says what it means by default, and somebody says what it means here. A contribution that
|
|
// could not be settled would have to be edited to be reused anywhere else. The two differ in one
|
|
// respect, and the caller decides it: a file takes keys it did not declare (a setting may add to a
|
|
// configuration), a contribution or served fact does not (overridden).
|
|
func settle(base map[string]any, layers []Layer, protected map[string]bool, what string) (
|
|
map[string]any, error) {
|
|
merged := deepCopy(base)
|
|
for _, layer := range layers {
|
|
if layer.Default {
|
|
// A module's own defaults fill ${setting:<key>} only (novox/hq ADR 0262). A mergeable
|
|
// file's content already is its defaults, and a contribution or served fact declares its
|
|
// own; laid on here, a default would reach every mergeable file of its module (issue 168).
|
|
continue
|
|
}
|
|
for key, value := range layer.Values {
|
|
if key == PortsSetting {
|
|
// Where the machine puts a port is the mesh's to apply, not a value for a file or
|
|
// for what a consumer is told (novox/hq ADR 0100); it reaches both as the port.
|
|
continue
|
|
}
|
|
if protected[key] {
|
|
// The module said it must own this one. Refused rather than ignored: a setting
|
|
// that is quietly dropped is somebody believing they changed something.
|
|
return nil, fmt.Errorf(
|
|
"%s sets %q on %v, and that module keeps %q for itself — it is not settable",
|
|
layer.From, key, what, key)
|
|
}
|
|
merged[key] = mergeValue(merged[key], value)
|
|
}
|
|
}
|
|
return merged, nil
|
|
}
|
|
|
|
// mergeValue combines one value with the one over it.
|
|
//
|
|
// Two objects merge key by key, so setting one field of a nested block does not delete its
|
|
// siblings. Anything else is replaced whole: a list that merged element-wise could neither be
|
|
// shortened nor reordered, and there is no correct guess about which element is "the same one".
|
|
func mergeValue(under, over any) any {
|
|
underMap, isUnderMap := under.(map[string]any)
|
|
overMap, isOverMap := over.(map[string]any)
|
|
if !isUnderMap || !isOverMap {
|
|
return over
|
|
}
|
|
merged := deepCopy(underMap)
|
|
for k, v := range overMap {
|
|
merged[k] = mergeValue(merged[k], v)
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func deepCopy(in map[string]any) map[string]any {
|
|
out := map[string]any{}
|
|
for k, v := range in {
|
|
if nested, ok := v.(map[string]any); ok {
|
|
out[k] = deepCopy(nested)
|
|
continue
|
|
}
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
// UnusedSettings names settings that reach nothing.
|
|
//
|
|
// Somebody who sets a key on a module with nothing mergeable, or misspells one, has changed
|
|
// nothing — and would find out by the machine not behaving differently, which is the slowest
|
|
// way there is. This is what makes that visible at the moment they set it.
|
|
//
|
|
// Where a key can land: any mergeable file takes any key; a file asking for `${setting:<key>}`
|
|
// takes that key (ADR 0155); a contribution or a served fact takes a key it declares, and no other
|
|
// (novox/hq 04-ISSUES/173); and the mesh's own words — `expose`, `ports`, `reach`, `endpoints` —
|
|
// are read by the mesh. A key none of those takes is stray, and is said so rather than dropped.
|
|
func UnusedSettings(m Manifest, layers []Layer) []string {
|
|
for _, r := range m.Resources {
|
|
if how, _ := r["merge"].(string); how != "" {
|
|
return nil
|
|
}
|
|
}
|
|
// A computed module has no resources here to look at — they are worked out per node, and
|
|
// whether a setting lands is not knowable until then. Silence rather than a wrong answer:
|
|
// claiming every setting on the private network is stray would be worse than saying nothing.
|
|
if m.Computed != "" {
|
|
return nil
|
|
}
|
|
|
|
lands := settingKeysUsedBy(m)
|
|
// A key the module gives a default reaches what asks for it (novox/hq ADR 0262): setting it
|
|
// overrides the default, which is how a running module gains a setting without a gap between.
|
|
for key := range m.Settings {
|
|
lands[key] = true
|
|
}
|
|
for _, values := range m.Contributes {
|
|
for key := range values {
|
|
lands[key] = true
|
|
}
|
|
}
|
|
for _, locals := range m.ContributesMany {
|
|
for _, values := range locals {
|
|
for key := range values {
|
|
lands[key] = true
|
|
}
|
|
}
|
|
}
|
|
for _, served := range m.Serves {
|
|
for key := range served {
|
|
lands[key] = true
|
|
}
|
|
}
|
|
var unused []string
|
|
for _, layer := range layers {
|
|
for key := range layer.Values {
|
|
if lands[key] {
|
|
continue
|
|
}
|
|
// `expose` is a real destination for a module that listens: it overrides a port's
|
|
// source (novox/hq ADR 0046), validated in Exposure, so it is not stray here.
|
|
if key == ExposeSetting && len(m.Listens) > 0 {
|
|
continue
|
|
}
|
|
// `ports` gives a module's port a machine port on one node (novox/hq ADR 0100),
|
|
// validated in GivenPorts, so it is not stray here either.
|
|
if key == PortsSetting {
|
|
continue
|
|
}
|
|
// `reach` says how far one of this module's endpoints reaches (novox/hq ADR 0138) — the
|
|
// filter's source, which names are composed, and therefore which authority certifies
|
|
// them. Validated in Reaches, so not stray.
|
|
if key == ReachSetting && len(m.Listens) > 0 {
|
|
continue
|
|
}
|
|
// `endpoints` configures a module's endpoints by name — the machine port, the subdomain and
|
|
// the reach as one block each (novox/hq ADR 0138). Validated in Endpoints, so not stray.
|
|
if key == EndpointsSetting && len(m.Listens) > 0 {
|
|
continue
|
|
}
|
|
// `places` puts a declared directory where this machine keeps it, `accesses` says where
|
|
// the operator's data is (novox/hq issue 153). Validated in Places and AccessPlaces.
|
|
if key == PlacesSetting && len(directoriesOf(m)) > 0 {
|
|
continue
|
|
}
|
|
if key == AccessesSetting && len(m.Accesses) > 0 {
|
|
continue
|
|
}
|
|
// `networks` keeps a found network for a taken container on one adopted machine
|
|
// (novox/hq ADR 0163). Validated in KeptNetworks, so not stray.
|
|
if key == NetworksSetting {
|
|
continue
|
|
}
|
|
unused = append(unused, fmt.Sprintf(
|
|
"%s sets %q, and %s has no file that merges it, asks for no ${setting:%s}, and "+
|
|
"declares no %q in what it contributes or serves",
|
|
layer.From, key, m.Module, key, key))
|
|
}
|
|
}
|
|
sort.Strings(unused)
|
|
return unused
|
|
}
|
|
|
|
func stringsOf(v any) []string {
|
|
raw, ok := v.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for _, item := range raw {
|
|
if s, ok := item.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// NetworksSetting is the settings key that keeps a found network for a taken container, on one
|
|
// adopted machine (novox/hq ADR 0163, rule 4):
|
|
//
|
|
// {"networks": {"server": ["predecessor_default"]}}
|
|
//
|
|
// has the module's container `server` also join `predecessor_default` once taken, so a neighbour
|
|
// that resolves it by name on that network keeps resolving it. Migration scaffolding in the sense
|
|
// of ADR 0104: assigned only on an adopted machine, reported while it stands, removed when the
|
|
// neighbours are taken. Keyed by the container's resource id; the value is the networks it keeps.
|
|
const NetworksSetting = "networks"
|
|
|
|
// KeptNetworks reads which found networks each of a module's containers keeps, by container id.
|
|
//
|
|
// Refused from a mesh-wide layer — a found network is a fact about one machine — for an id the
|
|
// module declares no container under, for a name that is not a network's, and on a machine that
|
|
// is not adopted: the setting exists so neighbours the mesh has not taken yet keep reaching the
|
|
// container, and a converged machine has no such neighbours.
|
|
func KeptNetworks(m Manifest, layers []Layer, adopted bool) (map[string][]string, error) {
|
|
containers := map[string]bool{}
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["type"]) == "container" {
|
|
containers[fmt.Sprint(r["id"])] = true
|
|
}
|
|
}
|
|
out := map[string][]string{}
|
|
for _, layer := range layers {
|
|
raw, ok := layer.Values[NetworksSetting]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if layer.From == MeshWideLayer {
|
|
return nil, fmt.Errorf("%s: %s is given per node — a found network is a fact about one "+
|
|
"machine; set it with --node", m.Module, NetworksSetting)
|
|
}
|
|
if !adopted {
|
|
return nil, fmt.Errorf("%s: %s keeps a found network for neighbours the mesh has not taken "+
|
|
"yet, and %s is converged — nothing on it is found; clear the setting", m.Module,
|
|
NetworksSetting, layer.From)
|
|
}
|
|
blocks, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: %s is a { container: [network, …] } map, and %q set it to "+
|
|
"something else", m.Module, NetworksSetting, layer.From)
|
|
}
|
|
for id, body := range blocks {
|
|
if !containers[id] {
|
|
return nil, fmt.Errorf("%s: %s names the container %q, which it does not declare — "+
|
|
"the setting reaches nothing; it declares %s", m.Module, NetworksSetting, id,
|
|
orNothing(sortedKeys(containers)))
|
|
}
|
|
names := stringsOf(body)
|
|
if len(names) == 0 {
|
|
return nil, fmt.Errorf("%s: %s for %q is a list of network names, and %q set it to %v",
|
|
m.Module, NetworksSetting, id, layer.From, body)
|
|
}
|
|
for _, n := range names {
|
|
if !networkName.MatchString(n) {
|
|
return nil, fmt.Errorf("%s: %s for %q names %q, which is not a network name",
|
|
m.Module, NetworksSetting, id, n)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
out[id] = names
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// networkName is what a container runtime accepts as a network's name.
|
|
var networkName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*$`)
|
|
|
|
// JudgeSettings composes a module's settings against its definition and refuses the first thing
|
|
// that cannot work, naming the module, the layer and the key (novox/hq ADR 0163, rule 6).
|
|
//
|
|
// **The same judgement where a setting is stored and where a machine is declared.** Stored, a
|
|
// setting that cannot compose is refused before it is kept; composed later, a definition that has
|
|
// moved under a stored setting leaves that module out of the machine's declaration rather than
|
|
// the machine without one. Every reader of settings runs here: a port given, an exposure, a reach,
|
|
// an endpoint, a placement, an access, a kept network, a mergeable file's keys and a file's
|
|
// `${setting:…}`. A key that reaches nothing is not here: it cannot break a composition, so it is
|
|
// refused where it is stored (SetSettings, with UnusedSettings) and said where a plan is read,
|
|
// and never costs a module its place.
|
|
func JudgeSettings(m Manifest, layers []Layer, adopted bool) error {
|
|
// With no layers too: a definition may ask for a setting nobody made — an access placed by
|
|
// nobody, a file's ${setting:…} nothing sets — and that is the same statement, missing.
|
|
if _, err := GivenPorts(m, layers); err != nil {
|
|
return err
|
|
}
|
|
if _, err := Reaches(m, layers); err != nil {
|
|
return err
|
|
}
|
|
if _, err := Endpoints(m, layers); err != nil {
|
|
return err
|
|
}
|
|
if _, err := Places(m, layers); err != nil {
|
|
return err
|
|
}
|
|
if _, _, err := accessesFor(m, layers); err != nil {
|
|
return err
|
|
}
|
|
if _, err := KeptNetworks(m, layers, adopted); err != nil {
|
|
return err
|
|
}
|
|
for _, r := range m.Resources {
|
|
settled, err := ApplySettings(r, layers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
copied := map[string]any{}
|
|
for k, v := range settled {
|
|
copied[k] = v
|
|
}
|
|
if err := settingInto(copied, WithDefaults(m, layers), m.Module); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|