diff --git a/cmd/mesh-controller/composed_keys_test.go b/cmd/mesh-controller/composed_keys_test.go index 6dd11288..d29be2c9 100644 --- a/cmd/mesh-controller/composed_keys_test.go +++ b/cmd/mesh-controller/composed_keys_test.go @@ -99,7 +99,7 @@ func TestAChangedUserListIsANewRevisionOfTheBusModulesCompositionAlone(t *testin d["resources"] = append(d["resources"].([]any), map[string]any{"id": "nats.bus-users", "type": "file", "module": "nats", "path": "/var/lib/mesh/nats/users.conf", "content": users, "mode": "0600"}) raw, _ := json.Marshal(d) - comps, _, err := compositionsOf("g14", raw, fps, nil) + comps, _, err := compositionsOf("g14", raw, fps, nil, nil) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/composed_live.go b/cmd/mesh-controller/composed_live.go index 15cbc799..45724876 100644 --- a/cmd/mesh-controller/composed_live.go +++ b/cmd/mesh-controller/composed_live.go @@ -84,7 +84,7 @@ func composeModules(ctx context.Context, open *stores, nodes []string) (map[stri if err := open.inventory.RecordComposedPlans(ctx, node, plansKept(declared)); err != nil { errs = append(errs, fmt.Errorf("%s: its provisioner's plans: %w", node, err)) } - comps, moved, err := compositionsOf(node, body, fps, manifests) + comps, moved, err := compositionsOf(node, body, fps, manifests, declared.modules) if err != nil { errs = append(errs, fmt.Errorf("%s: %w", node, err)) continue diff --git a/cmd/mesh-controller/composed_writer.go b/cmd/mesh-controller/composed_writer.go index e9389d1d..141cf5ae 100644 --- a/cmd/mesh-controller/composed_writer.go +++ b/cmd/mesh-controller/composed_writer.go @@ -64,7 +64,7 @@ type composedPlace struct { // fingerprint fps gives it, the node's own with its adoption and provisioner, and every resource of a module fps names // no fingerprint for in the node's own, its module taken off it and named in moved. Data says the module's manifest // declares data. -func compositionsOf(node string, body []byte, fps map[string]string, manifests map[string]catalogue.Manifest) ( +func compositionsOf(node string, body []byte, fps map[string]string, manifests map[string]catalogue.Manifest, runs []string) ( out map[string]sdkdelivery.Composed, moved []string, err error) { var envelope struct { @@ -110,6 +110,22 @@ func compositionsOf(node string, body []byte, fps map[string]string, manifests m if err != nil { return nil, nil, err } + // A module the node runs that composes no resource of its own (all its needs met by others) still has its + // composition, empty, at its fingerprint: its want names that composition (the assigner's seventh rule), and without + // one no want could ever be written. + for _, module := range runs { + fp := fps[module] + if !strings.HasPrefix(fp, sdkdelivery.SourcePrefix2) { + continue + } + key, err := sdkdelivery.ComposedKey(node, module, fp) + if err != nil { + continue + } + if _, ok := out[key]; !ok { + out[key] = sdkdelivery.Composed{Node: node, Module: module, FP: fp, Resources: []json.RawMessage{}} + } + } for key, c := range out { if m, ok := manifests[c.Module]; ok && c.Module != "" { c.Data = len(m.DataItems()) > 0 diff --git a/cmd/mesh-controller/composed_writer_test.go b/cmd/mesh-controller/composed_writer_test.go index 8ffd4552..65efa8df 100644 --- a/cmd/mesh-controller/composed_writer_test.go +++ b/cmd/mesh-controller/composed_writer_test.go @@ -218,7 +218,7 @@ func TestTheCompositionsOfANodeAreItsDeclaration(t *testing.T) { Resources []json.RawMessage `json:"resources"` } _ = json.Unmarshal(body, &d) - comps, moved, err := compositionsOf(node, body, fps, nil) + comps, moved, err := compositionsOf(node, body, fps, nil, nil) if err != nil { t.Fatalf("%s: %v", node, err) } @@ -253,7 +253,7 @@ func TestTheCompositionsOfANodeAreItsDeclaration(t *testing.T) { // unchanged it writes nothing; a changed input writes a new revision of that key alone, by compare-and-set. func TestACompositionIsWrittenOnceAndAgainOnlyWhenItChanges(t *testing.T) { body, fps := capturedBody(t, "g14") - comps, _, err := compositionsOf("g14", body, fps, nil) + comps, _, err := compositionsOf("g14", body, fps, nil, nil) if err != nil { t.Fatal(err) } @@ -335,3 +335,28 @@ func TestCompositionsOutsideTheFiveNewestAreDeleted(t *testing.T) { } } } + +// 1945 (found by the served test): a module the node runs that composes no resource of its own still has its +// composition, empty, at its fingerprint, so a want can name it; a module the node runs with no fingerprint of the new +// form gets none. +func TestAModuleWithNoResourceOfItsOwnStillHasItsComposition(t *testing.T) { + body := []byte(`{"declaration":1,"resources":[]}`) + fp := sdkdelivery.SourcePrefix2 + strings.Repeat("b", 64) + comps, _, err := compositionsOf("laptop", body, map[string]string{"board": fp}, nil, []string{"board", "legacy"}) + if err != nil { + t.Fatal(err) + } + key, _ := sdkdelivery.ComposedKey("laptop", "board", fp) + c, ok := comps[key] + if !ok || len(c.Resources) != 0 { + t.Fatalf("the board's composition: %+v (%v)", c, ok) + } + if _, err := sdkdelivery.EncodeComposed(c); err != nil { + t.Errorf("an empty composition does not encode: %v", err) + } + for k := range comps { + if strings.Contains(k, "legacy") { + t.Errorf("a module with no fingerprint of the new form was composed: %s", k) + } + } +}