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:
+128
-53
@@ -218,26 +218,32 @@ func controlPlaneResourceIn(manifest []byte) string {
|
||||
// deliverStores carries the substrate's own database connections into the module.
|
||||
//
|
||||
// The pairing is read from the manifest rather than assumed, so that whatever the catalogue calls
|
||||
// these secrets is what is delivered: a container asking for `MESH_STORE_INVENTORY_FILE` names a
|
||||
// path, and the module's own-secret that writes that path is the secret to accept the connection
|
||||
// as. That is one lookup and it cannot get the wrong secret — the alternative, guessing that the
|
||||
// secret is called `inventory`, would seal a connection string under a name nothing reads and
|
||||
// leave the mesh to invent random bytes for the one that is.
|
||||
// these secrets is what is delivered. The installer does not guess that the secret holding the
|
||||
// inventory connection is called `inventory`; it follows the manifest from the variable to the
|
||||
// secret, and a manifest whose two ends do not meet is refused rather than half-delivered.
|
||||
//
|
||||
// **What is delivered is what the substrate already has, and only that.** The mesh generates an
|
||||
// own-secret nobody supplied, which is right for something coming into existence and wrong for
|
||||
// something that already exists. So every variable the module fills from a secret is looked up in
|
||||
// the substrate's control plane: what it names is accepted, what it does not is left for the mesh
|
||||
// to make. A store connection missing from the substrate is the one exception and is an error —
|
||||
// a control plane that cannot open a context is not a control plane.
|
||||
func deliverStores(ctx context.Context, o Options, control controlPlane, manifest []byte,
|
||||
substrate *declaration.Declaration, say func(string)) ([]string, error) {
|
||||
|
||||
wanted, err := storeSecretsIn(manifest)
|
||||
wanted, err := secretsByVariableIn(manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(wanted) == 0 {
|
||||
if !anyStoreIn(wanted) {
|
||||
return nil, fmt.Errorf(
|
||||
"the %s module's manifest asks for no store connections. A control plane reaches each "+
|
||||
"context through its own credential (novox/hq ADR 0008), so a manifest naming none "+
|
||||
"describes a control plane that can open nothing.\n"+
|
||||
"the %s module's manifest fills no %s… variable from a secret. A control plane reaches "+
|
||||
"each context through its own credential (novox/hq ADR 0008), so a manifest naming "+
|
||||
"none describes a control plane that can open nothing.\n"+
|
||||
"The shape this installer delivers into is a file per context, named by an "+
|
||||
"own-secret, with %s<CONTEXT>%s in the container's environment pointing at it",
|
||||
ControlPlaneModule, storeVariablePrefix, storeFileSuffix)
|
||||
"own-secret, with %s<CONTEXT>%s pointing at it — directly, or at where that file is "+
|
||||
"mounted inside the container",
|
||||
ControlPlaneModule, storeVariablePrefix, storeVariablePrefix, storeFileSuffix)
|
||||
}
|
||||
|
||||
temporary, err := controlPlaneIn(substrate)
|
||||
@@ -246,24 +252,29 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
|
||||
}
|
||||
|
||||
var delivered []string
|
||||
for _, context := range sortedKeys(wanted) {
|
||||
secret := wanted[context]
|
||||
connection := temporary.Env[storeVariablePrefix+context]
|
||||
if strings.TrimSpace(connection) == "" {
|
||||
return delivered, fmt.Errorf(
|
||||
"the %s module wants the %s store's connection and the bundle this installer "+
|
||||
"produced does not name one: its control plane has no %s.\n"+
|
||||
"These connections are the substrate's, created at genesis — the mesh cannot "+
|
||||
"invent them and the installer will not guess at one",
|
||||
ControlPlaneModule, strings.ToLower(context), storeVariablePrefix+context)
|
||||
for _, variable := range sortedKeys(wanted) {
|
||||
secret := wanted[variable]
|
||||
value := strings.TrimSpace(temporary.Env[variable])
|
||||
if value == "" {
|
||||
if strings.HasPrefix(variable, storeVariablePrefix) {
|
||||
return delivered, fmt.Errorf(
|
||||
"the %s module wants %s and the bundle this installer produced does not name "+
|
||||
"one.\n"+
|
||||
"That connection is the substrate's, created at genesis — the mesh cannot "+
|
||||
"invent it and the installer will not guess at one",
|
||||
ControlPlaneModule, variable)
|
||||
}
|
||||
// Not something the substrate made. The mesh generates its own, which is exactly what
|
||||
// an own-secret is for; said so that nothing about the delivery is silent.
|
||||
say(" the mesh will make " + secret + " — the substrate names no " + variable)
|
||||
continue
|
||||
}
|
||||
|
||||
// Into the container as a file, because `secret accept` reads a file or a prompt and the
|
||||
// installer has neither a terminal to be prompted at nor a way to write to a command's
|
||||
// standard input through the runner every applier in this repository shares.
|
||||
at := "/accepting-" + strings.ToLower(context)
|
||||
if err := control.carrying(ctx, "mesh-store-"+strings.ToLower(context),
|
||||
[]byte(connection), at); err != nil {
|
||||
at := "/accepting-" + secret
|
||||
if err := control.carrying(ctx, "mesh-accepting-"+secret, []byte(value), at); err != nil {
|
||||
return delivered, err
|
||||
}
|
||||
if _, err := control.tell(ctx, "secret", "accept", o.Node, ControlPlaneModule, secret,
|
||||
@@ -271,60 +282,124 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
|
||||
return delivered, err
|
||||
}
|
||||
delivered = append(delivered, secret)
|
||||
say(" accepted " + secret + " — the " + strings.ToLower(context) +
|
||||
" store, as the substrate made it")
|
||||
say(" accepted " + secret + " — " + variable + ", as the substrate made it")
|
||||
}
|
||||
return delivered, nil
|
||||
}
|
||||
|
||||
// storeSecretsIn pairs each context with the secret its connection must be accepted as.
|
||||
// secretsByVariableIn maps each environment variable the module fills from a secret to that
|
||||
// secret's name.
|
||||
//
|
||||
// Read out of the manifest twice over: the container's environment says which contexts are wanted
|
||||
// and what file each expects, and the module's own-secrets say which secret writes which file. A
|
||||
// pair that does not meet is refused rather than half-delivered.
|
||||
func storeSecretsIn(manifest []byte) (map[string]string, error) {
|
||||
// Two shapes, because the catalogue uses both:
|
||||
//
|
||||
// - `MESH_STORE_<CONTEXT>_FILE` in the container's environment, naming a path the process reads.
|
||||
// The path may be the own-secret's own path, or — more usually — where that file is mounted
|
||||
// inside the container, in which case the volumes say which is which. Following the mount is
|
||||
// not a nicety: a manifest that keeps its secrets under `/var/lib/mesh/…` and mounts them at
|
||||
// `/run/secrets/…` is the ordinary case, and matching on the path alone would find nothing and
|
||||
// refuse a correct manifest.
|
||||
// - `VAR=${secret:name}` inside a file resource the container reads its environment from, which
|
||||
// is how a value that is not a path gets in at all.
|
||||
//
|
||||
// A `…_FILE` variable whose file nothing writes is refused: the mesh would seal nothing there and
|
||||
// the process would find an empty file where a credential has to be, which presents as a container
|
||||
// that will not start, a long way from the cause.
|
||||
func secretsByVariableIn(manifest []byte) (map[string]string, error) {
|
||||
var m struct {
|
||||
OwnSecrets map[string]string `json:"own-secrets"`
|
||||
Resources []struct {
|
||||
Type string `json:"type"`
|
||||
Env map[string]string `json:"env"`
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Env map[string]string `json:"env"`
|
||||
Volumes []string `json:"volumes"`
|
||||
} `json:"resources"`
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &m); err != nil {
|
||||
return nil, fmt.Errorf("the %s module's manifest is not readable: %w", ControlPlaneModule, err)
|
||||
}
|
||||
|
||||
byPath := map[string]string{}
|
||||
secretAt := map[string]string{}
|
||||
for name, path := range m.OwnSecrets {
|
||||
byPath[path] = name
|
||||
secretAt[path] = name
|
||||
}
|
||||
|
||||
wanted := map[string]string{}
|
||||
for _, r := range m.Resources {
|
||||
if r.Type != "container" {
|
||||
continue
|
||||
}
|
||||
for key, path := range r.Env {
|
||||
if !strings.HasPrefix(key, storeVariablePrefix) || !strings.HasSuffix(key, storeFileSuffix) {
|
||||
continue
|
||||
switch r.Type {
|
||||
case "file":
|
||||
for variable, secret := range secretsInContent(r.Content) {
|
||||
wanted[variable] = secret
|
||||
}
|
||||
context := strings.TrimSuffix(strings.TrimPrefix(key, storeVariablePrefix), storeFileSuffix)
|
||||
secret, ok := byPath[path]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"the %s module's container reads the %s store's connection from %s, and no "+
|
||||
"own-secret of that module writes that file.\n"+
|
||||
"So the mesh would seal nothing there and the control plane would find an "+
|
||||
"empty file where a connection string has to be. The manifest has to name "+
|
||||
"the two ends the same",
|
||||
ControlPlaneModule, strings.ToLower(context), path)
|
||||
case "container":
|
||||
inside := mountedFrom(r.Volumes)
|
||||
for key, path := range r.Env {
|
||||
if !strings.HasPrefix(key, storeVariablePrefix) ||
|
||||
!strings.HasSuffix(key, storeFileSuffix) {
|
||||
continue
|
||||
}
|
||||
on := path
|
||||
if from, mounted := inside[path]; mounted {
|
||||
on = from
|
||||
}
|
||||
secret, named := secretAt[on]
|
||||
if !named {
|
||||
return nil, fmt.Errorf(
|
||||
"the %s module's container reads %s from %s, and no own-secret of that "+
|
||||
"module writes that file.\n"+
|
||||
"So the mesh would seal nothing there and the control plane would find "+
|
||||
"an empty file where a connection string has to be. The manifest has to "+
|
||||
"name the two ends the same, directly or through a mount",
|
||||
ControlPlaneModule, key, path)
|
||||
}
|
||||
wanted[strings.TrimSuffix(key, storeFileSuffix)] = secret
|
||||
}
|
||||
wanted[context] = secret
|
||||
}
|
||||
}
|
||||
return wanted, nil
|
||||
}
|
||||
|
||||
// mountedFrom is where each path inside a container comes from outside it.
|
||||
func mountedFrom(volumes []string) map[string]string {
|
||||
inside := map[string]string{}
|
||||
for _, volume := range volumes {
|
||||
parts := strings.Split(volume, ":")
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
inside[parts[1]] = parts[0]
|
||||
}
|
||||
return inside
|
||||
}
|
||||
|
||||
// secretsInContent finds `VAR=${secret:name}` lines in a file the container reads its environment
|
||||
// from.
|
||||
func secretsInContent(content string) map[string]string {
|
||||
found := map[string]string{}
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
variable, value, is := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !is {
|
||||
continue
|
||||
}
|
||||
const opens = "${secret:"
|
||||
if !strings.HasPrefix(value, opens) || !strings.HasSuffix(value, "}") {
|
||||
continue
|
||||
}
|
||||
found[variable] = strings.TrimSuffix(strings.TrimPrefix(value, opens), "}")
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// anyStoreIn reports whether any of these variables is a context's connection.
|
||||
func anyStoreIn(wanted map[string]string) bool {
|
||||
for variable := range wanted {
|
||||
if strings.HasPrefix(variable, storeVariablePrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
|
||||
Reference in New Issue
Block a user