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 } // 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:}` (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}) } 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 { 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:}` // 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) 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, layers, m.Module); err != nil { return err } } return nil }