wip: add spot pod creation

- Add support for creating spot pods using the RunPod API.
- Implement `create_spot_pod` function in the `RunPod` client.
- Refactor RunPod client to handle different query types and response structures.
- Improve error handling and logging for GraphQL requests.
- Update example to demonstrate spot pod creation.

Co-authored-by: mahmmoud.hassanein <mahmmoud.hassanein@gmail.com>
This commit is contained in:
2025-01-20 21:34:16 +02:00
parent d54a1e5a34
commit 4422d67701
4 changed files with 180 additions and 138 deletions

View File

@@ -10,12 +10,40 @@ mut rp := runpod.get_or_create(
)!
// Create a new pod
pod_response := rp.create_pod(
name: 'RunPod Tensorflow'
image_name: 'runpod/tensorflow'
// pod_response := rp.create_on_demand_pod(
// name: 'RunPod Tensorflow'
// image_name: 'runpod/tensorflow'
// env: [
// runpod.EnvironmentVariableInput{
// key: 'JUPYTER_PASSWORD'
// value: 'rn51hunbpgtltcpac3ol'
// },
// ]
// )!
// println('Created pod with ID: ${pod_response.id}')
// create spot pod
// "mutation { podRentInterruptable( input: { bidPerGpu: 0.2, cloudType: SECURE, gpuCount: 1, volumeInGb: 40, containerDiskInGb: 40, minVcpuCount: 2, minMemoryInGb: 15, gpuTypeId: \"NVIDIA RTX A6000\", name: \"RunPod Pytorch\", imageName: \"runpod/pytorch\", dockerArgs: \"\", ports: \"8888/http\", volumeMountPath: \"/workspace\", env: [{ key: \"JUPYTER_PASSWORD\", value: \"vunw9ybnzqwpia2795p2\" }] } ) { id imageName env machineId machine { podHostId } } }"
spot_pod_resp := rp.create_spot_pod(
port: 1826
bid_per_gpu: 0.2
cloud_type: .secure
gpu_count: 1
volume_in_gb: 40
container_disk_in_gb: 40
min_vcpu_count: 2
min_memory_in_gb: 15
gpu_type_id: 'NVIDIA RTX A6000'
name: 'RunPod Pytorch'
image_name: 'runpod/pytorch'
docker_args: ''
ports: '8888/http'
volume_mount_path: '/workspace'
env: [
{"JUPYTER_PASSWORD": "rn51hunbpgtltcpac3ol"}
runpod.EnvironmentVariableInput{
key: 'JUPYTER_PASSWORD'
value: 'rn51hunbpgtltcpac3ol'
},
]
)!
println('Created pod with ID: ${pod_response.id}')
println('Created spot pod with ID: ${spot_pod_resp.id}')

View File

@@ -28,19 +28,25 @@ struct GqlQuery {
}
// GraphQL response wrapper
struct GqlResponse {
data GqlResponseData
struct GqlResponse[T] {
data map[string]T
}
struct GqlResponseData {
pod_find_and_deploy_on_demand PodFindAndDeployOnDemandResponse @[json: 'podFindAndDeployOnDemand']
}
// struct GqlResponseData[T] {
// pod_find_and_deploy_on_demand T @[json: 'podFindAndDeployOnDemand']
// }
fn (mut rp RunPod) create_pod_request[T, R](request T, response R) !R {
gql := rp.build_query[T, R](request, response)
fn (mut rp RunPod) create_pop_find_and_deploy_on_demand_request(request PodFindAndDeployOnDemandRequest) !PodFindAndDeployOnDemandResponse {
gql := build_query(BuildQueryArgs{
query_type: .mutation
method_name: 'podFindAndDeployOnDemand'
}, request, PodFindAndDeployOnDemandResponse{})
println('gql: ${gql}')
response_ := rp.make_request[GqlResponse](.post, '/graphql', gql)!
response_ := rp.make_request[GqlResponse[PodFindAndDeployOnDemandResponse]](.post,
'/graphql', gql)!
println('response: ${json.encode(response_)}')
return response
return response_.data['podFindAndDeployOnDemand'] or {
return error('Could not find podFindAndDeployOnDemand in response data: ${response_.data}')
}
// return response.data.pod_find_and_deploy_on_demand
}

View File

@@ -23,7 +23,7 @@ pub mut:
base_url string = 'https://api.runpod.io/v1'
}
enum CloudType {
pub enum CloudType {
all
secure
community
@@ -59,11 +59,18 @@ pub mut:
docker_args string = '' @[json: 'dockerArgs']
ports string = '8888/http' @[json: 'ports']
volume_mount_path string = '/workspace' @[json: 'volumeMountPath']
env []map[string]string = [] @[json: 'env']
env []EnvironmentVariableInput @[json: 'env']
}
pub struct EnvironmentVariableInput {
pub:
key string
value string
}
// Represents the nested machine structure in the response
struct Machine {
pub struct Machine {
pub:
pod_host_id string @[json: 'podHostId']
}
@@ -72,7 +79,7 @@ pub struct PodFindAndDeployOnDemandResponse {
pub:
id string @[json: 'id']
image_name string @[json: 'imageName']
env []map[string]string @[json: 'env']
env []string @[json: 'env']
machine_id int @[json: 'machineId']
machine Machine @[json: 'machine']
}
@@ -88,76 +95,58 @@ pub fn new(api_key string) !&RunPod {
}
// create_endpoint creates a new endpoint
pub fn (mut rp RunPod) create_pod(pod PodFindAndDeployOnDemandRequest) !PodFindAndDeployOnDemandResponse {
pub fn (mut rp RunPod) create_on_demand_pod(pod PodFindAndDeployOnDemandRequest) !PodFindAndDeployOnDemandResponse {
response_type := PodFindAndDeployOnDemandResponse{}
request_type := pod
response := rp.create_pod_request[PodFindAndDeployOnDemandRequest, PodFindAndDeployOnDemandResponse](request_type,
response_type)!
response := rp.create_pop_find_and_deploy_on_demand_request(request_type)!
return response
}
// // list_endpoints lists all endpoints
// pub fn (mut rp RunPod) list_endpoints() ![]Endpoint {
// response := rp.list_endpoints_request()!
// endpoints := json.decode([]Endpoint, response) or {
// return error('Failed to parse endpoints from response: ${response}')
// }
// return endpoints
// }
@[params]
pub struct PodRentInterruptableInput {
pub mut:
port int @[json: 'port']
network_volume_id string @[json: 'networkVolumeId'; omitempty]
start_jupyter bool @[json: 'startJupyter']
start_ssh bool @[json: 'startSsh']
bid_per_gpu f32 @[json: 'bidPerGpu']
cloud_type CloudType @[json: 'cloudType']
container_disk_in_gb int @[json: 'containerDiskInGb']
country_code string @[json: 'countryCode'; omitempty]
docker_args string @[json: 'dockerArgs'; omitempty]
env []EnvironmentVariableInput @[json: 'env']
gpu_count int @[json: 'gpuCount']
gpu_type_id string @[json: 'gpuTypeId'; omitempty]
image_name string @[json: 'imageName'; omitempty]
min_disk int @[json: 'minDisk']
min_download int @[json: 'minDownload']
min_memory_in_gb int @[json: 'minMemoryInGb']
min_upload int @[json: 'minUpload']
min_vcpu_count int @[json: 'minVcpuCount']
name string @[json: 'name'; omitempty]
ports string @[json: 'ports'; omitempty]
stop_after string @[json: 'stopAfter'; omitempty]
support_public_ip bool @[json: 'supportPublicIp']
template_id string @[json: 'templateId'; omitempty]
terminate_after string @[json: 'terminateAfter'; omitempty]
volume_in_gb int @[json: 'volumeInGb']
volume_key string @[json: 'volumeKey'; omitempty]
volume_mount_path string @[json: 'volumeMountPath'; omitempty]
data_center_id string @[json: 'dataCenterId'; omitempty]
cuda_version string @[json: 'cudeVersion'; omitempty]
allowed_cuda_versions []string @[json: 'allowedCudaVersions']
}
// // get_endpoint gets an endpoint by ID
// pub fn (mut rp RunPod) get_endpoint(id string) !Endpoint {
// response := rp.get_endpoint_request(id)!
// endpoint := json.decode(Endpoint, response) or {
// return error('Failed to parse endpoint from response: ${response}')
// }
// return endpoint
// }
// // delete_endpoint deletes an endpoint
// pub fn (mut rp RunPod) delete_endpoint(id string) ! {
// rp.delete_endpoint_request(id)!
// }
// // list_gpu_instances lists available GPU instances
// pub fn (mut rp RunPod) list_gpu_instances() ![]GPUInstance {
// response := rp.list_gpu_instances_request()!
// instances := json.decode([]GPUInstance, response) or {
// return error('Failed to parse GPU instances from response: ${response}')
// }
// return instances
// }
// // run_on_endpoint runs a request on an endpoint
// pub fn (mut rp RunPod) run_on_endpoint(endpoint_id string, request RunRequest) !RunResponse {
// response := rp.run_on_endpoint_request(endpoint_id, request)!
// run_response := json.decode(RunResponse, response) or {
// return error('Failed to parse run response: ${response}')
// }
// return run_response
// }
// // get_run_status gets the status of a run
// pub fn (mut rp RunPod) get_run_status(endpoint_id string, run_id string) !RunResponse {
// response := rp.get_run_status_request(endpoint_id, run_id)!
// run_response := json.decode(RunResponse, response) or {
// return error('Failed to parse run status response: ${response}')
// }
// return run_response
// }
// // cfg_play configures a RunPod instance from heroscript parameters
// fn cfg_play(p paramsparser.Params) ! {
// mut rp := RunPod{
// name: p.get_default('name', 'default')!
// api_key: p.get('api_key')!
// base_url: p.get_default('base_url', 'https://api.runpod.io/v1')!
// }
// set(rp)!
// }
// fn obj_init(obj_ RunPod) !RunPod {
// // never call get here, only thing we can do here is work on object itself
// mut obj := obj_
// return obj
// }
pub fn (mut rp RunPod) create_spot_pod(input PodRentInterruptableInput) !PodFindAndDeployOnDemandResponse {
gql := build_query(BuildQueryArgs{
query_type: .mutation
method_name: 'podRentInterruptable'
}, input, PodFindAndDeployOnDemandResponse{})
println('gql: ${gql}')
response_ := rp.make_request[GqlResponse[PodFindAndDeployOnDemandResponse]](.post,
'/graphql', gql)!
println('response: ${response_}')
return response_.data['podRentInterruptable'] or {
return error('Could not find podRentInterruptable in response data: ${response_.data}')
}
}

View File

@@ -3,7 +3,7 @@ module runpod
import freeflowuniverse.herolib.core.httpconnection
import json
fn (mut rp RunPod) get_field_name(field FieldData) string {
fn get_field_name(field FieldData) string {
mut field_name := ''
// Process attributes to fetch the JSON field name or fallback to field name
if field.attrs.len > 0 {
@@ -20,53 +20,49 @@ fn (mut rp RunPod) get_field_name(field FieldData) string {
return field_name
}
fn (mut rp RunPod) get_request_fields[T](struct_ T) string {
fn get_request_fields[T](struct_ T) string {
// Start the current level
mut body_ := '{ '
mut fields := []string{}
$for field in T.fields {
mut string_ := ''
string_ += rp.get_field_name(field)
omit := 'omitempty' in field.attrs
mut empty_string := false
$if field.typ is string {
empty_string = struct_.$(field.name) == ''
}
if !omit || !empty_string {
string_ += get_field_name(field)
string_ += ': '
$if field.is_enum {
string_ += struct_.$(field.name).to_string()
}
$if field.typ is string {
} $else $if field.typ is string {
item := struct_.$(field.name)
string_ += "\"${item}\""
} $else $if field.is_array {
string_ += '['
for i in struct_.$(field.name) {
string_ += get_request_fields(i)
}
$if field.typ is int {
string_ += ']'
} $else $if field.is_struct {
string_ += get_request_fields(struct_.$(field.name))
} $else {
item := struct_.$(field.name)
string_ += '${item}'
}
// TODO: Loop only on the env map
$if field.is_array {
for i in struct_.$(field.name) {
for k, v in i {
string_ += '[{ '
string_ += "key: \"${k}\", value: \"${v}\""
string_ += ' }]'
}
}
}
$if field.is_struct {
rp.get_request_fields(struct_.$(field.name))
}
fields << string_
}
}
body_ += fields.join(', ')
body_ += ' }'
return body_
}
fn (mut rp RunPod) get_response_fields[R](struct_ R) string {
fn get_response_fields[R](struct_ R) string {
// Start the current level
mut body_ := '{ '
@@ -74,9 +70,9 @@ fn (mut rp RunPod) get_response_fields[R](struct_ R) string {
$if field.is_struct {
// Recursively process nested structs
body_ += '${field.name} '
body_ += rp.get_response_fields(struct_.$(field.name))
body_ += get_response_fields(struct_.$(field.name))
} $else {
body_ += rp.get_field_name(field)
body_ += get_field_name(field)
body_ += ' '
}
}
@@ -84,16 +80,28 @@ fn (mut rp RunPod) get_response_fields[R](struct_ R) string {
return body_
}
fn (mut rp RunPod) build_query[T, R](request T, response R) string {
pub enum QueryType {
query
mutation
}
@[params]
pub struct BuildQueryArgs {
pub:
query_type QueryType // query or mutation
method_name string
}
fn build_query[T, R](args BuildQueryArgs, request T, response R) string {
// Convert input to JSON
// input_json := json.encode(request)
// Build the GraphQL mutation string
mut request_fields := rp.get_request_fields(request)
mut response_fields := rp.get_response_fields(response)
mut request_fields := get_request_fields(request)
mut response_fields := get_response_fields(response)
// Wrap the query correctly
query := 'mutation { podFindAndDeployOnDemand(input: ${request_fields}) ${response_fields} }'
query := '${args.query_type.to_string()} { ${args.method_name}(input: ${request_fields}) ${response_fields} }'
// Wrap in the final structure
gql := GqlQuery{
@@ -104,6 +112,17 @@ fn (mut rp RunPod) build_query[T, R](request T, response R) string {
return json.encode(gql)
}
fn (q QueryType) to_string() string {
return match q {
.query {
'query'
}
.mutation {
'mutation'
}
}
}
enum HTTPMethod {
get
post