package config import ( "fmt" "slices" "sort" ) // AgentSettings are the nodes' agent settings, kept with the cluster's // (BACKLOG E3): everything agent.yaml can say except what an agent needs to // start — its id, the control planes to dial, its address and its // certificate. Default applies to every node, and Nodes, by node id, over // it; both over the node's own agent.yaml. type AgentSettings struct { // Default is agent.yaml's keys for every node. Default map[string]any `koanf:"default"` // Nodes are agent.yaml's keys for one node each, by node id, over // Default. Nodes map[string]map[string]any `koanf:"nodes"` } // agentBootstrapKeys are what an agent needs before it can be told anything: // they stay in agent.yaml. var agentBootstrapKeys = []string{"node_id", "server", "servers", "hostname", "mgmt_addr", "tls"} // For is the settings for one node: Default with the node's own over it. // Empty when there are none. func (a AgentSettings) For(node string) map[string]any { out := clone(stringKeys(a.Default)) if n, ok := a.Nodes[node]; ok { merge(out, stringKeys(n)) } return out } // Check refuses settings that set an agent's bootstrap keys, or that would // not load as an agent's configuration for some node. func (a AgentSettings) Check() error { check := func(where string, m map[string]any) error { for _, k := range agentBootstrapKeys { if _, ok := m[k]; ok { return fmt.Errorf("%s.%s stays in each node's agent.yaml: an agent needs it before it can be told anything", where, k) } } return nil } if err := check("agents.default", stringKeys(a.Default)); err != nil { return err } nodes := make([]string, 0, len(a.Nodes)) for n := range a.Nodes { nodes = append(nodes, n) } sort.Strings(nodes) for _, n := range nodes { if err := check("agents.nodes."+n, stringKeys(a.Nodes[n])); err != nil { return err } } // Each node's settings as its agent would load them, over nothing: what // its own file adds cannot be known here, and what is wrong in the // settings alone is wrong on every node. for _, n := range append([]string{""}, nodes...) { b, err := Marshal(a.For(n)) if err != nil { return err } if _, err := LoadAgentWith("", b); err != nil { where := "agents.default" if n != "" { where = "agents.nodes." + n } return fmt.Errorf("%s: %w", where, err) } } return nil } // AgentBootstrapKeys are the keys agent.yaml keeps, for saying so. func AgentBootstrapKeys() []string { return slices.Clone(agentBootstrapKeys) } // stringKeys turns YAML's map[any]any, wherever it hides, into the // map[string]any the rest of this package merges. func stringKeys(m map[string]any) map[string]any { out := make(map[string]any, len(m)) for k, v := range m { out[k] = normalize(v) } return out } func normalize(v any) any { switch t := v.(type) { case map[string]any: return stringKeys(t) case map[any]any: m := make(map[string]any, len(t)) for k, vv := range t { m[fmt.Sprint(k)] = normalize(vv) } return m case []any: out := make([]any, len(t)) for i, vv := range t { out[i] = normalize(vv) } return out } return v }