diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index c1f1e5c..0a4dcee 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -1961,6 +1961,90 @@ func TestAnUnreadSeatListLetsThroughOfTheOwnCompositionOnlyWhatTheHolderOfTheCat } } +// ADR 0636 §1, the whole case: a node that runs two modules inside its own composition (a build of the old form each), +// one of them the holder of the seat mesh-catalog, and reads no seat list. Its own composition, handed before the read, +// carries the holder; the read, said as an event, hands the rest, the other module's resources, with no timer. +func TestTheHolderOfTheCatalogIsStartedFromTheOwnCompositionBeforeTheListIsReadAndTheRestAfter(t *testing.T) { + catalog := `{"module":"catalog","version":"1","claims":[{"name":"` + meshcatalog.Seat + `"}],"capabilities":["systemd"],` + + `"resources":[{"id":"service","type":"service","unit":"catalog.service","state":"running","boot":"enabled"}]}` + other := `{"module":"other","version":"1","capabilities":["systemd"],` + + `"resources":[{"id":"service","type":"service","unit":"other.service","state":"running","boot":"enabled"},` + + `{"id":"extra","type":"service","unit":"extra.service","state":"running","boot":"enabled"}]}` + b := newFakeBus() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + env := b.env() + g := &gate{} + changed := make(chan struct{}, 1) + env.SeatList, env.SeatListChanged = g, changed + env.Running = func() []compositions.Composed { + return []compositions.Composed{{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"other.service","type":"service","unit":"other.service","state":"running","boot":"enabled"}`)}}} + } + c := New(env) + out, err := c.Watch(ctx, "n1.>") + if err != nil { + t.Fatal(err) + } + mods := map[string]string{"catalog": catalog, "other": other} + var placed []meshmoduleassigner.PlacedModule + for _, m := range []string{"catalog", "other"} { + digest := meshcatalogclient.ManifestDigest([]byte(mods[m])) + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: m, Manifest: mods[m]}) + mk, _ := meshcatalogclient.ManifestsKey(digest) + b.kv[buckets.Manifests+"/"+mk] = man + rec, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64), Manifest: digest, Repository: "novox/x", Commit: "c"}) + b.put(t, buckets.Modules, m, rec) + hk, _ := nodeengineclient.HaveKey("n1", m) + hv, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) + b.put(t, buckets.Have, hk, hv) + placed = append(placed, meshmoduleassigner.PlacedModule{Module: m}) + } + place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: placed}) + have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) + nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true}) + b.put(t, buckets.Placement, "placement.n1", place) + b.put(t, buckets.Have, "n1.node", have) + b.put(t, buckets.Wants, "want.n1.node", nodeWant) + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} { + b.send(bucket, agent.Update{Caught: true}) + } + servicesIn := func(u agent.Update) []string { + var comp compositions.Composed + if err := json.Unmarshal(u.Value, &comp); err != nil { + t.Fatalf("the own composition does not decode: %v", err) + } + var ids []string + for _, raw := range comp.Resources { + var h struct{ ID string } + _ = json.Unmarshal(raw, &h) + if strings.HasPrefix(h.ID, "catalog.") || strings.HasPrefix(h.ID, "other.") { + ids = append(ids, h.ID) + } + } + return ids + } + var first agent.Update + for first.Key == "" { + if u := next(t, out); u.Key == "n1.node" && !u.Deleted { + first = u + } + } + if got := servicesIn(first); !slices.Equal(got, []string{"other.service", "catalog.service"}) { + t.Fatalf("before the read the own composition carries services %v, want what ran and the holder's alone", got) + } + g.open.Store(true) + changed <- struct{}{} + for { + u := next(t, out) + if u.Key == "n1.node" && !u.Deleted { + if got := servicesIn(u); !slices.Equal(got, []string{"catalog.service", "other.service", "other.extra"}) { + t.Fatalf("after the read the own composition carries services %v, want what ran, the rest of the module and the holder", got) + } + return + } + } +} + // ADR 0636 §1: a reader the node could not start is no reader that gates nothing: the node holds like one that waits. func TestACompositionHoldsWhenTheSeatListReaderCouldNotStart(t *testing.T) { gate := Unread{}