package ownmanifest import ( "encoding/json" "fmt" "os" "strings" "testing" . "git.novox.be/novox/mesh-sdk/go/catalogue" ) // novox/hq issue 213: the controller is a Go program and was the one piece of the mesh's own Go // code still shipped as an image (ADR 0188 §1). Its own manifest, composed for the machine that runs // it, is a Go bundle run by the host as a process — and no container. func TestTheControllerIsAProcessAndNoContainer(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } m, err := ParseManifest(raw) if err != nil { t.Fatalf("the controller's own manifest does not parse:\n%v", err) } if m.Build == nil || len(m.Build.Artifacts) != 1 { t.Fatalf("the controller builds %+v; it is one bundle", m.Build) } a := m.Build.Artifacts[0] if a.Kind != ArtifactBundle || a.Language != "go" || a.System == "" || BinaryOf(a) != "mesh-controller" { t.Fatalf("the controller's artifact is %+v, not a Go bundle naming its system and binary", a) } for _, c := range m.Capabilities { if c == "container-runtime" { t.Error("the controller still requires a container runtime on its machine") } } digest := "sha256:" + strings.Repeat("c", 64) control, err := m.Resolve([]Built{{Name: a.Name, Kind: ArtifactBundle, Reference: ArtifactStoreScheme + "mesh-controller/" + a.Name + "@" + digest, Digest: digest}}) if err != nil { t.Fatal(err) } // The node's runtime does not launch it: it serves its seat's verbs itself. if loads := control.Bundles[0].Loads; len(loads) != 0 { t.Errorf("the node's runtime would launch the controller as a tools bundle: %v", loads) } needed := map[string]map[string]string{"mesh-controller": {}} for name := range m.OwnSecrets { needed["mesh-controller"][name] = "sealed-" + name } out, err := Resolution{Node: "anchor", Modules: []Manifest{control}}.Declaration(Rendering{ Needed: needed, ArtifactStore: "anchor.internal:5100", Seats: map[string]map[int]int{"mesh-store": {5432: 6852}}, }) if err != nil { t.Fatalf("the controller does not compose: %v", err) } var process, step map[string]any account, firstSecret := -1, -1 for i, r := range out { switch { case r["type"] == "container": t.Errorf("the controller's declaration still runs a container: %v", r) case r["id"] == "mesh-controller.controller": process = r case r["id"] == "mesh-controller.controller-prepare": step = r if process != nil { t.Error("the controller's preparation is placed after the process it prepares for") } case r["type"] == "user" && r["name"] == "mesh-controller": account = i case strings.HasPrefix(fmt.Sprint(r["id"]), "mesh-controller.needs-") && firstSecret < 0: firstSecret = i } } if process == nil { t.Fatalf("the controller's process is not in its declaration: %v", out) } if run, _ := json.Marshal(process["run"]); string(run) != `["./mesh-controller","serve"]` { t.Errorf("the controller is run as %s, not its own bundle's binary", run) } if process["source"] != "anchor.internal:5100/mesh-controller/"+a.Name+"@"+digest || process["digest"] != digest { t.Errorf("the controller's bundle is fetched from %v (%v)", process["source"], process["digest"]) } // The user: an account the host declares, which owns what the process reads. if process["user"] != "mesh-controller" || account < 0 { t.Errorf("the controller runs as %v, and the account declared is at %d", process["user"], account) } if firstSecret >= 0 && account > firstSecret { t.Error("the controller's secrets are written before the account they belong to exists") } for _, r := range out { if strings.HasPrefix(fmt.Sprint(r["id"]), "mesh-controller.needs-") && r["owner"] != "mesh-controller" { t.Errorf("%v belongs to %v, which the controller's process cannot read", r["id"], r["owner"]) } } if dir := fileNamed(out, "mesh-controller.mesh-state"); dir == nil || dir["owner"] != "mesh-controller" { t.Errorf("the controller's state directory is not its account's to enter: %v", dir) } // Each mount became a path the process reads: nothing it is told is a path inside a container. state := fmt.Sprint(fileNamed(out, "mesh-controller.mesh-state")["path"]) env, _ := process["env"].(map[string]any) for key, value := range env { v := fmt.Sprint(value) if strings.HasPrefix(v, "/run/secrets") || strings.HasPrefix(v, "/broker-tls") { t.Errorf("%s=%s is a path inside the container the controller no longer runs in", key, v) } if strings.HasSuffix(key, "_FILE") && !strings.HasPrefix(v, state+"/") { t.Errorf("%s=%s is not one of the files the mesh places for it", key, v) } } if env["MESH_BROKER_CERTIFICATE"] != "/var/lib/mesh-broker-tls/tls.crt" { t.Errorf("the controller reads the broker's certificate from %v", env["MESH_BROKER_CERTIFICATE"]) } if env["MESH_STORE_INVENTORY_PORT"] != "6852" { t.Errorf("the controller is told the store is on %v; the node put it on 6852", env["MESH_STORE_INVENTORY_PORT"]) } // The handover: the container it ran as goes only once this is running. if got, _ := json.Marshal(process["replaces"]); string(got) != `["mesh-controller.server"]` { t.Errorf("the controller's process replaces %s, not the container it ran as", got) } // And its state is prepared first, by the same program as the same account. if step == nil || step["run-once"] != true || step["user"] != "mesh-controller" { t.Fatalf("the controller's preparation is %v", step) } if run, _ := json.Marshal(step["run"]); string(run) != `["./mesh-controller","prepare"]` { t.Errorf("the controller's preparation runs %s", run) } }