Settings: changing a module's config without editing its file
Managed files are generated and never edited, so somebody's intention about one has to live where the generator can see it. It does now: the module ships defaults, settings go over the top by key, and the file is produced from both. Upstream can rewrite its half freely and the keys somebody chose survive. Two layers, both from the start. The mesh's settings for a module, then one machine's over those. A node that differs is expressed by differing, rather than by restating everything the rest already say -- which would pin all of it against future changes for no reason. An override beats a default and there is nothing to resolve. A setting is a statement about that key made deliberately; the default was only ever what to do in the absence of one. So when upstream changes a key somebody has set, there is no conflict, no merge markers, and nothing to ask. Nested blocks merge and lists are replaced whole. Setting one field of a block must not delete its siblings, or every setting would restate the whole block and pin all of it. 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". A module can keep specific keys for itself -- a socket path its own code depends on -- and setting one is REFUSED rather than ignored. A setting quietly dropped is somebody believing they changed something. Settings that reach nothing are named at the moment they would be used, not discovered later by the machine not behaving differently. `plan --files` prints what a machine would be given before it is sent, because "1 resource" does not tell you whether the merge landed. One test kept with a note that it does not defend this code: output stability comes from Go's encoder sorting map keys, so it passes with the merging removed. Worth having as the thing that would catch a change of encoder, but it is not evidence about anything written here, and it was checked.
This commit is contained in:
+141
-13
@@ -83,6 +83,8 @@ func run() error {
|
||||
return moduleCommand(ctx, args[1:])
|
||||
case "assign", "unassign":
|
||||
return assignCommand(ctx, args[0], args[1:])
|
||||
case "settings":
|
||||
return settingsCommand(ctx, args[1:])
|
||||
case "plan":
|
||||
return planCommand(ctx, args[1:])
|
||||
case "push":
|
||||
@@ -122,6 +124,9 @@ func usage() {
|
||||
status what the mesh is behind on, and which nodes
|
||||
assign <node> <module> put a module on a node
|
||||
unassign <node> <module> take it off
|
||||
settings set <module> <file> what a module's config should say, for the whole mesh
|
||||
settings set <module> <file> --node <n> ...or for one machine
|
||||
settings clear <module> [--node <n>] take a layer away
|
||||
plan <node> what that node would run, and why
|
||||
push [<node>] send a node everything it should be
|
||||
version what this binary is
|
||||
@@ -799,7 +804,7 @@ func assignCommand(ctx context.Context, verb string, args []string) error {
|
||||
// Resolved immediately, because an assignment that cannot be applied should be said now
|
||||
// rather than at the next push. The assignment is kept either way: it is what a person meant,
|
||||
// and the refusal is about the set rather than about this one.
|
||||
if _, err := planFor(ctx, inv, args[0]); err != nil {
|
||||
if _, _, err := planFor(ctx, inv, args[0]); err != nil {
|
||||
fmt.Println()
|
||||
return err
|
||||
}
|
||||
@@ -808,23 +813,23 @@ func assignCommand(ctx context.Context, verb string, args []string) error {
|
||||
}
|
||||
|
||||
// planFor works out everything a node should run, from what was assigned to it.
|
||||
func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (catalogue.Resolution, error) {
|
||||
func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (catalogue.Resolution, catalogue.SettingsBy, error) {
|
||||
shelf, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
assigned, err := inv.Assigned(ctx, nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
capabilities, err := inv.ProfileOf(ctx, nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, err
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
var site string
|
||||
for _, p := range places {
|
||||
@@ -856,21 +861,57 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca
|
||||
elsewhere = append(elsewhere, got.Claims...)
|
||||
}
|
||||
|
||||
return catalogue.Resolve(shelf, assigned,
|
||||
resolved, err := catalogue.Resolve(shelf, assigned,
|
||||
catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities}, elsewhere)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
// Settings for everything that resolved, including modules nobody assigned directly: a
|
||||
// requirement pulled in by something else is still configurable, and finding out that it is
|
||||
// not only when you try would be an arbitrary line nobody could predict.
|
||||
settings := catalogue.SettingsBy{}
|
||||
var stray []string
|
||||
for _, m := range resolved.Modules {
|
||||
layers, err := inv.SettingsFor(ctx, nodeName, m.Module)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
if len(layers) == 0 {
|
||||
continue
|
||||
}
|
||||
settings[m.Module] = layers
|
||||
stray = append(stray, catalogue.UnusedSettings(m, layers)...)
|
||||
}
|
||||
if len(stray) > 0 {
|
||||
// Somebody set something that reaches no file. Said here rather than discovered by the
|
||||
// machine not behaving differently, which is the slowest way there is.
|
||||
return catalogue.Resolution{}, nil, fmt.Errorf(
|
||||
"these settings reach nothing:\n - %s", strings.Join(stray, "\n - "))
|
||||
}
|
||||
return resolved, settings, nil
|
||||
}
|
||||
|
||||
func planCommand(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("plan <node>")
|
||||
set := flag.NewFlagSet("plan", flag.ContinueOnError)
|
||||
// Because "one resource" does not tell you whether the settings landed. Being able to read
|
||||
// the file before it is sent is the difference between believing a merge worked and knowing.
|
||||
show := set.Bool("files", false, "print the files this node would be given")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(positionals) != 1 {
|
||||
return errors.New("plan <node> [--files]")
|
||||
}
|
||||
args = positionals
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inv.Close()
|
||||
|
||||
plan, err := planFor(ctx, inv, args[0])
|
||||
plan, settings, err := planFor(ctx, inv, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -885,7 +926,26 @@ func planCommand(ctx context.Context, args []string) error {
|
||||
for _, c := range plan.Claims {
|
||||
fmt.Printf(" holds %s, one per %s\n", c.Claim, c.Scope)
|
||||
}
|
||||
fmt.Printf("\n%d resource(s)\n", len(plan.Declaration()))
|
||||
resources, err := plan.Declaration(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for module, layers := range settings {
|
||||
for _, layer := range layers {
|
||||
fmt.Printf(" %-20s settings from %s\n", module, layer.From)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n%d resource(s)\n", len(resources))
|
||||
|
||||
if *show {
|
||||
for _, r := range resources {
|
||||
content, ok := r["content"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n--- %v %v ---\n%s", r["id"], r["path"], content)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -953,12 +1013,17 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
plan, err := planFor(ctx, inv, n.Name)
|
||||
plan, settings, err := planFor(ctx, inv, n.Name)
|
||||
if err != nil {
|
||||
refusals = append(refusals, fmt.Sprintf("%s:\n%v", n.Name, err))
|
||||
continue
|
||||
}
|
||||
sending = append(sending, ready{n, append(resources.Resources, plan.Declaration()...)})
|
||||
fromModules, err := plan.Declaration(settings)
|
||||
if err != nil {
|
||||
refusals = append(refusals, fmt.Sprintf("%s:\n%v", n.Name, err))
|
||||
continue
|
||||
}
|
||||
sending = append(sending, ready{n, append(resources.Resources, fromModules...)})
|
||||
}
|
||||
|
||||
if len(refusals) > 0 {
|
||||
@@ -1057,3 +1122,66 @@ func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
|
||||
rest = rest[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func settingsCommand(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("settings set <module> <file> [--node <node>], or settings clear <module> [--node <node>]")
|
||||
}
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inv.Close()
|
||||
|
||||
set := flag.NewFlagSet("settings", flag.ContinueOnError)
|
||||
node := set.String("node", "", "one machine, rather than the whole mesh")
|
||||
positionals, err := parseAround(set, args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
where := "the whole mesh"
|
||||
if *node != "" {
|
||||
where = *node
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "set":
|
||||
if len(positionals) != 2 {
|
||||
return errors.New("settings set <module> <settings.json> [--node <node>]")
|
||||
}
|
||||
raw, err := os.ReadFile(positionals[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var values map[string]any
|
||||
if err := json.Unmarshal(raw, &values); err != nil {
|
||||
return fmt.Errorf("%s is not a settings file: %w", positionals[1], err)
|
||||
}
|
||||
if err := inv.SetSettings(ctx, *node, positionals[0], values); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range values {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Printf("%s on %s: %s\n", positionals[0], where, strings.Join(keys, ", "))
|
||||
fmt.Println(" run `push` to send it")
|
||||
return nil
|
||||
|
||||
case "clear":
|
||||
if len(positionals) != 1 {
|
||||
return errors.New("settings clear <module> [--node <node>]")
|
||||
}
|
||||
if err := inv.ClearSettings(ctx, *node, positionals[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s on %s is back to what the module says\n", positionals[0], where)
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("settings has no %q; it has set and clear", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user