H2 engine-if-config: a resource is declared only while a config value holds
The composer leaves out each resource whose `if-config` holds none of its conditions against the module's config values (with defaults), and drops the key from the rest, as the node-engine parses strictly. A resource that leaves is removed by the existing orphan removal. One table test replaces the two tests of setting_into_test.go. Builds: design 42 zfs-owns-zfs §2, A-21 (declaration side), A-22
This commit is contained in:
@@ -659,6 +659,9 @@ func resolutionCompose(r mcat.Resolution, with mcat.Rendering, owner map[string]
|
||||
// parses strictly and knows no such field.
|
||||
absent := AbsentFor(r, m.Module)
|
||||
resources = withoutAbsent(resources, absent)
|
||||
// **A resource declared only while a config value holds** (design 42 zfs-owns-zfs §2): left out while none of
|
||||
// its `if-config` conditions holds, and the key dropped from the rest.
|
||||
resources = withoutUnconfigured(resources, WithDefaults(m, with.Settings[m.Module]))
|
||||
|
||||
// No container is given the mesh's names (novox/hq ADR 0148). It used to be: every
|
||||
// container got the whole roster as `--add-host` entries at creation, and a name that
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// withoutUnconfigured drops each resource whose `if-config` holds none of its conditions against the module's config
|
||||
// values (design 42 zfs-owns-zfs §2, ADR 0679 §16 (b)), and the key itself from every resource that keeps it: the
|
||||
// node-engine parses strictly and knows no such key. What leaves the declaration this way is removed as any resource
|
||||
// that leaves it (ADR 0269 §2), by the node-engine's own orphan removal.
|
||||
func withoutUnconfigured(resources []map[string]any, layers []mcat.Layer) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(resources))
|
||||
for _, r := range resources {
|
||||
conditions, has := r[mcat.IfConfigField]
|
||||
if !has {
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
if !ifConfigHolds(conditions, layers) {
|
||||
continue
|
||||
}
|
||||
kept := make(map[string]any, len(r))
|
||||
for k, v := range r {
|
||||
if k != mcat.IfConfigField {
|
||||
kept[k] = v
|
||||
}
|
||||
}
|
||||
out = append(out, kept)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ifConfigHolds is whether any condition of a list holds: `<key>` when the config value is set and is neither false,
|
||||
// off, none, empty nor an empty list; `<key>=<value>` when the value equals it, or a list value holds it as an entry.
|
||||
func ifConfigHolds(conditions any, layers []mcat.Layer) bool {
|
||||
list, _ := conditions.([]any)
|
||||
for _, c := range list {
|
||||
key, want, equals := strings.Cut(fmt.Sprint(c), "=")
|
||||
value, set := settingValue(layers, key)
|
||||
if !set {
|
||||
continue
|
||||
}
|
||||
if !equals {
|
||||
if !isOff(value) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if entries, isList := value.([]any); isList {
|
||||
for _, e := range entries {
|
||||
if plainly(e) == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if plainly(value) == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isOff is a config value that says "not here": false, off, none, an empty text or an empty list.
|
||||
func isOff(value any) bool {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return !v
|
||||
case []any:
|
||||
return len(v) == 0
|
||||
case nil:
|
||||
return true
|
||||
}
|
||||
switch plainly(value) {
|
||||
case "", "false", "off", "none":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,42 +1,121 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// An operator's value reaches a file from the assignment's settings, the node's layer over the
|
||||
// mesh's (novox/hq ADR 0112, ADR 0155), and a value nothing set is refused by name.
|
||||
func TestASettingReachesAFileFromTheLayers(t *testing.T) {
|
||||
file := map[string]any{"id": "env", "type": "file", "path": "/x/mail.env",
|
||||
"content": "DOMAIN=${setting:domain}\nSITENAME=${setting:sitename}\nWORKERS=${setting:workers}\n"}
|
||||
layers := []mcat.Layer{
|
||||
{From: "the mesh", Values: map[string]any{"domain": "example.tld", "sitename": "Mesh", "workers": float64(4)}},
|
||||
{From: "this node", Values: map[string]any{"sitename": "This one"}},
|
||||
// A config value reaches a resource from the assignment's layers, the node's over the mesh's (novox/hq ADR 0112,
|
||||
// ADR 0155), and a value nothing set is refused by name; and a resource under `if-config` is declared only while a
|
||||
// config value holds (design 42 zfs-owns-zfs §2, ADR 0679 §16 (b)). One table of the two, each row its layers, what it
|
||||
// does to a resource and what comes of it.
|
||||
func TestAConfigValueReachesAResourceAndDecidesWhetherItIsDeclared(t *testing.T) {
|
||||
filled := func(content string) func([]mcat.Layer) (string, error) {
|
||||
return func(layers []mcat.Layer) (string, error) {
|
||||
file := map[string]any{"id": "env", "type": "file", "path": "/x/mail.env", "content": content}
|
||||
err := settingInto(file, layers, "mail")
|
||||
return file["content"].(string), err
|
||||
}
|
||||
}
|
||||
if err := settingInto(file, layers, "mail"); err != nil {
|
||||
t.Fatal(err)
|
||||
// declared is the ids of what is left of a resource under each condition, and whether the key is gone from it.
|
||||
declared := func(conditions ...any) func([]mcat.Layer) (string, error) {
|
||||
return func(layers []mcat.Layer) (string, error) {
|
||||
kept := withoutUnconfigured([]map[string]any{
|
||||
{"id": "plain", "type": "file"}, {"id": "cond", "type": "file", mcat.IfConfigField: conditions}}, layers)
|
||||
var ids []string
|
||||
for _, r := range kept {
|
||||
if _, has := r[mcat.IfConfigField]; has {
|
||||
return "", errKeyKept
|
||||
}
|
||||
ids = append(ids, r["id"].(string))
|
||||
}
|
||||
return strings.Join(ids, ","), nil
|
||||
}
|
||||
}
|
||||
if file["content"] != "DOMAIN=example.tld\nSITENAME=This one\nWORKERS=4\n" {
|
||||
t.Fatalf("filled as %q", file["content"])
|
||||
values := func(kv map[string]any) []mcat.Layer { return []mcat.Layer{{From: "the mesh", Values: kv}} }
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
layers []mcat.Layer
|
||||
do func([]mcat.Layer) (string, error)
|
||||
want string // what comes of it
|
||||
errs []string // or what the refusal says
|
||||
}{
|
||||
{"the node's layer over the mesh's", []mcat.Layer{
|
||||
{From: "the mesh", Values: map[string]any{"domain": "example.tld", "sitename": "Mesh", "workers": float64(4)}},
|
||||
{From: "this node", Values: map[string]any{"sitename": "This one"}}},
|
||||
filled("DOMAIN=${setting:domain}\nSITENAME=${setting:sitename}\nWORKERS=${setting:workers}\n"),
|
||||
"DOMAIN=example.tld\nSITENAME=This one\nWORKERS=4\n", nil},
|
||||
{"a value nothing set is refused, the file left as it was", values(map[string]any{"other": "x"}),
|
||||
filled("DOMAIN=${setting:domain}\n"), "DOMAIN=${setting:domain}\n",
|
||||
[]string{"${setting:domain}", "settings set mail", "set today: other"}},
|
||||
{"if-config: a key never set leaves it out", nil, declared("k"), "plain", nil},
|
||||
{"if-config: a key set to text declares it", values(map[string]any{"k": "x"}), declared("k"), "plain,cond", nil},
|
||||
{"if-config: a key set to false, off, none, empty or an empty list leaves it out", values(map[string]any{"k": false}),
|
||||
declared("k"), "plain", nil},
|
||||
{"if-config: off", values(map[string]any{"k": "off"}), declared("k"), "plain", nil},
|
||||
{"if-config: none", values(map[string]any{"k": "none"}), declared("k"), "plain", nil},
|
||||
{"if-config: empty text", values(map[string]any{"k": ""}), declared("k"), "plain", nil},
|
||||
{"if-config: an empty list", values(map[string]any{"k": []any{}}), declared("k"), "plain", nil},
|
||||
{"if-config: a list of entries", values(map[string]any{"k": []any{"a"}}), declared("k"), "plain,cond", nil},
|
||||
{"if-config: a value equal to the condition", values(map[string]any{"k": "on"}), declared("k=on"), "plain,cond", nil},
|
||||
{"if-config: a value other than the condition", values(map[string]any{"k": "off"}), declared("k=on"), "plain", nil},
|
||||
{"if-config: a list holding the entry", values(map[string]any{"k": []any{"a", "b"}}), declared("k=b"), "plain,cond", nil},
|
||||
{"if-config: a list without the entry", values(map[string]any{"k": []any{"a"}}), declared("k=b"), "plain", nil},
|
||||
{"if-config: any of the conditions", values(map[string]any{"j": "x"}), declared("k", "j"), "plain,cond", nil},
|
||||
{"if-config: a value from a default layer", []mcat.Layer{{Default: true, Values: map[string]any{"k": "on"}}},
|
||||
declared("k=on"), "plain,cond", nil},
|
||||
{"if-config: a number equal to the condition", values(map[string]any{"k": float64(64)}), declared("k=64"), "plain,cond", nil},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
got, err := row.do(row.layers)
|
||||
if len(row.errs) == 0 && err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(row.errs) > 0 && err == nil {
|
||||
t.Fatalf("a refusal was expected, got %q", got)
|
||||
}
|
||||
for _, want := range row.errs {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("the refusal lacks %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
if got != row.want {
|
||||
t.Fatalf("got %q, want %q", got, row.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestASettingNothingSetIsRefusedByName(t *testing.T) {
|
||||
file := map[string]any{"id": "env", "type": "file", "content": "DOMAIN=${setting:domain}\n"}
|
||||
err := settingInto(file, []mcat.Layer{{From: "the mesh", Values: map[string]any{"other": "x"}}}, "mail")
|
||||
if err == nil {
|
||||
t.Fatal("a setting nothing set was written as something")
|
||||
}
|
||||
for _, want := range []string{"${setting:domain}", "settings set mail", "set today: other"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("the refusal lacks %q: %v", want, err)
|
||||
// errKeyKept is what a row of the table above says when `if-config` reached the node-engine, which parses strictly.
|
||||
var errKeyKept = errors.New("the if-config key was kept in a declared resource")
|
||||
|
||||
// The composition takes a resource out while its condition does not hold, and a value going from set to unset takes it
|
||||
// out of the next composition, which the node-engine removes as it removes any resource that left (design 42 §2).
|
||||
func TestAConfigConditionedResourceIsComposedWhileItsValueHolds(t *testing.T) {
|
||||
m := mustParse(t, `{"module":"zfs","version":"1","config":{"zed":{"type":"string","default":"off"}},"resources":[
|
||||
{"id":"conf","type":"file","path":"/etc/zfs/mesh.conf","content":"x\n"},
|
||||
{"id":"zed","type":"file","path":"/etc/zfs/zed.rc","content":"y\n","if-config":["zed=on"]}]}`)
|
||||
for _, row := range []struct {
|
||||
layers []mcat.Layer
|
||||
zed bool
|
||||
}{{nil, false}, {[]mcat.Layer{{From: "this node", Values: map[string]any{"zed": "on"}}}, true}} {
|
||||
out, err := Declaration(mcat.Resolution{Node: "workstation", Modules: []mcat.Manifest{m}},
|
||||
mcat.Rendering{Settings: mcat.SettingsBy{"zfs": row.layers}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fileNamed(out, "zfs.conf") == nil {
|
||||
t.Error("the unconditioned file was left out")
|
||||
}
|
||||
zed := fileNamed(out, "zfs.zed")
|
||||
if (zed != nil) != row.zed {
|
||||
t.Errorf("with %v the conditioned file is declared: %v, want %v", row.layers, zed != nil, row.zed)
|
||||
}
|
||||
if _, has := zed[mcat.IfConfigField]; has {
|
||||
t.Errorf("%s reached the node-engine, which parses strictly", mcat.IfConfigField)
|
||||
}
|
||||
}
|
||||
// Left as it was: the literal placeholder must never reach a machine.
|
||||
if file["content"] != "DOMAIN=${setting:domain}\n" {
|
||||
t.Fatalf("content was changed on refusal: %q", file["content"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user