package bootstrap import ( "archive/tar" "bytes" "compress/gzip" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" "github.com/novox/mesh-host/internal/system" ) // novox/hq issue 223: what genesis raises is what the controller's process takes over. The temporary // controller composes the genesis container, and the host records it as `.`; the // first declaration the mesh composes from the controller's real manifest names that same id under // the process's `replaces` (the composer prefixes both alike — mesh-controller's // TestTheControllerIsAProcessAndNoContainer). So the first apply hands over: the process is started, // seen up, and only then is the genesis container removed — one controller before, one after, never // none and never two left. func TestTheFirstApplyHandsTheGenesisContainerOverToTheProcess(t *testing.T) { restore := apply.ForTests(t.TempDir(), t.TempDir()) defer restore() form, err := processFormOf([]byte(processFormManifest)) if err != nil { t.Fatal(err) } genesis, err := genesisForm([]byte(processFormManifest), form, builtImage) if err != nil { t.Fatal(err) } // What the host records for the genesis container, as the composer names it. recorded := "" var m struct { Resources []map[string]any `json:"resources"` } if err := json.Unmarshal(genesis, &m); err != nil { t.Fatal(err) } for _, r := range m.Resources { if r["type"] == "container" { recorded = ControlPlaneModule + "." + r["id"].(string) } } known := store.State{Resources: []store.Applied{{Origin: store.OriginDeclared, ID: recorded, Type: "container", Target: ControlPlaneModule}}} // The controller's first composed declaration: its process, replacing what the manifest names. body, digest := aBundle(t) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(body) })) defer server.Close() replaces, _ := json.Marshal([]string{ControlPlaneModule + "." + form.Replaces}) d, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"` + ControlPlaneModule + "." + form.Process + `","type":"process","name":"mesh-controller","source":"` + server.URL + `/c.tgz","digest":"` + digest + `","run":["./mesh-controller","serve"],"replaces":` + string(replaces) + `}]}`)) if err != nil { t.Fatal(err) } var commands []string started, container := false, true run := func(_ context.Context, name string, args ...string) (string, error) { line := name + " " + strings.Join(args, " ") commands = append(commands, line) switch { case strings.HasPrefix(line, "systemctl restart mesh-controller.service"): started = true case strings.HasPrefix(line, "systemctl show mesh-controller.service") && started: return "ActiveState=active\nSubState=running\nMainPID=7\nNRestarts=0\n", nil case strings.HasPrefix(line, "docker rm -f mesh-controller"): if !started { t.Error("the genesis container was removed before the process was started") } container = false case strings.HasPrefix(line, "docker container inspect") && !container: return "", errors.New("no such container") } return "", nil } sys, err := system.For("arch") if err != nil { t.Fatal(err) } _, after, err := apply.Apply(context.Background(), sys, d, known, store.OriginDeclared, run, nil, nil) if err != nil { t.Fatalf("the first apply did not hand over: %v\n%s", err, strings.Join(commands, "\n")) } if container { t.Fatalf("the genesis container is still running beside the process — two controllers:\n%s", strings.Join(commands, "\n")) } if _, still := after.Find(recorded); still { t.Error("the host still records the genesis container") } } func aBundle(t *testing.T) ([]byte, string) { t.Helper() var raw bytes.Buffer zipped := gzip.NewWriter(&raw) w := tar.NewWriter(zipped) content := "#!/bin/sh\n" if err := w.WriteHeader(&tar.Header{Name: "mesh-controller", Mode: 0o755, Size: int64(len(content)), Typeflag: tar.TypeReg}); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(content)) _ = w.Close() _ = zipped.Close() sum := sha256.Sum256(raw.Bytes()) return raw.Bytes(), "sha256:" + hex.EncodeToString(sum[:]) }