Raise genesis's store as the postgres module's server, so the store step adopts it as it runs (hq issue 609, ADR 0338)
mesh/merge-gate head 4dde21f0ab20 merged into trunk 7d1d6bffe30c — pass: the change touches no module of the mesh's graph
mesh/repo-check head 4dde21f0ab20 merged into trunk 7d1d6bffe30c — pass: its merge-check.sh passed
mesh/delivery delivered

The bundle raised the store as mesh-store, on upstream postgres, in a docker volume; the postgres
module keeps it as postgres, on its pgvector image, in /var/lib/mesh-store owned 999:70, reading its
superuser password from a file. The store step refused, and genesis stopped at step 17 of 24.

The bundle now raises the store with the module's name, image, data directory and password file, and
every step of the bundle and the installer reaches it by that name. The refusal to raise a store over
data it cannot open looks for a cluster in the store's directory, no longer for the old volume.
This commit is contained in:
jochen
2026-10-13 10:47:53 +02:00
parent 7d1d6bffe3
commit 4dde21f0ab
9 changed files with 208 additions and 27 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ func TestTheBundleCarriesTheFoundationAndTheControlPlaneAndNothingElse(t *testin
} }
sort.Strings(images) sort.Strings(images)
want := []string{"mesh-controller", "nats", "postgres"} want := []string{"mesh-controller", "nats", "pgvector"}
if strings.Join(images, ",") != strings.Join(want, ",") { if strings.Join(images, ",") != strings.Join(want, ",") {
t.Fatalf("the bundle carries %v; expected exactly %v.\n\n"+ t.Fatalf("the bundle carries %v; expected exactly %v.\n\n"+
"Adding one is a change to what every first node becomes, and to ADR 0006's "+ "Adding one is a change to what every first node becomes, and to ADR 0006's "+
+26 -14
View File
@@ -28,11 +28,12 @@
// image, and this file is applied on a machine with no mesh to ask about anything (ADR 0006). The // image, and this file is applied on a machine with no mesh to ask about anything (ADR 0006). The
// registry prefix is the lab's; the digests are upstream's. // registry prefix is the lab's; the digests are upstream's.
// //
// The store's data is a NAMED VOLUME, not a directory on the machine. A directory the host // **The store is raised where the `postgres` module keeps it** (novox/hq ADR 0338): the container
// creates is owned by root, and the database runs as somebody else inside the container — so it // `postgres`, on the module's pgvector image, its data in the directory /var/lib/mesh-store owned by
// could not write, and the container crash-looped. A named volume lets the image set up its own // the image's postgres user, its superuser password read from the module's own-secret file — so the
// ownership, and outlives the container, which is what you want for the thing holding the mesh's // store step adopts it as it runs. Pinned upstream by the module's own digest, not through the lab's
// state. // registry prefix, because the two must be the same reference (mesh-host internal/bootstrap holds
// them to each other against the module's manifest).
{ {
"declaration": 1, "declaration": 1,
@@ -83,17 +84,28 @@
"boot": "enabled", "boot": "enabled",
"restart-on": ["base-filter"] "restart-on": ["base-filter"]
}, },
// The store's data is a directory owned by the image's postgres user (999), made before the
// container starts: a directory the runtime made would be root's, and the database could not
// write it. Measured on the module's image 2026-10-13: it initialises a new store on a directory
// made so (novox/hq ADR 0338 §5).
{
"id": "store-data",
"type": "directory",
"path": "/var/lib/mesh-store",
"mode": "0700",
"owner": "999:70"
},
{ {
"id": "store", "id": "store",
"type": "container", "type": "container",
"name": "mesh-store", "name": "postgres",
"image": "192.0.2.250:5000/postgres@sha256:7abf537131b66ed5af448d90653abf1679b0c7e9a1f07efdd4c3108a401b259a", "image": "pgvector/pgvector@sha256:cf134a767f474095eeba57e0117be8e568e011a63f33fbf252f14c9b760f8e6f",
"env": { "env": {
"POSTGRES_PASSWORD": "bootstrap", "POSTGRES_PASSWORD": "bootstrap",
"PGDATA": "/var/lib/postgresql/data/pgdata" "PGDATA": "/var/lib/postgresql/data/pgdata"
}, },
"ports": ["5432:5432"], "ports": ["5432:5432"],
"volumes": ["mesh-store-data:/var/lib/postgresql/data"] "volumes": ["/var/lib/mesh-store:/var/lib/postgresql/data"]
}, },
// Over TCP, not the socket. While the store initialises it runs a temporary server on the // Over TCP, not the socket. While the store initialises it runs a temporary server on the
// socket ONLY, then stops it and starts the real one — so a socket check passes, the action // socket ONLY, then stops it and starts the real one — so a socket check passes, the action
@@ -102,21 +114,21 @@
{ {
"id": "store-ready", "id": "store-ready",
"type": "action", "type": "action",
"in": "mesh-store", "in": "postgres",
"command": ["sh", "-c", "for i in $(seq 1 180); do pg_isready -h 127.0.0.1 -U postgres >/dev/null 2>&1 && exit 0; sleep 1; done; echo 'the store did not answer within 180s; its own last words follow'; pg_isready -h 127.0.0.1 -U postgres; tail -n 20 /var/lib/postgresql/data/log/*.log 2>/dev/null; exit 1"], "command": ["sh", "-c", "for i in $(seq 1 180); do pg_isready -h 127.0.0.1 -U postgres >/dev/null 2>&1 && exit 0; sleep 1; done; echo 'the store did not answer within 180s; its own last words follow'; pg_isready -h 127.0.0.1 -U postgres; tail -n 20 /var/lib/postgresql/data/log/*.log 2>/dev/null; exit 1"],
"verify": ["pg_isready", "-h", "127.0.0.1", "-U", "postgres"] "verify": ["pg_isready", "-h", "127.0.0.1", "-U", "postgres"]
}, },
{ {
"id": "inventory-database", "id": "inventory-database",
"type": "action", "type": "action",
"in": "mesh-store", "in": "postgres",
"command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE inventory'"], "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE inventory'"],
"verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw inventory"] "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw inventory"]
}, },
{ {
"id": "identity-database", "id": "identity-database",
"type": "action", "type": "action",
"in": "mesh-store", "in": "postgres",
"command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE identity'"], "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE identity'"],
"verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw identity"] "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw identity"]
}, },
@@ -126,20 +138,20 @@
{ {
"id": "licences-database", "id": "licences-database",
"type": "action", "type": "action",
"in": "mesh-store", "in": "postgres",
"command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE licences'"], "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE licences'"],
"verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw licences"] "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw licences"]
}, },
{ {
"id": "context-schemas", "id": "context-schemas",
"type": "action", "type": "action",
"command": ["docker", "run", "--rm", "--network", "container:mesh-store", "command": ["docker", "run", "--rm", "--network", "container:postgres",
"-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable", "-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable",
"-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable", "-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable",
"-e", "MESH_STORE_LICENCES=postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable", "-e", "MESH_STORE_LICENCES=postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable",
"192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace",
"migrate"], "migrate"],
"verify": ["sh", "-c", "docker exec mesh-store psql -U postgres -d inventory -tAc \"select to_regclass('public.node')\" | grep -qx node && docker exec mesh-store psql -U postgres -d identity -tAc \"select to_regclass('public.signing_key')\" | grep -qx signing_key && docker exec mesh-store psql -U postgres -d licences -tAc \"select to_regclass('public.licence')\" | grep -qx licence"] "verify": ["sh", "-c", "docker exec postgres psql -U postgres -d inventory -tAc \"select to_regclass('public.node')\" | grep -qx node && docker exec postgres psql -U postgres -d identity -tAc \"select to_regclass('public.signing_key')\" | grep -qx signing_key && docker exec postgres psql -U postgres -d licences -tAc \"select to_regclass('public.licence')\" | grep -qx licence"]
}, },
// **The bus is raised where the `nats` module keeps it** (novox/hq ADR 0333 §1): the container // **The bus is raised where the `nats` module keeps it** (novox/hq ADR 0333 §1): the container
// `mesh-broker-nats`, its configuration and user list in /var/lib/nats-module/conf, its streams in // `mesh-broker-nats`, its configuration and user list in /var/lib/nats-module/conf, its streams in
+1 -1
View File
@@ -84,7 +84,7 @@ func InstallStore(ctx context.Context, o Options, control controlPlane,
say); err != nil { say); err != nil {
return err return err
} }
say(" adopted mesh-store — the foundation's store is now the " + module + " module") say(" adopted " + store.Name + " — the foundation's store is now the " + module + " module")
return nil return nil
} }
+1 -1
View File
@@ -25,7 +25,7 @@ import (
const ( const (
// foundationStore is the foundation's postgres container — the mesh's own memory, raised from the // foundationStore is the foundation's postgres container — the mesh's own memory, raised from the
// bundle. gitea's bootstrap database lives here too, so a mesh runs one postgres (issue 051). // bundle. gitea's bootstrap database lives here too, so a mesh runs one postgres (issue 051).
foundationStore = "mesh-store" foundationStore = "postgres"
// giteaBootstrap is the gitea server raised directly at genesis, before gitea is a module — // giteaBootstrap is the gitea server raised directly at genesis, before gitea is a module —
// under the name the gitea MODULE declares for its container, so the module finds it and holds // under the name the gitea MODULE declares for its container, so the module finds it and holds
// it rather than raising a second forge beside it (novox/hq ADR 0163, rule 7; issue 090). // it rather than raising a second forge beside it (novox/hq ADR 0163, rule 7; issue 090).
+1 -1
View File
@@ -68,7 +68,7 @@ func TestGivenPortsMoveOnlyTheMachinesSide(t *testing.T) {
if got.Places == 0 { if got.Places == 0 {
t.Fatal("nothing was rewritten") t.Fatal("nothing was rewritten")
} }
storeC := containerNamed(r.Declaration, "mesh-store") storeC := containerNamed(r.Declaration, "postgres")
if len(storeC.Ports) != 1 || storeC.Ports[0] != "5433:5432" { if len(storeC.Ports) != 1 || storeC.Ports[0] != "5433:5432" {
t.Errorf("the store publishes %v", storeC.Ports) t.Errorf("the store publishes %v", storeC.Ports)
} }
+13 -7
View File
@@ -47,6 +47,9 @@ const (
// StoreSuperuserFile is where the store's superuser password lives on the machine — the // StoreSuperuserFile is where the store's superuser password lives on the machine — the
// postgres module's own-secret path, so genesis and adoption write the same file. // postgres module's own-secret path, so genesis and adoption write the same file.
StoreSuperuserFile = "/var/lib/postgres/superuser.secret" StoreSuperuserFile = "/var/lib/postgres/superuser.secret"
// StoreDataDir is where the store keeps its data — the postgres module's data directory, so
// genesis raises the store where the module adopts it (novox/hq ADR 0338 §1).
StoreDataDir = "/var/lib/mesh-store"
// BusControllerFile is where the controller's password on the bus lives on the machine. Not a // BusControllerFile is where the controller's password on the bus lives on the machine. Not a
// module's path: no module reads it — the permanent controller is handed the address the // module's path: no module reads it — the permanent controller is handed the address the
// temporary one was given — and genesis keeps it only so a second run puts the same account // temporary one was given — and genesis keeps it only so a second run puts the same account
@@ -62,7 +65,7 @@ const (
// What the template says, matched exactly. A template that says something else is a template // What the template says, matched exactly. A template that says something else is a template
// this installer does not know how to make safe, and it says so rather than guessing. // this installer does not know how to make safe, and it says so rather than guessing.
templateStorePassword = `"POSTGRES_PASSWORD": "bootstrap"` templateStorePassword = `"POSTGRES_PASSWORD": "bootstrap"`
templateStoreVolumes = `"volumes": ["mesh-store-data:/var/lib/postgresql/data"]` templateStoreVolumes = `"volumes": ["` + StoreDataDir + `:/var/lib/postgresql/data"]`
templateStoreURL = "postgres:bootstrap@" templateStoreURL = "postgres:bootstrap@"
// BusURLPlaceholder is the controller's account in its bus address, as the template names it. // BusURLPlaceholder is the controller's account in its bus address, as the template names it.
@@ -265,8 +268,8 @@ func Masking(say func(string), c RootCredentials) func(string) {
// RefuseExistingServers stops a run that would put a made credential in front of a store raised // RefuseExistingServers stops a run that would put a made credential in front of a store raised
// by an earlier installer with the template's. // by an earlier installer with the template's.
// //
// The store's password is set by initdb, once, on an empty volume. A machine that already holds // The store's password is set by initdb, once, on an empty data directory. A machine that already holds
// `mesh-store-data` and has no credential file was raised with `bootstrap`, and minting a new // a cluster in the store's data directory and has no credential file was raised with `bootstrap`, and minting a new
// value here would make a bundle that dials with a password the server does not have — failing // value here would make a bundle that dials with a password the server does not have — failing
// three steps later, in the schemas, with nothing pointing back here. Refused by name instead, // three steps later, in the schemas, with nothing pointing back here. Refused by name instead,
// with the way forward. // with the way forward.
@@ -278,11 +281,14 @@ func RefuseExistingServers(ctx context.Context, run Runner, c RootCredentials) e
if !c.StoreMade { if !c.StoreMade {
return nil return nil
} }
if _, err := run(ctx, "docker", "volume", "inspect", "mesh-store-data"); err != nil { // Where the store keeps its data, as the postgres module does (novox/hq ADR 0338 §2): a cluster
return nil // no such volume: a fresh machine, which is the case this installer makes // there is one initdb already set a password for. The docker volume genesis used before is not
// the store's data any more, and a machine holding only that is not refused.
if _, err := run(ctx, "test", "-e", StoreDataDir+"/pgdata/PG_VERSION"); err != nil {
return nil // no cluster there: a fresh machine, which is the case this installer makes
} }
return fmt.Errorf( return fmt.Errorf(
"this machine already holds the store's data (volume mesh-store-data) and no credential at %s, so it was raised "+ "this machine already holds the store's data (in "+StoreDataDir+") and no credential at %s, so it was raised "+
"by an earlier installer with the template's password. A new one made here would not open it. "+ "by an earlier installer with the template's password. A new one made here would not open it. "+
"Put the password the store has into %s (0600, the value alone) and run again; then change it "+ "Put the password the store has into %s (0600, the value alone) and run again; then change it "+
"on the server and accept the new value — this installer does not rotate a running store", "on the server and accept the new value — this installer does not rotate a running store",
@@ -311,7 +317,7 @@ func RewriteRoot(r *Rewritten, c RootCredentials) (RootRewrite, error) {
return out, err return out, err
} }
if bundle, err = replaceOnce(bundle, templateStoreVolumes, if bundle, err = replaceOnce(bundle, templateStoreVolumes,
`"volumes": ["mesh-store-data:/var/lib/postgresql/data", "`+StoreSuperuserFile+":"+storeSuperuserMount+`:ro"]`, `"volumes": ["`+StoreDataDir+`:/var/lib/postgresql/data", "`+StoreSuperuserFile+":"+storeSuperuserMount+`:ro"]`,
"the store's volumes"); err != nil { "the store's volumes"); err != nil {
return out, err return out, err
} }
+1 -1
View File
@@ -59,7 +59,7 @@ func TestTheProducedBundleCarriesNoWellKnownCredential(t *testing.T) {
switch x := res.(type) { switch x := res.(type) {
case *declaration.Container: case *declaration.Container:
switch x.Name { switch x.Name {
case "mesh-store": case "postgres":
store = true store = true
if _, has := x.Env["POSTGRES_PASSWORD"]; has { if _, has := x.Env["POSTGRES_PASSWORD"]; has {
t.Error("the store still takes its password from its environment") t.Error("the store still takes its password from its environment")
+163
View File
@@ -0,0 +1,163 @@
package bootstrap
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
)
// Defends novox/hq ADR 0338 (issue 609): genesis raises its store as the postgres module's server, so
// the store step adopts it as it runs. Until then the bundle raised `mesh-store`, on another image, in a
// docker volume, and genesis stopped at step 17 of 24.
// theStoreGenesisRaises is the store container of the bundle as the installer applies it: rewritten for
// this machine's credentials, so its password is the file the module reads too.
func theStoreGenesisRaises(t *testing.T) *declaration.Container {
t.Helper()
r := producedBundle(t)
store, err := storeIn(r.Declaration)
if err != nil {
t.Fatal(err)
}
return store
}
// The bundle's store is exactly the catalogue's postgres module's server, read from the module's own
// manifest (ADR 0338 §1): its name, its image (so a drift of either pin fails here), where its data is,
// and the file its superuser password is read from, with the environment that says so.
func TestTheBundleRaisesTheStoreAsTheCataloguesPostgresModuleDeclaresIt(t *testing.T) {
manifest := theCataloguesModule(t, "postgres")
var m struct {
Resources []struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Image string `json:"image"`
Env map[string]string `json:"env"`
Ports []string `json:"ports"`
Volumes []string `json:"volumes"`
} `json:"resources"`
}
if err := json.Unmarshal(manifest, &m); err != nil {
t.Fatal(err)
}
ours := theStoreGenesisRaises(t)
found := false
for _, r := range m.Resources {
if r.Type != "container" || r.ID != "server" {
continue
}
found = true
if ours.Name != r.Name {
t.Errorf("the bundle raises the store as %q; the module's server is %q", ours.Name, r.Name)
}
if ours.Image != r.Image {
t.Errorf("the bundle raises the store on %s; the module pins %s — the two pins move together", ours.Image, r.Image)
}
for _, key := range []string{"POSTGRES_PASSWORD_FILE", "PGDATA"} {
if ours.Env[key] != r.Env[key] {
t.Errorf("the bundle's store has %s=%q; the module's %q", key, ours.Env[key], r.Env[key])
}
}
if _, has := ours.Env["POSTGRES_PASSWORD"]; has {
t.Error("the bundle's store takes its password from its environment; the module reads a file")
}
// The module's own directory for its state is <data root>/<module>, where genesis keeps the
// superuser's file (StoreSuperuserFile).
var theirs []string
for _, v := range r.Volumes {
theirs = append(theirs, strings.ReplaceAll(v, "${dir:state}", "/var/lib/postgres"))
}
if strings.Join(ours.Volumes, " ") != strings.Join(theirs, " ") {
t.Errorf("the bundle's store mounts %v; the module's %v", ours.Volumes, theirs)
}
if strings.Join(ours.Ports, " ") != strings.Join(r.Ports, " ") {
t.Errorf("the bundle publishes the store on %v; the module on %v", ours.Ports, r.Ports)
}
// And the store step's own check agrees: by name and image, before anything is registered.
if err := serverMatchesFoundation(manifest, ours, "postgres"); err != nil {
t.Errorf("the store step would refuse the store the bundle raises: %v", err)
}
}
if !found {
t.Fatal("the postgres module declares no server container")
}
}
// The store's data directory is made by the bundle before the store starts, owned by the image's
// postgres user, as the module declares it (ADR 0338 §1, and the measurement under its §5).
func TestTheBundleMakesTheStoresDataDirectoryAsTheModuleDoes(t *testing.T) {
r := producedBundle(t)
at, store := -1, -1
for i, res := range r.Declaration.Resources {
if d, ok := res.(*declaration.Directory); ok && d.Path == StoreDataDir {
at = i
if d.Mode != "0700" || d.Owner != "999:70" {
t.Errorf("the store's data directory is made %s %s; the module makes it 0700 999:70", d.Mode, d.Owner)
}
}
if res.Identity() == StoreID {
store = i
}
}
if at < 0 || store < 0 || at > store {
t.Fatalf("the store's data directory %s is not made before the store (directory at %d, store at %d)", StoreDataDir, at, store)
}
}
// No step of the bundle and no step of the installer names `mesh-store`, which is the seat's name only
// (ADR 0338 §2): every step that reaches the store reaches the container the bundle raised.
func TestNoStepReachesTheStoreByTheSeatsName(t *testing.T) {
text := string(theRealBundle(t))
for _, by := range []string{`"in": "mesh-store"`, `container:mesh-store`, `exec mesh-store`, `"name": "mesh-store"`, `mesh-store-data`} {
if strings.Contains(text, by) {
t.Errorf("the bundle still reaches the store as %s", by)
}
}
store := theStoreGenesisRaises(t)
if foundationStore != store.Name {
t.Errorf("the packages step reaches the store as %q; the bundle raises it as %q", foundationStore, store.Name)
}
r := producedBundle(t)
for _, res := range r.Declaration.Resources {
if a, ok := res.(*declaration.Action); ok && a.In != "" && a.In != store.Name && strings.Contains(a.ID, "database") {
t.Errorf("the step %s reaches the store in %q", a.ID, a.In)
}
}
}
// The installer's refusal to raise a store over data it cannot open looks where the store keeps its data
// (ADR 0338 §2): a cluster there and no credential is refused; the old docker volume alone is not.
func TestTheRefusalOverExistingDataLooksInTheStoresDirectory(t *testing.T) {
made := RootCredentials{Store: "s", Bus: "b", StoreMade: true}
machine := func(cluster, volume bool) Runner {
return func(_ context.Context, name string, args ...string) (string, error) {
joined := name + " " + strings.Join(args, " ")
switch {
case joined == "test -e "+StoreDataDir+"/pgdata/PG_VERSION":
if cluster {
return "", nil
}
case strings.HasPrefix(joined, "docker volume inspect"):
if volume {
return "[]", nil
}
}
return "", errors.New("exit status 1")
}
}
err := RefuseExistingServers(context.Background(), machine(true, false), made)
if err == nil || !strings.Contains(err.Error(), StoreDataDir) {
t.Fatalf("a cluster in %s and no credential was not refused naming it: %v", StoreDataDir, err)
}
if err := RefuseExistingServers(context.Background(), machine(false, true), made); err != nil {
t.Fatalf("a machine holding only the old volume was refused: %v", err)
}
if err := RefuseExistingServers(context.Background(), machine(true, false), RootCredentials{Store: "s"}); err != nil {
t.Fatalf("a kept credential was refused: %v", err)
}
}
+1 -1
View File
@@ -89,7 +89,7 @@ func TestAFoundationThatIsUpAndAnsweringIsAccepted(t *testing.T) {
// run-once and scheduled shapes are excluded on purpose — a step that has exited is not a // run-once and scheduled shapes are excluded on purpose — a step that has exited is not a
// fault. The name is `temp-mesh-controller` because the permanent one is a module and takes the // fault. The name is `temp-mesh-controller` because the permanent one is a module and takes the
// plain name (novox/hq ADR 0067), which is what makes the two of them coexist at all. // plain name (novox/hq ADR 0067), which is what makes the two of them coexist at all.
want := []string{"mesh-store", "mesh-broker-nats", "temp-mesh-controller"} want := []string{"postgres", "mesh-broker-nats", "temp-mesh-controller"}
if len(verified.Running) != len(want) { if len(verified.Running) != len(want) {
t.Fatalf("confirmed %v running, want %v", verified.Running, want) t.Fatalf("confirmed %v running, want %v", verified.Running, want)
} }