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.
145 lines
5.4 KiB
Go
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")
|
|
}
|
|
}
|