package builder import ( "context" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // A module's files kept as template files in its repository (novox/hq ADR 0349 §2, design 57 package T1, issue // 654): the build packs files/ into the module's templates archive and reads each template's text back from the // packed bytes into the file's content, so the manifest the mesh holds is the one it would hold for the text inline. const templatedModule = `{"module":"meshboard","version":"1", "build":{"artifacts":[ {"name":"server","kind":"image","from":"Dockerfile"}, {"name":"files","kind":"archive","from":"files"}]}, "resources":[ {"id":"svc","type":"container","name":"meshboard","artifact":"server"}, {"id":"conf","type":"file","path":"/etc/meshboard/meshboard.conf","template":"files/meshboard.conf"}, {"id":"deeper","type":"file","path":"/etc/meshboard/deeper.conf","template":"files/a/deeper.conf"}]}` var templatedFiles = map[string]string{ "Dockerfile": "FROM scratch", "files/meshboard.conf": "listen = 8080\n\"quoted\" é\n", "files/a/deeper.conf": "deeper\n", "files/unused.conf": "packed, named by nothing", } func contentOf(t *testing.T, m catalogue.Manifest, id string) string { t.Helper() for _, r := range m.Resources { if r["id"] == id { if _, has := r[catalogue.TemplateField]; has { t.Fatalf("%s still names its template in the built manifest: %v", id, r) } text, ok := r["content"].(string) if !ok { t.Fatalf("%s carries no content: %v", id, r) } return text } } t.Fatalf("no resource %s in %v", id, m.Resources) return "" } func TestABuildReadsEachTemplateBackFromItsArchive(t *testing.T) { r, workspace := aRepository(t, templatedModule, templatedFiles) got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } if text := contentOf(t, got.Manifest, "conf"); text != templatedFiles["files/meshboard.conf"] { t.Fatalf("the template's text is %q, not the file's", text) } if text := contentOf(t, got.Manifest, "deeper"); text != templatedFiles["files/a/deeper.conf"] { t.Fatalf("the template's text is %q, not the file's", text) } if r.archives["meshboard/files"] == "" { t.Fatalf("the templates archive was not published: %v", r.archives) } if problem := catalogue.HeldTemplateProblem(got.Manifest); problem != "" { t.Fatal(problem) } } func TestABuildWhoseTemplateIsMissingFailsNamingIt(t *testing.T) { files := map[string]string{} for k, v := range templatedFiles { files[k] = v } delete(files, "files/a/deeper.conf") r, workspace := aRepository(t, templatedModule, files) _, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err == nil || !strings.Contains(err.Error(), "files/a/deeper.conf") { t.Fatalf("a build whose template is missing did not fail naming it: %v", err) } } // A walk that takes its check's build copies the archive by digest and builds nothing: the templates are read back // from the tree packed again, which is the archive only when it packs to the digest taken. func TestAWalkTakingAChecksBuildReadsTheTemplatesBackToo(t *testing.T) { withVersion(t, "builder-1") contents := map[string]string{"mod/" + ManifestName: templatedModule} for k, v := range templatedFiles { contents["mod/"+k] = v } scratch := newScratch() r, run, workspace := aModuleWith(t, "tree-one", contents) checked := aCheckBuiltBy(t, scratch, r, run, workspace) r, run, workspace = aModuleWith(t, "tree-one", contents) got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)}, "https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatal(err) } if got.Reused == nil || len(r.archives) > 0 { t.Fatalf("the walk built again: reused %+v, why %q, published %v", got.Reused, got.Why, r.archives) } if text := contentOf(t, got.Manifest, "conf"); text != templatedFiles["files/meshboard.conf"] { t.Fatalf("the template's text is %q, not the file's", text) } } // The tree packed again must be the archive taken; text read from anything else is not the archive's. func TestTemplateTextsAreRefusedWhenTheTreeDoesNotPackToTheDigestTaken(t *testing.T) { m, err := catalogue.ParseManifest([]byte(templatedModule)) if err != nil { t.Fatal(err) } within := writeTree(t, t.TempDir(), templatedFiles) built := []catalogue.Built{{Name: "files", Kind: catalogue.ArtifactArchive, Digest: "sha256:" + strings.Repeat("0", 64)}} if _, err := withTemplateTexts(m, within, built); err == nil || !strings.Contains(err.Error(), "digest") { t.Fatalf("text was read from a tree that does not pack to the archive taken: %v", err) } } // writeTree writes files under dir and answers dir. func writeTree(t *testing.T, dir string, files map[string]string) string { t.Helper() for name, body := range files { full := filepath.Join(dir, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(body), 0o644); err != nil { t.Fatal(err) } } return dir }