From bcd2e0afab214a85879612ad55e5d40f1e33850d Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Fri, 18 Oct 2024 11:52:06 -0500 Subject: [PATCH] Runpod: New action components --- .../runpod/actions/create-pod/create-pod.mjs | 182 ++++++++++++++++++ components/runpod/actions/get-pod/get-pod.mjs | 42 ++++ .../runpod/actions/start-pod/start-pod.mjs | 58 ++++++ .../runpod/actions/stop-pod/stop-pod.mjs | 52 +++++ components/runpod/common/mutations.mjs | 25 +++ components/runpod/common/queries.mjs | 94 +++++++++ components/runpod/common/utils.mjs | 79 ++++++++ components/runpod/package.json | 9 +- components/runpod/runpod.app.mjs | 77 +++++++- pnpm-lock.yaml | 44 ++++- 10 files changed, 654 insertions(+), 8 deletions(-) create mode 100644 components/runpod/actions/create-pod/create-pod.mjs create mode 100644 components/runpod/actions/get-pod/get-pod.mjs create mode 100644 components/runpod/actions/start-pod/start-pod.mjs create mode 100644 components/runpod/actions/stop-pod/stop-pod.mjs create mode 100644 components/runpod/common/mutations.mjs create mode 100644 components/runpod/common/queries.mjs create mode 100644 components/runpod/common/utils.mjs diff --git a/components/runpod/actions/create-pod/create-pod.mjs b/components/runpod/actions/create-pod/create-pod.mjs new file mode 100644 index 0000000000000..f497479aabad2 --- /dev/null +++ b/components/runpod/actions/create-pod/create-pod.mjs @@ -0,0 +1,182 @@ +import app from "../../runpod.app.mjs"; +import mutations from "../../common/mutations.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "runpod-create-pod", + name: "Create Pod", + description: "Creates a new pod with the specified parameters. [See the documentation](https://docs.runpod.io/sdks/graphql/manage-pods#create-spot-pod)", + version: "0.0.1", + type: "action", + props: { + app, + bidPerGpu: { + propDefinition: [ + app, + "bidPerGpu", + ], + }, + cloudType: { + type: "string", + label: "Cloud Type", + description: "The type of cloud to use for the pod.", + options: [ + "SECURE", + "COMMUNITY", + "ALL", + ], + }, + gpuCount: { + propDefinition: [ + app, + "gpuCount", + ], + }, + volumeInGb: { + type: "integer", + label: "Volume In GB", + description: "The size of the volume in GB.", + }, + containerDiskInGb: { + type: "integer", + label: "Container Disk In GB", + description: "The size of the container disk in GB.", + }, + minVcpuCount: { + type: "integer", + label: "Minimum VCPU Count", + description: "The minimum number of VCPUs.", + }, + minMemoryInGb: { + type: "integer", + label: "Minimum Memory In GB", + description: "The minimum memory size in GB.", + }, + gpuTypeId: { + propDefinition: [ + app, + "gpuTypeId", + ], + }, + name: { + type: "string", + label: "Name", + description: "The name of the pod.", + }, + imageName: { + type: "string", + label: "Image Name", + description: "The name of the image to use for the pod.", + }, + ports: { + type: "string", + label: "Ports", + description: "The ports to use for the pod.", + }, + volumeMountPath: { + type: "string", + label: "Volume Mount Path", + description: "The path where the volume will be mounted.", + }, + env: { + type: "string[]", + label: "Environment Variables", + description: "The environment variables to set for the pod. Each row should be formated with JSON string like this `{\"key\": \"ENV_NAME\", \"value\": \"ENV_VALUE\"}`", + optional: true, + }, + dockerArgs: { + type: "string", + label: "Docker Args", + description: "The arguments to pass to the docker command.", + optional: true, + }, + startJupyter: { + type: "boolean", + label: "Start Jupyter", + description: "Whether to start Jupyter for the pod.", + optional: true, + }, + startSsh: { + type: "boolean", + label: "Start SSH", + description: "Whether to start SSH for the pod.", + optional: true, + }, + stopAfter: { + type: "string", + label: "Stop After", + description: "The duration after which the pod will be stopped.", + optional: true, + }, + supportPublicIp: { + type: "boolean", + label: "Support Public IP", + description: "Whether to support public IP for the pod.", + optional: true, + }, + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the template to use for the pod.", + optional: true, + }, + }, + methods: { + createPod(variables) { + return this.app.makeRequest({ + query: mutations.createPod, + variables, + }); + }, + }, + async run({ $ }) { + const { + createPod, + bidPerGpu, + cloudType, + gpuCount, + volumeInGb, + containerDiskInGb, + minVcpuCount, + minMemoryInGb, + gpuTypeId, + name, + imageName, + ports, + volumeMountPath, + env, + dockerArgs, + startJupyter, + startSsh, + stopAfter, + supportPublicIp, + templateId, + } = this; + + const response = await createPod({ + input: utils.cleanInput({ + bidPerGpu, + cloudType, + gpuCount, + volumeInGb, + containerDiskInGb, + minVcpuCount, + minMemoryInGb, + gpuTypeId, + name, + imageName, + ports, + volumeMountPath, + env: utils.parseArray(env), + dockerArgs, + startJupyter, + startSsh, + stopAfter, + supportPublicIp, + templateId, + }), + }); + $.export("$summary", `Successfully created a new pod with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/runpod/actions/get-pod/get-pod.mjs b/components/runpod/actions/get-pod/get-pod.mjs new file mode 100644 index 0000000000000..3b4c22da51e2a --- /dev/null +++ b/components/runpod/actions/get-pod/get-pod.mjs @@ -0,0 +1,42 @@ +import app from "../../runpod.app.mjs"; +import queries from "../../common/queries.mjs"; + +export default { + key: "runpod-get-pod", + name: "Get Pod Details", + description: "Get details of a pod by ID. [See the documentation](https://docs.runpod.io/sdks/graphql/manage-pods#get-pod-by-id).", + version: "0.0.1", + type: "action", + props: { + app, + podId: { + description: "The ID of the pod to get details for.", + propDefinition: [ + app, + "podId", + ], + }, + }, + methods: { + getPodDetails(variables) { + return this.app.makeRequest({ + query: queries.getPod, + variables, + }); + }, + }, + async run({ $ }) { + const { + getPodDetails, + podId, + } = this; + + const response = await getPodDetails({ + input: { + podId, + }, + }); + $.export("$summary", `Succesfully retrieved details for pod with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/runpod/actions/start-pod/start-pod.mjs b/components/runpod/actions/start-pod/start-pod.mjs new file mode 100644 index 0000000000000..b212004b9f6b7 --- /dev/null +++ b/components/runpod/actions/start-pod/start-pod.mjs @@ -0,0 +1,58 @@ +import app from "../../runpod.app.mjs"; +import mutations from "../../common/mutations.mjs"; + +export default { + key: "runpod-start-pod", + name: "Start Pod", + description: "Starts a stopped pod, making it available for use. [See the documentation](https://docs.runpod.io/sdks/graphql/manage-pods#start-spot-pod)", + version: "0.0.1", + type: "action", + props: { + app, + podId: { + propDefinition: [ + app, + "podId", + ], + }, + gpuCount: { + propDefinition: [ + app, + "gpuCount", + ], + }, + bidPerGpu: { + propDefinition: [ + app, + "bidPerGpu", + ], + }, + }, + methods: { + startPod(variables) { + return this.app.makeRequest({ + query: mutations.startPod, + variables, + }); + }, + }, + async run({ $ }) { + const { + startPod, + podId, + gpuCount, + bidPerGpu, + } = this; + + const response = await startPod({ + input: { + podId, + gpuCount, + bidPerGpu: parseFloat(bidPerGpu), + }, + }); + + $.export("$summary", `Sucessfully started pod with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/runpod/actions/stop-pod/stop-pod.mjs b/components/runpod/actions/stop-pod/stop-pod.mjs new file mode 100644 index 0000000000000..62a97e38a733e --- /dev/null +++ b/components/runpod/actions/stop-pod/stop-pod.mjs @@ -0,0 +1,52 @@ +import app from "../../runpod.app.mjs"; +import mutations from "../../common/mutations.mjs"; + +export default { + key: "runpod-stop-pod", + name: "Stop Pod", + description: "Stops a running pod, freeing up resources and preventing further charges. [See the documentation](https://docs.runpod.io/sdks/graphql/manage-pods#stop-pods)", + version: "0.0.1", + type: "action", + props: { + app, + podId: { + description: "The ID of the pod to stop.", + propDefinition: [ + app, + "podId", + ], + }, + incrementVersion: { + type: "boolean", + label: "Increment Version", + description: "Whether to increment the pod version after stopping it.", + optional: true, + }, + }, + methods: { + stopPod(variables) { + return this.app.makeRequest({ + query: mutations.stopPod, + variables, + }); + }, + }, + async run({ $ }) { + const { + stopPod, + podId, + incrementVersion, + } = this; + + const response = await stopPod({ + input: { + podId, + incrementVersion, + }, + }); + + $.export("$summary", `Succesfully stopped pod with ID \`${response.id}\`.`); + + return response; + }, +}; diff --git a/components/runpod/common/mutations.mjs b/components/runpod/common/mutations.mjs new file mode 100644 index 0000000000000..cde9ee17d9778 --- /dev/null +++ b/components/runpod/common/mutations.mjs @@ -0,0 +1,25 @@ +import { gql } from "graphql-request"; + +export default { + createPod: gql` + mutation createPod($input: PodRentInterruptableInput!) { + podRentInterruptable(input: $input) { + id + } + } + `, + startPod: gql` + mutation startPod($input: PodBidResumeInput!) { + podResume(input: $input) { + id + } + } + `, + stopPod: gql` + mutation stopPod($input: PodStopInput!) { + podStop(input: $input) { + id + } + } + `, +}; diff --git a/components/runpod/common/queries.mjs b/components/runpod/common/queries.mjs new file mode 100644 index 0000000000000..b2fa2a38a646d --- /dev/null +++ b/components/runpod/common/queries.mjs @@ -0,0 +1,94 @@ +import { gql } from "graphql-request"; + +export default { + getPod: gql` + query pod($input: PodFilter) { + pod(input: $input) { + lowestBidPriceToResume + aiApiId + apiKey + consumerUserId + containerDiskInGb + containerRegistryAuthId + costMultiplier + costPerHr + createdAt + adjustedCostPerHr + desiredStatus + dockerArgs + dockerId + env + gpuCount + gpuPowerLimitPercent + gpus { + ...GpuFragment + } + id + imageName + lastStatusChange + locked + machineId + memoryInGb + name + podType + port + ports + registry { + ...PodRegistryFragment + } + templateId + uptimeSeconds + vcpuCount + version + volumeEncrypted + volumeInGb + volumeKey + volumeMountPath + lastStartedAt + cpuFlavorId + machineType + slsVersion + networkVolumeId + cpuFlavor { + ...CpuFlavorFragment + } + runtime { + ...PodRuntimeFragment + } + machine { + ...PodMachineInfoFragment + } + latestTelemetry { + ...PodTelemetryFragment + } + endpoint { + ...EndpointFragment + } + networkVolume { + ...NetworkVolumeFragment + } + savingsPlans { + ...SavingsPlanFragment + } + } + } + `, + listPods: gql` + query listPods { + myself { + pods { + id + name + } + } + } + `, + listGpuTypes: gql` + query listGpuTypes($input: GpuTypeFilter) { + gpuTypes(input: $input) { + id + displayName + } + } + `, +}; diff --git a/components/runpod/common/utils.mjs b/components/runpod/common/utils.mjs new file mode 100644 index 0000000000000..be4e358187f89 --- /dev/null +++ b/components/runpod/common/utils.mjs @@ -0,0 +1,79 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function cleanInput(input) { + return Object.fromEntries( + Object.entries(input) + .filter(([ + , value, + ]) => value !== null && value !== undefined) + .map(([ + key, + value, + ]) => { + if (typeof value === "string" && !isNaN(value)) { + const parsedValue = + value.includes(".") + ? parseFloat(value) + : parseInt(value, 10); + return [ + key, + parsedValue, + ]; + } + return [ + key, + value, + ]; + }), + ); +} + +function isJson(value) { + value = + typeof(value) !== "string" + ? JSON.stringify(value) + : value; + + try { + value = JSON.parse(value); + } catch (e) { + return false; + } + + return typeof(value) === "object" && value !== null; +} + +function valueToObject(value) { + if (!isJson(value)) { + return value; + } + return JSON.parse(value); +} + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid JSON array object"); + } +} + +export default { + cleanInput, + parseArray: (value) => parseArray(value)?.map(valueToObject), +}; diff --git a/components/runpod/package.json b/components/runpod/package.json index 18b4c7bb47e60..1cbf0fd361a62 100644 --- a/components/runpod/package.json +++ b/components/runpod/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/runpod", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream RunPod Components", "main": "runpod.app.mjs", "keywords": [ @@ -11,5 +11,10 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3", + "graphql": "^16.9.0", + "graphql-request": "^7.1.0" } -} \ No newline at end of file +} diff --git a/components/runpod/runpod.app.mjs b/components/runpod/runpod.app.mjs index b7ea347fb1b3c..5435b7bfb455b 100644 --- a/components/runpod/runpod.app.mjs +++ b/components/runpod/runpod.app.mjs @@ -1,11 +1,78 @@ +import "graphql/language/index.js"; +import { GraphQLClient } from "graphql-request"; +import queries from "./common/queries.mjs"; + export default { type: "app", app: "runpod", - propDefinitions: {}, + propDefinitions: { + bidPerGpu: { + type: "string", + label: "Bid Per GPU", + description: "The bid amount per GPU for spot pods.", + }, + gpuCount: { + type: "integer", + label: "GPU Count", + description: "The number of GPUs to allocate to the pod. Set to 0 for pods without GPU.", + }, + podId: { + type: "string", + label: "Pod ID", + description: "The ID of the pod to start.", + async options() { + const { myself: { pods } } = await this.listPods(); + return pods.map(({ + id: value, name: label, + }) => ({ + label: label || value, + value, + })); + }, + }, + gpuTypeId: { + type: "string", + label: "GPU Type ID", + description: "The ID of the GPU type to use for the pod.", + async options({ input = {} }) { + const { gpuTypes } = await this.listGpuTypes({ + input, + }); + return gpuTypes.map(({ + id: value, displayName: label, + }) => ({ + label: label || value, + value, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl() { + return `https://api.runpod.io/graphql?api_key=${this.$auth.api_key}`; + }, + getClient() { + return new GraphQLClient(this.getUrl(), { + headers: { + "Content-Type": "application/json", + }, + }); + }, + makeRequest({ + query, variables, + } = {}) { + return this.getClient().request(query, variables); + }, + listPods() { + return this.makeRequest({ + query: queries.listPods, + }); + }, + listGpuTypes(variables) { + return this.makeRequest({ + query: queries.listGpuTypes, + variables, + }); }, }, -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f58d06fce9cd9..b96d32ec0d27e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8456,7 +8456,14 @@ importers: specifiers: {} components/runpod: - specifiers: {} + specifiers: + '@pipedream/platform': 3.0.3 + graphql: ^16.9.0 + graphql-request: ^7.1.0 + dependencies: + '@pipedream/platform': 3.0.3 + graphql: 16.9.0 + graphql-request: 7.1.0_graphql@16.9.0 components/rytr: specifiers: {} @@ -16797,6 +16804,14 @@ packages: graphql: 16.8.1 dev: false + /@graphql-typed-document-node/core/3.2.0_graphql@16.9.0: + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + graphql: 16.9.0 + dev: false + /@grpc/grpc-js/1.11.1: resolution: {integrity: sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==} engines: {node: '>=12.10.0'} @@ -26725,6 +26740,28 @@ packages: zod: 3.23.8 dev: false + /graphql-request/7.1.0_graphql@16.9.0: + resolution: {integrity: sha512-Ouu/lYVFhARS1aXeZoVJWnGT6grFJXTLwXJuK4mUGGRo0EUk1JkyYp43mdGmRgUVezpRm6V5Sq3t8jBDQcajng==} + hasBin: true + peerDependencies: + '@dprint/formatter': ^0.3.0 + '@dprint/typescript': ^0.91.1 + dprint: ^0.46.2 + graphql: 14 - 16 + peerDependenciesMeta: + '@dprint/formatter': + optional: true + '@dprint/typescript': + optional: true + dprint: + optional: true + dependencies: + '@graphql-typed-document-node/core': 3.2.0_graphql@16.9.0 + '@molt/command': 0.9.0 + graphql: 16.9.0 + zod: 3.23.8 + dev: false + /graphql/15.8.0: resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==} engines: {node: '>= 10.x'} @@ -26734,6 +26771,11 @@ packages: resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + /graphql/16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: false + /gtoken/5.3.2: resolution: {integrity: sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==} engines: {node: '>=10'}