Files
mesh-controller/internal/inventory/config_move_test.go
T

219 lines
8.6 KiB
Go

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 `<node>.<module>`, 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)
}
}