diff --git a/cmd/mesh-controller/merge_gate.go b/cmd/mesh-controller/merge_gate.go index ae1224eb..f4dcdd59 100644 --- a/cmd/mesh-controller/merge_gate.go +++ b/cmd/mesh-controller/merge_gate.go @@ -585,11 +585,18 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve return err } m, err := catalogue.ParseManifestWith(raw, predefined) + // The manifest as written, still naming its templates: what the stand-in build resolves, as a real build + // does, so a moved module is handed its templates (issue 876). + written := m + var texts map[string]string if err == nil { // Each template's text from the tree beside the manifest, as the build reads it back from the archive it // packs (novox/hq ADR 0349 §2): so a change to a template alone is a change to its module. m, err = catalogue.WithTemplatesFrom(m, filepath.Dir(path)) } + if err == nil && written.Templated() { + texts, err = catalogue.ReadTemplates(written, filepath.Dir(path)) + } if err != nil { if inScope(dir) { failures = append(failures, fmt.Sprintf("%s: the controller judging this (%s) refuses it — %s", @@ -614,7 +621,7 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve "of one module name (issue 239)", orRoot(dir), m.Module, s.Repository)) return nil } - resolved, err := standInBuild(m) + resolved, err := standInBuild(written, texts) if err == nil { // As registration reads a built manifest: strictly, again (takeIn). var raw []byte @@ -737,15 +744,24 @@ func builtAlike(m catalogue.Manifest) string { // standInBuild resolves a manifest as a build would, with artifacts nobody built: a digest made from the // module's and the artifact's names, in the store's own vocabulary. Composition and validation read the // shape of a reference, never its bytes. -func standInBuild(m catalogue.Manifest) (catalogue.Manifest, error) { +func standInBuild(m catalogue.Manifest, texts map[string]string) (catalogue.Manifest, error) { if m.Build == nil { return m.Resolve(nil) } + // The templates archive carries each template's text by its path inside the archive, as a build reads it back. + archive, inside := catalogue.TemplatesArtifact(m), m.TemplatesDir()+"/" + read := map[string]string{} + for template, text := range texts { + read[strings.TrimPrefix(template, inside)] = text + } var built []catalogue.Built for _, a := range m.Build.Artifacts { sum := sha256.Sum256([]byte("gate\x00" + m.Module + "\x00" + a.Name)) digest := fmt.Sprintf("sha256:%x", sum) b := catalogue.Built{Name: a.Name, Kind: a.Kind, Digest: digest} + if a.Name == archive && archive != "" { + b.Texts = read + } switch a.Kind { case catalogue.ArtifactImage, catalogue.ArtifactUpstream: b.Reference = catalogue.ArtifactStoreScheme + m.Module + "/" + a.Name + "@" + digest diff --git a/cmd/mesh-controller/template_test.go b/cmd/mesh-controller/template_test.go index 8ad78f36..2a14e3e4 100644 --- a/cmd/mesh-controller/template_test.go +++ b/cmd/mesh-controller/template_test.go @@ -185,11 +185,11 @@ func TestTheMergeGateResolvesATemplatedModuleToItsInlineTwin(t *testing.T) { if err != nil { t.Fatal(err) } - m, err = catalogue.WithTemplatesFrom(m, dir) + texts, err := catalogue.ReadTemplates(m, dir) if err != nil { t.Fatal(err) } - resolved, err := standInBuild(m) + resolved, err := standInBuild(m, texts) if err != nil { t.Fatal(err) } @@ -239,3 +239,34 @@ func TestModuleCheckReadsTemplatesFromTheTemplatesDirectoryTheManifestNames(t *t t.Fatalf("a template outside the templates directory the manifest names passed:\n%s", got.String()) } } + +// Issue 876: the gate judges a moved module that names its templates as registration reads it after a real build, +// handed its templates; it refused every such module as "marked moved and names no template". This calls the gate's +// own shelf (shelfWithChange), which needs no database. +func TestReplay876(t *testing.T) { + moved := strings.Replace(templatedNotifier, `"version":"1",`, `"version":"1","moved":true,`, 1) + judge := func(manifest string, files map[string]string) (map[string]catalogue.Manifest, []string) { + tree := t.TempDir() + aTemplatedModule(t, filepath.Join(tree, "modules", "notifier"), manifest, files) + out, failures, err := shelfWithChange(map[string]catalogue.Manifest{}, nil, snapshot.Facts{}, + "novox/mesh-catalog", tree, map[string]string{}, nil) + if err != nil { + t.Fatal(err) + } + return out, failures + } + out, failures := judge(moved, map[string]string{"files/notifier.conf": "volume = 3\n"}) + if len(failures) > 0 { + t.Fatalf("a moved module naming its templates was refused: %v", failures) + } + if h := out["notifier"].Handed; h == nil || h.Templates["conf"] != "notifier.conf" { + t.Fatalf("the gate did not hand the moved module its templates: %+v", h) + } + + // A moved module naming no template is still refused. + none := strings.Replace(strings.Replace(moved, `"template":"files/notifier.conf"`, + `"content":"volume = 3\n"`, 1), `"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},`, "", 1) + if _, failures := judge(none, nil); len(failures) != 1 || !strings.Contains(failures[0], "names no template") { + t.Fatalf("a moved module naming no template was not refused: %v", failures) + } +} diff --git a/internal/catalogue/consumers.go b/internal/catalogue/consumers.go index c2200c87..46a64e0e 100644 --- a/internal/catalogue/consumers.go +++ b/internal/catalogue/consumers.go @@ -61,7 +61,7 @@ func consumersPlaceholderProblems(m Manifest, r map[string]any) []string { // client alone fills it, so on a machine that does not hand the module it is **left out** of the declaration, never // written as that text or as an empty list the provider would read as no consumers, and never the reason the module is // refused: a provider not handed there keeps working as it did, from its grant files (novox/hq ADR 0373, issue 780). -// A moved module judged from its repository's tree (WithTemplatesFrom: `module check`, the merge gate), on a machine +// A moved module judged from its repository's tree (WithTemplatesFrom: `module check`), on a machine // that would hand it, has no templates archive to hand and is judged as it composes; there the placeholder is judged // and taken out of the text, since what is composed so is never sent (judged answers whether the file is so judged). func consumersHandedOnly(m Manifest, r map[string]any, with Rendering) (judged, leftOut bool) {