package bootstrap import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" ) // imageFormManifest is a controller from before issue 213: it builds an image and runs a container. const imageFormManifest = `{"module":"mesh-controller","version":"1","resources":[ {"id":"server","type":"container","name":"mesh-controller","network":"host","artifact":"server"}], "build":{"artifacts":[{"name":"server","kind":"image","from":"Dockerfile"}]}}` // processFormManifest is the controller's manifest as novox/mesh-controller declares it after issue // 213: a Go bundle the host runs as a process, replacing the container it ran as. const processFormManifest = `{ "module": "mesh-controller", "version": "1", "slug": "control", "prepares": true, "claims": [{"name": "mesh-controller", "scope": "mesh"}], "accesses": [{"path": "/var/lib/mesh-broker-tls", "mode": "read"}], "own-secrets": {"inventory": "${dir:mesh-state}/inventory", "bus": "${dir:mesh-state}/bus"}, "secrets-owner": "mesh-controller", "tools": ["status"], "resources": [ {"id": "account", "type": "user", "name": "mesh-controller", "shell": "/usr/bin/nologin", "home": "/var/lib/mesh-controller"}, {"id": "mesh-state", "type": "directory", "mode": "0700", "place": "mesh", "owner": "mesh-controller"}, {"id": "controller", "type": "process", "name": "mesh-controller", "artifact": "controller", "run": ["./mesh-controller", "serve"], "user": "mesh-controller", "env": {"MESH_BROKER_CERTIFICATE": "/var/lib/mesh-broker-tls/tls.crt", "MESH_STORE_INVENTORY_FILE": "${dir:mesh-state}/inventory", "MESH_STORE_INVENTORY_PORT": "${seat:mesh-store:5432}", "MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus"}, "replaces": ["server"]} ], "build": {"artifacts": [{"name": "controller", "kind": "bundle", "language": "go", "system": "arch", "from": "cmd/mesh-controller", "binary": "mesh-controller"}]} }` // aRepository answers the carried builder's git as a clone of a repository holding this manifest and // a Dockerfile, and hands every other command to then. func aRepository(t *testing.T, manifest string, then func(string, []string) (string, error)) func(string, []string) (string, error) { t.Helper() return func(name string, args []string) (string, error) { if name == "docker" && len(args) > 5 && args[0] == "run" && args[4] == "--entrypoint" && args[5] == "git" { host := strings.TrimSuffix(args[3], ":/ws") joined := strings.Join(args, " ") switch { case strings.Contains(joined, " clone "): src := filepath.Join(host, "src") if err := os.MkdirAll(src, 0o755); err != nil { return "", err } if err := os.WriteFile(filepath.Join(src, "module.json"), []byte(manifest), 0o644); err != nil { return "", err } return "", os.WriteFile(filepath.Join(src, "Dockerfile"), []byte("FROM scratch\n"), 0o644) case strings.Contains(joined, "rev-parse"): return "a1b2c3d4e5f6\n", nil } return "", nil } return then(name, args) } } // novox/hq issue 223: a controller declared as a process is raised at genesis as a container built // from its own Dockerfile, not by the builder — which has no Go toolchain at genesis and would refuse. func TestAProcessFormControllerIsBuiltFromItsDockerfileAndRaisedAsAContainer(t *testing.T) { runtime := &asked{answer: aRepository(t, processFormManifest, func(name string, args []string) (string, error) { if name == "docker" && args[0] == "build" { return builtImage + "\n", nil } t.Fatalf("genesis ran %s %v; a process-form controller is built from its Dockerfile alone", name, args) return "", nil })} built, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test", Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {}) if err != nil { t.Fatal(err) } if built.Image != builtImage || built.Commit != "a1b2c3d4e5f6" { t.Errorf("built %q from %q", built.Image, built.Commit) } if runtime.ran("mesh-builder:test build") { t.Error("the builder was asked to build a controller it cannot build at genesis") } var m map[string]any if err := json.Unmarshal(built.Manifest, &m); err != nil { t.Fatalf("the genesis manifest is not JSON: %v", err) } if _, has := m["prepares"]; has { t.Error("the genesis manifest prepares its state; the temporary controller already did, and a pinned image is nothing the controller can derive a step from") } if _, has := m["build"]; has { t.Error("the genesis manifest still says how it is built; it is handed over resolved") } var container map[string]any for _, raw := range m["resources"].([]any) { r := raw.(map[string]any) if r["type"] == "process" { t.Errorf("the genesis manifest still runs the process: %v", r) } if r["type"] == "container" { container = r } } if container == nil { t.Fatal("the genesis manifest runs no container") } for key, want := range map[string]any{"id": "server", "name": "mesh-controller", "image": builtImage, "network": "host"} { if container[key] != want { t.Errorf("the genesis container's %s is %v, not %v", key, container[key], want) } } if _, has := container["user"]; has { t.Error("the genesis container says a user; the host's container has no such field, the image's USER is who it runs as") } if m["secrets-owner"] != "65534:65534" { t.Errorf("the secrets belong to %v, which the container cannot read as", m["secrets-owner"]) } volumes, _ := json.Marshal(container["volumes"]) for _, want := range []string{"${dir:mesh-state}/inventory:${dir:mesh-state}/inventory:ro", "/var/lib/mesh-broker-tls/tls.crt:/var/lib/mesh-broker-tls/tls.crt:ro"} { if !strings.Contains(string(volumes), want) { t.Errorf("the container does not mount %s: %s", want, volumes) } } if strings.Contains(string(volumes), "seat:") { t.Errorf("a word that is not a path was mounted: %s", volumes) } // And it is what step 9 installs: pinned, its container found, its stores delivered from its // environment — the same path an image-form controller takes. pinned, _, err := pinImage(built.Manifest, built.Image, "registry.internal:5000/mesh-controller@sha256:"+strings.Repeat("e", 64), ControlPlaneModule) if err != nil { t.Fatal(err) } if id := controlPlaneResourceIn(pinned); id != "server" { t.Errorf("step 9 finds the controller's container as %q", id) } wanted, err := secretsByVariableIn(pinned) if err != nil { t.Fatalf("step 9 cannot deliver the stores into the genesis container: %v", err) } if wanted["MESH_STORE_INVENTORY"] != "inventory" { t.Errorf("step 9 delivers %v", wanted) } } // An older controller — an image and a container — is still built by the builder, as before. func TestAnImageFormControllerIsStillBuiltByTheBuilder(t *testing.T) { manifest := `{"module":"mesh-controller","version":"1","resources":[` + `{"id":"server","type":"container","name":"mesh-controller","image":"` + builtImage + `"}]}` runtime := &asked{answer: aRepository(t, imageFormManifest, func(name string, args []string) (string, error) { if name == "docker" && args[0] == "build" { t.Fatal("an image-form controller was built from its Dockerfile rather than by the builder") } return `{"module":"mesh-controller","commit":"a1b2c3d4","manifest":` + manifest + `,"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}`, nil })} built, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test", Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {}) if err != nil { t.Fatal(err) } if string(built.Manifest) != manifest || !runtime.ran("mesh-builder:test build") { t.Errorf("the image form did not go through the builder: %s", built.Manifest) } } func TestAProcessFormNamingNoOneReplacementIsRefused(t *testing.T) { for _, bad := range []string{`"replaces": []`, `"replaces": ["a", "b"]`} { raw := strings.Replace(processFormManifest, `"replaces": ["server"]`, bad, 1) if _, err := processFormOf([]byte(raw)); err == nil { t.Errorf("a process saying %s was accepted; genesis would not know what to name its container", bad) } } }