The controller's manifest now declares a Go bundle the host runs as a process (novox/hq issue 213). Genesis cannot run that: the bundle is fetched from the artifact store and compiled in a toolchain, and the mesh makes both long after the controller. The builder, asked to build the manifest at genesis, refuses for lack of the Go toolchain. So genesis raises the controller as before, as a container, and the first push hands it over to the process through `replaces` (issue 223, option b). - Step 3 clones the controller at the commit with the carried builder's git and reads its manifest. In the image form (an older controller) it builds through the builder as before. In the process form it builds the repository's own Dockerfile and hands step 9 a manifest of its own shape: the process becomes a container with the id the process `replaces`, the image genesis built, host network, and every host path the process's env names mounted at that same path read-only. Secrets belong to the image's user (65534) until the process's account takes them over. `prepares` is dropped: the temporary controller from the same commit already migrated the stores, and a pinned image is nothing the controller can derive a step from. - Steps 4 to 9 are unchanged: they take the manifest as they did. - apply.ForTests lets the bootstrap's test apply a process. The first composed declaration from the process manifest names `mesh-controller.server`, which is what the host recorded for the genesis container, so the first apply hands over and leaves one controller.
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"
|
|
|
|
"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 `<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[:])
|
|
}
|