diff --git a/cmd/pod/create.go b/cmd/pod/create.go index b0ce127..6b10f40 100644 --- a/cmd/pod/create.go +++ b/cmd/pod/create.go @@ -41,6 +41,7 @@ var ( createImageName string createTemplateID string createComputeType string + createBidPerGPU float32 createGpuTypeID string createGpuCount int createVolumeInGb int @@ -61,6 +62,7 @@ func init() { createCmd.Flags().StringVar(&createTemplateID, "template-id", "", "template id (use 'runpodctl template search' to find templates)") createCmd.Flags().StringVar(&createImageName, "image", "", "docker image name (required if no template)") createCmd.Flags().StringVar(&createComputeType, "compute-type", "GPU", "compute type (GPU or CPU)") + createCmd.Flags().Float32Var(&createBidPerGPU, "bid-per-gpu", 0, "bid per gpu for spot pod creation") createCmd.Flags().StringVar(&createGpuTypeID, "gpu-id", "", "gpu id (from 'runpodctl gpu list')") createCmd.Flags().IntVar(&createGpuCount, "gpu-count", 1, "number of gpus") createCmd.Flags().IntVar(&createVolumeInGb, "volume-in-gb", 0, "volume size in gb") @@ -100,6 +102,12 @@ func runCreate(cmd *cobra.Command, args []string) error { if computeType == "CPU" && gpuTypeID != "" { return fmt.Errorf("--gpu-id is not supported for compute type CPU") } + if computeType == "CPU" && createBidPerGPU > 0 { + return fmt.Errorf("--bid-per-gpu is only supported for compute type GPU") + } + if createBidPerGPU < 0 { + return fmt.Errorf("--bid-per-gpu must be greater than 0") + } cloudType := strings.ToUpper(strings.TrimSpace(createCloudType)) if cloudType == "" { @@ -198,6 +206,11 @@ func createPodGraphQL(gpuTypeID, cloudType string, supportPublicIP bool) (map[st } } + if createBidPerGPU > 0 { + req.BidPerGpu = createBidPerGPU + return gqlClient.CreateSpotPod(req) + } + return gqlClient.CreatePod(req) } diff --git a/cmd/pod/create_test.go b/cmd/pod/create_test.go new file mode 100644 index 0000000..c3a28f0 --- /dev/null +++ b/cmd/pod/create_test.go @@ -0,0 +1,36 @@ +package pod + +import "testing" + +func TestCreateCmd_HasBidPerGPUFlag(t *testing.T) { + if createCmd.Flags().Lookup("bid-per-gpu") == nil { + t.Fatal("expected --bid-per-gpu flag") + } +} + +func TestRunCreate_RejectsBidPerGPUForCPU(t *testing.T) { + origTemplateID := createTemplateID + origImageName := createImageName + origComputeType := createComputeType + origBidPerGPU := createBidPerGPU + + t.Cleanup(func() { + createTemplateID = origTemplateID + createImageName = origImageName + createComputeType = origComputeType + createBidPerGPU = origBidPerGPU + }) + + createTemplateID = "" + createImageName = "ubuntu:22.04" + createComputeType = "CPU" + createBidPerGPU = 0.2 + + err := runCreate(createCmd, nil) + if err == nil { + t.Fatal("expected error") + } + if err.Error() != "--bid-per-gpu is only supported for compute type GPU" { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/docs/runpodctl_pod_create.md b/docs/runpodctl_pod_create.md index 21a248c..c08ab58 100644 --- a/docs/runpodctl_pod_create.md +++ b/docs/runpodctl_pod_create.md @@ -29,6 +29,7 @@ runpodctl pod create [flags] ### Options ``` + --bid-per-gpu float32 bid per gpu for spot pod creation --cloud-type string cloud type (SECURE or COMMUNITY) (default "SECURE") --compute-type string compute type (GPU or CPU) (default "GPU") --container-disk-in-gb int container disk size in gb (default 20) @@ -59,4 +60,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 9-Apr-2026 diff --git a/internal/api/graphql.go b/internal/api/graphql.go index 93b254b..51a0c67 100644 --- a/internal/api/graphql.go +++ b/internal/api/graphql.go @@ -220,6 +220,7 @@ type PodEnvVar struct { // CreatePodGQLInput is the input for creating a pod via GraphQL type CreatePodGQLInput struct { + BidPerGpu float32 `json:"bidPerGpu,omitempty"` CloudType string `json:"cloudType,omitempty"` ContainerDiskInGb int `json:"containerDiskInGb"` DataCenterId string `json:"dataCenterId,omitempty"` @@ -296,6 +297,65 @@ func (c *GraphQLClient) CreatePod(input *CreatePodGQLInput) (map[string]interfac return data.Data.Pod, nil } +// CreateSpotPod creates a spot pod via GraphQL (podRentInterruptable). +func (c *GraphQLClient) CreateSpotPod(input *CreatePodGQLInput) (map[string]interface{}, error) { + gqlInput := GraphQLInput{ + Query: ` + mutation createSpotPod($input: PodRentInterruptableInput!) { + podRentInterruptable(input: $input) { + id + name + imageName + desiredStatus + costPerHr + containerDiskInGb + volumeInGb + volumeMountPath + gpuCount + memoryInGb + vcpuCount + ports + lastStatusChange + env + machine { + gpuDisplayName + location + } + } + } + `, + Variables: map[string]interface{}{"input": input}, + } + + body, err := c.Query(gqlInput) + if err != nil { + return nil, err + } + + var data struct { + Data struct { + Pod map[string]interface{} `json:"podRentInterruptable"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + + if err := json.Unmarshal(body, &data); err != nil { + return nil, err + } + + if len(data.Errors) > 0 { + return nil, fmt.Errorf("%s", data.Errors[0].Message) + } + + if data.Data.Pod == nil { + return nil, fmt.Errorf("spot pod creation returned nil response") + } + + return data.Data.Pod, nil +} + // LegacyPod is the pod structure from GraphQL API (for backwards compatibility) type LegacyPod struct { ID string `json:"id"` diff --git a/internal/api/graphql_pod_test.go b/internal/api/graphql_pod_test.go new file mode 100644 index 0000000..9efef7d --- /dev/null +++ b/internal/api/graphql_pod_test.go @@ -0,0 +1,54 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestCreateSpotPod_IncludesBidPerGPU(t *testing.T) { + var gotBid float64 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var input GraphQLInput + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + t.Fatalf("decode request: %v", err) + } + + payload, _ := input.Variables["input"].(map[string]interface{}) + if value, ok := payload["bidPerGpu"].(float64); ok { + gotBid = value + } + + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "data": map[string]interface{}{ + "podRentInterruptable": map[string]interface{}{ + "id": "pod-1", + }, + }, + }) + })) + defer server.Close() + + client := &GraphQLClient{ + url: server.URL, + apiKey: "test-key", + httpClient: server.Client(), + userAgent: "test", + } + + _, err := client.CreateSpotPod(&CreatePodGQLInput{ + BidPerGpu: 0.2, + GpuCount: 1, + GpuTypeId: "NVIDIA GeForce RTX 4090", + ImageName: "ubuntu:22.04", + StartSsh: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotBid != 0.2 { + t.Fatalf("expected bidPerGpu 0.2, got %v", gotBid) + } +}