From d5f60e7e05714bb5770ac928080d0689ad311db1 Mon Sep 17 00:00:00 2001 From: ryank90 Date: Tue, 22 Sep 2026 16:44:03 +0100 Subject: [PATCH 1/2] fix(serverless): scope apps workers to a version The default page is the active version, which is empty when nothing is pinned. --version all or a version id is the way to see the rest, and the next-page cursor has to replay that scope. --- docs/runware_serverless_apps_workers.md | 21 ++++++++++++----- internal/cmd/serverless/apps.go | 30 +++++++++++++++++-------- internal/cmd/serverless/display_test.go | 11 +++++---- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/docs/runware_serverless_apps_workers.md b/docs/runware_serverless_apps_workers.md index 99da7cc..6643510 100644 --- a/docs/runware_serverless_apps_workers.md +++ b/docs/runware_serverless_apps_workers.md @@ -6,6 +6,11 @@ List and inspect workers for a serverless application List workers observed for an application. +The default page is the active version only. An application with no active +version therefore answers an empty default page: it has no pinned version, not +that it has no workers. Pass --version all to include every version, or +--version to scope to one. + The default state is all: terminal stopped rows stay in the page until they are purged. Pass --state live to drop them. --state live with --status stopped is refused by the API (422), because an empty page would read as "this app @@ -18,9 +23,12 @@ runware serverless apps workers [flags] ### Examples ``` - # list workers for an application + # list workers for the active version runware serverless apps workers my-app + # include workers from every version + runware serverless apps workers my-app --version all + # omit terminal stopped rows runware serverless apps workers my-app --state live @@ -34,11 +42,12 @@ runware serverless apps workers [flags] ### Options ``` - --cursor string Pagination cursor from a previous nextCursor - -h, --help help for workers - --limit int Maximum number of workers to return (1-100) - --state string Include stopped rows (all, the API default) or drop them (live) - --status string Filter by status (ready, busy, pending, …) + --cursor string Pagination cursor from a previous nextCursor + -h, --help help for workers + --limit int Maximum number of workers to return (1-100) + --state string Include stopped rows (all, the API default) or drop them (live) + --status string Filter by status (ready, busy, pending, …) + --version string Scope to a version ID, or all (default: the active version) ``` ### Options inherited from parent commands diff --git a/internal/cmd/serverless/apps.go b/internal/cmd/serverless/apps.go index 54bc398..d164542 100644 --- a/internal/cmd/serverless/apps.go +++ b/internal/cmd/serverless/apps.go @@ -287,10 +287,11 @@ for worker output.`, func newAppsWorkersCmd(logger *log.Logger) *cobra.Command { var ( - limit int - cursor string - status string - state string + limit int + cursor string + status string + state string + version string ) cmd := &cobra.Command{ @@ -298,13 +299,21 @@ func newAppsWorkersCmd(logger *log.Logger) *cobra.Command { Short: "List and inspect workers for a serverless application", Long: `List workers observed for an application. +The default page is the active version only. An application with no active +version therefore answers an empty default page: it has no pinned version, not +that it has no workers. Pass --version all to include every version, or +--version to scope to one. + The default state is all: terminal stopped rows stay in the page until they are purged. Pass --state live to drop them. --state live with --status stopped is refused by the API (422), because an empty page would read as "this app has never run".`, - Example: ` # list workers for an application + Example: ` # list workers for the active version runware serverless apps workers my-app + # include workers from every version + runware serverless apps workers my-app --version all + # omit terminal stopped rows runware serverless apps workers my-app --state live @@ -328,11 +337,12 @@ has never run".`, return err } var params *serverlessapi.ListWorkersParams - if limit > 0 || cursor != "" || status != "" || state != "" { + if limit > 0 || cursor != "" || status != "" || state != "" || version != "" { params = &serverlessapi.ListWorkersParams{} params.Limit, params.Cursor = listPageParams(limit, cursor) params.Status = statusVal params.State = stateVal + params.VersionId = optionalStringPtr(version) } spin := cmdutil.NewSpinner(fmt.Sprintf("Fetching workers for %s...", id)) @@ -346,7 +356,7 @@ has never run".`, } spin.Stop() - return printPage(cmdutil.FormatFor(cmd), page, workersResult(page.Data), cmd.ErrOrStderr(), extraWorkersCursorFlags(state, status)) + return printPage(cmdutil.FormatFor(cmd), page, workersResult(page.Data), cmd.ErrOrStderr(), extraWorkersCursorFlags(state, status, version)) }, } @@ -354,6 +364,7 @@ has never run".`, cmd.Flags().StringVar(&cursor, "cursor", "", "Pagination cursor from a previous nextCursor") cmd.Flags().StringVar(&status, "status", "", "Filter by status (ready, busy, pending, …)") cmd.Flags().StringVar(&state, "state", "", "Include stopped rows (all, the API default) or drop them (live)") + cmd.Flags().StringVar(&version, "version", "", "Scope to a version ID, or all (default: the active version)") cmd.AddCommand(newAppsWorkersShowCmd(logger)) return cmd } @@ -455,9 +466,10 @@ func extraStatusCursorFlag(value string) string { } // extraWorkersCursorFlags repeats workers list filters a next-page --cursor is bound to. -func extraWorkersCursorFlags(state, status string) string { +func extraWorkersCursorFlags(state, status, version string) string { parts := appendFlag(nil, "--state", state) - return strings.Join(appendFlag(parts, "--status", status), " ") + parts = appendFlag(parts, "--status", status) + return strings.Join(appendFlag(parts, "--version", version), " ") } func extraTypeCursorFlag(value string) string { diff --git a/internal/cmd/serverless/display_test.go b/internal/cmd/serverless/display_test.go index 1d2c57e..dd1b1ce 100644 --- a/internal/cmd/serverless/display_test.go +++ b/internal/cmd/serverless/display_test.go @@ -188,16 +188,19 @@ func TestExtraStatusCursorFlag(t *testing.T) { } func TestExtraWorkersCursorFlags(t *testing.T) { - if got := extraWorkersCursorFlags("live", "ready"); got != "--state live --status ready" { + if got := extraWorkersCursorFlags("live", "ready", ""); got != "--state live --status ready" { t.Fatalf("both: got %q", got) } - if got := extraWorkersCursorFlags("live", ""); got != "--state live" { + if got := extraWorkersCursorFlags("live", "", ""); got != "--state live" { t.Fatalf("state only: got %q", got) } - if got := extraWorkersCursorFlags("", "ready"); got != "--status ready" { + if got := extraWorkersCursorFlags("", "ready", ""); got != "--status ready" { t.Fatalf("status only: got %q", got) } - if got := extraWorkersCursorFlags("", ""); got != "" { + if got := extraWorkersCursorFlags("", "", "all"); got != "--version all" { + t.Fatalf("version only: got %q", got) + } + if got := extraWorkersCursorFlags("", "", ""); got != "" { t.Fatalf("empty: got %q", got) } } From 461e68e0c25d7448954d0360b3d4df7677fa5edb Mon Sep 17 00:00:00 2001 From: ryank90 Date: Tue, 22 Sep 2026 20:29:23 +0100 Subject: [PATCH 2/2] fix(serverless): validate apps workers --version locally A typo now fails as a usage error instead of a 4xx. --version accepts all (any case) or a version UUID. --- internal/cmd/serverless/apps.go | 25 ++++++++++++++++++++++--- internal/cmd/serverless/display_test.go | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/internal/cmd/serverless/apps.go b/internal/cmd/serverless/apps.go index d164542..d117990 100644 --- a/internal/cmd/serverless/apps.go +++ b/internal/cmd/serverless/apps.go @@ -336,13 +336,17 @@ has never run".`, if err != nil { return err } + versionVal, err := parseWorkersVersion(version) + if err != nil { + return err + } var params *serverlessapi.ListWorkersParams - if limit > 0 || cursor != "" || status != "" || state != "" || version != "" { + if limit > 0 || cursor != "" || status != "" || state != "" || versionVal != "" { params = &serverlessapi.ListWorkersParams{} params.Limit, params.Cursor = listPageParams(limit, cursor) params.Status = statusVal params.State = stateVal - params.VersionId = optionalStringPtr(version) + params.VersionId = optionalStringPtr(versionVal) } spin := cmdutil.NewSpinner(fmt.Sprintf("Fetching workers for %s...", id)) @@ -356,7 +360,7 @@ has never run".`, } spin.Stop() - return printPage(cmdutil.FormatFor(cmd), page, workersResult(page.Data), cmd.ErrOrStderr(), extraWorkersCursorFlags(state, status, version)) + return printPage(cmdutil.FormatFor(cmd), page, workersResult(page.Data), cmd.ErrOrStderr(), extraWorkersCursorFlags(state, status, versionVal)) }, } @@ -465,6 +469,21 @@ func extraStatusCursorFlag(value string) string { return strings.Join(appendFlag(nil, "--status", value), " ") } +// parseWorkersVersion accepts "all" (any case) or a version UUID. A typo +// fails here instead of as a 4xx from the API. +func parseWorkersVersion(version string) (string, error) { + if version == "" { + return "", nil + } + if strings.EqualFold(version, "all") { + return "all", nil + } + if _, err := uuid.Parse(version); err != nil { + return "", fmt.Errorf("--version must be a version ID or all") + } + return version, nil +} + // extraWorkersCursorFlags repeats workers list filters a next-page --cursor is bound to. func extraWorkersCursorFlags(state, status, version string) string { parts := appendFlag(nil, "--state", state) diff --git a/internal/cmd/serverless/display_test.go b/internal/cmd/serverless/display_test.go index dd1b1ce..9711cc6 100644 --- a/internal/cmd/serverless/display_test.go +++ b/internal/cmd/serverless/display_test.go @@ -200,11 +200,34 @@ func TestExtraWorkersCursorFlags(t *testing.T) { if got := extraWorkersCursorFlags("", "", "all"); got != "--version all" { t.Fatalf("version only: got %q", got) } + if got := extraWorkersCursorFlags("live", "ready", "all"); got != "--state live --status ready --version all" { + t.Fatalf("all three: got %q", got) + } if got := extraWorkersCursorFlags("", "", ""); got != "" { t.Fatalf("empty: got %q", got) } } +func TestParseWorkersVersion(t *testing.T) { + got, err := parseWorkersVersion("") + if err != nil || got != "" { + t.Fatalf("empty: got=%q err=%v", got, err) + } + got, err = parseWorkersVersion("ALL") + if err != nil || got != "all" { + t.Fatalf("all: got=%q err=%v", got, err) + } + id := "22222222-2222-2222-2222-222222222222" + got, err = parseWorkersVersion(id) + if err != nil || got != id { + t.Fatalf("uuid: got=%q err=%v", got, err) + } + _, err = parseWorkersVersion("nope") + if err == nil || !strings.Contains(err.Error(), "version ID or all") { + t.Fatalf("typo: %v", err) + } +} + func TestParseAppEventType(t *testing.T) { got, err := parseAppEventType("") if err != nil || got != nil {