mesh/merge-gate head 52600ec7bd61 merged into trunk cdcba9ca7375 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 52600ec7bd61 merged into trunk cdcba9ca7375 — pass: its merge-check.sh passed
mesh/delivery delivered
122 lines
4.3 KiB
Go
122 lines
4.3 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"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 `<module>.<its id>`; 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[:])
|
|
}
|