Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/pod/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var (
createImageName string
createTemplateID string
createComputeType string
createBidPerGPU float32
createGpuTypeID string
createGpuCount int
createVolumeInGb int
Expand All @@ -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")
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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)
}

Expand Down
36 changes: 36 additions & 0 deletions cmd/pod/create_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 2 additions & 1 deletion docs/runpodctl_pod_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
60 changes: 60 additions & 0 deletions internal/api/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
54 changes: 54 additions & 0 deletions internal/api/graphql_pod_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}