Review of the first cut found four things. A directory mounted into a container is no longer looked inside, not even for the files this host wrote there. The controller records every provider's received and contributions file as a plain file under a mounted directory, so folding those in would have recreated the route proxy — which re-reads its routes live, by design — on every route change, and killed every provisioner sidecar, which polls what it receives, mid-reconcile on every grant. Whether a service reads a file under its directory once or watches it is the service's; restart-on is how a module says "once", and it stays the opt-in. Env-files and files mounted directly remain by content. Genesis wrote the superuser secret as `value\n`; `secret accept` strips the line ending by design, so the postgres module declared `value` — and with a mounted file's content in the spec, phase three would have recreated the store it meant to adopt in place, with the temporary control plane connected to it. Genesis now writes the value alone. readCredentialFile tolerated both endings already. Pinned with the bytes the genesis code path writes, then the module's declaration of the same container: it must reconcile. A container carrying a label from before the host folded in what it reads is accepted rather than recreated, when that label matches the spec as it used to be computed: what it reads is recorded then, a change is caught from that record from the next apply on, and the label is renewed at the next genuine recreate. Recreating them all would have been a restart storm across the mesh in declaration order, the store first. The trade-off is stated in the code: a container already stale at upgrade time is not caught, and could not have been either way. The record of what a container read is looked up by its name when its declared id has none — the bundle's `store` becomes `postgres.server` for the same container — so a change on the day it is adopted still names the file. The by-target lookup takes the most recently applied record, since the bundle's record for the same target is never removed by the mesh's. novox/hq 04-ISSUES/103
131 lines
4.8 KiB
Go
131 lines
4.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends phase three's premise (phase3.go): the store genesis raised is adopted by the postgres
|
|
// module IN PLACE — same name, same image, same spec — so the applier reconciles it and never
|
|
// recreates the mesh's memory with the temporary control plane connected to it.
|
|
//
|
|
// The host folds a mounted file's content into the container's spec (novox/hq 04-ISSUES/103), so
|
|
// this now depends on a byte: the superuser file genesis writes and mounts must be the same bytes
|
|
// the module later declares. The module's value is what `secret accept` took — the operator's
|
|
// file with its line ending removed and nothing else (mesh-control, asSupplied). Reproduced before
|
|
// it was fixed: genesis wrote `value\n`, the module wrote `value`, and the store was recreated
|
|
// during install.
|
|
|
|
// labelled is a runtime that keeps the spec label the host gives a container and hands it back.
|
|
type labelled struct {
|
|
spec map[string]string
|
|
created []string
|
|
removed []string
|
|
}
|
|
|
|
func (l *labelled) run(_ context.Context, _ string, args ...string) (string, error) {
|
|
switch args[0] {
|
|
case "info":
|
|
return "27.0\n", nil
|
|
case "inspect":
|
|
spec, ok := l.spec[args[len(args)-1]]
|
|
if !ok {
|
|
return "", errors.New("no such container")
|
|
}
|
|
return "true\t" + spec + "\n", nil
|
|
case "rm":
|
|
l.removed = append(l.removed, args[len(args)-1])
|
|
delete(l.spec, args[len(args)-1])
|
|
case "run":
|
|
var name, spec string
|
|
for i, a := range args {
|
|
if a == "--name" {
|
|
name = args[i+1]
|
|
}
|
|
if a == "--label" && strings.HasPrefix(args[i+1], "mesh-host.spec=") {
|
|
spec = strings.TrimPrefix(args[i+1], "mesh-host.spec=")
|
|
}
|
|
}
|
|
l.spec[name] = spec
|
|
l.created = append(l.created, name)
|
|
return "made\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func TestTheStoreGenesisRaisedIsAdoptedInPlaceNotRecreated(t *testing.T) {
|
|
dir := t.TempDir()
|
|
secret := filepath.Join(dir, "superuser.secret")
|
|
|
|
// The bytes genesis really writes — the code path, not a fixture that agrees with it.
|
|
if _, made, err := keptOrMade(secret, false); err != nil || !made {
|
|
t.Fatalf("genesis did not make the superuser secret: made=%v err=%v", made, err)
|
|
}
|
|
onDisk, err := os.ReadFile(secret)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
image := "docker.io/library/postgres@sha256:" + strings.Repeat("ab", 32)
|
|
mounts := `"volumes":["mesh-store-data:/var/lib/postgresql/data","` + secret + `:` + storeSuperuserMount + `:ro"]`
|
|
|
|
// The foundation's store, as the produced bundle raises it (RewriteRoot): the file mounted,
|
|
// declared by nothing — genesis wrote it before there was a declaration to name it.
|
|
raise, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[
|
|
{"id":"` + StoreID + `","type":"container","name":"mesh-store","image":"` + image + `",
|
|
"env":{"POSTGRES_PASSWORD_FILE":"` + storeSuperuserMount + `"},` + mounts + `}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime := &labelled{spec: map[string]string{}}
|
|
_, known, err := apply.Apply(context.Background(), arch(t), raise, store.State{}, store.OriginCarried,
|
|
runtime.run, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("raising the foundation's store: %v", err)
|
|
}
|
|
if len(runtime.created) != 1 {
|
|
t.Fatalf("the store was not raised once: %v", runtime.created)
|
|
}
|
|
|
|
// The postgres module's declaration of the same store: the superuser file as `secret accept`
|
|
// took it in — its line ending removed and nothing else — then the same container.
|
|
accepted := strings.TrimRight(string(onDisk), "\r\n")
|
|
adopt, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[
|
|
{"id":"postgres.superuser","type":"file","path":"` + secret + `","content":"` + accepted + `","mode":"0600"},
|
|
{"id":"postgres.server","type":"container","name":"mesh-store","image":"` + image + `",
|
|
"env":{"POSTGRES_PASSWORD_FILE":"` + storeSuperuserMount + `"},` + mounts + `}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime.created, runtime.removed = nil, nil
|
|
report, _, err := apply.Apply(context.Background(), arch(t), adopt, known, store.OriginDeclared,
|
|
runtime.run, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("adopting the store: %v", err)
|
|
}
|
|
|
|
if len(runtime.removed) > 0 || len(runtime.created) > 0 {
|
|
t.Fatalf("the module's declaration recreated the store genesis raised (removed %v, created %v): "+
|
|
"the file genesis mounted and the file the module declares are not the same bytes",
|
|
runtime.removed, runtime.created)
|
|
}
|
|
for _, o := range report.Outcomes {
|
|
if o.ID == "postgres.server" && o.Action != "unchanged" {
|
|
t.Errorf("the store was not adopted in place: %+v", o)
|
|
}
|
|
if o.ID == "postgres.superuser" && o.Action != "unchanged" {
|
|
t.Errorf("the module rewrote the superuser file genesis wrote: %+v", o)
|
|
}
|
|
}
|
|
}
|