cli: module issue — deliver a module its scoped broker account (ADR 0048)
'module issue <module> --node <m>' looks up the module's emits/consumes from
the catalogue, ensures the bus exchanges exist, creates its scoped account
(CreateModuleAccount), and seals an amqps {url,fingerprint} to the node as the
module's broker own-secret — the same delivery as 'builder issue', now generic.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -135,6 +135,7 @@ func usage() {
|
|||||||
module list what modules this mesh knows about
|
module list what modules this mesh knows about
|
||||||
module moved <name> <commit> the source has a newer commit than the mesh built
|
module moved <name> <commit> the source has a newer commit than the mesh built
|
||||||
module forget <name> remove one, unless a node is running it
|
module forget <name> remove one, unless a node is running it
|
||||||
|
module issue <name> --node <m> a broker account for a module, scoped to its emits and consumes
|
||||||
status [--json] what is wrong, what is quiet, and what is out of date
|
status [--json] what is wrong, what is quiet, and what is out of date
|
||||||
board [--listen ADDR] the same three questions, as a page that holds nothing
|
board [--listen ADDR] the same three questions, as a page that holds nothing
|
||||||
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
|
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@@ -10,6 +12,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-control/internal/broker"
|
||||||
"github.com/novox/mesh-control/internal/catalogue"
|
"github.com/novox/mesh-control/internal/catalogue"
|
||||||
"github.com/novox/mesh-control/internal/inventory"
|
"github.com/novox/mesh-control/internal/inventory"
|
||||||
"github.com/novox/mesh-control/internal/overlay"
|
"github.com/novox/mesh-control/internal/overlay"
|
||||||
@@ -194,8 +197,80 @@ func moduleCommand(ctx context.Context, args []string) error {
|
|||||||
fmt.Printf("%s forgotten\n", args[1])
|
fmt.Printf("%s forgotten\n", args[1])
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
|
case "issue":
|
||||||
|
// A module's broker account, scoped by its emits and consumes (novox/hq ADR 0048) and
|
||||||
|
// sealed to the machine that will run it — the generic case the builder was the first of.
|
||||||
|
set := flag.NewFlagSet("module issue", flag.ContinueOnError)
|
||||||
|
forNode := set.String("node", "",
|
||||||
|
"the machine that will run it, so the credential is delivered instead of printed")
|
||||||
|
positionals, err := parseAround(set, args[1:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(positionals) != 1 {
|
||||||
|
return errors.New("module issue <module> --node <machine>")
|
||||||
|
}
|
||||||
|
module := positionals[0]
|
||||||
|
if *forNode == "" {
|
||||||
|
return errors.New("module issue needs --node: a module's account is sealed to the " +
|
||||||
|
"machine that runs it, and the mesh cannot read it back to print")
|
||||||
|
}
|
||||||
|
|
||||||
|
shelf, err := inv.Catalogue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m, ok := shelf[module]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("this mesh knows no module %q; `module add` it first", module)
|
||||||
|
}
|
||||||
|
|
||||||
|
management, err := broker.ManagementFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The substrate owns the bus; make sure it exists before a module binds onto it.
|
||||||
|
if err := management.EnsureEventExchanges(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
password := base64.RawURLEncoding.EncodeToString(secret)
|
||||||
|
account, err := management.CreateModuleAccount(ctx, *forNode, module, password, m.Emits, m.Consumes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
known, err := broker.FromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot deliver a credential without knowing where the broker is: %w", err)
|
||||||
|
}
|
||||||
|
// The URL and what verifies the broker, together — a mesh's broker presents its own
|
||||||
|
// certificate, in no public trust store, so a URL alone fails at TLS (as `builder issue`).
|
||||||
|
held, err := json.Marshal(struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Fingerprint string `json:"fingerprint,omitempty"`
|
||||||
|
}{
|
||||||
|
URL: fmt.Sprintf("amqps://%s:%s@%s/", account, password, known.Address),
|
||||||
|
Fingerprint: known.Fingerprint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := inv.AcceptSecretForModule(ctx, *forNode, module, "broker", string(held)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("broker account %s created for %s, scoped to what it emits and consumes\n",
|
||||||
|
account, module)
|
||||||
|
fmt.Printf(" sealed to %s. It arrives with the next push — `push %s` to send it\n",
|
||||||
|
*forNode, *forNode)
|
||||||
|
return nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("module has no %q; it has add, list, moved and forget", args[0])
|
return fmt.Errorf("module has no %q; it has add, list, moved, forget and issue", args[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user