A push leaves out a module whose settings do not compose and sends the rest, which is right, but only plan said so: nfs-server ran nowhere for days while the operator believed it ran. The self-check now raises needs-operator for a setting nobody gave, naming the setting and the command, and left-out for any other cause; both warnings, cleared once the module composes or is unassigned. status and node show list the same modules as assigned, not applied.
215 lines
7.9 KiB
Go
215 lines
7.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// An operator's value, where a definition needs one (novox/hq ADR 0112, ADR 0155, design 27).
|
|
//
|
|
// A mail server's domain, a site's name, the address a proxy forwards from: values that are true of
|
|
// one installation and of no other, and that a module's software must be told. They had nowhere to
|
|
// live but the definition, which is how a catalogue meant for any mesh came to name this one
|
|
// (novox/hq issues 122, 134). ADR 0112 names the operator as one of the four providers; this is the
|
|
// operator answering.
|
|
//
|
|
// `${setting:<key>}` in a file's content, and in a service's unit name, is filled from the module's settings layers — the mesh's,
|
|
// then this node's — the same layers a mergeable JSON file and a contribution already take, so
|
|
// `settings set <module>` is the one place a person's values go. **Refused when no layer sets it**,
|
|
// naming the key and the remedy: a definition that carried a default for a mail domain would be
|
|
// carrying the very literal this removes, and a blank written silently would be a service that
|
|
// comes up wrong somewhere that names neither the module nor the key.
|
|
|
|
// settingRef is how a definition asks for an operator's value: ${setting:<key>}.
|
|
var settingRef = regexp.MustCompile(`\$\{setting:([a-z0-9][a-z0-9_.-]*)\}`)
|
|
|
|
// settingsUsed is every key a file's content asks for, once each, in order of first use.
|
|
func settingsUsed(content string) []string {
|
|
var keys []string
|
|
seen := map[string]bool{}
|
|
for _, m := range settingRef.FindAllStringSubmatch(content, -1) {
|
|
if !seen[m[1]] {
|
|
seen[m[1]] = true
|
|
keys = append(keys, m[1])
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// UnsetSettingError is a module whose definition says ${setting:<key>} where nothing sets that key: typed, so
|
|
// that whoever reads why a module was left out of a machine can tell a setting nobody gave — the operator's
|
|
// to give, named with the command that gives it — from any other reason (novox/hq issue 380). Its words are
|
|
// the refusal's, unchanged.
|
|
type UnsetSettingError struct {
|
|
Module, Setting string
|
|
said string
|
|
}
|
|
|
|
func (e *UnsetSettingError) Error() string { return e.said }
|
|
|
|
// settingInto fills a file's ${setting:…} placeholders from the layers over a module.
|
|
//
|
|
// The last layer setting a key wins, which is the node's over the mesh's over the module's own
|
|
// default (novox/hq ADR 0262) — the same order settle applies to a mergeable file. The caller lays
|
|
// the defaults under the layers with WithDefaults. A value that is not a string is written the way a program would read
|
|
// it (a number without a trailing .000000, a boolean as true/false).
|
|
func settingInto(resource map[string]any, layers []Layer, module string) error {
|
|
if fmt.Sprint(resource["type"]) == "service" {
|
|
return settingIntoUnit(resource, layers, module)
|
|
}
|
|
if fmt.Sprint(resource["type"]) != "file" {
|
|
return nil
|
|
}
|
|
content, ok := resource["content"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for _, key := range settingsUsed(content) {
|
|
value, set := settingValue(layers, key)
|
|
if !set {
|
|
return &UnsetSettingError{Module: module, Setting: key, said: fmt.Sprintf(
|
|
"%s has a file that says ${setting:%s}, and nothing sets %q for it — an operator's "+
|
|
"value is the assignment's, never the definition's (novox/hq ADR 0112), and only a "+
|
|
"preference has a default in the definition (ADR 0262): "+
|
|
"`settings set %s <file>` with {%q: …}%s",
|
|
module, key, key, module, key, orNoSettings(layers))}
|
|
}
|
|
content = strings.ReplaceAll(content, "${setting:"+key+"}", plainly(value))
|
|
}
|
|
resource["content"] = content
|
|
return nil
|
|
}
|
|
|
|
// unitPart is what a setting may put into a unit's name: the characters systemd allows in a unit name,
|
|
// less the instance's `@` and the escape's `\`, and at least one of them. Anything else — a space, a slash,
|
|
// a newline that would begin a directive in the unit file the name ends up in — is refused, never written.
|
|
var unitPart = regexp.MustCompile(`^[A-Za-z0-9:_.][A-Za-z0-9:_.-]{0,63}$`)
|
|
|
|
// unitInstance is the one place a setting may stand in a unit's name: the whole instance of a template,
|
|
// after its `@` and before its suffix — `zfs-scrub-weekly@${setting:scrub-pool}.timer` — so a value can
|
|
// make the unit another instance of the same template and nothing else (third review of mesh-catalog #147).
|
|
var unitInstance = regexp.MustCompile(`^[A-Za-z0-9:_.-]+@\$\{setting:[a-z0-9][a-z0-9_.-]*\}\.[a-z]+$`)
|
|
|
|
// UnitSettingProblems refuses, where a manifest is read, a service whose unit name carries a setting
|
|
// anywhere but as a template's whole instance.
|
|
func UnitSettingProblems(m Manifest) []string {
|
|
var problems []string
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["type"]) != "service" {
|
|
continue
|
|
}
|
|
unit, _ := r["unit"].(string)
|
|
if len(settingsUsed(unit)) == 0 && !strings.Contains(unit, "${setting:") {
|
|
continue
|
|
}
|
|
if !unitInstance.MatchString(unit) {
|
|
problems = append(problems, fmt.Sprintf("%s: the service %v's unit %q carries a setting outside a template's "+
|
|
"instance; a setting may stand only as the whole instance, after the @ and before the suffix "+
|
|
"(name@${setting:key}.timer)", m.Module, r["id"], unit))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// settingIntoUnit fills ${setting:…} in a service resource's unit name: a module that holds the
|
|
// distribution's timer for the pool the operator names cannot write the pool into its definition
|
|
// (novox/hq ADR 0112). Refused, with the key and why, when nothing sets it or the value would not make a
|
|
// unit's name; the resource is left as it was.
|
|
func settingIntoUnit(resource map[string]any, layers []Layer, module string) error {
|
|
unit, ok := resource["unit"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for _, key := range settingsUsed(unit) {
|
|
value, set := settingValue(layers, key)
|
|
if !set {
|
|
return &UnsetSettingError{Module: module, Setting: key, said: fmt.Sprintf(
|
|
"%s has a service whose unit says ${setting:%s}, and nothing sets %q for it — an operator's "+
|
|
"value is the assignment's, never the definition's (novox/hq ADR 0112): "+
|
|
"`settings set %s <file>` with {%q: …}%s",
|
|
module, key, key, module, key, orNoSettings(layers))}
|
|
}
|
|
v := plainly(value)
|
|
if !unitPart.MatchString(v) {
|
|
return fmt.Errorf("%s: the setting %q is %q, which cannot be the instance of the unit %s: an instance "+
|
|
"takes letters, digits, ':', '_', '.' and '-', does not begin with '-' (a systemctl option), and is "+
|
|
"at most 64 characters", module, key, v, unit)
|
|
}
|
|
unit = strings.ReplaceAll(unit, "${setting:"+key+"}", v)
|
|
}
|
|
resource["unit"] = unit
|
|
return nil
|
|
}
|
|
|
|
func settingValue(layers []Layer, key string) (any, bool) {
|
|
var value any
|
|
set := false
|
|
for _, layer := range layers {
|
|
if v, has := layer.Values[key]; has {
|
|
value, set = v, true
|
|
}
|
|
}
|
|
return value, set
|
|
}
|
|
|
|
func orNoSettings(layers []Layer) string {
|
|
var keys []string
|
|
for _, l := range layers {
|
|
if l.Default {
|
|
continue
|
|
}
|
|
for k := range l.Values {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
if len(keys) == 0 {
|
|
return "; no setting is set for this module"
|
|
}
|
|
sort.Strings(keys)
|
|
return "; set today: " + strings.Join(keys, ", ")
|
|
}
|
|
|
|
// settingKeysUsedBy is every key a module's files, contributions and served facts ask for, so a
|
|
// setting that lands in one is not called stray.
|
|
func settingKeysUsedBy(m Manifest) map[string]bool {
|
|
used := map[string]bool{}
|
|
note := func(s string) {
|
|
for _, k := range settingsUsed(s) {
|
|
used[k] = true
|
|
}
|
|
}
|
|
for _, r := range m.Resources {
|
|
switch fmt.Sprint(r["type"]) {
|
|
case "file":
|
|
if content, ok := r["content"].(string); ok {
|
|
note(content)
|
|
}
|
|
case "service":
|
|
if unit, ok := r["unit"].(string); ok {
|
|
note(unit)
|
|
}
|
|
}
|
|
}
|
|
inValues := func(values map[string]any) {
|
|
for _, v := range values {
|
|
if s, ok := v.(string); ok {
|
|
note(s)
|
|
}
|
|
}
|
|
}
|
|
for _, values := range m.Contributes {
|
|
inValues(values)
|
|
}
|
|
for _, locals := range m.ContributesMany {
|
|
for _, values := range locals {
|
|
inValues(values)
|
|
}
|
|
}
|
|
for _, values := range m.Serves {
|
|
inValues(values)
|
|
}
|
|
return used
|
|
}
|