A resource's id is `<module>.<its own id>` and a module's name may itself contain a dot — novox.be is one — so the owner of a resource is everything before the *last* dot. The preparation step's id used a dot, which made its owner unreadable by that rule; it uses a hyphen, and the id says what it belongs to whichever way a reader splits it.
142 lines
5.2 KiB
Go
142 lines
5.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A module version prepares its state before it runs (novox/hq ADR 0135).
|
|
//
|
|
// What the mesh derives is the module's own resource, run once with one word, placed immediately in
|
|
// front of the thing it prepares for. What matters in these tests is that the derivation is a copy
|
|
// rather than a second description: the failure it replaces was a hand-written step repeating six
|
|
// fields of the resource it preceded, each free to drift from it.
|
|
|
|
func aPreparingModule() Manifest {
|
|
return Manifest{
|
|
Module: "gitea",
|
|
Prepares: true,
|
|
Resources: []map[string]any{
|
|
{"id": "state", "type": "directory", "path": "/var/lib/gitea", "mode": "0700"},
|
|
{"id": "server", "type": "container", "name": "mesh-gitea-server",
|
|
"image": "gitea/gitea@" + digest, "ports": []any{"3000:3000"}},
|
|
{"id": "runtime", "type": "container", "name": "mesh-gitea",
|
|
"image": ArtifactStoreScheme + "gitea/runtime@" + digest, "network": "host",
|
|
"env": map[string]any{"MESH_GITEA_STATE_DIR": "/run/state"},
|
|
"volumes": []any{"/var/lib/gitea:/run/state:ro"},
|
|
"ports": []any{"9000:9000"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func declaredFor(t *testing.T, m Manifest) []map[string]any {
|
|
t.Helper()
|
|
// The manifest as the mesh holds it: a build resolved the module's own artifact into the
|
|
// reference it recorded, which is also how the composition knows whose code a resource runs.
|
|
out, err := Resolution{Node: "anchor", Modules: []Manifest{m}}.Declaration(
|
|
Rendering{ArtifactStore: "anchor.internal:5100"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func idsOf(resources []map[string]any) []string {
|
|
var ids []string
|
|
for _, r := range resources {
|
|
ids = append(ids, fmt.Sprint(r["id"]))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// The step runs the module's own code, and comes immediately before it — not before the upstream
|
|
// server the module packages, which may be the very thing the state lives in.
|
|
func TestThePreparationRunsTheModulesOwnCodeAndComesRightBeforeIt(t *testing.T) {
|
|
out := declaredFor(t, aPreparingModule())
|
|
ids := idsOf(out)
|
|
at := -1
|
|
for i, id := range ids {
|
|
if id == "gitea.runtime-prepare" {
|
|
at = i
|
|
}
|
|
}
|
|
if at < 0 {
|
|
t.Fatalf("nothing prepares this module's state: %v", ids)
|
|
}
|
|
// A module's name may contain a dot, so a resource's module is everything before the last one —
|
|
// which the derived id must not add to, or a machine reads the wrong owner from it.
|
|
if strings.Count("gitea.runtime-prepare", ".") != 1 {
|
|
t.Fatal("the derived id adds a dot, so what owns it cannot be read from it")
|
|
}
|
|
if ids[at+1] != "gitea.runtime" {
|
|
t.Fatalf("the preparation is not immediately before the module's own code: %v", ids)
|
|
}
|
|
for _, id := range ids[:at] {
|
|
if id == "gitea.runtime" {
|
|
t.Fatalf("the module's own code runs before its state is prepared: %v", ids)
|
|
}
|
|
}
|
|
}
|
|
|
|
// It is given exactly what the module's own code is given. Asserted field by field against the
|
|
// resource it was derived from, because writing it twice is the fault this replaces.
|
|
func TestThePreparationIsGivenWhatTheModuleIsGiven(t *testing.T) {
|
|
out := declaredFor(t, aPreparingModule())
|
|
declared := byID(out)
|
|
step, workload := declared["gitea.runtime-prepare"], declared["gitea.runtime"]
|
|
if step == nil || workload == nil {
|
|
t.Fatalf("expected both, got %v", idsOf(out))
|
|
}
|
|
for _, field := range []string{"image", "network", "env", "volumes", "type"} {
|
|
if fmt.Sprint(step[field]) != fmt.Sprint(workload[field]) {
|
|
t.Errorf("the preparation's %s is %v and the module's is %v", field, step[field], workload[field])
|
|
}
|
|
}
|
|
if once, _ := step["run-once"].(bool); !once {
|
|
t.Error("the preparation is not a step, so nothing waits for it and nothing is gated by it")
|
|
}
|
|
if fmt.Sprint(step["args"]) != fmt.Sprint([]any{PreparationArgument}) {
|
|
t.Errorf("the preparation is asked for as %v", step["args"])
|
|
}
|
|
if fmt.Sprint(step["name"]) == fmt.Sprint(workload["name"]) {
|
|
t.Error("the preparation and the workload have one name, so one removes the other")
|
|
}
|
|
// A published port cannot be bound twice, and the version being replaced is still running.
|
|
if _, published := step["ports"]; published {
|
|
t.Errorf("the preparation publishes a port the running version holds: %v", step["ports"])
|
|
}
|
|
}
|
|
|
|
// A module that says nothing about preparing gets nothing, which is most modules.
|
|
func TestAModuleThatPreparesNothingGetsNoStep(t *testing.T) {
|
|
m := aPreparingModule()
|
|
m.Prepares = false
|
|
for _, id := range idsOf(declaredFor(t, m)) {
|
|
if id == "gitea.runtime-prepare" {
|
|
t.Fatal("a module that prepares nothing was given a preparation")
|
|
}
|
|
}
|
|
}
|
|
|
|
// A module whose own code the mesh cannot find has nothing to ask, and saying so where the manifest
|
|
// is read beats a declaration that quietly prepares nothing.
|
|
func TestAModuleThatPreparesAndRunsNoneOfItsOwnCodeIsRefused(t *testing.T) {
|
|
m := Manifest{
|
|
Module: "gitea",
|
|
Prepares: true,
|
|
Resources: []map[string]any{
|
|
// Only the upstream server it packages: nothing here runs gitea's own code.
|
|
{"id": "server", "type": "container", "name": "mesh-gitea-server", "image": "gitea/gitea@" + digest},
|
|
},
|
|
}
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ParseManifest(raw); err == nil {
|
|
t.Fatal("a module that prepares its state with nothing of its own to run was accepted")
|
|
}
|
|
}
|