diff --git a/cmd/hub/get.go b/cmd/hub/get.go new file mode 100644 index 0000000..7a6781a --- /dev/null +++ b/cmd/hub/get.go @@ -0,0 +1,53 @@ +package hub + +import ( + "fmt" + "strings" + + "github.com/runpod/runpodctl/internal/api" + "github.com/runpod/runpodctl/internal/output" + + "github.com/spf13/cobra" +) + +var getCmd = &cobra.Command{ + Use: "get ", + Short: "get hub repo details", + Long: `get details for a hub repo by id or owner/name. + +examples: + runpodctl hub get clma1kziv00064iog9u6acj6z # by listing id + runpodctl hub get runpod/worker-vllm # by owner/name`, + Args: cobra.ExactArgs(1), + RunE: runGet, +} + +func runGet(cmd *cobra.Command, args []string) error { + arg := args[0] + + client, err := api.NewClient() + if err != nil { + output.Error(err) + return err + } + + var listing *api.Listing + + if strings.Contains(arg, "/") { + parts := strings.SplitN(arg, "/", 2) + if parts[0] == "" || parts[1] == "" { + return fmt.Errorf("invalid format %q; use owner/name or a listing id", arg) + } + listing, err = client.GetListingFromRepo(parts[0], parts[1]) + } else { + listing, err = client.GetListing(arg) + } + + if err != nil { + output.Error(err) + return fmt.Errorf("failed to get hub repo: %w", err) + } + + format := output.ParseFormat(cmd.Flag("output").Value.String()) + return output.Print(listing, &output.Config{Format: format}) +} diff --git a/cmd/hub/hub.go b/cmd/hub/hub.go new file mode 100644 index 0000000..6e07bbb --- /dev/null +++ b/cmd/hub/hub.go @@ -0,0 +1,18 @@ +package hub + +import ( + "github.com/spf13/cobra" +) + +// Cmd is the hub command group +var Cmd = &cobra.Command{ + Use: "hub", + Short: "browse the runpod hub", + Long: "browse and search the runpod hub for deployable repos", +} + +func init() { + Cmd.AddCommand(listCmd) + Cmd.AddCommand(searchCmd) + Cmd.AddCommand(getCmd) +} diff --git a/cmd/hub/hub_test.go b/cmd/hub/hub_test.go new file mode 100644 index 0000000..607e4db --- /dev/null +++ b/cmd/hub/hub_test.go @@ -0,0 +1,25 @@ +package hub + +import ( + "testing" +) + +func TestHubCmd_Structure(t *testing.T) { + if Cmd.Use != "hub" { + t.Errorf("expected use 'hub', got %s", Cmd.Use) + } + + expectedSubcommands := []string{"list", "search ", "get "} + for _, expected := range expectedSubcommands { + found := false + for _, cmd := range Cmd.Commands() { + if cmd.Use == expected { + found = true + break + } + } + if !found { + t.Errorf("expected subcommand %q not found", expected) + } + } +} diff --git a/cmd/hub/list.go b/cmd/hub/list.go new file mode 100644 index 0000000..0be7d51 --- /dev/null +++ b/cmd/hub/list.go @@ -0,0 +1,73 @@ +package hub + +import ( + "github.com/runpod/runpodctl/internal/api" + "github.com/runpod/runpodctl/internal/output" + + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "list hub repos", + Long: `list repos from the runpod hub. + +by default shows the top 10 repos ordered by stars. + +examples: + runpodctl hub list # top 10 by stars + runpodctl hub list --type SERVERLESS # only serverless repos + runpodctl hub list --type POD # only pod repos + runpodctl hub list --category ai --limit 20 # filter by category + runpodctl hub list --order-by deploys # order by deploys + runpodctl hub list --owner runpod # filter by repo owner`, + Args: cobra.NoArgs, + RunE: runList, +} + +var ( + listCategory string + listOrderBy string + listOrderDir string + listLimit int + listOffset int + listOwner string + listType string +) + +func init() { + listCmd.Flags().StringVar(&listCategory, "category", "", "filter by category") + listCmd.Flags().StringVar(&listOrderBy, "order-by", "stars", "order by: createdAt, deploys, releasedAt, stars, updatedAt, views") + listCmd.Flags().StringVar(&listOrderDir, "order-dir", "desc", "order direction: asc or desc") + listCmd.Flags().IntVar(&listLimit, "limit", 10, "max number of results to return") + listCmd.Flags().IntVar(&listOffset, "offset", 0, "offset for pagination") + listCmd.Flags().StringVar(&listOwner, "owner", "", "filter by repo owner") + listCmd.Flags().StringVar(&listType, "type", "", "filter by type: POD or SERVERLESS") +} + +func runList(cmd *cobra.Command, args []string) error { + client, err := api.NewClient() + if err != nil { + output.Error(err) + return err + } + + opts := &api.ListingsOptions{ + Category: listCategory, + OrderBy: listOrderBy, + OrderDirection: listOrderDir, + Limit: listLimit, + Offset: listOffset, + Owner: listOwner, + Type: listType, + } + + listings, err := client.ListListings(opts) + if err != nil { + output.Error(err) + return err + } + + format := output.ParseFormat(cmd.Flag("output").Value.String()) + return output.Print(listings, &output.Config{Format: format}) +} diff --git a/cmd/hub/search.go b/cmd/hub/search.go new file mode 100644 index 0000000..e955418 --- /dev/null +++ b/cmd/hub/search.go @@ -0,0 +1,78 @@ +package hub + +import ( + "fmt" + + "github.com/runpod/runpodctl/internal/api" + "github.com/runpod/runpodctl/internal/output" + + "github.com/spf13/cobra" +) + +var searchCmd = &cobra.Command{ + Use: "search ", + Short: "search hub repos", + Long: `search for repos in the runpod hub. + +examples: + runpodctl hub search vllm # search for "vllm" + runpodctl hub search whisper --type SERVERLESS # search serverless repos + runpodctl hub search stable-diffusion --limit 5 # limit results`, + Args: cobra.ExactArgs(1), + RunE: runSearch, +} + +var ( + searchCategory string + searchOrderBy string + searchOrderDir string + searchLimit int + searchOffset int + searchOwner string + searchType string +) + +func init() { + searchCmd.Flags().StringVar(&searchCategory, "category", "", "filter by category") + searchCmd.Flags().StringVar(&searchOrderBy, "order-by", "stars", "order by: createdAt, deploys, releasedAt, stars, updatedAt, views") + searchCmd.Flags().StringVar(&searchOrderDir, "order-dir", "desc", "order direction: asc or desc") + searchCmd.Flags().IntVar(&searchLimit, "limit", 10, "max number of results to return") + searchCmd.Flags().IntVar(&searchOffset, "offset", 0, "offset for pagination") + searchCmd.Flags().StringVar(&searchOwner, "owner", "", "filter by repo owner") + searchCmd.Flags().StringVar(&searchType, "type", "", "filter by type: POD or SERVERLESS") +} + +func runSearch(cmd *cobra.Command, args []string) error { + searchTerm := args[0] + + client, err := api.NewClient() + if err != nil { + output.Error(err) + return err + } + + opts := &api.ListingsOptions{ + SearchQuery: searchTerm, + Category: searchCategory, + OrderBy: searchOrderBy, + OrderDirection: searchOrderDir, + Limit: searchLimit, + Offset: searchOffset, + Owner: searchOwner, + Type: searchType, + } + + listings, err := client.ListListings(opts) + if err != nil { + output.Error(err) + return err + } + + if len(listings) == 0 { + fmt.Printf("no hub repos found matching %q\n", searchTerm) + return nil + } + + format := output.ParseFormat(cmd.Flag("output").Value.String()) + return output.Print(listings, &output.Config{Format: format}) +} diff --git a/cmd/root.go b/cmd/root.go index adbd9f0..205b8ef 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/runpod/runpodctl/cmd/datacenter" "github.com/runpod/runpodctl/cmd/doctor" "github.com/runpod/runpodctl/cmd/gpu" + "github.com/runpod/runpodctl/cmd/hub" "github.com/runpod/runpodctl/cmd/legacy" "github.com/runpod/runpodctl/cmd/model" "github.com/runpod/runpodctl/cmd/pod" @@ -43,6 +44,7 @@ resources: pod manage gpu pods serverless manage serverless endpoints (alias: sls) template manage templates (alias: tpl) + hub browse the runpod hub model manage model repository network-volume manage network volumes (alias: nv) registry manage container registry auth (alias: reg) @@ -87,6 +89,7 @@ func registerCommands() { rootCmd.AddCommand(model.Cmd) rootCmd.AddCommand(volume.Cmd) rootCmd.AddCommand(registry.Cmd) + rootCmd.AddCommand(hub.Cmd) // Info commands rootCmd.AddCommand(user.Cmd) diff --git a/cmd/serverless/create.go b/cmd/serverless/create.go index f94e017..d06e57c 100644 --- a/cmd/serverless/create.go +++ b/cmd/serverless/create.go @@ -1,7 +1,9 @@ package serverless import ( + "encoding/json" "fmt" + "math/rand" "strings" "github.com/runpod/runpodctl/internal/api" @@ -13,26 +15,38 @@ import ( var createCmd = &cobra.Command{ Use: "create", Short: "create a new endpoint", - Long: "create a new serverless endpoint", + Long: `create a new serverless endpoint. + +requires either --template-id or --hub-id. + +examples: + # create from a template + runpodctl serverless create --template-id --gpu-id "NVIDIA GeForce RTX 4090" + + # create from a hub repo + runpodctl hub search vllm # find the hub id + runpodctl serverless create --hub-id --gpu-id "NVIDIA GeForce RTX 4090"`, Args: cobra.NoArgs, RunE: runCreate, } var ( - createName string - createTemplateID string - createComputeType string - createGpuTypeID string - createGpuCount int - createWorkersMin int - createWorkersMax int + createName string + createTemplateID string + createHubID string + createComputeType string + createGpuTypeID string + createGpuCount int + createWorkersMin int + createWorkersMax int createDataCenterIDs string createNetworkVolumeID string ) func init() { createCmd.Flags().StringVar(&createName, "name", "", "endpoint name") - createCmd.Flags().StringVar(&createTemplateID, "template-id", "", "template id (required)") + createCmd.Flags().StringVar(&createTemplateID, "template-id", "", "template id (required if no --hub-id)") + createCmd.Flags().StringVar(&createHubID, "hub-id", "", "hub listing id (alternative to --template-id)") createCmd.Flags().StringVar(&createComputeType, "compute-type", "GPU", "compute type (GPU or CPU)") createCmd.Flags().StringVar(&createGpuTypeID, "gpu-id", "", "gpu id (from 'runpodctl gpu list')") createCmd.Flags().IntVar(&createGpuCount, "gpu-count", 1, "number of gpus per worker") @@ -41,10 +55,16 @@ func init() { createCmd.Flags().StringVar(&createDataCenterIDs, "data-center-ids", "", "comma-separated list of data center ids") createCmd.Flags().StringVar(&createNetworkVolumeID, "network-volume-id", "", "network volume id to attach") - createCmd.MarkFlagRequired("template-id") //nolint:errcheck } func runCreate(cmd *cobra.Command, args []string) error { + if createTemplateID == "" && createHubID == "" { + return fmt.Errorf("either --template-id or --hub-id is required\n\nuse 'runpodctl hub search ' to find hub repos\nuse 'runpodctl template search ' to find templates") + } + if createTemplateID != "" && createHubID != "" { + return fmt.Errorf("--template-id and --hub-id are mutually exclusive; use one or the other") + } + client, err := api.NewClient() if err != nil { output.Error(err) @@ -64,6 +84,86 @@ func runCreate(cmd *cobra.Command, args []string) error { if strings.Contains(gpuTypeID, ",") { return fmt.Errorf("only one gpu id is supported; use --gpu-count for multiple gpus of the same type") } + + // hub-id path: resolve listing, create via graphql (REST api doesn't support hubReleaseId) + if createHubID != "" { + listing, err := client.GetListing(createHubID) + if err != nil { + output.Error(err) + return fmt.Errorf("failed to get hub listing: %w", err) + } + if listing.ListedRelease == nil { + return fmt.Errorf("hub listing %q has no published release", createHubID) + } + + release := listing.ListedRelease + + // build inline template from the hub release (same as web ui) + var imageName string + if release.Build != nil { + imageName = release.Build.ImageName + } + if imageName == "" { + return fmt.Errorf("hub listing %q has no built image; the release may still be building", createHubID) + } + + containerDisk := 10 + var hubConfig api.HubReleaseConfig + if release.Config != "" { + if err := json.Unmarshal([]byte(release.Config), &hubConfig); err == nil { + if hubConfig.ContainerDiskInGb > 0 { + containerDisk = hubConfig.ContainerDiskInGb + } + } + } + + endpointName := createName + if endpointName == "" { + endpointName = listing.Title + } + + //nolint:gosec + templateName := fmt.Sprintf("%s__template__%s", endpointName, randomString(7)) + + gqlReq := &api.EndpointCreateGQLInput{ + Name: endpointName, + HubReleaseID: release.ID, + Template: &api.EndpointTemplateInput{ + Name: templateName, + ImageName: imageName, + ContainerDiskInGb: containerDisk, + DockerArgs: "", + Env: []*api.PodEnvVar{}, + }, + GpuCount: createGpuCount, + WorkersMin: createWorkersMin, + WorkersMax: createWorkersMax, + } + + // use gpu ids from hub config if not explicitly provided + if gpuTypeID != "" { + gqlReq.GpuIDs = gpuTypeID + } else if hubConfig.GpuIDs != "" { + gqlReq.GpuIDs = hubConfig.GpuIDs + } + if createNetworkVolumeID != "" { + gqlReq.NetworkVolumeID = createNetworkVolumeID + } + if createDataCenterIDs != "" { + gqlReq.Locations = createDataCenterIDs + } + + endpoint, err := client.CreateEndpointGQL(gqlReq) + if err != nil { + output.Error(err) + return fmt.Errorf("failed to create endpoint: %w", err) + } + + format := output.ParseFormat(cmd.Flag("output").Value.String()) + return output.Print(endpoint, &output.Config{Format: format}) + } + + // template-id path: create via REST if gpuTypeID != "" { req.GpuTypeIDs = []string{gpuTypeID} } @@ -85,3 +185,12 @@ func runCreate(cmd *cobra.Command, args []string) error { format := output.ParseFormat(cmd.Flag("output").Value.String()) return output.Print(endpoint, &output.Config{Format: format}) } + +func randomString(n int) string { + const letters = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] //nolint:gosec + } + return string(b) +} diff --git a/cmd/template/create.go b/cmd/template/create.go index 2251b64..9c24c72 100644 --- a/cmd/template/create.go +++ b/cmd/template/create.go @@ -62,11 +62,15 @@ func runCreate(cmd *cobra.Command, args []string) error { ImageName: createImageName, IsServerless: createIsServerless, ContainerDiskInGb: createContainerDiskInGb, - VolumeInGb: createVolumeInGb, - VolumeMountPath: createVolumeMountPath, Readme: createReadme, } + // serverless templates do not support volume fields + if !createIsServerless { + req.VolumeInGb = createVolumeInGb + req.VolumeMountPath = createVolumeMountPath + } + if createPorts != "" { req.Ports = strings.Split(createPorts, ",") } diff --git a/docs/runpodctl.md b/docs/runpodctl.md index 0b8218c..cd42c18 100644 --- a/docs/runpodctl.md +++ b/docs/runpodctl.md @@ -15,6 +15,7 @@ resources: pod manage gpu pods serverless manage serverless endpoints (alias: sls) template manage templates (alias: tpl) + hub browse the runpod hub model manage model repository network-volume manage network volumes (alias: nv) registry manage container registry auth (alias: reg) @@ -48,6 +49,7 @@ deprecated * [runpodctl datacenter](runpodctl_datacenter.md) - list datacenters * [runpodctl doctor](runpodctl_doctor.md) - diagnose and fix cli issues * [runpodctl gpu](runpodctl_gpu.md) - list available gpu types +* [runpodctl hub](runpodctl_hub.md) - browse the runpod hub * [runpodctl model](runpodctl_model.md) - manage model repository * [runpodctl network-volume](runpodctl_network-volume.md) - manage network volumes * [runpodctl pod](runpodctl_pod.md) - manage gpu pods @@ -61,4 +63,4 @@ deprecated * [runpodctl user](runpodctl_user.md) - show account info * [runpodctl version](runpodctl_version.md) - print the version -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_billing.md b/docs/runpodctl_billing.md index 02e12c6..641d158 100644 --- a/docs/runpodctl_billing.md +++ b/docs/runpodctl_billing.md @@ -25,4 +25,4 @@ view billing history for pods, serverless, and network volumes * [runpodctl billing pods](runpodctl_billing_pods.md) - view pod billing history * [runpodctl billing serverless](runpodctl_billing_serverless.md) - view serverless billing history -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_billing_network-volume.md b/docs/runpodctl_billing_network-volume.md index 942bb01..d667c2d 100644 --- a/docs/runpodctl_billing_network-volume.md +++ b/docs/runpodctl_billing_network-volume.md @@ -29,4 +29,4 @@ runpodctl billing network-volume [flags] * [runpodctl billing](runpodctl_billing.md) - view billing history -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_billing_pods.md b/docs/runpodctl_billing_pods.md index fe79d4e..c0121e8 100644 --- a/docs/runpodctl_billing_pods.md +++ b/docs/runpodctl_billing_pods.md @@ -32,4 +32,4 @@ runpodctl billing pods [flags] * [runpodctl billing](runpodctl_billing.md) - view billing history -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_billing_serverless.md b/docs/runpodctl_billing_serverless.md index 7f30b17..317f023 100644 --- a/docs/runpodctl_billing_serverless.md +++ b/docs/runpodctl_billing_serverless.md @@ -32,4 +32,4 @@ runpodctl billing serverless [flags] * [runpodctl billing](runpodctl_billing.md) - view billing history -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_completion.md b/docs/runpodctl_completion.md index cf98b61..971895f 100644 --- a/docs/runpodctl_completion.md +++ b/docs/runpodctl_completion.md @@ -26,4 +26,4 @@ runpodctl completion [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_datacenter.md b/docs/runpodctl_datacenter.md index 9f9799a..62150cb 100644 --- a/docs/runpodctl_datacenter.md +++ b/docs/runpodctl_datacenter.md @@ -23,4 +23,4 @@ list datacenters and their gpu availability * [runpodctl](runpodctl.md) - cli for runpod.io * [runpodctl datacenter list](runpodctl_datacenter_list.md) - list all datacenters -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_datacenter_list.md b/docs/runpodctl_datacenter_list.md index f7dc2be..daa1762 100644 --- a/docs/runpodctl_datacenter_list.md +++ b/docs/runpodctl_datacenter_list.md @@ -26,4 +26,4 @@ runpodctl datacenter list [flags] * [runpodctl datacenter](runpodctl_datacenter.md) - list datacenters -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_doctor.md b/docs/runpodctl_doctor.md index 5bd3444..f6a76d9 100644 --- a/docs/runpodctl_doctor.md +++ b/docs/runpodctl_doctor.md @@ -26,4 +26,4 @@ runpodctl doctor [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_gpu.md b/docs/runpodctl_gpu.md index cf23762..59bb590 100644 --- a/docs/runpodctl_gpu.md +++ b/docs/runpodctl_gpu.md @@ -23,4 +23,4 @@ list available gpu types and their availability * [runpodctl](runpodctl.md) - cli for runpod.io * [runpodctl gpu list](runpodctl_gpu_list.md) - list available gpu types -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_gpu_list.md b/docs/runpodctl_gpu_list.md index 43e309d..e9412d5 100644 --- a/docs/runpodctl_gpu_list.md +++ b/docs/runpodctl_gpu_list.md @@ -27,4 +27,4 @@ runpodctl gpu list [flags] * [runpodctl gpu](runpodctl_gpu.md) - list available gpu types -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_hub.md b/docs/runpodctl_hub.md new file mode 100644 index 0000000..4ceec3b --- /dev/null +++ b/docs/runpodctl_hub.md @@ -0,0 +1,28 @@ +## runpodctl hub + +browse the runpod hub + +### Synopsis + +browse and search the runpod hub for deployable repos + +### Options + +``` + -h, --help help for hub +``` + +### Options inherited from parent commands + +``` + -o, --output string output format (json, yaml) (default "json") +``` + +### SEE ALSO + +* [runpodctl](runpodctl.md) - cli for runpod.io +* [runpodctl hub get](runpodctl_hub_get.md) - get hub repo details +* [runpodctl hub list](runpodctl_hub_list.md) - list hub repos +* [runpodctl hub search](runpodctl_hub_search.md) - search hub repos + +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_hub_get.md b/docs/runpodctl_hub_get.md new file mode 100644 index 0000000..810fd4b --- /dev/null +++ b/docs/runpodctl_hub_get.md @@ -0,0 +1,33 @@ +## runpodctl hub get + +get hub repo details + +### Synopsis + +get details for a hub repo by id or owner/name. + +examples: + runpodctl hub get clma1kziv00064iog9u6acj6z # by listing id + runpodctl hub get runpod/worker-vllm # by owner/name + +``` +runpodctl hub get [flags] +``` + +### Options + +``` + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + -o, --output string output format (json, yaml) (default "json") +``` + +### SEE ALSO + +* [runpodctl hub](runpodctl_hub.md) - browse the runpod hub + +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_hub_list.md b/docs/runpodctl_hub_list.md new file mode 100644 index 0000000..c9677d9 --- /dev/null +++ b/docs/runpodctl_hub_list.md @@ -0,0 +1,46 @@ +## runpodctl hub list + +list hub repos + +### Synopsis + +list repos from the runpod hub. + +by default shows the top 10 repos ordered by stars. + +examples: + runpodctl hub list # top 10 by stars + runpodctl hub list --type SERVERLESS # only serverless repos + runpodctl hub list --type POD # only pod repos + runpodctl hub list --category ai --limit 20 # filter by category + runpodctl hub list --order-by deploys # order by deploys + runpodctl hub list --owner runpod # filter by repo owner + +``` +runpodctl hub list [flags] +``` + +### Options + +``` + --category string filter by category + -h, --help help for list + --limit int max number of results to return (default 10) + --offset int offset for pagination + --order-by string order by: createdAt, deploys, releasedAt, stars, updatedAt, views (default "stars") + --order-dir string order direction: asc or desc (default "desc") + --owner string filter by repo owner + --type string filter by type: POD or SERVERLESS +``` + +### Options inherited from parent commands + +``` + -o, --output string output format (json, yaml) (default "json") +``` + +### SEE ALSO + +* [runpodctl hub](runpodctl_hub.md) - browse the runpod hub + +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_hub_search.md b/docs/runpodctl_hub_search.md new file mode 100644 index 0000000..c75a05b --- /dev/null +++ b/docs/runpodctl_hub_search.md @@ -0,0 +1,41 @@ +## runpodctl hub search + +search hub repos + +### Synopsis + +search for repos in the runpod hub. + +examples: + runpodctl hub search vllm # search for "vllm" + runpodctl hub search whisper --type SERVERLESS # search serverless repos + runpodctl hub search stable-diffusion --limit 5 # limit results + +``` +runpodctl hub search [flags] +``` + +### Options + +``` + --category string filter by category + -h, --help help for search + --limit int max number of results to return (default 10) + --offset int offset for pagination + --order-by string order by: createdAt, deploys, releasedAt, stars, updatedAt, views (default "stars") + --order-dir string order direction: asc or desc (default "desc") + --owner string filter by repo owner + --type string filter by type: POD or SERVERLESS +``` + +### Options inherited from parent commands + +``` + -o, --output string output format (json, yaml) (default "json") +``` + +### SEE ALSO + +* [runpodctl hub](runpodctl_hub.md) - browse the runpod hub + +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_model.md b/docs/runpodctl_model.md index f36b065..12c2593 100644 --- a/docs/runpodctl_model.md +++ b/docs/runpodctl_model.md @@ -25,4 +25,4 @@ manage models in the runpod model repository * [runpodctl model list](runpodctl_model_list.md) - list models * [runpodctl model remove](runpodctl_model_remove.md) - remove a model -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_model_add.md b/docs/runpodctl_model_add.md index 875d64e..23173a6 100644 --- a/docs/runpodctl_model_add.md +++ b/docs/runpodctl_model_add.md @@ -38,4 +38,4 @@ runpodctl model add [flags] * [runpodctl model](runpodctl_model.md) - manage model repository -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_model_list.md b/docs/runpodctl_model_list.md index d39c680..1052cab 100644 --- a/docs/runpodctl_model_list.md +++ b/docs/runpodctl_model_list.md @@ -29,4 +29,4 @@ runpodctl model list [flags] * [runpodctl model](runpodctl_model.md) - manage model repository -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_model_remove.md b/docs/runpodctl_model_remove.md index 6940cad..96fd755 100644 --- a/docs/runpodctl_model_remove.md +++ b/docs/runpodctl_model_remove.md @@ -28,4 +28,4 @@ runpodctl model remove [flags] * [runpodctl model](runpodctl_model.md) - manage model repository -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_network-volume.md b/docs/runpodctl_network-volume.md index a006894..fe3a646 100644 --- a/docs/runpodctl_network-volume.md +++ b/docs/runpodctl_network-volume.md @@ -27,4 +27,4 @@ manage network volumes on runpod * [runpodctl network-volume list](runpodctl_network-volume_list.md) - list all network volumes * [runpodctl network-volume update](runpodctl_network-volume_update.md) - update a network volume -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_network-volume_create.md b/docs/runpodctl_network-volume_create.md index 9b0ad2e..8eec315 100644 --- a/docs/runpodctl_network-volume_create.md +++ b/docs/runpodctl_network-volume_create.md @@ -29,4 +29,4 @@ runpodctl network-volume create [flags] * [runpodctl network-volume](runpodctl_network-volume.md) - manage network volumes -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_network-volume_delete.md b/docs/runpodctl_network-volume_delete.md index 140dd71..9c00167 100644 --- a/docs/runpodctl_network-volume_delete.md +++ b/docs/runpodctl_network-volume_delete.md @@ -26,4 +26,4 @@ runpodctl network-volume delete [flags] * [runpodctl network-volume](runpodctl_network-volume.md) - manage network volumes -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_network-volume_get.md b/docs/runpodctl_network-volume_get.md index f805941..66e2926 100644 --- a/docs/runpodctl_network-volume_get.md +++ b/docs/runpodctl_network-volume_get.md @@ -26,4 +26,4 @@ runpodctl network-volume get [flags] * [runpodctl network-volume](runpodctl_network-volume.md) - manage network volumes -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_network-volume_list.md b/docs/runpodctl_network-volume_list.md index 63e9dbb..b4992a5 100644 --- a/docs/runpodctl_network-volume_list.md +++ b/docs/runpodctl_network-volume_list.md @@ -26,4 +26,4 @@ runpodctl network-volume list [flags] * [runpodctl network-volume](runpodctl_network-volume.md) - manage network volumes -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_network-volume_update.md b/docs/runpodctl_network-volume_update.md index 923a279..cacf16f 100644 --- a/docs/runpodctl_network-volume_update.md +++ b/docs/runpodctl_network-volume_update.md @@ -28,4 +28,4 @@ runpodctl network-volume update [flags] * [runpodctl network-volume](runpodctl_network-volume.md) - manage network volumes -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod.md b/docs/runpodctl_pod.md index f7f3ab0..6c33696 100644 --- a/docs/runpodctl_pod.md +++ b/docs/runpodctl_pod.md @@ -31,4 +31,4 @@ manage gpu pods on runpod * [runpodctl pod stop](runpodctl_pod_stop.md) - stop a running pod * [runpodctl pod update](runpodctl_pod_update.md) - update an existing pod -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_create.md b/docs/runpodctl_pod_create.md index 21a248c..8086abc 100644 --- a/docs/runpodctl_pod_create.md +++ b/docs/runpodctl_pod_create.md @@ -59,4 +59,4 @@ runpodctl pod create [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_delete.md b/docs/runpodctl_pod_delete.md index 05dd527..1c7a37d 100644 --- a/docs/runpodctl_pod_delete.md +++ b/docs/runpodctl_pod_delete.md @@ -26,4 +26,4 @@ runpodctl pod delete [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_get.md b/docs/runpodctl_pod_get.md index 32cc691..ae3bba3 100644 --- a/docs/runpodctl_pod_get.md +++ b/docs/runpodctl_pod_get.md @@ -28,4 +28,4 @@ runpodctl pod get [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_list.md b/docs/runpodctl_pod_list.md index 8b52a4a..3ebc1b9 100644 --- a/docs/runpodctl_pod_list.md +++ b/docs/runpodctl_pod_list.md @@ -32,4 +32,4 @@ runpodctl pod list [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_reset.md b/docs/runpodctl_pod_reset.md index 7a1432e..c6a44ea 100644 --- a/docs/runpodctl_pod_reset.md +++ b/docs/runpodctl_pod_reset.md @@ -26,4 +26,4 @@ runpodctl pod reset [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_restart.md b/docs/runpodctl_pod_restart.md index 2a6fe17..565e6f8 100644 --- a/docs/runpodctl_pod_restart.md +++ b/docs/runpodctl_pod_restart.md @@ -26,4 +26,4 @@ runpodctl pod restart [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_start.md b/docs/runpodctl_pod_start.md index dbd043f..ec72e42 100644 --- a/docs/runpodctl_pod_start.md +++ b/docs/runpodctl_pod_start.md @@ -26,4 +26,4 @@ runpodctl pod start [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_stop.md b/docs/runpodctl_pod_stop.md index 4ab4f83..00ebe83 100644 --- a/docs/runpodctl_pod_stop.md +++ b/docs/runpodctl_pod_stop.md @@ -26,4 +26,4 @@ runpodctl pod stop [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_pod_update.md b/docs/runpodctl_pod_update.md index 1a2e92d..87ea646 100644 --- a/docs/runpodctl_pod_update.md +++ b/docs/runpodctl_pod_update.md @@ -33,4 +33,4 @@ runpodctl pod update [flags] * [runpodctl pod](runpodctl_pod.md) - manage gpu pods -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_receive.md b/docs/runpodctl_receive.md index 113bb93..76cf7ee 100644 --- a/docs/runpodctl_receive.md +++ b/docs/runpodctl_receive.md @@ -26,4 +26,4 @@ runpodctl receive [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_registry.md b/docs/runpodctl_registry.md index 1c288c1..31a96d2 100644 --- a/docs/runpodctl_registry.md +++ b/docs/runpodctl_registry.md @@ -26,4 +26,4 @@ manage container registry authentication on runpod * [runpodctl registry get](runpodctl_registry_get.md) - get registry auth details * [runpodctl registry list](runpodctl_registry_list.md) - list all registry auths -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_registry_create.md b/docs/runpodctl_registry_create.md index 22fa944..b28027b 100644 --- a/docs/runpodctl_registry_create.md +++ b/docs/runpodctl_registry_create.md @@ -29,4 +29,4 @@ runpodctl registry create [flags] * [runpodctl registry](runpodctl_registry.md) - manage container registry auth -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_registry_delete.md b/docs/runpodctl_registry_delete.md index f8601b9..bf45b92 100644 --- a/docs/runpodctl_registry_delete.md +++ b/docs/runpodctl_registry_delete.md @@ -26,4 +26,4 @@ runpodctl registry delete [flags] * [runpodctl registry](runpodctl_registry.md) - manage container registry auth -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_registry_get.md b/docs/runpodctl_registry_get.md index 1492012..bb2744b 100644 --- a/docs/runpodctl_registry_get.md +++ b/docs/runpodctl_registry_get.md @@ -26,4 +26,4 @@ runpodctl registry get [flags] * [runpodctl registry](runpodctl_registry.md) - manage container registry auth -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_registry_list.md b/docs/runpodctl_registry_list.md index 8aaa07d..83194eb 100644 --- a/docs/runpodctl_registry_list.md +++ b/docs/runpodctl_registry_list.md @@ -26,4 +26,4 @@ runpodctl registry list [flags] * [runpodctl registry](runpodctl_registry.md) - manage container registry auth -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_send.md b/docs/runpodctl_send.md index 70b5c64..18bfa81 100644 --- a/docs/runpodctl_send.md +++ b/docs/runpodctl_send.md @@ -27,4 +27,4 @@ runpodctl send [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_serverless.md b/docs/runpodctl_serverless.md index 427d80f..4dbf928 100644 --- a/docs/runpodctl_serverless.md +++ b/docs/runpodctl_serverless.md @@ -27,4 +27,4 @@ manage serverless endpoints on runpod * [runpodctl serverless list](runpodctl_serverless_list.md) - list all endpoints * [runpodctl serverless update](runpodctl_serverless_update.md) - update an endpoint -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_serverless_create.md b/docs/runpodctl_serverless_create.md index b83f896..ccbf920 100644 --- a/docs/runpodctl_serverless_create.md +++ b/docs/runpodctl_serverless_create.md @@ -4,7 +4,17 @@ create a new endpoint ### Synopsis -create a new serverless endpoint +create a new serverless endpoint. + +requires either --template-id or --hub-id. + +examples: + # create from a template + runpodctl serverless create --template-id --gpu-id "NVIDIA GeForce RTX 4090" + + # create from a hub repo + runpodctl hub search vllm # find the hub id + runpodctl serverless create --hub-id --gpu-id "NVIDIA GeForce RTX 4090" ``` runpodctl serverless create [flags] @@ -18,9 +28,10 @@ runpodctl serverless create [flags] --gpu-count int number of gpus per worker (default 1) --gpu-id string gpu id (from 'runpodctl gpu list') -h, --help help for create + --hub-id string hub listing id (alternative to --template-id) --name string endpoint name --network-volume-id string network volume id to attach - --template-id string template id (required) + --template-id string template id (required if no --hub-id) --workers-max int maximum number of workers (default 3) --workers-min int minimum number of workers ``` @@ -35,4 +46,4 @@ runpodctl serverless create [flags] * [runpodctl serverless](runpodctl_serverless.md) - manage serverless endpoints -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_serverless_delete.md b/docs/runpodctl_serverless_delete.md index 6ca48a7..7d9f6ee 100644 --- a/docs/runpodctl_serverless_delete.md +++ b/docs/runpodctl_serverless_delete.md @@ -26,4 +26,4 @@ runpodctl serverless delete [flags] * [runpodctl serverless](runpodctl_serverless.md) - manage serverless endpoints -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_serverless_get.md b/docs/runpodctl_serverless_get.md index 0f89b22..5531128 100644 --- a/docs/runpodctl_serverless_get.md +++ b/docs/runpodctl_serverless_get.md @@ -28,4 +28,4 @@ runpodctl serverless get [flags] * [runpodctl serverless](runpodctl_serverless.md) - manage serverless endpoints -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_serverless_list.md b/docs/runpodctl_serverless_list.md index bd2725f..28bd671 100644 --- a/docs/runpodctl_serverless_list.md +++ b/docs/runpodctl_serverless_list.md @@ -28,4 +28,4 @@ runpodctl serverless list [flags] * [runpodctl serverless](runpodctl_serverless.md) - manage serverless endpoints -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_serverless_update.md b/docs/runpodctl_serverless_update.md index f5f8b37..00f32e1 100644 --- a/docs/runpodctl_serverless_update.md +++ b/docs/runpodctl_serverless_update.md @@ -32,4 +32,4 @@ runpodctl serverless update [flags] * [runpodctl serverless](runpodctl_serverless.md) - manage serverless endpoints -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_ssh.md b/docs/runpodctl_ssh.md index 1737bfe..72f39db 100644 --- a/docs/runpodctl_ssh.md +++ b/docs/runpodctl_ssh.md @@ -25,4 +25,4 @@ manage ssh keys and show ssh info for pods. uses the api key from RUNPOD_API_KEY * [runpodctl ssh info](runpodctl_ssh_info.md) - show ssh info for a pod * [runpodctl ssh list-keys](runpodctl_ssh_list-keys.md) - list all ssh keys -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_ssh_add-key.md b/docs/runpodctl_ssh_add-key.md index feb3d52..398cb9d 100644 --- a/docs/runpodctl_ssh_add-key.md +++ b/docs/runpodctl_ssh_add-key.md @@ -28,4 +28,4 @@ runpodctl ssh add-key [flags] * [runpodctl ssh](runpodctl_ssh.md) - manage ssh keys and connections -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_ssh_info.md b/docs/runpodctl_ssh_info.md index 5ab8d59..fb06867 100644 --- a/docs/runpodctl_ssh_info.md +++ b/docs/runpodctl_ssh_info.md @@ -27,4 +27,4 @@ runpodctl ssh info [flags] * [runpodctl ssh](runpodctl_ssh.md) - manage ssh keys and connections -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_ssh_list-keys.md b/docs/runpodctl_ssh_list-keys.md index 4523c9e..9ef6f57 100644 --- a/docs/runpodctl_ssh_list-keys.md +++ b/docs/runpodctl_ssh_list-keys.md @@ -26,4 +26,4 @@ runpodctl ssh list-keys [flags] * [runpodctl ssh](runpodctl_ssh.md) - manage ssh keys and connections -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template.md b/docs/runpodctl_template.md index e495589..eddd9c6 100644 --- a/docs/runpodctl_template.md +++ b/docs/runpodctl_template.md @@ -28,4 +28,4 @@ manage templates on runpod * [runpodctl template search](runpodctl_template_search.md) - search templates * [runpodctl template update](runpodctl_template_update.md) - update a template -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template_create.md b/docs/runpodctl_template_create.md index 7dca12d..45bd364 100644 --- a/docs/runpodctl_template_create.md +++ b/docs/runpodctl_template_create.md @@ -37,4 +37,4 @@ runpodctl template create [flags] * [runpodctl template](runpodctl_template.md) - manage templates -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template_delete.md b/docs/runpodctl_template_delete.md index 6c219a1..022d647 100644 --- a/docs/runpodctl_template_delete.md +++ b/docs/runpodctl_template_delete.md @@ -26,4 +26,4 @@ runpodctl template delete [flags] * [runpodctl template](runpodctl_template.md) - manage templates -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template_get.md b/docs/runpodctl_template_get.md index 614917e..a3dca66 100644 --- a/docs/runpodctl_template_get.md +++ b/docs/runpodctl_template_get.md @@ -26,4 +26,4 @@ runpodctl template get [flags] * [runpodctl template](runpodctl_template.md) - manage templates -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template_list.md b/docs/runpodctl_template_list.md index a4f41a1..abe8c85 100644 --- a/docs/runpodctl_template_list.md +++ b/docs/runpodctl_template_list.md @@ -41,4 +41,4 @@ runpodctl template list [flags] * [runpodctl template](runpodctl_template.md) - manage templates -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template_search.md b/docs/runpodctl_template_search.md index b21de71..d693b6b 100644 --- a/docs/runpodctl_template_search.md +++ b/docs/runpodctl_template_search.md @@ -37,4 +37,4 @@ runpodctl template search [flags] * [runpodctl template](runpodctl_template.md) - manage templates -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_template_update.md b/docs/runpodctl_template_update.md index 329eb54..f75b175 100644 --- a/docs/runpodctl_template_update.md +++ b/docs/runpodctl_template_update.md @@ -31,4 +31,4 @@ runpodctl template update [flags] * [runpodctl template](runpodctl_template.md) - manage templates -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_update.md b/docs/runpodctl_update.md index d9bec13..cc1dc3d 100644 --- a/docs/runpodctl_update.md +++ b/docs/runpodctl_update.md @@ -26,4 +26,4 @@ runpodctl update [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_user.md b/docs/runpodctl_user.md index d93694f..ad8ab85 100644 --- a/docs/runpodctl_user.md +++ b/docs/runpodctl_user.md @@ -26,4 +26,4 @@ runpodctl user [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/docs/runpodctl_version.md b/docs/runpodctl_version.md index 27ba418..068eda6 100644 --- a/docs/runpodctl_version.md +++ b/docs/runpodctl_version.md @@ -22,4 +22,4 @@ runpodctl version [flags] * [runpodctl](runpodctl.md) - cli for runpod.io -###### Auto generated by spf13/cobra on 23-Mar-2026 +###### Auto generated by spf13/cobra on 26-Mar-2026 diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 3c7543e..7d5126e 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -455,6 +455,296 @@ func TestCLI_PodCreateCPU(t *testing.T) { } } +// --- Hub tests --- + +func TestCLI_HubList(t *testing.T) { + stdout, stderr, err := runCLI("hub", "list") + if err != nil { + t.Fatalf("failed to run hub list: %v\nstderr: %s", err, stderr) + } + + var listings []map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listings); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + if len(listings) == 0 { + t.Error("expected at least one hub listing") + } + + t.Logf("found %d hub listings", len(listings)) +} + +func TestCLI_HubListTypeServerless(t *testing.T) { + stdout, stderr, err := runCLI("hub", "list", "--type", "SERVERLESS", "--limit", "5") + if err != nil { + t.Fatalf("failed to run hub list --type SERVERLESS: %v\nstderr: %s", err, stderr) + } + + var listings []map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listings); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + for _, l := range listings { + lType, _ := l["type"].(string) + if lType != "SERVERLESS" { + t.Errorf("expected type SERVERLESS, got %q for %v", lType, l["repoName"]) + } + } + + if len(listings) == 0 { + t.Error("expected at least one serverless hub listing") + } + t.Logf("found %d serverless hub listings (limited to 5)", len(listings)) +} + +func TestCLI_HubListTypePod(t *testing.T) { + stdout, stderr, err := runCLI("hub", "list", "--type", "POD", "--limit", "5") + if err != nil { + t.Fatalf("failed to run hub list --type POD: %v\nstderr: %s", err, stderr) + } + + var listings []map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listings); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + for _, l := range listings { + lType, _ := l["type"].(string) + if lType != "POD" { + t.Errorf("expected type POD, got %q for %v", lType, l["repoName"]) + } + } + + t.Logf("found %d pod hub listings (limited to 5)", len(listings)) +} + +func TestCLI_HubListPagination(t *testing.T) { + stdout1, _, err := runCLI("hub", "list", "--limit", "3") + if err != nil { + t.Skip("skipping pagination test - can't get first page") + } + + stdout2, _, err := runCLI("hub", "list", "--limit", "3", "--offset", "3") + if err != nil { + t.Skip("skipping pagination test - can't get second page") + } + + var page1, page2 []map[string]interface{} + json.Unmarshal([]byte(stdout1), &page1) + json.Unmarshal([]byte(stdout2), &page2) + + if len(page1) == 0 || len(page2) == 0 { + t.Skip("skipping pagination test - not enough listings") + } + + page2FirstID := page2[0]["id"] + for _, l := range page1 { + if l["id"] == page2FirstID { + t.Error("pagination not working - same listing on both pages") + } + } + + t.Logf("pagination works: page1=%d listings, page2=%d listings", len(page1), len(page2)) +} + +func TestCLI_HubSearch(t *testing.T) { + stdout, stderr, err := runCLI("hub", "search", "vllm") + if err != nil { + t.Fatalf("failed to search hub: %v\nstderr: %s", err, stderr) + } + + var listings []map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listings); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + if len(listings) == 0 { + t.Error("expected at least one result for 'vllm'") + } + + // verify at least one result is the official vllm worker + found := false + for _, l := range listings { + repoName, _ := l["repoName"].(string) + if repoName == "worker-vllm" { + found = true + break + } + } + if !found { + t.Error("expected worker-vllm in search results") + } + + t.Logf("found %d hub repos matching 'vllm'", len(listings)) +} + +func TestCLI_HubSearchNoResults(t *testing.T) { + stdout, stderr, err := runCLI("hub", "search", "zzz_nonexistent_repo_12345") + if err != nil { + t.Fatalf("failed to search hub: %v\nstderr: %s", err, stderr) + } + + // should print "no hub repos found" to stdout (not JSON) + if !strings.Contains(stdout, "no hub repos found") { + t.Errorf("expected 'no hub repos found' message, got: %s", stdout) + } +} + +func TestCLI_HubGetByID(t *testing.T) { + // first get a listing id from list + stdout, _, err := runCLI("hub", "list", "--limit", "1") + if err != nil { + t.Skip("skipping hub get test - can't list hub") + } + + var listings []map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listings); err != nil || len(listings) == 0 { + t.Skip("skipping hub get test - no listings") + } + + listingID := listings[0]["id"].(string) + stdout, stderr, err := runCLI("hub", "get", listingID) + if err != nil { + t.Fatalf("failed to get hub listing %s: %v\nstderr: %s", listingID, err, stderr) + } + + var listing map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listing); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + if listing["id"] != listingID { + t.Errorf("expected listing id %s, got %v", listingID, listing["id"]) + } + + // get should include listedRelease + if listing["listedRelease"] == nil { + t.Error("expected listedRelease in get response") + } + + t.Logf("got hub listing: %v (type=%v)", listing["title"], listing["type"]) +} + +func TestCLI_HubGetByOwnerName(t *testing.T) { + stdout, stderr, err := runCLI("hub", "get", "runpod-workers/worker-vllm") + if err != nil { + t.Fatalf("failed to get hub listing by owner/name: %v\nstderr: %s", err, stderr) + } + + var listing map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listing); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + if listing["repoOwner"] != "runpod-workers" { + t.Errorf("expected repoOwner 'runpod-workers', got %v", listing["repoOwner"]) + } + if listing["repoName"] != "worker-vllm" { + t.Errorf("expected repoName 'worker-vllm', got %v", listing["repoName"]) + } + + t.Logf("got hub listing by owner/name: %v", listing["title"]) +} + +func TestCLI_HubGetBuildImage(t *testing.T) { + // verify build.imageName is returned in get response + stdout, stderr, err := runCLI("hub", "get", "runpod-workers/worker-vllm") + if err != nil { + t.Fatalf("failed to get hub listing: %v\nstderr: %s", err, stderr) + } + + var listing map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &listing); err != nil { + t.Fatalf("output is not valid json: %v", err) + } + + release, ok := listing["listedRelease"].(map[string]interface{}) + if !ok || release == nil { + t.Fatal("expected listedRelease in response") + } + + build, ok := release["build"].(map[string]interface{}) + if !ok || build == nil { + t.Fatal("expected build in listedRelease") + } + + imageName, ok := build["imageName"].(string) + if !ok || imageName == "" { + t.Fatal("expected imageName in build") + } + + t.Logf("build image: %s", imageName) +} + +// --- Serverless create from hub --- + +func TestCLI_ServerlessCreateRequiresTemplateOrHub(t *testing.T) { + _, stderr, err := runCLI("serverless", "create", "--gpu-id", "NVIDIA GeForce RTX 4090") + if err == nil { + t.Fatal("expected error when creating serverless without template or hub") + } + if !strings.Contains(stderr, "either --template-id or --hub-id is required") { + t.Errorf("expected helpful error, got: %s", stderr) + } +} + +func TestCLI_ServerlessCreateMutuallyExclusive(t *testing.T) { + _, stderr, err := runCLI("serverless", "create", "--template-id", "x", "--hub-id", "y") + if err == nil { + t.Fatal("expected error when both --template-id and --hub-id are provided") + } + if !strings.Contains(stderr, "mutually exclusive") { + t.Errorf("expected mutually exclusive error, got: %s", stderr) + } +} + +func TestCLI_ServerlessCreateFromHub(t *testing.T) { + // create a serverless endpoint from the vllm hub listing + name := "e2e-test-hub-" + time.Now().Format("20060102150405") + stdout, stderr, err := runCLI("serverless", "create", + "--hub-id", "cm8h09d9n000008jvh2rqdsmb", // vllm listing + "--name", name, + "--workers-max", "1", + ) + if err != nil { + t.Fatalf("failed to create serverless endpoint from hub: %v\nstderr: %s", err, stderr) + } + + var endpoint map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &endpoint); err != nil { + t.Fatalf("output is not valid json: %v\noutput: %s", err, stdout) + } + + endpointID, ok := endpoint["id"].(string) + if !ok || strings.TrimSpace(endpointID) == "" { + t.Fatal("expected endpoint id in response") + } + + // cleanup immediately + t.Cleanup(func() { + _, _, err := runCLI("serverless", "delete", endpointID) + if err != nil { + t.Logf("warning: failed to delete test endpoint %s: %v", endpointID, err) + } else { + t.Logf("cleaned up endpoint %s", endpointID) + } + }) + + if endpoint["name"] != name { + t.Errorf("expected name %q, got %v", name, endpoint["name"]) + } + + // verify gpu ids were pulled from hub config + gpuIDs, _ := endpoint["gpuIds"].(string) + if gpuIDs == "" { + t.Error("expected gpuIds from hub config") + } + + t.Logf("created endpoint %s from hub (gpuIds=%s)", endpointID, gpuIDs) +} + func TestCLI_EndpointList(t *testing.T) { stdout, stderr, err := runCLI("serverless", "list") if err != nil { @@ -1439,6 +1729,9 @@ func TestCLI_HelpCoverage(t *testing.T) { {"project", "dev"}, {"project", "build"}, {"project", "deploy"}, + {"hub", "list"}, + {"hub", "search"}, + {"hub", "get"}, {"serverless", "create"}, {"serverless", "update"}, {"serverless", "delete"}, diff --git a/evals/deploy-comfyui-serverless.eval.md b/evals/deploy-comfyui-serverless.eval.md new file mode 100644 index 0000000..bebda60 --- /dev/null +++ b/evals/deploy-comfyui-serverless.eval.md @@ -0,0 +1,23 @@ +# Deploy ComfyUI as a serverless endpoint + +## Prompt + +I want to run ComfyUI as a serverless endpoint on Runpod. + +## Expected behavior + +The agent should: + +1. Search the hub for ComfyUI: `runpodctl hub search comfyui` or `runpodctl hub search ComfyUI` +2. Identify the `runpod-workers/worker-comfyui` listing and note its hub ID +3. Optionally get details: `runpodctl hub get ` or `runpodctl hub get runpod-workers/worker-comfyui` +4. Create a serverless endpoint using the hub ID: `runpodctl serverless create --hub-id --name ""` +5. Verify the endpoint was created successfully +6. Clean up the endpoint after verification: `runpodctl serverless delete ` + +## Assertions + +- Agent uses `runpodctl hub search` or `runpodctl hub list` to discover the ComfyUI listing +- Agent uses `runpodctl serverless create --hub-id` (not `--template-id`) to deploy +- Agent cleans up the created endpoint with `runpodctl serverless delete` +- Agent does NOT try to create a template manually diff --git a/evals/deploy-vllm-serverless.eval.md b/evals/deploy-vllm-serverless.eval.md new file mode 100644 index 0000000..b716e62 --- /dev/null +++ b/evals/deploy-vllm-serverless.eval.md @@ -0,0 +1,23 @@ +# Deploy vLLM as a serverless endpoint + +## Prompt + +I want to run vLLM as a serverless endpoint on Runpod. + +## Expected behavior + +The agent should: + +1. Search the hub for vLLM: `runpodctl hub search vllm` +2. Identify the official `runpod-workers/worker-vllm` listing and note its hub ID +3. Optionally get details: `runpodctl hub get ` or `runpodctl hub get runpod-workers/worker-vllm` +4. Create a serverless endpoint using the hub ID: `runpodctl serverless create --hub-id --name ""` +5. Verify the endpoint was created successfully +6. Clean up the endpoint after verification: `runpodctl serverless delete ` + +## Assertions + +- Agent uses `runpodctl hub search` or `runpodctl hub list` to discover the vLLM listing +- Agent uses `runpodctl serverless create --hub-id` (not `--template-id`) to deploy +- Agent cleans up the created endpoint with `runpodctl serverless delete` +- Agent does NOT try to create a template manually diff --git a/internal/api/endpoints.go b/internal/api/endpoints.go index c66f085..ea7d6e9 100644 --- a/internal/api/endpoints.go +++ b/internal/api/endpoints.go @@ -32,7 +32,8 @@ type EndpointListResponse struct { // EndpointCreateRequest is the request to create an endpoint type EndpointCreateRequest struct { Name string `json:"name,omitempty"` - TemplateID string `json:"templateId"` + TemplateID string `json:"templateId,omitempty"` + HubReleaseID string `json:"hubReleaseId,omitempty"` ComputeType string `json:"computeType,omitempty"` GpuTypeIDs []string `json:"gpuTypeIds,omitempty"` GpuCount int `json:"gpuCount,omitempty"` @@ -141,3 +142,80 @@ func (c *Client) DeleteEndpoint(endpointID string) error { _, err := c.Delete("/endpoints/" + endpointID) return err } + +// EndpointCreateGQLInput is the input for creating an endpoint via GraphQL +// Used when hubReleaseId is needed (REST API doesn't support it) +type EndpointCreateGQLInput struct { + Name string `json:"name"` + HubReleaseID string `json:"hubReleaseId,omitempty"` + TemplateID string `json:"templateId,omitempty"` + Template *EndpointTemplateInput `json:"template,omitempty"` + GpuIDs string `json:"gpuIds,omitempty"` + GpuCount int `json:"gpuCount,omitempty"` + WorkersMin int `json:"workersMin,omitempty"` + WorkersMax int `json:"workersMax,omitempty"` + Locations string `json:"locations,omitempty"` + NetworkVolumeID string `json:"networkVolumeId,omitempty"` +} + +// EndpointTemplateInput is the inline template for endpoint creation via GraphQL +type EndpointTemplateInput struct { + Name string `json:"name"` + ImageName string `json:"imageName,omitempty"` + ContainerDiskInGb int `json:"containerDiskInGb"` + DockerArgs string `json:"dockerArgs"` + Env []*PodEnvVar `json:"env"` +} + +// CreateEndpointGQL creates an endpoint via GraphQL (saveEndpoint mutation) +func (c *Client) CreateEndpointGQL(req *EndpointCreateGQLInput) (*Endpoint, error) { + query := ` + mutation SaveEndpoint($input: EndpointInput!) { + saveEndpoint(input: $input) { + id + name + gpuIds + networkVolumeId + locations + idleTimeout + scalerType + scalerValue + workersMin + workersMax + gpuCount + } + } + ` + + variables := map[string]interface{}{ + "input": req, + } + + data, err := c.graphqlRequest(query, variables) + if err != nil { + return nil, err + } + + var resp struct { + Data struct { + SaveEndpoint *Endpoint `json:"saveEndpoint"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if len(resp.Errors) > 0 { + return nil, fmt.Errorf("graphql error: %s", resp.Errors[0].Message) + } + + if resp.Data.SaveEndpoint == nil { + return nil, fmt.Errorf("endpoint creation returned nil response") + } + + return resp.Data.SaveEndpoint, nil +} diff --git a/internal/api/hub.go b/internal/api/hub.go new file mode 100644 index 0000000..ac8b67a --- /dev/null +++ b/internal/api/hub.go @@ -0,0 +1,303 @@ +package api + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Listing represents a hub listing (repo) +type Listing struct { + ID string `json:"id"` + Title string `json:"title,omitempty"` + RepoName string `json:"repoName"` + RepoOwner string `json:"repoOwner"` + RepoOwnerAvatarUrl string `json:"repoOwnerAvatarUrl,omitempty"` + Description string `json:"description,omitempty"` + Category string `json:"category,omitempty"` + Type string `json:"type"` + Tags []string `json:"tags,omitempty"` + Stars int `json:"stars"` + Views int `json:"views"` + Deploys int `json:"deploys"` + OpenIssues int `json:"openIssues"` + Watchers int `json:"watchers"` + Language string `json:"language,omitempty"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + ListedRelease *HubRelease `json:"listedRelease,omitempty"` +} + +// HubRelease represents a release of a hub listing +type HubRelease struct { + ID string `json:"id"` + Name string `json:"name"` + TagName string `json:"tagName"` + Body string `json:"body,omitempty"` + Readme string `json:"readme,omitempty"` + Branch string `json:"branch,omitempty"` + License string `json:"license,omitempty"` + Config string `json:"config,omitempty"` + Deploys int `json:"deploys"` + IconUrl string `json:"iconUrl,omitempty"` + AuthorName string `json:"authorName,omitempty"` + CreatedAt string `json:"createdAt"` + ReleasedAt string `json:"releasedAt"` + UpdatedAt string `json:"updatedAt"` + Build *GitBuild `json:"build,omitempty"` + Tests string `json:"tests,omitempty"` +} + +// GitBuild represents a build from a hub release +type GitBuild struct { + ImageName string `json:"imageName,omitempty"` +} + +// HubReleaseConfig is the parsed config from a hub release +type HubReleaseConfig struct { + ContainerDiskInGb int `json:"containerDiskInGb,omitempty"` + GpuIDs string `json:"gpuIds,omitempty"` + GpuCount int `json:"gpuCount,omitempty"` + Env []HubReleaseConfigEnv `json:"env,omitempty"` +} + +// HubReleaseConfigEnv is an env var entry in the hub release config +type HubReleaseConfigEnv struct { + Key string `json:"key"` + Input *HubReleaseConfigEnvInput `json:"input,omitempty"` +} + +// HubReleaseConfigEnvInput describes the env var input metadata +type HubReleaseConfigEnvInput struct { + Default interface{} `json:"default,omitempty"` +} + +// ListingsOptions for listing/searching hub entries +type ListingsOptions struct { + SearchQuery string + Category string + Limit int + Offset int + OrderBy string + OrderDirection string + Owner string + Type string // client-side filter: POD or SERVERLESS +} + +// ListListings returns hub listings with optional filtering +func (c *Client) ListListings(opts *ListingsOptions) ([]Listing, error) { + query := ` + query Listings($input: ListingsInput!) { + listings(input: $input) { + id + title + repoName + repoOwner + description + category + type + tags + stars + views + deploys + language + createdAt + updatedAt + } + } + ` + + input := map[string]interface{}{} + if opts != nil { + if opts.SearchQuery != "" { + input["searchQuery"] = opts.SearchQuery + } + if opts.Category != "" { + input["category"] = opts.Category + } + if opts.Limit > 0 { + input["limit"] = opts.Limit + } + if opts.Offset > 0 { + input["offset"] = opts.Offset + } + if opts.OrderBy != "" { + input["orderBy"] = opts.OrderBy + } + if opts.OrderDirection != "" { + input["orderDirection"] = opts.OrderDirection + } + if opts.Owner != "" { + input["owner"] = opts.Owner + } + } + + variables := map[string]interface{}{ + "input": input, + } + + data, err := c.graphqlRequest(query, variables) + if err != nil { + return nil, err + } + + var resp struct { + Data struct { + Listings []Listing `json:"listings"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if len(resp.Errors) > 0 { + return nil, fmt.Errorf("graphql error: %s", resp.Errors[0].Message) + } + + listings := resp.Data.Listings + + // client-side type filter (ListingsInput has no type field) + if opts != nil && opts.Type != "" { + filterType := strings.ToUpper(opts.Type) + var filtered []Listing + for _, l := range listings { + if strings.ToUpper(l.Type) == filterType { + filtered = append(filtered, l) + } + } + listings = filtered + } + + return listings, nil +} + +const listingFullFields = ` + id + title + repoName + repoOwner + repoOwnerAvatarUrl + description + category + type + tags + stars + views + deploys + openIssues + watchers + language + createdAt + updatedAt + listedRelease { + id + name + tagName + body + readme + branch + license + config + deploys + iconUrl + authorName + createdAt + releasedAt + updatedAt + build { + imageName + } + tests + } +` + +// GetListing returns a single hub listing by ID +func (c *Client) GetListing(listingID string) (*Listing, error) { + query := fmt.Sprintf(` + query GetListing($id: String!) { + listing(id: $id) { + %s + } + } + `, listingFullFields) + + variables := map[string]interface{}{ + "id": listingID, + } + + data, err := c.graphqlRequest(query, variables) + if err != nil { + return nil, err + } + + var resp struct { + Data struct { + Listing *Listing `json:"listing"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if len(resp.Errors) > 0 { + return nil, fmt.Errorf("graphql error: %s", resp.Errors[0].Message) + } + + if resp.Data.Listing == nil { + return nil, fmt.Errorf("hub listing not found: %s", listingID) + } + + return resp.Data.Listing, nil +} + +// GetListingFromRepo returns a hub listing by repo owner and name +func (c *Client) GetListingFromRepo(owner, name string) (*Listing, error) { + query := fmt.Sprintf(` + query GetListingFromRepo($repoOwner: String!, $repoName: String!) { + listingFromRepo(repoOwner: $repoOwner, repoName: $repoName) { + %s + } + } + `, listingFullFields) + + variables := map[string]interface{}{ + "repoOwner": owner, + "repoName": name, + } + + data, err := c.graphqlRequest(query, variables) + if err != nil { + return nil, err + } + + var resp struct { + Data struct { + Listing *Listing `json:"listingFromRepo"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if len(resp.Errors) > 0 { + return nil, fmt.Errorf("graphql error: %s", resp.Errors[0].Message) + } + + if resp.Data.Listing == nil { + return nil, fmt.Errorf("hub listing not found: %s/%s", owner, name) + } + + return resp.Data.Listing, nil +}