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:
@@ -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}')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user