diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index f53efb3..7f0e1d1 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -19,6 +19,7 @@ import ( "github.com/nats-io/nats.go" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/link" + "github.com/novox/mesh-host/internal/selfcompose" ) // The node agent whole, against a real bus with JetStream started in the test: the wants bucket, the node's composition @@ -311,3 +312,68 @@ func TestNatsTheHandPathAppliesACompositionTheOperatorSigned(t *testing.T) { return h.FP == fp && h.Composed == dig && h.Phase == delivery.PhaseRunning }) } + +// Issue 1126 (ADR 0547 ยง2, the milestone's H8): the node agent started whole against a real bus, on a node the catalogue +// places, composes a module from its want, the indexer's manifest, its record and its mesh-config value, and applies it, +// with no key in the controller's compositions; a changed config value is composed again on its event and applied as a +// revision of what runs. +func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { + b := newItBed(t) + kvOf := func(bucket string) nats.KeyValue { + kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: bucket, History: 5}) + if err != nil { + t.Fatal(err) + } + return kv + } + placement, modules, manifests := kvOf(delivery.PlacementBucket), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket) + resolved := kvOf(selfcompose.ResolvedBucket) + kvOf(delivery.BuilderBuildsBucket) + path := filepath.Join(b.dir, "web.conf") + man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` + + `"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0644","content":"greeting=${config:greeting}\n"}]}` + digest := delivery.ManifestDigest([]byte(man)) + mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "web", JSON: man}) + mkey, _ := delivery.ManifestsKey(digest) + fp := fpIt("a") + modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)}) + place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}}) + rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode, Capabilities: []string{"service-manager"}}}) + pkey, _ := delivery.PlacementKey(itNode) + hkey, _ := delivery.HaveNodeKey(itNode) + for _, w := range []struct { + kv nats.KeyValue + key string + value []byte + }{{manifests, mkey, mraw}, {modules, "web", modv}, {placement, pkey, place}, {b.haves, hkey, rec}, + {resolved, itNode + ".web.greeting", []byte(`{"value":"bonjour"}`)}} { + if _, err := w.kv.Put(w.key, w.value); err != nil { + t.Fatal(err) + } + } + want, _ := delivery.EncodeWant(delivery.Want{FP: fp}) + wkey, _ := delivery.WantKey(itNode, "web") + if _, err := b.wants.Put(wkey, want); err != nil { + t.Fatal(err) + } + + stop := b.start(false) + defer stop() + within(t, 3*time.Second, "the node did not compose and apply its own module", func() bool { return b.file("web") == "greeting=bonjour\n" }) + within(t, 2*time.Second, "the have does not name the build", func() bool { + h := b.have("web") + return h.FP == fp && h.Composed != "" && h.Phase == delivery.PhaseRunning + }) + if keys, _ := b.composed.Keys(); len(keys) != 0 { + t.Errorf("the controller's compositions hold %v; the node composed its own", keys) + } + first := b.have("web").Composed + if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut"}`)); err != nil { + 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" }) + 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 == delivery.PhaseRunning + }) +}