Files
mesh-controller/internal/catalogue/build_test.go
T
jschoubben 44ba100595 A module says what it builds, and the built manifest is a different
document

The manifest in a repository names artifacts; the manifest the mesh holds
names digests. Keeping them the same file would mean a repository
carrying a digest — wrong the moment anybody edits anything, and pinning
a value nobody could have checked.

So a resource says `"artifact": "server"`, and resolving a build rewrites
it to the image reference or the archive's source and digest, removing
the build-time word entirely. The host has never heard of an artifact and
its strict decoder would refuse one, at the worst moment.

A module that builds nothing is ordinary and needs no build section —
most of what a person installs is configuration, and a field that exists
to be left blank is a field nobody fills in correctly.

Refusals worth having:

- an artifact declared and not produced blames THE BUILD, not the
  resource. Both are failures and the remedies are in different places;
  telling somebody to fix the wrong one costs an afternoon. Found by
  injection: the first version's message could not be told apart from
  the resource-level one, so the check was not actually tested.
- a build reads its own repository and nothing else. An input path
  leaving it makes what gets built depend on whatever happens to be on
  the machine building it.
- two artifacts with one name, because a resource naming it could mean
  either.
2026-08-30 03:29:17 +02:00

145 lines
5.4 KiB
Go

package catalogue
import (
"strings"
"testing"
)
// The manifest in a repository and the manifest the mesh holds are two documents.
//
// A digest is not knowable until something is built, so a repository carrying one would be a
// repository whose file is wrong the moment anybody edits anything — and the mesh would be pinning
// a value nobody could have checked.
func buildable() Manifest {
return Manifest{
Module: "meshboard", Version: "1",
Build: &Build{Artifacts: []Artifact{
{Name: "server", Kind: ArtifactImage, From: "Dockerfile"},
{Name: "theme", Kind: ArtifactArchive, From: "files"},
}},
Resources: []map[string]any{
{"id": "svc", "type": "container", "name": "meshboard", "artifact": "server"},
{"id": "look", "type": "archive", "path": "/opt/meshboard", "artifact": "theme"},
{"id": "dir", "type": "directory", "path": "/opt/meshboard"},
},
}
}
func wasBuilt() []Built {
return []Built{
{Name: "server", Kind: ArtifactImage,
Reference: "registry.invalid/meshboard@sha256:" + strings.Repeat("a", 64)},
{Name: "theme", Kind: ArtifactArchive,
Reference: "https://store.invalid/theme.tar.gz",
Digest: "sha256:" + strings.Repeat("b", 64)},
}
}
func TestAResourceNamingAnArtifactBecomesOneNamingTheThing(t *testing.T) {
got, err := buildable().Resolve(wasBuilt())
if err != nil {
t.Fatal(err)
}
for _, r := range got.Resources {
if r["artifact"] != nil {
// A build-time word the host has never heard of. Leaving it would be a field the
// strict decoder refuses on the machine, at the worst moment.
t.Fatalf("%v still names an artifact: %v", r["id"], r)
}
}
if got.Resources[0]["image"] != "registry.invalid/meshboard@sha256:"+strings.Repeat("a", 64) {
t.Fatalf("the image was not filled in: %v", got.Resources[0])
}
if got.Resources[1]["source"] != "https://store.invalid/theme.tar.gz" ||
got.Resources[1]["digest"] != "sha256:"+strings.Repeat("b", 64) {
t.Fatalf("the archive was not filled in: %v", got.Resources[1])
}
// And a resource that names nothing is untouched.
if got.Resources[2]["path"] != "/opt/meshboard" || len(got.Resources[2]) != 3 {
t.Fatalf("an ordinary resource was changed: %v", got.Resources[2])
}
// The built manifest carries no build section: it is the derived document, and something
// holding both would invite somebody to build from it again.
if got.Build != nil {
t.Fatal("the built manifest still says how to build itself")
}
}
func TestAnArtifactDeclaredAndNotBuiltIsRefused(t *testing.T) {
// A build that did not do what the manifest asked. Said here rather than by a machine later
// reporting an empty image.
_, err := buildable().Resolve(wasBuilt()[:1])
if err == nil {
t.Fatal("a manifest resolved with an artifact missing")
}
if !strings.Contains(err.Error(), "theme") {
t.Fatalf("the refusal does not name what is missing: %v", err)
}
// And it must blame the build rather than the resource. A resource asking for something that
// does not exist is a manifest fault; a build not producing what the manifest declared is a
// build fault, and telling somebody to fix the wrong one costs an afternoon.
if !strings.Contains(err.Error(), "the build did not produce") {
t.Fatalf("the refusal blames the wrong thing: %v", err)
}
}
func TestAResourceNamingSomethingTheModuleDoesNotBuildIsRefused(t *testing.T) {
m := buildable()
m.Resources = append(m.Resources,
map[string]any{"id": "other", "type": "archive", "path": "/x", "artifact": "nothing"})
if _, err := m.Resolve(wasBuilt()); err == nil {
t.Fatal("a resource naming an artifact nobody builds was accepted")
}
}
func TestAModuleThatBuildsNothingIsOrdinary(t *testing.T) {
// Most of what a person installs is configuration. Requiring an empty build section would be
// a field that exists to be left blank.
m := Manifest{Module: "shell", Version: "1", Resources: []map[string]any{
{"id": "rc", "type": "file", "path": "/etc/zsh/zshrc", "content": "setopt"},
}}
got, err := m.Resolve(nil)
if err != nil {
t.Fatal(err)
}
if len(got.Resources) != 1 {
t.Fatalf("got %v", got.Resources)
}
}
func TestTwoArtifactsWithOneNameAreRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"image","from":"Dockerfile"},
{"name":"x","kind":"archive","from":"files"}]}}`))
if err == nil {
t.Fatal("two artifacts with one name were accepted")
}
if !strings.Contains(err.Error(), "could mean either") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
func TestAnArtifactBuiltFromOutsideItsRepositoryIsRefused(t *testing.T) {
// A build reads its own repository and nothing else. A path leaving it would make what gets
// built depend on whatever happens to be on the machine doing the building.
for _, from := range []string{"/etc/passwd", "../elsewhere"} {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"archive","from":"` + from + `"}]}}`))
if err == nil {
t.Fatalf("%q was accepted as a build input", from)
}
if !strings.Contains(err.Error(), "outside its own repository") {
t.Fatalf("refused for the wrong reason: %v", err)
}
}
}
func TestAnArtifactOfAnUnknownKindIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"binary","from":"main.go"}]}}`))
if err == nil {
t.Fatal("an artifact of an unknown kind was accepted")
}
}