diff --git a/cmd/mesh-controller/main.go b/cmd/mesh-controller/main.go index e3e346f7..e56bcac1 100644 --- a/cmd/mesh-controller/main.go +++ b/cmd/mesh-controller/main.go @@ -73,6 +73,8 @@ func run() error { } switch args[0] { + case "register": + return registerCommand(ctx, args[1:]) case "build", "rebuild": // **The hand build goes with the asks** (novox/hq ADR 0501 §5): refused by name, naming the replacement. return removedByName(args[0]) diff --git a/cmd/mesh-controller/register.go b/cmd/mesh-controller/register.go new file mode 100644 index 00000000..1c7a727b --- /dev/null +++ b/cmd/mesh-controller/register.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + "github.com/novox/mesh-controller/internal/builderkeys" +) + +// The terminal's exception becomes a registration, not a build (novox/hq ADR 0501 §4; issue 1851, A11b). +// +// `register / `, at the controller's terminal alone: the operator names the module, +// its repository and its path, and the controller registers it from the builder's done key of the fingerprint the +// indexer's trunk value of that module names — the registration a build asked at the terminal used to make for a +// repository the catalogue does not build (ADR 0432 §3, ADR 0420 §7). No verb reaches it (terminalOnlyCommands), and it +// asks no build: a key not done is waited for, and said. +func registerCommand(ctx context.Context, args []string) error { + if len(args) != 3 { + return errors.New("register / (the path \".\" for a module at the repository's root)") + } + if !startedAtTheTerminal() { + return terminalRefusal("register is run at the controller's terminal only, never through a verb: it registers " + + "a module from a repository the catalogue does not build, and whoever may call a verb includes agents " + + "(novox/hq ADR 0266, ADR 0501 §4). Nothing was done") + } + module, repository, path := args[0], strings.Trim(args[1], "/"), strings.Trim(args[2], "/") + if path == sdkdelivery.RootPath { + path = "" + } + open, err := openStores(ctx) + if err != nil { + return err + } + defer open.Close() + keys, closeKeys, err := openBuilderKeys() + if err != nil { + return err + } + defer closeKeys() + trunk, err := keys.ModuleAt(ctx, module, "") + if errors.Is(err, builderkeys.ErrNoValue) { + return fmt.Errorf("the indexer holds no trunk value of %s: a module is registered from the builder's key of a "+ + "merged trunk, and %s has none yet. Nothing was registered", module, module) + } + if err != nil { + return err + } + if err := sameModuleSource(module, repository, path, trunk); err != nil { + return err + } + manifest, kept, err := takeInKey(ctx, open.inventory, keys, keyTake{Module: module, Trunk: trunk, + Ask: registrationAsk{Terminal: true}}) + if errors.Is(err, errKeyWaits) { + return fmt.Errorf("%s is not registered yet, and nothing was asked: %v. Run register again once its key is done", + module, err) + } + if err != nil { + return err + } + fmt.Printf("%s %s registered from the builder's key %s (%s at %.8s), as asked at the controller's terminal; "+ + "assigned nowhere\n", manifest.Module, manifest.Version, short(kept.SourceFingerprint), repository, kept.Commit) + return nil +} + +// sameModuleSource refuses a registration whose repository or path is not the one the indexer's trunk value names: the +// operator names exactly what is registered (ADR 0501 §4). +func sameModuleSource(module, repository, path string, trunk sdkdelivery.Module) error { + if !strings.EqualFold(trunk.Repository, repository) || keyPath(trunk) != path { + return fmt.Errorf("the indexer holds %s in %s at %q, not in %s at %q: the registration names exactly what it "+ + "registers. Nothing was registered", module, trunk.Repository, orRoot(keyPath(trunk)), repository, orRoot(path)) + } + return nil +} diff --git a/cmd/mesh-controller/register_test.go b/cmd/mesh-controller/register_test.go new file mode 100644 index 00000000..99b1e45f --- /dev/null +++ b/cmd/mesh-controller/register_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "errors" + "strings" + "testing" + + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// A11b (ADR 0501 §4): the terminal's registration names exactly the module's repository and path the indexer holds, +// and no verb reaches it. +func TestTheTerminalsRegistrationNamesItsSourceExactlyAndNoVerbReachesIt(t *testing.T) { + trunk := sdkdelivery.Module{Repository: "someone/app", Path: sdkdelivery.RootPath, Commit: "c0ffee"} + if err := sameModuleSource("app", "someone/app", "", trunk); err != nil { + t.Fatalf("the source the indexer holds is refused: %v", err) + } + if err := sameModuleSource("app", "someone/other", "", trunk); err == nil || !strings.Contains(err.Error(), "someone/app") { + t.Fatalf("another repository is taken: %v", err) + } + if err := sameModuleSource("app", "someone/app", "modules/app", trunk); err == nil { + t.Fatal("another path is taken") + } + // The generic verb only reads; and register is the terminal's only, whatever reads. + if _, err := argvFor("command", map[string]any{"command": "register app someone/app ."}); err == nil { + t.Fatal("a verb reaches the terminal's registration") + } + var refused *heldAtTheTerminal + if err := terminalOnly([]string{"register", "app", "someone/app", "."}); !errors.As(err, &refused) { + t.Fatalf("register is not the terminal's only: %v", err) + } + t.Setenv(verbVar, "command") + var held *heldAtTheTerminal + if err := registerCommand(context.Background(), []string{"app", "someone/app", "."}); !errors.As(err, &held) { + t.Fatalf("register through a verb: %v", err) + } +} diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index c07fe9ba..46485477 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -1784,6 +1784,9 @@ var terminalOnlyCommands = map[string]string{ "broker": "the bus's accounts", "api": "the controller's API keys", "licence": "the licences' secrets", + // The terminal's exception of ADR 0501 §4: a module from a repository the catalogue does not build, registered + // from its done key. + "register": "a module's registration from a repository the catalogue does not build", } // givenAtTheDesk is exactly the line the `give` and `secret-ask` verbs compose, and nothing beside it: `secret ask