package main import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // The digest route-proxy's `trust` artifact runs: one alpine image is held for both (ADR 0480 clause 3). It was // read from mesh-controller's modules/route-proxy/module.json and is the one hello-web runs. const alpineDigest = "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" type manifest struct { Tools []string `json:"tools"` Resources []map[string]any `json:"resources"` Listens []struct { Name string `json:"name"` Port int `json:"port"` } `json:"listens"` Data struct { Own []struct { ID string `json:"id"` Path string `json:"path"` } `json:"own"` } `json:"data"` Build map[string]any `json:"build"` } func readManifest(t *testing.T) manifest { t.Helper() var m manifest if err := json.Unmarshal([]byte(readFile(t, "../../module.json")), &m); err != nil { t.Fatal(err) } return m } func (m manifest) resource(id string) map[string]any { for _, r := range m.Resources { if r["id"] == id { return r } } return nil } func (m manifest) artifact(name string) map[string]any { arts, _ := m.Build["artifacts"].([]any) for _, a := range arts { if a, _ := a.(map[string]any); a["name"] == name { return a } } return nil } func strs(v any) []string { var out []string l, _ := v.([]any) for _, e := range l { out = append(out, e.(string)) } return out } // Clause 2: a Go bundle from a package of the module's own go.mod, stamped for alpine; an archive resource unpacks // it; the container mounts that read-only at /opt/mesh, calls the program there and names the archive in restart-on. func TestAutomxIsAGoBundleForAlpineMountedAtOptMeshAndRestartedWithIt(t *testing.T) { m := readManifest(t) b := m.artifact("automx") if b == nil || b["kind"] != "bundle" || b["language"] != "go" || b["system"] != "alpine" || b["from"] != "cmd/automx" || b["binary"] != "automx" { t.Fatalf("artifact automx is %v, want a Go bundle, system alpine, from cmd/automx, binary automx", b) } if _, loads := b["loads"]; loads { t.Errorf("a program run in a container is not loaded by the tool runner: %v", b["loads"]) } goMod := readFile(t, "../../go.mod") if !strings.HasPrefix(goMod, "module mailu\n") { t.Errorf("the module's go.mod is %q; the bundle is a package of it", goMod) } if _, err := os.Stat(filepath.Join("..", "..", filepath.FromSlash(b["from"].(string)), "main.go")); err != nil { t.Errorf("the bundle's package is not in the module's directory: %v", err) } unpack := m.resource("program-automx") if unpack == nil || unpack["type"] != "archive" || unpack["artifact"] != "automx" || unpack["unknown"] != "program" { t.Fatalf("resource program-automx is %v, want an archive of the bundle", unpack) } where, _ := unpack["path"].(string) c := m.resource("automx") if c == nil || c["type"] != "container" { t.Fatalf("no container automx: %v", c) } if got := strs(c["volumes"]); len(got) != 1 || got[0] != where+":/opt/mesh:ro" { t.Errorf("volumes %v, want the archive's place %q mounted read-only at /opt/mesh", got, where) } if got := strs(c["args"]); len(got) != 1 || got[0] != "/opt/mesh/automx" { t.Errorf("args %v, want the program at /opt/mesh/automx", got) } if !contains(strs(c["restart-on"]), "program-automx") { t.Errorf("restart-on %v does not name the archive: a new program would leave the old one running", c["restart-on"]) } if !contains(strs(c["restart-on"]), "config-env") { t.Errorf("restart-on %v does not name the environment file the program reads at its start", c["restart-on"]) } } // Clause 3: an upstream artifact pinned by digest, the one route-proxy's trust artifact runs; never an image // built from a Dockerfile (clause 11), and no Dockerfile or base to build one on. func TestAutomxRunsTheMirroredAlpineImageAndBuildsNoImage(t *testing.T) { m := readManifest(t) c := m.resource("automx") base := m.artifact(c["artifact"].(string)) if base == nil || base["kind"] != "upstream" || base["from"] != alpineDigest { t.Errorf("the container runs artifact %v, want an upstream artifact %s", base, alpineDigest) } if _, has := c["image"]; has { t.Errorf("the container names an image %v, fetched from a registry on the machine", c["image"]) } for _, a := range m.Build["artifacts"].([]any) { if a.(map[string]any)["kind"] == "image" { t.Errorf("artifact %v is an image built from a Dockerfile (ADR 0452)", a) } } if _, has := m.Build["on"]; has { t.Errorf("build.on %v: nothing is built on a base image any more", m.Build["on"]) } for _, gone := range []string{"../../automx/Dockerfile", "../../automx"} { if _, err := os.Stat(gone); err == nil { t.Errorf("%s is still there", gone) } } } // Clause 1: the container stays as it was for what watches it. func TestAutomxKeepsItsContainerNameNetworkPortAndEnvironment(t *testing.T) { m := readManifest(t) c := m.resource("automx") if c["name"] != "mailu-automx" || c["network"] != "mailu" { t.Errorf("container %v on network %v, want mailu-automx on mailu", c["name"], c["network"]) } if got := strs(c["ports"]); len(got) != 1 || got[0] != "4243" { t.Errorf("ports %v, want 4243", got) } if got := strs(c["env-file"]); len(got) != 1 || got[0] != "${dir:state}/mailu.env" { t.Errorf("env-file %v, want mailu's environment only", got) } if !strings.Contains("\n"+readFile(t, "../../files/mailu.env"), "\nAUTOMX_PORT=4243\n") { t.Errorf("mailu.env does not say AUTOMX_PORT=4243") } for _, l := range m.Listens { if l.Name == "autoconfig" && l.Port == 4243 { return } } t.Errorf("the autoconfig endpoint is not on 4243: %v", m.Listens) } // Clause 10: a database nobody reads is gone from the declaration (the file stays on disk), and the container says // whether it answers with one captured request and its captured answer. func TestAutomxKeepsNoDatabaseAndSaysWhetherItAnswers(t *testing.T) { m := readManifest(t) if m.resource("data-automx") != nil { t.Errorf("the data-automx directory is still declared") } for _, it := range m.Data.Own { if strings.Contains(it.ID, "automx") || strings.Contains(it.Path, "data-automx") { t.Errorf("data item %v is still declared", it) } } for _, tool := range m.Tools { if tool == "automx_rows" { t.Errorf("the tool automx_rows reads a database that no longer exists (issue 1700)") } } h, _ := m.resource("automx")["health"].(map[string]any) if h["kind"] != "http" || h["endpoint"] != "autoconfig" { t.Fatalf("health %v, want an http check of the autoconfig endpoint", h) } // The request is captured fixture 06 and the answer its status and body. rq := readRequest(t, filepath.Join(captured, "06-autoconfig-root")) if u := strings.SplitN(rq.url, "/", 4); "/"+u[len(u)-1] != h["path"] || rq.method != "GET" { t.Errorf("health path %v is not captured request %s %s", h["path"], rq.method, rq.url) } if st := readFile(t, filepath.Join(captured, "06-autoconfig-root.status")); !strings.HasPrefix(st, "200 ") || h["status"] != float64(200) { t.Errorf("health status %v, captured %q", h["status"], st) } if body, _ := h["body"].(string); body == "" || !strings.Contains(readFile(t, filepath.Join(captured, "06-autoconfig-root.body")), body) { t.Errorf("health body %q is not in the captured answer", h["body"]) } } func contains(l []string, s string) bool { for _, e := range l { if e == s { return true } } return false }