package catalogue import ( "encoding/json" "fmt" "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 a module's own values. Exported for what a provider serves, which is // settled where the mesh is walked rather than where a node is declared. func Settle(base map[string]any, layers []Layer) (map[string]any, error) { return settle(base, layers, nil, "what is served") } // 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. 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 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 reached no file. // // 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. func UnusedSettings(m Manifest, layers []Layer) []string { for _, r := range m.Resources { if how, _ := r["merge"].(string); how != "" { return nil } } // A contribution is a destination too. A route's hostname is exactly the kind of thing that // differs between one mesh and the next, and calling it stray would refuse the one setting // most modules that publish anything will have. if len(m.Contributes) > 0 { 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 } var unused []string for _, layer := range layers { for key := range layer.Values { // `expose` is a real destination for a module that listens: it overrides a port's // source (novox/hq ADR 0051), validated in Exposure, so it is not stray here. if key == ExposeSetting && len(m.Listens) > 0 { continue } unused = append(unused, fmt.Sprintf( "%s sets %q, and %s has no file or contribution to merge it into", layer.From, key, m.Module)) } } 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 }