Leave out a module whose stored manifest has an unknown field, and raise it (hq ADR 0262 review)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/setting-defaults failed: a member failed
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/setting-defaults failed: a member failed
A key dropped silently ran a module without what its manifest says, and a key inside a block still failed the whole catalogue. Judge a key by what it is about, and narrow the listing to one machine.
This commit is contained in:
@@ -547,6 +547,12 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
if len(positionals) == 1 {
|
||||
only = positionals[0]
|
||||
}
|
||||
if *node != "" {
|
||||
// A machine the mesh does not know is refused, never answered with an empty listing.
|
||||
if _, err := inv.NodeByName(ctx, *node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -557,6 +563,10 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
if len(m.Settings) == 0 || (only != "" && m.Module != only) {
|
||||
continue
|
||||
}
|
||||
if *node != "" && !containsString(e.On, *node) {
|
||||
// Asked for one machine: a module not on it has no value there to say.
|
||||
continue
|
||||
}
|
||||
p := preferencesOf{Manifest: m, On: map[string][]catalogue.SettingSource{}}
|
||||
for _, n := range e.On {
|
||||
if *node != "" && n != *node {
|
||||
@@ -572,7 +582,11 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
listed = append(listed, p)
|
||||
}
|
||||
if only != "" && len(listed) == 0 {
|
||||
fmt.Printf("%s declares no preferences\n", only)
|
||||
fmt.Printf("%s declares no preferences%s\n", only, onNode(*node))
|
||||
return nil
|
||||
}
|
||||
if len(listed) == 0 && *node != "" {
|
||||
fmt.Printf("no module on %s declares a preference\n", *node)
|
||||
return nil
|
||||
}
|
||||
fmt.Print(describePreferences(listed))
|
||||
@@ -610,6 +624,14 @@ func describeEffective(module, where string, values []catalogue.SettingSource) s
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// onNode is ` on <node>` for one machine, nothing for the whole mesh.
|
||||
func onNode(node string) string {
|
||||
if node == "" {
|
||||
return ""
|
||||
}
|
||||
return " on " + node
|
||||
}
|
||||
|
||||
// preferencesOf is one module's preferences and its value on each machine it is assigned to.
|
||||
type preferencesOf struct {
|
||||
Manifest catalogue.Manifest
|
||||
|
||||
@@ -478,6 +478,9 @@ func settlingPending(ctx context.Context, open *stores) {
|
||||
for _, line := range settlePending(ctx, open, time.Now()) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
for _, line := range raiseUnknownFields(ctx, open.inventory) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
@@ -515,9 +515,8 @@ func sortedKeysOf(m map[string]string) []string {
|
||||
// 0163, rule 6), one line each: the machine is told everything else, and is told it was left out.
|
||||
func reportLeftOut(node string, declared sendable) {
|
||||
for _, m := range declared.LeftOut {
|
||||
fmt.Printf("%s: %s left out — a setting stored for it cannot compose with its definition; "+
|
||||
"what the machine holds for it is kept and its containers are untouched. %s\n",
|
||||
node, m, declared.leftOutWhy[m])
|
||||
fmt.Printf("%s: %s left out — what the machine holds for it is kept and its containers are "+
|
||||
"untouched. %s\n", node, m, declared.leftOutWhy[m])
|
||||
}
|
||||
// And whom it serves nothing, because their identity overflows what the provision keeps (ADR
|
||||
// 0225): the machine is sent everything else, and the consumer is named.
|
||||
|
||||
@@ -764,6 +764,12 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
// Every module's preferences, their defaults and each machine's value (novox/hq ADR 0262):
|
||||
// the one interface for them, so no module builds a settings tool of its own. Asked for by
|
||||
// name, or by naming no module, since a layer is always some module's.
|
||||
if str("module") == "" && str("values") != "" {
|
||||
return nil, errors.New("settings: a module is needed to set values; name it with module")
|
||||
}
|
||||
if str("module") == "" && on("clear") {
|
||||
return nil, errors.New("settings: a module is needed to clear a layer; name it with module")
|
||||
}
|
||||
if list := str("list"); list != "" || str("module") == "" {
|
||||
if list != "" && list != "preferences" {
|
||||
return nil, fmt.Errorf("settings lists %q only; %q is not a listing", "preferences", list)
|
||||
|
||||
@@ -37,6 +37,14 @@ func TestSettingsListPreferences(t *testing.T) {
|
||||
t.Errorf("%v: %v %v, want %s", c.args, argv, err, c.want)
|
||||
}
|
||||
}
|
||||
for args, want := range map[string]map[string]any{
|
||||
"a module is needed to set values": {"values": `{"width": 300}`},
|
||||
"a module is needed to clear a layer": {"clear": "true"},
|
||||
} {
|
||||
if _, err := argvFor("settings", want); err == nil || !strings.Contains(err.Error(), args) {
|
||||
t.Errorf("%v: %v, want %q", want, err, args)
|
||||
}
|
||||
}
|
||||
if _, err := argvFor("settings", map[string]any{"list": "everything"}); err == nil {
|
||||
t.Error("a listing other than preferences was taken")
|
||||
}
|
||||
@@ -69,3 +77,33 @@ func TestPreferencesSayEachMachinesValueAndItsSource(t *testing.T) {
|
||||
t.Fatal("an empty listing")
|
||||
}
|
||||
}
|
||||
|
||||
// The listing over the real stores: each machine's value with its source; a machine names only the
|
||||
// modules on it; a machine the mesh does not know is refused (novox/hq ADR 0262).
|
||||
func TestPreferencesListedFromTheStores(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
||||
Settings: map[string]catalogue.SettingDeclaration{
|
||||
"font-size": {Kind: catalogue.KindPreference, Default: float64(10), Why: "readable at 100 DPI"},
|
||||
},
|
||||
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644",
|
||||
"content": "font = ${setting:font-size}\n"}}})
|
||||
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := open.inventory.SetSettings(ctx, "laptop", "notes", map[string]any{"font-size": float64(16)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := stdoutOf(t, func() error { return settingsCommand(ctx, []string{"preferences"}) })
|
||||
if !strings.Contains(all, "notes (on laptop)") || !strings.Contains(all, "laptop: 16 (the node)") ||
|
||||
!strings.Contains(all, "font-size, default 10: readable at 100 DPI") {
|
||||
t.Fatalf("the listing:\n%s", all)
|
||||
}
|
||||
if got := stdoutOf(t, func() error { return settingsCommand(ctx, []string{"preferences", "--node", "anchor"}) }); got != "no module on anchor declares a preference\n" {
|
||||
t.Fatalf("a machine without the module:\n%s", got)
|
||||
}
|
||||
if err := settingsCommand(ctx, []string{"preferences", "--node", "nowhere"}); err == nil {
|
||||
t.Fatal("a machine the mesh does not know was answered")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// A stored manifest with a key this controller does not know (novox/hq ADR 0262). The module is left out
|
||||
// of every machine's declaration by name; this is the loud half: a condition per module until the
|
||||
// controller is updated, or the module is registered again in a shape this controller reads.
|
||||
|
||||
const (
|
||||
sourceUnknownFields = "the catalogue"
|
||||
kindUnknownField = "unknown-field"
|
||||
)
|
||||
|
||||
// unknownFieldObservations is one condition for each module of the catalogue whose stored manifest has
|
||||
// a key this controller does not know.
|
||||
func unknownFieldObservations(known map[string]catalogue.Manifest) []conditions.Observation {
|
||||
names := make([]string, 0, len(known))
|
||||
for name, m := range known {
|
||||
if m.UnknownField() != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
var out []conditions.Observation
|
||||
for _, name := range names {
|
||||
m := known[name]
|
||||
out = append(out, conditions.Observation{
|
||||
Scope: conditions.ScopeMesh, ID: name, Kind: kindUnknownField, Severity: conditions.Warning,
|
||||
Resolver: conditions.ResolverOperator, Source: sourceUnknownFields,
|
||||
Summary: catalogue.UnknownFieldReason(m),
|
||||
Said: m.UnknownField(),
|
||||
Headline: name + " is left out until the controller is updated",
|
||||
Explanation: name + " uses a field this controller does not know, so it is left out of every machine it is on, and nothing of it changes there until the controller is updated.",
|
||||
Needs: "update the controller, or register " + name + " again at a version this controller knows.",
|
||||
Resolved: "the controller reads " + name + " again",
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// raiseUnknownFields raises those conditions and clears the ones no longer true, on the controller's
|
||||
// tick. A catalogue that could not be read raises and clears nothing: "none" is not said for "could not
|
||||
// tell" (ADR 0227 rule 4).
|
||||
func raiseUnknownFields(ctx context.Context, inv *inventory.Inventory) []string {
|
||||
if conditionsFrom == nil {
|
||||
return nil
|
||||
}
|
||||
known, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("the catalogue could not be read to say which modules it cannot read: %v", err)}
|
||||
}
|
||||
if err := conditionsFrom.Reconcile(ctx, sourceUnknownFields, unknownFieldObservations(known)); err != nil {
|
||||
return []string{fmt.Sprintf("the modules with a field this controller does not know could not be kept as conditions: %v", err)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
)
|
||||
|
||||
// A module whose stored manifest has a key this controller does not know is a condition, in plain
|
||||
// words, until it is read again; every other module raises nothing (novox/hq ADR 0262).
|
||||
func TestAModuleWithAnUnknownFieldIsACondition(t *testing.T) {
|
||||
var later, now catalogue.Manifest
|
||||
if err := json.Unmarshal([]byte(`{"module": "dunst", "version": "2", "settings": {}, "a-field-from-later": 1}`), &later); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`{"module": "xorg", "version": "1"}`), &now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
observed := unknownFieldObservations(map[string]catalogue.Manifest{"dunst": later, "xorg": now})
|
||||
if len(observed) != 1 || observed[0].ID != "dunst" || observed[0].Kind != kindUnknownField {
|
||||
t.Fatalf("observed: %+v", observed)
|
||||
}
|
||||
o := observed[0]
|
||||
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
|
||||
Resolved: o.Resolved, Needs: o.Needs}); !ok {
|
||||
t.Fatalf("not plain: %s", why)
|
||||
}
|
||||
|
||||
k, _ := withConditionsInMemory(t)
|
||||
if err := k.Reconcile(t.Context(), sourceUnknownFields, observed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, open, _ := k.Get(t.Context(), o.Key()); !open {
|
||||
t.Fatal("not raised")
|
||||
}
|
||||
if err := k.Reconcile(t.Context(), sourceUnknownFields, unknownFieldObservations(map[string]catalogue.Manifest{"xorg": now})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
still, err := k.Open(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range still {
|
||||
if c.Key == o.Key() {
|
||||
t.Fatalf("not cleared once read again: %+v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user