bootstrap: follow the mount from the variable to the secret
The catalogue's mesh-control manifest landed while this was being written, and it
does what the ordinary case does: it keeps its secrets under /var/lib/mesh and
mounts them into the container at /run/secrets, so MESH_STORE_INVENTORY_FILE names
a path that no own-secret writes. Matching on the path alone found nothing and
would have refused a correct manifest.
So the lookup follows the volumes. It also reads the other shape the manifest uses
— `VAR=${secret:name}` inside the environment file a container reads — which is
how a value that is not a path gets in at all, and which is where the broker's two
credentials live.
That generalises what is delivered: every variable the module fills from a secret
is looked up in the substrate's control plane. What the substrate names is accepted
through `secret accept`; what it does not is left for the mesh to generate, and
said so. A store connection the substrate does not name stays an error — a control
plane that cannot open a context is not one.
Checked against the real manifest (mesh-catalog feat/control-plane-module): five
variables resolve, the placeholder pins in one place, and the container it waits
for is `mesh-control`.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -11,28 +11,47 @@ import (
|
||||
// that could go wrong quietly: pinning it to the wrong image, and delivering it store connections
|
||||
// the mesh invented rather than the ones the substrate actually made.
|
||||
|
||||
// theControlPlaneModule is the shape this installer codes against: one container using the
|
||||
// catalogue's placeholder-digest convention, with each store connection delivered as a sealed
|
||||
// secret written into a file and MESH_STORE_<CONTEXT>_FILE pointing at it.
|
||||
// theControlPlaneModule is the catalogue's manifest, trimmed to what this installer reads.
|
||||
//
|
||||
// A fixture rather than the file itself, unlike the substrate example the rewrite tests use: the
|
||||
// catalogue is a different repository on a different branch, and a test that read it would pass or
|
||||
// fail according to what somebody else had checked out. What it must stay faithful to is the
|
||||
// SHAPE — the placeholder digest, the own-secret per context, the mount from the machine's path to
|
||||
// the container's, and the environment file that fills what is not a path.
|
||||
const theControlPlaneModule = `{
|
||||
"module": "mesh-control",
|
||||
"version": "1",
|
||||
"slug": "control",
|
||||
"capabilities": ["container-runtime"],
|
||||
"claims": [{"name": "the-control-plane", "scope": "mesh"}],
|
||||
"own-secrets": {
|
||||
"inventory-store": "/var/lib/mesh/control/inventory",
|
||||
"identity-store": "/var/lib/mesh/control/identity",
|
||||
"licences-store": "/var/lib/mesh/control/licences"
|
||||
"inventory": "/var/lib/mesh/mesh-control/inventory",
|
||||
"identity": "/var/lib/mesh/mesh-control/identity",
|
||||
"licences": "/var/lib/mesh/mesh-control/licences",
|
||||
"broker": "/var/lib/mesh/mesh-control/broker",
|
||||
"broker-management": "/var/lib/mesh/mesh-control/broker-management"
|
||||
},
|
||||
"resources": [
|
||||
{"id": "state", "type": "directory", "path": "/var/lib/mesh/control", "mode": "0700"},
|
||||
{"id": "container", "type": "container", "name": "mesh-control",
|
||||
{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"},
|
||||
{"id": "broker-env", "type": "file", "path": "/var/lib/mesh/mesh-control/broker.env",
|
||||
"mode": "0600",
|
||||
"content": "MESH_BROKER_AMQP=${secret:broker}\nMESH_BROKER_MANAGEMENT=${secret:broker-management}\nMESH_BROKER_ADDRESS=${machine:at}:5671\n"},
|
||||
{"id": "server", "type": "container", "name": "mesh-control",
|
||||
"image": "mesh-control@` + placeholderDigest + `",
|
||||
"network": "host", "args": ["serve"],
|
||||
"env-file": ["/var/lib/mesh/mesh-control/broker.env"],
|
||||
"env": {
|
||||
"MESH_STORE_INVENTORY_FILE": "/var/lib/mesh/control/inventory",
|
||||
"MESH_STORE_IDENTITY_FILE": "/var/lib/mesh/control/identity",
|
||||
"MESH_STORE_LICENCES_FILE": "/var/lib/mesh/control/licences"
|
||||
}}
|
||||
"MESH_STORE_INVENTORY_FILE": "/run/secrets/inventory",
|
||||
"MESH_STORE_IDENTITY_FILE": "/run/secrets/identity",
|
||||
"MESH_STORE_LICENCES_FILE": "/run/secrets/licences",
|
||||
"MESH_BROKER_CERTIFICATE": "/broker-tls/tls.crt"
|
||||
},
|
||||
"volumes": [
|
||||
"mesh-broker-tls:/broker-tls:ro",
|
||||
"/var/lib/mesh/mesh-control/inventory:/run/secrets/inventory:ro",
|
||||
"/var/lib/mesh/mesh-control/identity:/run/secrets/identity:ro",
|
||||
"/var/lib/mesh/mesh-control/licences:/run/secrets/licences:ro"
|
||||
]}
|
||||
]
|
||||
}`
|
||||
|
||||
@@ -76,8 +95,8 @@ func TestAManifestAlreadyPinnedByHandIsRefused(t *testing.T) {
|
||||
// otherwise be left half pinned, and fail inside an apply rather than here.
|
||||
func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
|
||||
twice := strings.Replace(theControlPlaneModule,
|
||||
`{"id": "state", "type": "directory", "path": "/var/lib/mesh/control", "mode": "0700"},`,
|
||||
`{"id": "state", "type": "directory", "path": "/var/lib/mesh/control", "mode": "0700"},
|
||||
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"},`,
|
||||
`{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-control", "mode": "0700"},
|
||||
{"id": "migrate", "type": "container", "name": "mesh-control-migrate", "run-once": true,
|
||||
"image": "mesh-control@`+placeholderDigest+`", "args": ["migrate"]},`, 1)
|
||||
|
||||
@@ -99,22 +118,30 @@ func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) {
|
||||
// context. The pairing is read from the manifest so that whatever the catalogue calls these
|
||||
// secrets is what is delivered.
|
||||
func TestTheStoreConnectionsComeFromTheBundleThatMadeThem(t *testing.T) {
|
||||
wanted, err := storeSecretsIn([]byte(theControlPlaneModule))
|
||||
wanted, err := secretsByVariableIn([]byte(theControlPlaneModule))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for context, secret := range map[string]string{
|
||||
"INVENTORY": "inventory-store",
|
||||
"IDENTITY": "identity-store",
|
||||
"LICENCES": "licences-store",
|
||||
// **Through the mount.** The manifest keeps its secrets under /var/lib and the container reads
|
||||
// them at /run/secrets. Matching on the path alone would find nothing and refuse a correct
|
||||
// manifest, which is exactly the ordinary case in the catalogue.
|
||||
for variable, secret := range map[string]string{
|
||||
"MESH_STORE_INVENTORY": "inventory",
|
||||
"MESH_STORE_IDENTITY": "identity",
|
||||
"MESH_STORE_LICENCES": "licences",
|
||||
"MESH_BROKER_AMQP": "broker",
|
||||
"MESH_BROKER_MANAGEMENT": "broker-management",
|
||||
} {
|
||||
if wanted[context] != secret {
|
||||
t.Errorf("the %s store's connection would be accepted as %q, want %q",
|
||||
context, wanted[context], secret)
|
||||
if wanted[variable] != secret {
|
||||
t.Errorf("%s would be accepted as %q, want %q", variable, wanted[variable], secret)
|
||||
}
|
||||
}
|
||||
// And what the manifest fills from the machine rather than from a secret is left alone.
|
||||
if _, claimed := wanted["MESH_BROKER_ADDRESS"]; claimed {
|
||||
t.Error("the address the mesh composes from the machine was treated as a secret")
|
||||
}
|
||||
|
||||
// And the values are the substrate's own, taken from the produced bundle rather than composed.
|
||||
// The values are the substrate's own, taken from the produced bundle rather than composed.
|
||||
rewritten, err := Rewrite(theRealBundle(t), held)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -127,8 +154,9 @@ func TestTheStoreConnectionsComeFromTheBundleThatMadeThem(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(delivered) != 3 {
|
||||
t.Fatalf("%d connections were delivered, and the mesh holds three contexts: %v",
|
||||
// Three stores and both halves of the broker: everything the substrate made and nothing else.
|
||||
if len(delivered) != 5 {
|
||||
t.Fatalf("%d values were delivered, and the substrate names five: %v",
|
||||
len(delivered), delivered)
|
||||
}
|
||||
for _, secret := range delivered {
|
||||
@@ -138,15 +166,49 @@ func TestTheStoreConnectionsComeFromTheBundleThatMadeThem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A secret the substrate did not make is left for the mesh to make, and said so. Every other
|
||||
// secret in a mesh is one the mesh made; `secret accept` is only for what predates the mesh.
|
||||
func TestASecretTheSubstrateNeverMadeIsLeftToTheMesh(t *testing.T) {
|
||||
extra := strings.Replace(theControlPlaneModule,
|
||||
`"broker": "/var/lib/mesh/mesh-control/broker",`,
|
||||
`"broker": "/var/lib/mesh/mesh-control/broker",
|
||||
"something-new": "/var/lib/mesh/mesh-control/something-new",`, 1)
|
||||
extra = strings.Replace(extra,
|
||||
`"content": "MESH_BROKER_AMQP=${secret:broker}\n`,
|
||||
`"content": "MESH_SOMETHING_NEW=${secret:something-new}\nMESH_BROKER_AMQP=${secret:broker}\n`, 1)
|
||||
|
||||
rewritten, err := Rewrite(theRealBundle(t), held)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := &asked{answer: aMeshThatAgrees(nil)}
|
||||
control := controlPlane{container: "temp-mesh-control", run: runtime.run, timeout: time.Second}
|
||||
|
||||
var said []string
|
||||
delivered, err := deliverStores(context.Background(), Options{Node: "anchor"}, control,
|
||||
[]byte(extra), rewritten.Declaration, func(line string) { said = append(said, line) })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range delivered {
|
||||
if secret == "something-new" {
|
||||
t.Error("a value the substrate never made was accepted as though it had")
|
||||
}
|
||||
}
|
||||
if !strings.Contains(strings.Join(said, "\n"), "the mesh will make something-new") {
|
||||
t.Errorf("nothing was said about the secret the mesh has to make: %v", said)
|
||||
}
|
||||
}
|
||||
|
||||
// A manifest whose container reads a file no own-secret writes is refused. The mesh would seal
|
||||
// nothing there and the control plane would find an empty file where a connection string has to
|
||||
// be — which presents as a control plane that will not start, three steps from the cause.
|
||||
func TestAConnectionFileNothingWritesIsRefused(t *testing.T) {
|
||||
mismatched := strings.Replace(theControlPlaneModule,
|
||||
`"inventory-store": "/var/lib/mesh/control/inventory"`,
|
||||
`"inventory-store": "/var/lib/mesh/control/somewhere-else"`, 1)
|
||||
`"inventory": "/var/lib/mesh/mesh-control/inventory",`,
|
||||
`"inventory": "/var/lib/mesh/mesh-control/somewhere-else",`, 1)
|
||||
|
||||
_, err := storeSecretsIn([]byte(mismatched))
|
||||
_, err := secretsByVariableIn([]byte(mismatched))
|
||||
if err == nil {
|
||||
t.Fatal("a manifest whose two ends do not meet was accepted")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user