diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index f8da072e..8805a867 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -425,8 +425,16 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { candidates := kvOf(buckets.ProposalCandidates) packages := kvOf(link.PackagesBucket) path := filepath.Join(b.dir, "web.conf") - man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` + + // Beside the greeting: a file under `if-config`, a template with `config-lines` from a list, and two files under a + // value that makes the first of them fail (a file below a file), the second being left (design 42 zfs-owns-zfs §5). + more := func(name string) string { return filepath.Join(b.dir, "web-"+name+".conf") } + man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"},` + + `"mods":{"type":"list","description":"lines","default":["a","b"]}},` + `"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0644","content":"greeting=${config:greeting}\n"},` + + `{"id":"mods","type":"file","path":"` + more("mods") + `","mode":"0644","content":"${config-lines:mods:m=}\n"},` + + `{"id":"salut","type":"file","path":"` + more("salut") + `","mode":"0644","content":"salut\n","if-config":["greeting=salut"]},` + + `{"id":"bad","type":"file","path":"` + path + `/x","mode":"0644","content":"x\n","if-config":["greeting=boom"]},` + + `{"id":"after","type":"file","path":"` + more("after") + `","mode":"0644","content":"after\n","if-config":["greeting=boom"]},` + `{"id":"tools","type":"package","package":"web-tools"}],` + `"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}` digest := meshcatalogclient.ManifestDigest([]byte(man)) @@ -538,6 +546,36 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str t.Fatal(err) } within(t, 3*time.Second, "a changed config value was not composed and applied again", func() bool { return b.file("web") == "greeting=salut\n" }) + // Design 42 zfs-owns-zfs §5: the file under `if-config` appears with its value and goes with it, the template's lines + // follow the list, and a failing resource stops its module's apply with the later resource left. + other := func(name string) string { + got, _ := os.ReadFile(filepath.Join(b.dir, "web-"+name+".conf")) + return string(got) + } + setGreeting := func(value string) { + if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"`+value+`","source":"module"}`)); err != nil { + t.Fatal(err) + } + } + within(t, 3*time.Second, "the file under if-config did not appear with its value, or the lines do not follow the list", func() bool { + return other("salut") == "salut\n" && other("mods") == "m=a\nm=b\n" + }) + setGreeting("bonjour") + within(t, 3*time.Second, "the file under if-config did not go with its value", func() bool { + _, err := os.Stat(filepath.Join(b.dir, "web-salut.conf")) + return b.file("web") == "greeting=bonjour\n" && errors.Is(err, os.ErrNotExist) + }) + setGreeting("boom") + within(t, 5*time.Second, "the failing resource did not fail its module", func() bool { + return b.have("web").Phase == nec.PhaseFailed + }) + if got := other("after"); got != "" { + t.Errorf("a resource after the failed one was applied: %q", got) + } + setGreeting("salut") + within(t, 5*time.Second, "the module did not run again once its value held no failing resource", func() bool { + return b.file("web") == "greeting=salut\n" && b.have("web").Phase == nec.PhaseRunning + }) within(t, 2*time.Second, "the have does not name the revision", func() bool { h := b.have("web") return h.Fp == fp && h.Composed != first && h.Phase == nec.PhaseRunning