package inventory import ( "context" "errors" "reflect" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // configured is a module in the config form: one value a mergeable file takes, and a directory to place. func configured(t *testing.T, name string) catalogue.Manifest { t.Helper() m, err := catalogue.ParseManifest([]byte(`{"module": "` + name + `", "version": "1", "config": {"level": {"type": "string", "description": "how much it says", "default": "info"}}, "resources": [{"id": "data", "type": "directory", "mode": "0750"}, {"id": "conf", "type": "file", "path": "/etc/` + name + `.json", "mode": "0640", "content": "{\"level\": \"info\"}", "merge": "json", "takes-config": ["level"]}]}`)) if err != nil { t.Fatal(err) } return m } // standInConfig is mesh-config's resolved values as a test holds them: by `.`, and each read. type standInConfig struct { values map[string]map[string]any read []string fails error } func (s *standInConfig) read1(_ context.Context, node, module string) (map[string]any, error) { s.read = append(s.read, node+"."+module) if s.fails != nil { return nil, s.fails } return s.values[node+"."+module], nil } // aMeshForTheConfigMove is a store with a module in the config form and one still in the settings form, each with a // layer on anchor, the config one with placement beside it, and a whole-mesh layer of the config one. func aMeshForTheConfigMove(t *testing.T) (*Inventory, *standInConfig) { t.Helper() inv := ForTest(t) ctx := t.Context() old := catalogue.Manifest{Module: "older", Version: "1", Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/older.json", "mode": "0640", "content": `{}`, "merge": "json"}}} for _, m := range []catalogue.Manifest{configured(t, "newer"), old} { if err := inv.RegisterModule(ctx, m, Source{}); err != nil { t.Fatal(err) } } if _, err := inv.AddNode(ctx, "anchor"); err != nil { t.Fatal(err) } for _, m := range []string{"newer", "older"} { if _, err := inv.Assign(ctx, "anchor", m); err != nil { t.Fatal(err) } } if err := inv.SetSettings(ctx, "anchor", "newer", map[string]any{"level": "debug", "places": map[string]any{"data": map[string]any{"path": "/srv/newer"}}}); err != nil { t.Fatal(err) } if err := inv.SetSettings(ctx, "", "newer", map[string]any{"level": "warn"}); err != nil { t.Fatal(err) } if err := inv.SetSettings(ctx, "anchor", "older", map[string]any{"colour": "blue"}); err != nil { t.Fatal(err) } config := &standInConfig{values: map[string]map[string]any{"anchor.newer": {"level": "debug"}}} inv.ReadsConfigFrom(config.read1) return inv, config } // valuesOf is each layer's From and Values, as composition reads them. func valuesOf(layers []catalogue.Layer) map[string]map[string]any { out := map[string]map[string]any{} for _, l := range layers { out[l.From] = l.Values } return out } // **The layers are read until the move of config has run** (novox/hq ADR 0348 §9, issue 664): before it, composition // reads every module's layers and never asks mesh-config. After it, a module in the config form takes its values // from mesh-config, its layers keep their placement and are kept whole in the history; a module in the settings form // keeps its layer. func TestTheLayersAreReadUntilTheMoveOfConfigHasRun(t *testing.T) { inv, config := aMeshForTheConfigMove(t) ctx := t.Context() layers, err := inv.SettingsFor(ctx, "anchor", "newer") if err != nil { t.Fatal(err) } if got := valuesOf(layers); !reflect.DeepEqual(got, map[string]map[string]any{ catalogue.MeshWideLayer: {"level": "warn"}, "anchor": {"level": "debug"}}) || len(config.read) > 0 { t.Fatalf("before the move composition reads %v and asked mesh-config %v", got, config.read) } if moved, err := inv.ConfigMoved(ctx); err != nil || moved.Moved { t.Fatalf("the switch is on before the move: %+v (%v)", moved, err) } toMove, holding, err := inv.LayersToMove(ctx) if err != nil || len(holding) > 0 { t.Fatalf("the layers to move: %v, holding placement %v", err, holding) } var newer []LayerToMove for _, l := range toMove { if l.Module == "newer" { newer = append(newer, l) } } if len(newer) != 2 || newer[0].Node != "" || newer[1].Node != "anchor" { t.Fatalf("the layers to move are %+v", toMove) } if err := inv.FinishConfigMove(ctx, newer, "the move of config, run by a test", "release 57 is delivered"); err != nil { t.Fatal(err) } moved, err := inv.ConfigMoved(ctx) if err != nil || !moved.Moved || moved.Why != "release 57 is delivered" || !strings.Contains(moved.By, "a test") { t.Fatalf("the switch reads %+v (%v)", moved, err) } layers, err = inv.SettingsFor(ctx, "anchor", "newer") if err != nil { t.Fatal(err) } if got := valuesOf(layers); !reflect.DeepEqual(got, map[string]map[string]any{ catalogue.MeshWideLayer: {}, "anchor": {"level": "debug"}}) { t.Fatalf("after the move composition reads %v", got) } if len(config.read) == 0 { t.Fatal("after the move mesh-config was not asked") } for _, l := range layers { if l.From == "anchor" && l.Placement["places"] == nil { t.Fatalf("the placement beside the layer was lost: %+v", l) } } if held := inv.QueryForTest(t, `select values = '{}' from settings where module = 'newer'`); len(held) != 2 || held[0][0] != "true" || held[1][0] != "true" { t.Fatalf("the moved layers hold %v", held) } past := inv.QueryForTest(t, `select values->>'level', values ? 'places', why = $1 from settings_history where module = 'newer' and replaced_by = 'moved' order by node nulls first`, configMovedWhy) if len(past) != 2 || past[0][0] != `"warn"` || past[1][0] != `"debug"` || past[1][1] != "true" || past[0][2] != "true" { t.Fatalf("the history holds %v", past) } // The module in the settings form keeps its layer, and mesh-config is not asked for it. config.read = nil layers, err = inv.SettingsFor(ctx, "anchor", "older") if err != nil || !reflect.DeepEqual(valuesOf(layers), map[string]map[string]any{"anchor": {"colour": "blue"}}) || len(config.read) > 0 { t.Fatalf("the settings form's layer reads %v (%v), mesh-config asked %v", valuesOf(layers), err, config.read) } } // A module that holds a layer again — one moved to the config form after the move — reads its layer until the move // runs again, so no value is read from where it is not yet; mesh-config that cannot be read, or no reader at all, // refuses composition by name rather than composing without the values. func TestAModuleReadsMeshConfigOnlyWhereItsValuesAre(t *testing.T) { inv, config := aMeshForTheConfigMove(t) ctx := t.Context() toMove, _, err := inv.LayersToMove(ctx) if err != nil { t.Fatal(err) } var newer []LayerToMove for _, l := range toMove { if l.Module == "newer" { newer = append(newer, l) } } if err := inv.FinishConfigMove(ctx, newer, "a test", "moved"); err != nil { t.Fatal(err) } inv.ExecForTest(t, `update settings set values = '{"level": "trace"}' where module = 'newer' and node is not null`) layers, err := inv.SettingsFor(ctx, "anchor", "newer") if err != nil || valuesOf(layers)["anchor"]["level"] != "trace" { t.Fatalf("a module holding a layer again reads %v (%v)", valuesOf(layers), err) } inv.ExecForTest(t, `update settings set values = '{}' where module = 'newer'`) config.fails = errors.New("the bucket is gone") if _, err := inv.SettingsFor(ctx, "anchor", "newer"); err == nil || !strings.Contains(err.Error(), "the bucket is gone") || !strings.Contains(err.Error(), "newer on anchor") { t.Fatalf("an unreadable mesh-config composes: %v", err) } inv.ReadsConfigFrom(nil) if _, err := inv.SettingsFor(ctx, "anchor", "newer"); err == nil || !strings.Contains(err.Error(), "no way to read") { t.Fatalf("no reader composes: %v", err) } } // **The move ends only on the layers it read** (issue 664): a layer changed between the move reading it and its // end refuses the whole, and nothing is emptied or switched. func TestTheEndOfTheMoveRefusesALayerChangedSinceItWasRead(t *testing.T) { inv, _ := aMeshForTheConfigMove(t) ctx := t.Context() toMove, _, err := inv.LayersToMove(ctx) if err != nil { t.Fatal(err) } if err := inv.SetSettings(ctx, "anchor", "newer", map[string]any{"level": "error"}); err != nil { t.Fatal(err) } err = inv.FinishConfigMove(ctx, toMove, "a test", "moved") if err == nil || !strings.Contains(err.Error(), "newer on anchor changed") { t.Fatalf("a layer changed since it was read was moved: %v", err) } if moved, _ := inv.ConfigMoved(ctx); moved.Moved { t.Fatal("the switch is on after a refused move") } if held := inv.QueryForTest(t, `select count(*) from settings where values <> '{}'`); held[0][0] != "3" { t.Fatalf("a refused move emptied a layer: %v", held) } }