fix(summer): delegate admin, route and key commands and pass all delegate flags through
- delegateCommands() builds every delegate from one table of name, description and common-flags hint; toolCommands appends it - route:list, key:generate, admin:create and admin:reset-password are new delegates; the child keeps summer's stdin, so the password prompts work - migrate:rollback, queue:work and schedule:run lose their hand-written flag copies: --plugin, --queue and --once now pass through verbatim - the command list marks delegates with "(flags pass to the app binary)" and the help command prints a Long text with the common flags - console introduction, setup-and-maintenance and the root README describe the passthrough behaviour and the new delegates
This commit is contained in:
@@ -73,7 +73,7 @@ The `migrate`, `migrate:status`, `migrate:rollback`, `serve` and admin commands
|
||||
curl http://127.0.0.1:8080/items/1
|
||||
```
|
||||
|
||||
`summer migrate`, `summer migrate:status`, `summer migrate:rollback` and `summer serve`, run from the application directory, delegate to the built binary in `bin/`.
|
||||
`summer migrate`, `summer migrate:status`, `summer migrate:rollback`, `summer serve`, `summer queue:work`, `summer queue:clear`, `summer schedule:run`, `summer route:list`, `summer key:generate`, `summer admin:create` and `summer admin:reset-password`, run from the application directory, delegate to the built binary in `bin/` and pass their flags to it unchanged.
|
||||
|
||||
## Repository layout
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func main() {
|
||||
}
|
||||
|
||||
func toolCommands() []bonfire.Command {
|
||||
return []bonfire.Command{
|
||||
return append([]bonfire.Command{
|
||||
buildCommand(),
|
||||
makePluginCommand(),
|
||||
makeModelCommand(),
|
||||
@@ -42,17 +42,10 @@ func toolCommands() []bonfire.Command {
|
||||
parityReplayCommand(),
|
||||
parityBroadcastsCommand(),
|
||||
parityUpstreamCommand(),
|
||||
delegateCommand("migrate", "Run plugin migrations in the app binary"),
|
||||
delegateRollbackCommand(),
|
||||
delegateCommand("migrate:status", "Show per-plugin migration history in the app binary"),
|
||||
delegateCommand("serve", "Run the app HTTP server"),
|
||||
delegateQueueWorkCommand(),
|
||||
delegateScheduleRunCommand(),
|
||||
delegateCommand("queue:clear", "Clear pending queued jobs in the app binary"),
|
||||
docsBuildCommand(),
|
||||
docsSyncCommand(),
|
||||
docsServeCommand(),
|
||||
}
|
||||
}, delegateCommands()...)
|
||||
}
|
||||
|
||||
func buildCommand() bonfire.Command {
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestToolCommandNames(t *testing.T) {
|
||||
for _, c := range toolCommands() {
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "make:job", "make:admin-controller", "plugin:add", "new", "dev", "migrate", "migrate:rollback", "migrate:status", "serve", "queue:work", "queue:clear", "schedule:run", "parity:broadcasts", "parity:upstream", "docs:build", "docs:sync", "docs:serve"} {
|
||||
for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "make:job", "make:admin-controller", "plugin:add", "new", "dev", "migrate", "migrate:rollback", "migrate:status", "serve", "queue:work", "queue:clear", "schedule:run", "route:list", "key:generate", "admin:create", "admin:reset-password", "parity:broadcasts", "parity:upstream", "docs:build", "docs:sync", "docs:serve"} {
|
||||
if !slices.Contains(names, want) {
|
||||
t.Fatalf("missing %s in %v", want, names)
|
||||
}
|
||||
@@ -33,7 +33,6 @@ func TestToolCommandNames(t *testing.T) {
|
||||
"make:command": {"[plugin] [name]"},
|
||||
"make:job": {"[plugin] [name]"},
|
||||
"make:admin-controller": {"[plugin] [name]"},
|
||||
"schedule:run": {"--once"},
|
||||
"parity:broadcasts": {"--flow", "--step", "--ids", "127.0.0.1:8424"},
|
||||
"parity:upstream": {"--listen", "--ca-dir", "--out", "--mode", "--script", "--vars", "127.0.0.1:8425"},
|
||||
"docs:build": {"--out", "--src", "--root", "--base-url", "--check"},
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/build"
|
||||
@@ -16,10 +15,44 @@ import (
|
||||
// appRunner runs the app binary with argv; tests replace it.
|
||||
var appRunner = runAppBinary
|
||||
|
||||
func delegateCommand(name, description string) bonfire.Command {
|
||||
// delegateCommands returns every command summer runs on the app binary. One
|
||||
// table is the single source: name, base description and a hint naming the
|
||||
// common flags or arguments the app binary accepts.
|
||||
func delegateCommands() []bonfire.Command {
|
||||
rows := []struct{ name, description, hint string }{
|
||||
{"migrate", "Run plugin migrations in the app binary", ""},
|
||||
{"migrate:rollback", "Roll back the last migration of a plugin in the app binary", "--plugin <id>"},
|
||||
{"migrate:status", "Show per-plugin migration history in the app binary", ""},
|
||||
{"serve", "Run the app HTTP server", "--addr <host:port> (default :8080)"},
|
||||
{"queue:work", "Run background job workers in the app binary", "--queue <name> (repeatable)"},
|
||||
{"queue:clear", "Clear pending queued jobs in the app binary", `[queue] argument (default "default")`},
|
||||
{"schedule:run", "Run the scheduler in the app binary (or once for system cron)", "--once"},
|
||||
{"route:list", "List the app's routes from the app binary", ""},
|
||||
{"key:generate", "Print a fresh application key from the app binary", ""},
|
||||
{"admin:create", "Create a backend administrator in the app binary", "--email <email> (required), --login, --role <code>, --superuser; the password is prompted or read from stdin"},
|
||||
{"admin:reset-password", "Reset a backend administrator's password in the app binary", "<identifier> (login or email); the password is prompted or read from stdin"},
|
||||
}
|
||||
cmds := make([]bonfire.Command, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
cmds = append(cmds, delegateCommand(r.name, r.description, r.hint))
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
// delegateCommand returns a passthrough command that runs name on the app
|
||||
// binary with every argument summer was given.
|
||||
func delegateCommand(name, description, hint string) bonfire.Command {
|
||||
long := description + ".\n\nsummer runs bin/<binary> " + name +
|
||||
" in the application directory, building the binary first when it is missing. Every flag and argument after " +
|
||||
name + ", including --help, is passed to the app binary unchanged."
|
||||
if hint != "" {
|
||||
long += "\n\nCommon flags: " + hint + "."
|
||||
}
|
||||
long += "\n\nRun summer " + name + " --help inside an application to list all of them."
|
||||
return bonfire.Command{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Description: description + " (flags pass to the app binary)",
|
||||
Long: long,
|
||||
Passthrough: true,
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
args := in.Args()
|
||||
@@ -34,67 +67,6 @@ func delegateCommand(name, description string) bonfire.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func delegateRollbackCommand() bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "migrate:rollback",
|
||||
Description: "Roll back the last migration of a plugin in the app binary",
|
||||
Flags: []bonfire.Flag{{
|
||||
Name: "plugin",
|
||||
Description: "Plugin ID whose last migration to roll back",
|
||||
}},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
argv := []string{"migrate:rollback"}
|
||||
if plugin, ok := in.Flag("plugin"); ok && plugin != "" {
|
||||
argv = append(argv, "--plugin", plugin)
|
||||
}
|
||||
argv = append(argv, in.Args()...)
|
||||
return runAppBinary(ctx, argv, out)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func delegateQueueWorkCommand() bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "queue:work",
|
||||
Description: "Run background job workers in the app binary",
|
||||
Flags: []bonfire.Flag{{
|
||||
Name: "queue",
|
||||
Description: "Queue to work (repeatable; default all known queues)",
|
||||
Repeatable: true,
|
||||
}},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
argv := []string{"queue:work"}
|
||||
for _, q := range in.Flags("queue") {
|
||||
argv = append(argv, "--queue", q)
|
||||
}
|
||||
argv = append(argv, in.Args()...)
|
||||
return runAppBinary(ctx, argv, out)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func delegateScheduleRunCommand() bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "schedule:run",
|
||||
Description: "Run the scheduler in the app binary (or once for system cron)",
|
||||
Flags: []bonfire.Flag{{
|
||||
Name: "once",
|
||||
Description: "Run the entries due this minute and exit (for system cron)",
|
||||
Bare: true,
|
||||
}},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
argv := []string{"schedule:run"}
|
||||
if v, ok := in.Flag("once"); ok {
|
||||
if once, _ := strconv.ParseBool(v); once {
|
||||
argv = append(argv, "--once")
|
||||
}
|
||||
}
|
||||
argv = append(argv, in.Args()...)
|
||||
return runAppBinary(ctx, argv, out)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runAppBinary(ctx context.Context, argv []string, out bonfire.Output) error {
|
||||
if len(argv) == 0 {
|
||||
return fmt.Errorf("summer: missing app command")
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
@@ -42,3 +43,73 @@ func TestDelegatedServePassesFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateCommandsPassArgs(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"migrate:rollback", "--plugin", "acme.blog"},
|
||||
{"queue:work", "--queue", "a", "--queue", "b"},
|
||||
{"schedule:run", "--once"},
|
||||
{"queue:clear", "emails"},
|
||||
{"migrate"},
|
||||
{"migrate:status", "--help"},
|
||||
{"route:list"},
|
||||
{"key:generate"},
|
||||
{"admin:create", "--email", "you@example.com", "--superuser"},
|
||||
{"admin:reset-password", "admin@example.com"},
|
||||
} {
|
||||
got := captureAppRunner(t)
|
||||
if out, err := runSummer(t, args...); err != nil {
|
||||
t.Fatalf("%q: %v\n%s", args, err, out)
|
||||
}
|
||||
if !slices.Equal(*got, args) {
|
||||
t.Fatalf("%q: argv = %q, want %q", args, *got, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateCommandsHelp(t *testing.T) {
|
||||
hints := map[string][]string{
|
||||
"migrate:rollback": {"--plugin"},
|
||||
"queue:work": {"--queue"},
|
||||
"schedule:run": {"--once"},
|
||||
"serve": {"--addr"},
|
||||
"admin:create": {"--email", "--superuser"},
|
||||
}
|
||||
cmds := delegateCommands()
|
||||
if len(cmds) != 11 {
|
||||
t.Fatalf("delegateCommands() has %d commands, want 11", len(cmds))
|
||||
}
|
||||
got := captureAppRunner(t)
|
||||
for _, c := range cmds {
|
||||
out, err := runSummer(t, "help", c.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("help %s: %v", c.Name, err)
|
||||
}
|
||||
for _, want := range append([]string{"passed to the app binary"}, hints[c.Name]...) {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("help %s missing %q:\n%s", c.Name, want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if *got != nil {
|
||||
t.Fatalf("help ran the app binary with %q", *got)
|
||||
}
|
||||
|
||||
out, err := runSummer(t, "--help")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range cmds {
|
||||
found := false
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 && fields[0] == c.Name && strings.Contains(line, "(flags pass to the app binary)") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("summer --help does not mark %s:\n%s", c.Name, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ summer make:model --help
|
||||
./bin/acme migrate:rollback --help
|
||||
```
|
||||
|
||||
A command that `summer` delegates to the application binary, such as `summer serve`, answers `--help` with the binary's own help, so run it inside an application directory. Asking the help command of `summer` about a delegated command instead prints a short description and its common flags without running the binary.
|
||||
|
||||
## Commands that summer delegates
|
||||
|
||||
During development you often work from the application directory with `summer` alone. These `summer` commands find the application's `summer.yaml`, build `bin/<binary>` when it does not exist yet, and run the same command of the binary with the same arguments:
|
||||
@@ -39,8 +41,23 @@ During development you often work from the application directory with `summer` a
|
||||
| `summer queue:work` | `./bin/acme queue:work` |
|
||||
| `summer queue:clear` | `./bin/acme queue:clear` |
|
||||
| `summer schedule:run` | `./bin/acme schedule:run` |
|
||||
| `summer route:list` | `./bin/acme route:list` |
|
||||
| `summer key:generate` | `./bin/acme key:generate` |
|
||||
| `summer admin:create` | `./bin/acme admin:create` |
|
||||
| `summer admin:reset-password` | `./bin/acme admin:reset-password` |
|
||||
|
||||
`summer` does not rebuild an existing binary before it delegates. Run `summer build` after you change code, or keep `summer dev` running. Every other runtime command, such as `route:list`, `key:generate` or `admin:create`, is run on the binary directly.
|
||||
`summer` parses no flags of these commands: every flag and argument, including `--help`, goes to the binary unchanged, and the binary's input is your terminal, so the password prompts of `admin:create` and `admin:reset-password` work as usual:
|
||||
|
||||
```sh
|
||||
summer serve --addr 127.0.0.1:8123
|
||||
summer serve --help
|
||||
summer migrate:rollback --plugin acme.blog
|
||||
summer admin:create --email admin@example.com --superuser
|
||||
```
|
||||
|
||||
One leading `--` is ignored, so `summer serve -- --addr 127.0.0.1:8123` behaves the same as `summer serve --addr 127.0.0.1:8123`.
|
||||
|
||||
`summer` does not rebuild an existing binary before it delegates. Run `summer build` after you change code, or keep `summer dev` running. Every other runtime command, such as `deferred:purge` or the commands your plugins add, is run on the binary directly.
|
||||
|
||||
## The sections of this chapter
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ Copy the printed value into `SUMMER_APP__KEY`. When you rotate the key, keep the
|
||||
|
||||
The default `--addr` listens on every interface. Pass a loopback address during development, and put a reverse proxy in front of the binary in production. The routing options are in [surf](../../modules/surf/README.md).
|
||||
|
||||
From the application directory, `summer serve --addr 127.0.0.1:8123` passes the flag through to the built binary.
|
||||
|
||||
## Admin accounts
|
||||
|
||||
| Command | Arguments and flags | Purpose |
|
||||
@@ -79,6 +81,8 @@ The default `--addr` listens on every interface. Pass a loopback address during
|
||||
|
||||
Both commands prompt for the password without echoing it. In a script, pipe it on stdin, for example `printf '%s\n' "$ADMIN_PASSWORD" | ./bin/acme admin:create --email admin@example.com --superuser`. The `--password` flag still works but is deprecated and prints a warning, because the value stays in your shell history and is visible in the process list. The admin is described in [cabana](../../modules/cabana/README.md).
|
||||
|
||||
From the application directory, `summer admin:create --email admin@example.com --superuser` runs the same command on the built binary, prompt included.
|
||||
|
||||
## Queues and the scheduler
|
||||
|
||||
| Command | Arguments and flags | Purpose |
|
||||
|
||||
Reference in New Issue
Block a user