219 lines
8.6 KiB
Go
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)
|
|
}
|
|
}
|