diff --git a/.gitignore b/.gitignore
index 4acb03c..332d09b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
# Binaries for programs and plugins
+.DS_STORE
*.exe
*.exe~
*.dll
*.so
*.dylib
+runpodctl
# Test binary, built with `go test -c`
*.test
diff --git a/README.md b/README.md
index 2ea28ac..20527c7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# RunPod CLI
+# Runpod CLI
runpodctl is the CLI tool to automate / manage GPU pods for [runpod.io](https://runpod.io).
@@ -10,7 +10,7 @@ _Note: All pods automatically come with runpodctl installed with a pod-scoped AP
## Table of Contents
-- [RunPod CLI](#runpod-cli)
+- [Runpod CLI](#runpod-cli)
- [Table of Contents](#table-of-contents)
- [Get Started](#get-started)
- [Install](#install)
@@ -57,8 +57,8 @@ Please checkout this [video tutorial](https://www.youtube.com/watch?v=QN1vdGhjcR
- [Installing the latest version of runpodctl](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=1384s)
- [Uploading large datasets](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=2068s)
-- [File transfers from PC to RunPod](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=2106s)
-- [Downloading folders from RunPod](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=2549s)
+- [File transfers from PC to Runpod](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=2106s)
+- [Downloading folders from Runpod](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=2549s)
- [Adding runpodctl to your environment path](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=2589s)
- [Downloading model files](https://www.youtube.com/watch?v=QN1vdGhjcRc&t=4871s)
@@ -101,7 +101,7 @@ data.txt 100% |████████████████████| ( 5
### Using Google Drive
-You can use the following links for google colab
+You can use the following links for Google Colab
[Send](https://colab.research.google.com/drive/1UaODD9iGswnKF7SZfsvwHDGWWwLziOsr#scrollTo=2nlcIAY3gGLt)
@@ -109,7 +109,7 @@ You can use the following links for google colab
## Pod Commands
-Before using pod commands, configure the API key obtained from your [RunPod account](https://runpod.io/console/user/settings).
+Before using pod commands, configure the API key obtained from your [Runpod account](https://runpod.io/console/user/settings).
```bash
# configure API key
@@ -132,7 +132,7 @@ runpodctl start pod {podId} --bid=0.3
runpodctl stop pod {podId}
```
-For a comprehensive list of commands, visit [RunPod CLI documentation](docs/runpodctl.md).
+For a comprehensive list of commands, visit [Runpod CLI documentation](docs/runpodctl.md).
## Acknowledgements
diff --git a/api/query.go b/api/query.go
index 5ecfd8e..db383f8 100644
--- a/api/query.go
+++ b/api/query.go
@@ -8,7 +8,6 @@ import (
"net/http"
"os"
"runtime"
- "strings"
"time"
"github.com/spf13/viper"
@@ -46,8 +45,7 @@ func Query(input Input) (res *http.Response, err error) {
return
}
- sanitizedVersion := strings.TrimRight(Version, "\r\n")
- userAgent := "RunPod-CLI/" + sanitizedVersion + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
+ userAgent := "Runpod-CLI/" + Version + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
req.Header.Add("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
diff --git a/api/volume.go b/api/volume.go
index 8e11a02..0d83334 100644
--- a/api/volume.go
+++ b/api/volume.go
@@ -50,7 +50,7 @@ func GetNetworkVolumes() (volumes []*NetworkVolume, err error) {
err = errors.New(data.Errors[0].Message)
return nil, err
}
- if data == nil || data.Data == nil || data.Data.Myself == nil || data.Data.Myself.NetworkVolumes == nil {
+ if data.Data == nil || data.Data.Myself == nil || data.Data.Myself.NetworkVolumes == nil {
err = fmt.Errorf("data is nil: %s", string(rawData))
return nil, err
}
diff --git a/cmd/config/config.go b/cmd/config/config.go
index 3a3b946..8d2fcc5 100644
--- a/cmd/config/config.go
+++ b/cmd/config/config.go
@@ -20,7 +20,7 @@ var (
var ConfigCmd = &cobra.Command{
Use: "config",
Short: "Manage CLI configuration",
- Long: "RunPod CLI Config Settings",
+ Long: "Runpod CLI Config Settings",
RunE: func(c *cobra.Command, args []string) error {
if err := saveConfig(); err != nil {
return fmt.Errorf("error saving config: %w", err)
@@ -57,7 +57,7 @@ func getOrCreateSSHKey() ([]byte, error) {
if publicKey == nil {
fmt.Println("No existing local SSH key found, generating a new one.")
- publicKey, err = ssh.GenerateSSHKeyPair("RunPod-Key-Go")
+ publicKey, err = ssh.GenerateSSHKeyPair("Runpod-Key-Go")
if err != nil {
return nil, fmt.Errorf("failed to generate SSH key: %w", err)
}
@@ -102,11 +102,11 @@ func ensureSSHKeyInCloud(publicKey []byte) error {
}
func init() {
- ConfigCmd.Flags().StringVar(&apiKey, "apiKey", "", "RunPod API key")
+ ConfigCmd.Flags().StringVar(&apiKey, "apiKey", "", "Runpod API key")
viper.BindPFlag("apiKey", ConfigCmd.Flags().Lookup("apiKey")) //nolint
viper.SetDefault("apiKey", "")
- ConfigCmd.Flags().StringVar(&apiUrl, "apiUrl", "https://api.runpod.io/graphql", "RunPod API URL")
+ ConfigCmd.Flags().StringVar(&apiUrl, "apiUrl", "https://api.runpod.io/graphql", "Runpod API URL")
viper.BindPFlag("apiUrl", ConfigCmd.Flags().Lookup("apiUrl")) //nolint
ConfigCmd.MarkFlagRequired("apiKey")
diff --git a/cmd/exec/functions.go b/cmd/exec/functions.go
index 59a6b9f..1d04287 100644
--- a/cmd/exec/functions.go
+++ b/cmd/exec/functions.go
@@ -3,11 +3,11 @@ package exec
import (
"fmt"
- "github.com/runpod/runpodctl/cmd/project"
+ "github.com/runpod/runpodctl/cmd/ssh"
)
func PythonOverSSH(podID string, file string) error {
- sshConn, err := project.PodSSHConnection(podID)
+ sshConn, err := ssh.PodSSHConnection(podID)
if err != nil {
return fmt.Errorf("getting SSH connection: %w", err)
}
diff --git a/cmd/project.go b/cmd/project.go
deleted file mode 100644
index 705f77a..0000000
--- a/cmd/project.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package cmd
-
-import (
- "github.com/runpod/runpodctl/cmd/project"
-
- "github.com/spf13/cobra"
-)
-
-var projectCmd = &cobra.Command{
- Use: "project [command]",
- Short: "Manage RunPod projects",
- Long: "Develop and deploy projects entirely on RunPod's infrastructure.",
-}
-
-func init() {
- projectCmd.AddCommand(project.NewProjectCmd)
- projectCmd.AddCommand(project.StartProjectCmd)
- projectCmd.AddCommand(project.DeployProjectCmd)
- projectCmd.AddCommand(project.BuildProjectCmd)
-}
diff --git a/cmd/project/defaults.go b/cmd/project/defaults.go
deleted file mode 100644
index 1658d07..0000000
--- a/cmd/project/defaults.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package project
-
-// Returns default model name for the example model type
-func getDefaultModelName(modelType string) string {
- switch modelType {
- case "LLM":
- return "google/flan-t5-base"
- case "Stable_Diffusion":
- return "stabilityai/sdxl-turbo"
- case "Text_to_Audio":
- return "facebook/musicgen-small"
- }
-
- return ""
-}
diff --git a/cmd/project/exampleDockerfile b/cmd/project/exampleDockerfile
deleted file mode 100644
index 186e5bc..0000000
--- a/cmd/project/exampleDockerfile
+++ /dev/null
@@ -1,28 +0,0 @@
-# AUTOGENERATED Dockerfile using runpodctl project build
-
-# Base image -> https://github.com/runpod/containers/blob/main/official-templates/base/Dockerfile
-# DockerHub -> https://hub.docker.com/r/runpod/base/tags
-FROM <
>
-
-# The RunPod base image pre-installs many system dependencies to help you get started quickly.
-# Check the base image's Dockerfile before adding more dependencies.
-# IMPORTANT: The base image overrides the default Hugging Face cache location.
-
-# System dependencies
-# If you need system dependencies that are not included in the base image, add them here.
-# You must override the base image in runpod.toml with an image that includes your dependencies
-# for changes to propagate to your Project pod.
-
-# Python dependencies
-COPY <> /requirements.txt
-RUN python<> -m pip install --upgrade pip && \
- python<> -m pip install --upgrade -r /requirements.txt --no-cache-dir && \
- rm /requirements.txt
-
-# NOTE: The base image comes with multiple Python versions pre-installed.
-# It is recommended to specify the version of Python when running your code.
-
-# Add src files (Worker Template)
-ADD . /
-<>
-CMD python<> -u /<>
diff --git a/cmd/project/functions.go b/cmd/project/functions.go
deleted file mode 100644
index 856d0c9..0000000
--- a/cmd/project/functions.go
+++ /dev/null
@@ -1,653 +0,0 @@
-package project
-
-import (
- "embed"
- "errors"
- "fmt"
- "io/fs"
- "log"
- "os"
- "path"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/runpod/runpodctl/api"
-
- "github.com/pelletier/go-toml"
-)
-
-// TODO: embed all hidden files even those not at top level
-//
-//go:embed starter_examples/* starter_examples/*/.*
-var starterTemplates embed.FS
-
-//go:embed exampleDockerfile
-var dockerfileTemplate embed.FS
-
-const basePath string = "starter_examples"
-
-func baseDockerImage(cudaVersion string) string {
- return fmt.Sprintf("runpod/base:0.4.4-cuda%s", cudaVersion)
-}
-
-func copyFiles(files fs.FS, source string, dest string) error {
- return fs.WalkDir(files, source, func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- // Skip the base directory
- if path == source {
- return nil
- }
-
- relPath, err := filepath.Rel(source, path)
- if err != nil {
- return err
- }
-
- // Generate the corresponding path in the new project folder
- newPath := filepath.Join(dest, relPath)
- if d.IsDir() {
- if err := os.MkdirAll(newPath, os.ModePerm); err != nil {
- return err
- }
- } else {
- content, err := fs.ReadFile(files, path)
- if err != nil {
- return err
- }
- if err := os.WriteFile(newPath, content, 0o644); err != nil {
- return err
- }
- }
- return nil
- })
-}
-
-func createNewProject(projectName string, cudaVersion string, pythonVersion string, modelType string, modelName string, initCurrentDir bool) {
- projectFolder, err := os.Getwd()
- if err != nil {
- log.Fatalf("Failed to get current working directory: %v", err)
- }
-
- if !initCurrentDir {
- projectFolder = filepath.Join(projectFolder, projectName)
-
- if _, err := os.Stat(projectFolder); os.IsNotExist(err) {
- if err := os.Mkdir(projectFolder, 0o755); err != nil {
- log.Fatalf("Failed to create project directory: %v", err)
- }
- }
-
- if modelType == "" {
- modelType = "default"
- }
-
- if modelName == "" {
- modelName = getDefaultModelName(modelType)
- }
-
- examplePath := fmt.Sprintf("%s/%s", basePath, modelType)
- err = copyFiles(starterTemplates, examplePath, projectFolder)
- if err := copyFiles(starterTemplates, examplePath, projectFolder); err != nil {
- log.Fatalf("Failed to copy starter example: %v", err)
- }
-
- // Swap out the model name in handler.py
- handlerPath := fmt.Sprintf("%s/src/handler.py", projectFolder)
- handlerContentBytes, _ := os.ReadFile(handlerPath)
- handlerContent := string(handlerContentBytes)
- handlerContent = strings.ReplaceAll(handlerContent, "<>", modelName)
- os.WriteFile(handlerPath, []byte(handlerContent), 0o644)
-
- requirementsPath := fmt.Sprintf("%s/builder/requirements.txt", projectFolder)
- requirementsContentBytes, _ := os.ReadFile(requirementsPath)
- requirementsContent := string(requirementsContentBytes)
- // in requirements, replace <> with runpod-python import
- // TODO determine version to lock runpod-python at
- requirementsContent = strings.ReplaceAll(requirementsContent, "<>", "runpod")
- os.WriteFile(requirementsPath, []byte(requirementsContent), 0o644)
- }
-
- generateProjectToml(projectFolder, "runpod.toml", projectName, cudaVersion, pythonVersion)
-}
-
-func loadProjectConfig() *toml.Tree {
- projectFolder, _ := os.Getwd()
- tomlPath := filepath.Join(projectFolder, "runpod.toml")
- toml, err := toml.LoadFile(tomlPath)
- if err != nil {
- panic("runpod.toml not found in the current directory.")
- }
- return toml
-}
-
-func getProjectPod(projectId string) (string, error) {
- pods, err := api.GetPods()
- if err != nil {
- return "", err
- }
- for _, pod := range pods {
- if strings.Contains(pod.Name, projectId) {
- return pod.Id, nil
- }
- }
- return "", errors.New("pod does not exist for project")
-}
-
-func getProjectEndpoint(projectId string) (string, error) {
- endpoints, err := api.GetEndpoints()
- if err != nil {
- return "", err
- }
- for _, endpoint := range endpoints {
- if strings.Contains(endpoint.Name, projectId) {
- fmt.Println(endpoint.Id)
- return endpoint.Id, nil
- }
- }
- return "", errors.New("endpoint does not exist for project")
-}
-
-func attemptPodLaunch(config *toml.Tree, networkVolumeId string, environmentVariables map[string]string, selectedGpuTypes []string) (pod map[string]interface{}, err error) {
- projectConfig := config.Get("project").(*toml.Tree)
- // attempt to launch a pod with the given configuration.
- for _, gpuType := range selectedGpuTypes {
- fmt.Printf("Trying to get a Pod with %s... ", gpuType)
- podEnv := mapToApiEnv(environmentVariables)
- input := api.CreatePodInput{
- CloudType: "ALL",
- ContainerDiskInGb: int(projectConfig.Get("container_disk_size_gb").(int64)),
- // DeployCost: projectConfig.Get(""),
- DockerArgs: "",
- Env: podEnv,
- GpuCount: int(projectConfig.Get("gpu_count").(int64)),
- GpuTypeId: gpuType,
- ImageName: projectConfig.Get("base_image").(string),
- MinMemoryInGb: 1,
- MinVcpuCount: 1,
- Name: fmt.Sprintf("%s-dev (%s)", config.Get("name"), projectConfig.Get("uuid")),
- NetworkVolumeId: networkVolumeId,
- Ports: strings.ReplaceAll(projectConfig.Get("ports").(string), " ", ""),
- SupportPublicIp: true,
- StartSSH: true,
- // TemplateId: projectConfig.Get(""),
- VolumeInGb: 0,
- VolumeMountPath: projectConfig.Get("volume_mount_path").(string),
- }
- pod, err := api.CreatePod(&input)
- if err != nil {
- fmt.Println("Unavailable.")
- continue
- }
- fmt.Println("Success!")
- return pod, nil
- }
- return nil, errors.New("none of the selected GPU types were available")
-}
-
-func launchDevPod(config *toml.Tree, networkVolumeId string) (string, error) {
- fmt.Println("Deploying project Pod on RunPod...")
- // construct env vars
- environmentVariables := createEnvVars(config)
- // prepare gpu types
- selectedGpuTypes := []string{}
- tomlGpuTypes := config.GetPath([]string{"project", "gpu_types"})
- if tomlGpuTypes != nil {
- for _, v := range tomlGpuTypes.([]interface{}) {
- selectedGpuTypes = append(selectedGpuTypes, v.(string))
- }
- }
- tomlGpu := config.GetPath([]string{"project", "gpu"}) // legacy
- if tomlGpu != nil {
- selectedGpuTypes = append(selectedGpuTypes, tomlGpu.(string))
- }
- // attempt to launch a pod with the given configuration
- new_pod, err := attemptPodLaunch(config, networkVolumeId, environmentVariables, selectedGpuTypes)
- if err != nil {
- fmt.Println(err)
- return "", err
- }
- fmt.Printf("Check on Pod status at https://www.runpod.io/console/pods/%s\n", new_pod["id"].(string))
- return new_pod["id"].(string), nil
-}
-
-func createEnvVars(config *toml.Tree) map[string]string {
- environmentVariables := map[string]string{}
- tomlEnvVars := config.GetPath([]string{"project", "env_vars"})
- if tomlEnvVars != nil {
- tomlEnvVarsMap := tomlEnvVars.(*toml.Tree).ToMap()
- for k, v := range tomlEnvVarsMap {
- environmentVariables[k] = v.(string)
- }
- }
- environmentVariables["RUNPOD_PROJECT_ID"] = config.GetPath([]string{"project", "uuid"}).(string)
- return environmentVariables
-}
-
-func mapToApiEnv(env map[string]string) []*api.PodEnv {
- podEnv := []*api.PodEnv{}
- for k, v := range env {
- podEnv = append(podEnv, &api.PodEnv{Key: k, Value: v})
- }
- return podEnv
-}
-
-func formatAsDockerEnv(env map[string]string) string {
- result := ""
- for k, v := range env {
- result += fmt.Sprintf("ENV %s=%s\n", k, v)
- }
- return result
-}
-
-func startProject(networkVolumeId string) error {
- // parse project toml
- config := loadProjectConfig()
- fmt.Println(config)
-
- // Project ID
- projectId, ok := config.GetPath([]string{"project", "uuid"}).(string)
- if !ok {
- return fmt.Errorf("project ID not found in config")
- }
-
- // Project Name
- projectName, ok := config.GetPath([]string{"name"}).(string)
- if !ok {
- return fmt.Errorf("project name not found in config")
- }
-
- // check for existing pod
- projectPodId, err := getProjectPod(projectId)
- if projectPodId == "" || err != nil {
- // or try to get pod with one of gpu types
- projectPodId, err = launchDevPod(config, networkVolumeId)
- if err != nil {
- return err
- }
- }
-
- // open ssh connection
- sshConn, err := PodSSHConnection(projectPodId)
- if err != nil {
- fmt.Println("error establishing SSH connection to Pod: ", err)
- return err
- }
-
- fmt.Println(fmt.Sprintf("Project %s Pod (%s) created.", projectName, projectPodId))
- // create remote folder structure
- projectConfig := config.Get("project").(*toml.Tree)
- volumePath := projectConfig.Get("volume_mount_path").(string)
- projectPathUuid := path.Join(volumePath, projectConfig.Get("uuid").(string))
- projectPathUuidDev := path.Join(projectPathUuid, "dev")
- projectPathUuidProd := path.Join(projectPathUuid, "prod")
- remoteProjectPath := path.Join(projectPathUuidDev, projectName)
- var fastAPIPort int
- if strings.Contains(projectConfig.Get("ports").(string), "8080/http") && !strings.Contains(projectConfig.Get("ports").(string), "7270/http") {
- fastAPIPort = 8080
- } else {
- fastAPIPort = 7270
- }
- fmt.Printf("Checking remote project folder: %s on Pod %s\n", remoteProjectPath, projectPodId)
- sshConn.RunCommands([]string{fmt.Sprintf("mkdir -p %s %s", remoteProjectPath, projectPathUuidProd)})
- // rsync project files
- fmt.Printf("Syncing files to Pod %s\n", projectPodId)
- cwd, _ := os.Getwd()
- sshConn.Rsync(cwd, projectPathUuidDev, false)
- // activate venv on remote
- venvPath := "/" + path.Join(projectId, "venv")
- archivedVenvPath := path.Join(projectPathUuid, "dev-venv.tar.zst")
- fmt.Printf("Activating Python virtual environment %s on Pod %s\n", venvPath, projectPodId)
- sshConn.RunCommands([]string{
- fmt.Sprint(`
- DEPENDENCIES=("wget" "sudo" "lsof" "git" "rsync" "zstd")
-
- function check_and_install_dependencies() {
- for dep in "${DEPENDENCIES[@]}"; do
- if ! command -v $dep &> /dev/null; then
- echo "$dep could not be found, attempting to install..."
- apt-get update && apt-get install -y $dep
- if [ $? -eq 0 ]; then
- echo "$dep installed successfully."
- else
- echo "Failed to install $dep."
- exit 1
- fi
- fi
- done
-
- # Specifically check for inotifywait command from inotify-tools package
- if ! command -v inotifywait &> /dev/null; then
- echo "inotifywait could not be found, attempting to install inotify-tools..."
- if apt-get install -y inotify-tools; then
- echo "inotify-tools installed successfully."
- else
- echo "Failed to install inotify-tools."
- exit 1
- fi
- fi
-
- wget -qO- cli.runpod.net | sudo bash &> /dev/null
- }
- check_and_install_dependencies`),
- fmt.Sprintf(`
- if ! [ -f %s/bin/activate ]
- then
- if [ -f %s ]
- then
- echo "Retrieving existing venv from network volume..."
- mkdir -p %s && tar -xf %s -C %s
- else
- echo "Creating new venv..."
- python%s -m virtualenv %s
- fi
- fi`, venvPath, archivedVenvPath, venvPath, archivedVenvPath, venvPath, config.GetPath([]string{"runtime", "python_version"}).(string), venvPath),
- fmt.Sprintf(`source %s/bin/activate &&
- cd %s &&
- python -m pip install --upgrade pip &&
- python -m pip install -v --requirement %s --report /installreport.json`,
- venvPath, remoteProjectPath, config.GetPath([]string{"runtime", "requirements_path"}).(string)),
- })
-
- // create file watcher
- fmt.Println("Creating Project watcher...")
- go sshConn.SyncDir(cwd, projectPathUuidDev)
-
- // run launch api server / hot reload loop
- pipReqPath := path.Join(remoteProjectPath, config.GetPath([]string{"runtime", "requirements_path"}).(string))
- handlerPath := path.Join(remoteProjectPath, config.GetPath([]string{"runtime", "handler_path"}).(string))
- launchApiServer := fmt.Sprintf(`
- #!/bin/bash
- if [ -z "${BASE_RELEASE_VERSION}" ]; then
- API_PORT=%d
- PRINTED_API_PORT=$API_PORT
- else
- API_PORT=7271
- PRINTED_API_PORT=7270
- fi
- API_HOST="0.0.0.0"
- PYTHON_VENV_PATH="%s" # Path to the Python virutal environment used during development located on the Pod at //venv
- PROJECT_DIRECTORY="%s/%s"
- VENV_ARCHIVE_PATH="%s"
- HANDLER_PATH="%s"
- REQUIRED_FILES="%s"
-
- pkill inotify # Kill any existing inotify processes
-
- function start_api_server {
- lsof -ti:$API_PORT | xargs kill -9 2>/dev/null # Kill the old API server if it's still running
- python $1 --rp_serve_api --rp_api_host="$API_HOST" --rp_api_port=$API_PORT --rp_api_concurrency=1 &
- SERVER_PID=$!
- }
-
- function force_kill {
- if [[ -z "$1" ]]; then
- echo "No PID provided for force_kill."
- return
- fi
-
- kill $1 2>/dev/null
-
- for i in {1..5}; do # Wait up to 5 seconds, checking every second.
- if ! ps -p $1 > /dev/null 2>&1; then
- echo "Process $1 has been gracefully terminated."
- return
- fi
- sleep 1
- done
-
- echo "Graceful kill failed, attempting SIGKILL..."
- kill -9 $1 2>/dev/null
-
- for i in {1..5}; do # Wait up to 5 seconds, checking every second.
- if ! ps -p $1 >/dev/null 2>&1; then
- echo "Process $1 has been killed with SIGKILL."
- return
- fi
- sleep 1
- done
-
- echo "Failed to kill process with PID: $1 after SIGKILL attempt."
- exit 1
- }
-
- function cleanup {
- echo "Cleaning up..."
- force_kill $SERVER_PID
- }
- trap cleanup EXIT SIGINT
-
- if source $PYTHON_VENV_PATH/bin/activate; then
- echo -e "- Activated project environment."
- else
- echo "Failed to activate project environment."
- exit 1
- fi
-
- if cd $PROJECT_DIRECTORY; then
- echo -e "- Changed to project directory."
- else
- echo "Failed to change directory."
- exit 1
- fi
-
- function tar_venv {
- if ! [ $(cat /installreport.json | grep "install" | grep -c "\[\]") -eq 1 ]
- then
- tar -c -C $PYTHON_VENV_PATH . | zstd -T0 > /venv.tar.zst;
- mv /venv.tar.zst $VENV_ARCHIVE_PATH ;
- echo "Synced venv to network volume"
- fi
- }
-
- tar_venv &
-
- # Start the API server in the background, and save the PID
- start_api_server $HANDLER_PATH
-
- echo -e "- Started API server with PID: $SERVER_PID" && echo ""
- echo "Connect to the API server at:"
- echo "> https://$RUNPOD_POD_ID-$PRINTED_API_PORT.proxy.runpod.net" && echo ""
-
- #like inotifywait, but will only report the name of a file if it shouldn't be ignored according to .runpodignore
- #uses git check-ignore to ensure same syntax as gitignore, but git check-ignore expects to be run in a repo
- #so we must set up a git-repo-like file structure in some temp directory
- function notify_nonignored_file {
- local tmp_dir=$(mktemp -d)
- cp .runpodignore "$tmp_dir/.gitignore"
- cd "$tmp_dir" && git init -q # Setup a temporary git repo to leverage .gitignore
-
- local project_directory="$PROJECT_DIRECTORY"
-
- # Listen for file changes.
- inotifywait -q -r -e modify,create,delete --format '%%w%%f' "$project_directory" | while read -r file; do
- # Convert each file path to a relative path and check if it's ignored by git
- local rel_path=$(realpath --relative-to="$project_directory" "$file")
- if ! git check-ignore -q "$rel_path"; then
- echo "$rel_path"
- fi
- done
-
- cd - > /dev/null # Return to the original directory
- rm -rf "$tmp_dir"
- }
- trap '[[ -n $tmp_dir && -d $tmp_dir ]] && rm -rf "$tmp_dir"' EXIT
-
- monitor_and_restart() {
- while true; do
- if changed_file=$(notify_nonignored_file); then
- echo "Found changes in: $changed_file"
- else
- echo "No changes found."
- exit 1
- fi
-
- force_kill $SERVER_PID
-
- # Install new requirements if requirements.txt was changed
- if [[ $changed_file == *"requirements"* ]]; then
- echo "Installing new requirements..."
- python -m pip install --upgrade pip && python -m pip install -r $REQUIRED_FILES --report /installreport.json
- tar_venv &
- fi
-
- # Restart the API server in the background, and save the PID
- start_api_server $HANDLER_PATH
-
- echo "Restarted API server with PID: $SERVER_PID"
- done
- }
-
- monitor_and_restart
- `, fastAPIPort, venvPath, projectPathUuidDev, projectName, archivedVenvPath, handlerPath, pipReqPath)
- fmt.Println()
- fmt.Println("Starting project endpoint...")
- sshConn.RunCommand(launchApiServer)
- return nil
-}
-
-func deployProject(networkVolumeId string) (endpointId string, err error) {
- // parse project toml
- config := loadProjectConfig()
- projectId := config.GetPath([]string{"project", "uuid"}).(string)
- projectConfig := config.Get("project").(*toml.Tree)
- projectName := config.Get("name").(string)
- projectPathUuid := path.Join(projectConfig.Get("volume_mount_path").(string), projectConfig.Get("uuid").(string))
- projectPathUuidProd := path.Join(projectPathUuid, "prod")
- remoteProjectPath := path.Join(projectPathUuidProd, config.Get("name").(string))
- venvPath := path.Join(projectPathUuidProd, "venv")
- // check for existing pod
- fmt.Println("Finding a pod for initial file sync")
- projectPodId, err := getProjectPod(projectId)
- if projectPodId == "" || err != nil {
- // or try to get pod with one of gpu types
- projectPodId, err = launchDevPod(config, networkVolumeId)
- if err != nil {
- return "", err
- }
- }
- // open ssh connection
- sshConn, err := PodSSHConnection(projectPodId)
- if err != nil {
- fmt.Println("error establishing SSH connection to Pod: ", err)
- return "", err
- }
- // sync remote dev to remote prod
- sshConn.RunCommand(fmt.Sprintf("mkdir -p %s", remoteProjectPath))
- fmt.Printf("Syncing files to Pod %s prod\n", projectPodId)
- cwd, _ := os.Getwd()
- sshConn.Rsync(cwd, projectPathUuidProd, false)
- // activate venv on remote
- fmt.Printf("Activating Python virtual environment: %s on Pod %s\n", venvPath, projectPodId)
- sshConn.RunCommands([]string{
- fmt.Sprintf("python%s -m venv %s", config.GetPath([]string{"runtime", "python_version"}).(string), venvPath),
- fmt.Sprintf(`source %s/bin/activate &&
- cd %s &&
- python -m pip install --upgrade pip &&
- python -m pip install -v --requirement %s`,
- venvPath, remoteProjectPath, config.GetPath([]string{"runtime", "requirements_path"}).(string)),
- })
- env := mapToApiEnv(createEnvVars(config))
- // Construct the docker start command
- handlerPath := path.Join(remoteProjectPath, config.GetPath([]string{"runtime", "handler_path"}).(string))
- activateCmd := fmt.Sprintf(". %s/bin/activate", venvPath)
- pythonCmd := fmt.Sprintf("python -u %s", handlerPath)
- dockerStartCmd := "bash -c \"" + activateCmd + " && " + pythonCmd + "\""
- // deploy new template
- projectEndpointTemplateId, err := api.CreateTemplate(&api.CreateTemplateInput{
- Name: fmt.Sprintf("%s-endpoint-%s-%d", projectName, projectId, time.Now().UnixMilli()),
- ImageName: projectConfig.Get("base_image").(string),
- Env: env,
- DockerStartCmd: dockerStartCmd,
- IsServerless: true,
- ContainerDiskInGb: int(projectConfig.Get("container_disk_size_gb").(int64)),
- VolumeMountPath: projectConfig.Get("volume_mount_path").(string),
- StartSSH: true,
- IsPublic: false,
- Readme: "",
- })
- if err != nil {
- fmt.Println("error making template")
- return "", err
- }
- // deploy / update endpoint
- deployedEndpointId, err := getProjectEndpoint(projectId)
- // default endpoint settings
- minWorkers := 0
- maxWorkers := 3
- flashboot := true
- flashbootSuffix := " -fb"
- idleTimeout := 5
- endpointConfig, ok := config.Get("endpoint").(*toml.Tree)
- if ok {
- if min, ok := endpointConfig.Get("active_workers").(int64); ok {
- minWorkers = int(min)
- }
- if max, ok := endpointConfig.Get("max_workers").(int64); ok {
- maxWorkers = int(max)
- }
- if fb, ok := endpointConfig.Get("flashboot").(bool); ok {
- flashboot = fb
- }
- if !flashboot {
- flashbootSuffix = ""
- }
- if idle, ok := endpointConfig.Get("idle_timeout").(int64); ok {
- idleTimeout = int(idle)
- }
- }
- if err != nil {
- deployedEndpointId, err = api.CreateEndpoint(&api.CreateEndpointInput{
- Name: fmt.Sprintf("%s-endpoint-%s%s", projectName, projectId, flashbootSuffix),
- TemplateId: projectEndpointTemplateId,
- NetworkVolumeId: networkVolumeId,
- GpuIds: "AMPERE_16",
- IdleTimeout: idleTimeout,
- ScalerType: "QUEUE_DELAY",
- ScalerValue: 4,
- WorkersMin: minWorkers,
- WorkersMax: maxWorkers,
- })
- if err != nil {
- fmt.Println("error making endpoint")
- return "", err
- }
- } else {
- err = api.UpdateEndpointTemplate(deployedEndpointId, projectEndpointTemplateId)
- if err != nil {
- fmt.Println("error updating endpoint template")
- return "", err
- }
- }
- return deployedEndpointId, nil
-}
-
-func buildProjectDockerfile() {
- // parse project toml
- config := loadProjectConfig()
- projectConfig := config.Get("project").(*toml.Tree)
- runtimeConfig := config.Get("runtime").(*toml.Tree)
- // build Dockerfile
- dockerfileBytes, _ := dockerfileTemplate.ReadFile("exampleDockerfile")
- dockerfile := string(dockerfileBytes)
- // base image: from toml
- dockerfile = strings.ReplaceAll(dockerfile, "<>", projectConfig.Get("base_image").(string))
- // pip requirements
- dockerfile = strings.ReplaceAll(dockerfile, "<>", runtimeConfig.Get("requirements_path").(string))
- dockerfile = strings.ReplaceAll(dockerfile, "<>", runtimeConfig.Get("python_version").(string))
- // cmd: start handler
- dockerfile = strings.ReplaceAll(dockerfile, "<>", runtimeConfig.Get("handler_path").(string))
- if includeEnvInDockerfile {
- dockerEnv := formatAsDockerEnv(createEnvVars(config))
- dockerfile = strings.ReplaceAll(dockerfile, "<>", "\n"+dockerEnv)
- } else {
- dockerfile = strings.ReplaceAll(dockerfile, "<>", "")
- }
- // save to Dockerfile in project directory
- projectFolder, _ := os.Getwd()
- dockerfilePath := filepath.Join(projectFolder, "Dockerfile")
- os.WriteFile(dockerfilePath, []byte(dockerfile), 0o644)
- fmt.Printf("Dockerfile created at %s\n", dockerfilePath)
-}
diff --git a/cmd/project/ignore.go b/cmd/project/ignore.go
deleted file mode 100644
index 235f972..0000000
--- a/cmd/project/ignore.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package project
-
-import (
- "bufio"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/gobwas/glob"
-)
-
-var EXCLUDE_PATTERNS = []string{
- "__pycache__/",
- "*.pyc",
- ".*.swp",
- ".git/",
- "*.tmp",
- "*.log",
-}
-
-func GetIgnoreList() ([]string, error) {
- // Reads the .runpodignore file and returns a list of files to ignore.
- ignoreList := make([]string, len(EXCLUDE_PATTERNS))
- copy(ignoreList, EXCLUDE_PATTERNS)
-
- cwd, _ := os.Getwd()
- ignoreFile := filepath.Join(cwd, ".runpodignore")
-
- file, err := os.Open(ignoreFile)
- if err != nil {
- if os.IsNotExist(err) {
- return ignoreList, nil
- }
- return nil, err
- }
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if line != "" && !strings.HasPrefix(line, "#") {
- ignoreList = append(ignoreList, line)
- }
- }
-
- if err := scanner.Err(); err != nil {
- return nil, err
- }
-
- return ignoreList, nil
-}
-
-func ShouldIgnore(filePath string, ignoreList []string) (bool, error) {
- if ignoreList == nil {
- var err error
- ignoreList, err = GetIgnoreList()
- if err != nil {
- return false, err
- }
- }
-
- cwd, err := os.Getwd()
- if err != nil {
- return false, err
- }
-
- relativePath, err := filepath.Rel(cwd, filePath)
- if err != nil {
- return false, err
- }
-
- for _, pattern := range ignoreList {
- if strings.HasPrefix(pattern, "/") {
- pattern = pattern[1:]
- }
-
- if strings.HasSuffix(pattern, "/") {
- pattern += "*"
- }
-
- glober, err := glob.Compile(pattern)
- if err != nil {
- return false, err
- }
-
- if glober.Match(relativePath) {
- return true, nil
- }
- }
-
- return false, nil
-}
diff --git a/cmd/project/project.go b/cmd/project/project.go
deleted file mode 100644
index a8984af..0000000
--- a/cmd/project/project.go
+++ /dev/null
@@ -1,351 +0,0 @@
-package project
-
-import (
- "bufio"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/runpod/runpodctl/api"
-
- "github.com/manifoldco/promptui"
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-var (
- projectName string
- modelType string
- modelName string
- initCurrentDir bool
- setDefaultNetworkVolume bool
- includeEnvInDockerfile bool
- showPrefixInPodLogs bool
-)
-
-const inputPromptPrefix string = " > "
-
-func prompt(message string) string {
- reader := bufio.NewReader(os.Stdin)
- fmt.Print(inputPromptPrefix + message)
-
- selection, err := reader.ReadString('\n')
- if err != nil {
- fmt.Println("An error occurred while reading input. Please try again.", err)
- return prompt(message)
- }
-
- selection = strings.TrimSpace(selection)
- if selection == "" {
- return prompt(message)
- }
- return selection
-}
-
-func contains(input string, choices []string) bool {
- for _, choice := range choices {
- if input == choice {
- return true
- }
- }
- return false
-}
-
-func promptChoice(message string, choices []string, defaultChoice string) string {
- var selection string = ""
- for !contains(selection, choices) {
- selection = ""
- fmt.Println(message)
- fmt.Print(" Available options: ")
- for _, choice := range choices {
- fmt.Printf("%s", choice)
- if choice == defaultChoice {
- fmt.Print(" (default)")
- }
- if choice != choices[len(choices)-1] {
- fmt.Print(", ")
- }
-
- }
-
- fmt.Print("\n > ")
-
- fmt.Scanln(&selection)
-
- if selection == "" {
- return defaultChoice
- }
- }
- return selection
-}
-
-func selectNetworkVolume() (networkVolumeId string, err error) {
- networkVolumes, err := api.GetNetworkVolumes()
- if err != nil {
- fmt.Println("Error fetching network volumes:", err)
- return "", err
- }
- if len(networkVolumes) == 0 {
- fmt.Println("No network volumes found. Please create one and try again. (https://runpod.io/console/user/storage)")
- return "", errors.New("no network volumes found")
- }
-
- promptTemplates := &promptui.SelectTemplates{
- Label: inputPromptPrefix + "{{ . }}",
- Active: ` {{ "●" | cyan }} {{ .Name | cyan }}`,
- Inactive: ` {{ .Name | white }}`,
- Selected: ` {{ .Name | white }}`,
- }
-
- options := []NetVolOption{}
- for _, networkVolume := range networkVolumes {
- options = append(options, NetVolOption{Name: fmt.Sprintf("%s: %s (%d GB, %s)", networkVolume.Id, networkVolume.Name, networkVolume.Size, networkVolume.DataCenterId), Value: networkVolume.Id})
- }
- getNetworkVolume := promptui.Select{
- Label: "Select a Network Volume:",
- Items: options,
- Templates: promptTemplates,
- }
- i, _, err := getNetworkVolume.Run()
- if err != nil {
- // ctrl c for example
- return "", err
- }
- networkVolumeId = options[i].Value
- return networkVolumeId, nil
-}
-
-func selectStarterTemplate() (template string, err error) {
- type StarterTemplateOption struct {
- Name string // The string to display
- Value string // The actual value to use
- }
- templates, err := starterTemplates.ReadDir("starter_examples")
- if err != nil {
- fmt.Println("Something went wrong trying to fetch the starter project.")
- fmt.Println(err)
- return "", err
- }
- promptTemplates := &promptui.SelectTemplates{
- Label: inputPromptPrefix + "{{ . }}",
- Active: ` {{ "●" | cyan }} {{ .Name | cyan }}`,
- Inactive: ` {{ .Name | white }}`,
- Selected: ` {{ .Name | white }}`,
- }
- options := []StarterTemplateOption{}
- for _, template := range templates {
- // For the printed name, replace _ with spaces
- name := template.Name()
- name = strings.Replace(name, "_", " ", -1)
- options = append(options, StarterTemplateOption{Name: name, Value: template.Name()})
- }
- getStarterTemplate := promptui.Select{
- Label: "Select a Starter Project:",
- Items: options,
- Templates: promptTemplates,
- }
- i, _, err := getStarterTemplate.Run()
- if err != nil {
- // ctrl c for example
- return "", err
- }
- template = options[i].Value
- return template, nil
-}
-
-// Define a struct that holds the display string and the corresponding value
-type NetVolOption struct {
- Name string // The string to display
- Value string // The actual value to use
-}
-
-var NewProjectCmd = &cobra.Command{
- Use: "create",
- Aliases: []string{"new"},
- Args: cobra.ExactArgs(0),
- Short: "Creates a new project",
- Long: "Creates a new RunPod project folder on your local machine.",
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Print("Welcome to the RunPod Project Creator!\n--------------------------------------\n\n")
-
- // Project Name
- if projectName == "" {
- fmt.Print("Provide a name for your project:\n")
- projectName = prompt("")
- }
- fmt.Print("\n Project name set to '" + projectName + "'.\n\n")
-
- // Project Examples
- fmt.Print("Select a starter project to begin with:\n")
-
- if modelType == "" {
- starterExample, err := selectStarterTemplate()
- modelType = starterExample
- if err != nil {
- modelType = ""
- }
- }
-
- fmt.Println("")
-
- // Model Name
- if modelType != "Hello_World" {
- fmt.Print(" Enter the name of the Hugging Face model you would like to use:\n")
- fmt.Print(" Leave blank to use the default model for the selected project.\n > ")
- fmt.Scanln(&modelName)
- fmt.Println("")
- }
-
- // CUDA Version
- cudaVersion := promptChoice("Select a CUDA version for your project:",
- []string{"11.8.0", "12.1.0", "12.2.0"}, "11.8.0")
-
- fmt.Println("\n Using CUDA version: " + cudaVersion + "\n")
-
- // Python Version
- pythonVersion := promptChoice("Select a Python version for your project:",
- []string{"3.8", "3.9", "3.10", "3.11"}, "3.10")
-
- fmt.Println("\n Using Python version: " + pythonVersion)
-
- // Project Summary
- fmt.Println("\nProject Summary:")
- fmt.Println("----------------")
- fmt.Printf("- Project Name : %s\n", projectName)
- fmt.Printf("- Starter Project : %s\n", modelType)
- fmt.Printf("- CUDA version : %s\n", cudaVersion)
- fmt.Printf("- Python version : %s\n", pythonVersion)
-
- // Confirm
- currentDir, err := os.Getwd()
- if err != nil {
- fmt.Println("Error getting current directory:", err)
- return
- }
-
- projectDir := filepath.Join(currentDir, projectName)
- if _, err := os.Stat(projectDir); !os.IsNotExist(err) {
- fmt.Printf("\nA directory with the name '%s' already exists in the current path.\n", projectName)
- confirm := promptChoice("Continue with overwrite?", []string{"yes", "no"}, "no")
- if confirm != "yes" {
- fmt.Println("Project creation cancelled.")
- return
- }
- } else {
- fmt.Printf("\nCreating project '%s' in directory '%s'\n", projectName, projectDir)
- }
-
- // Create Project
- createNewProject(projectName, cudaVersion, pythonVersion, modelType, modelName, initCurrentDir)
- fmt.Printf("\nProject %s created successfully! \nNavigate to your project directory with `cd %s`\n\n", projectName, projectName)
- fmt.Println("Tip: Run `runpodctl project dev` to start a development session for your project.")
- },
-}
-
-var StartProjectCmd = &cobra.Command{
- Use: "dev",
- Aliases: []string{"start"},
- Args: cobra.ExactArgs(0),
- Short: "Start a development session for the current project",
- Long: "This command establishes a connection between your local development environment and your RunPod project environment, allowing for real-time synchronization of changes.",
- Run: func(cmd *cobra.Command, args []string) {
- // Check for the existence of 'runpod.toml' in the current directory
- if _, err := os.Stat("runpod.toml"); os.IsNotExist(err) {
- fmt.Println("No 'runpod.toml' found in the current directory.")
- fmt.Println("Please navigate to your project directory and try again.")
- return
- }
-
- config := loadProjectConfig()
- projectId := config.GetPath([]string{"project", "uuid"}).(string)
- networkVolumeId := viper.GetString(fmt.Sprintf("project_volumes.%s", projectId))
- cachedNetVolExists := false
- networkVolumes, err := api.GetNetworkVolumes()
- if err == nil {
- for _, networkVolume := range networkVolumes {
- if networkVolume.Id == networkVolumeId {
- cachedNetVolExists = true
- }
- }
- }
- if setDefaultNetworkVolume || networkVolumeId == "" || !cachedNetVolExists {
- netVolId, err := selectNetworkVolume()
- if err != nil {
- return
- }
- networkVolumeId = netVolId
- viper.Set(fmt.Sprintf("project_volumes.%s", projectId), networkVolumeId)
- viper.WriteConfig()
- }
- startProject(networkVolumeId)
- },
-}
-
-var DeployProjectCmd = &cobra.Command{
- Use: "deploy",
- Args: cobra.ExactArgs(0),
- Short: "deploys your project as an endpoint",
- Long: "deploys a serverless endpoint for the RunPod project in the current folder",
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println("Deploying project...")
- networkVolumeId, err := selectNetworkVolume()
- if err != nil {
- return
- }
- endpointId, err := deployProject(networkVolumeId)
- if err != nil {
- fmt.Println("Failed to deploy project: ", err)
- return
- }
- fmt.Printf("Project deployed successfully! Endpoint ID: %s\n", endpointId)
- fmt.Println("Monitor and edit your endpoint at:")
- fmt.Printf("https://www.runpod.io/console/serverless/user/endpoint/%s\n", endpointId)
- fmt.Println("The following URLs are available:")
- fmt.Printf(" - https://api.runpod.ai/v2/%s/runsync\n", endpointId)
- fmt.Printf(" - https://api.runpod.ai/v2/%s/run\n", endpointId)
- fmt.Printf(" - https://api.runpod.ai/v2/%s/health\n", endpointId)
- },
-}
-
-var BuildProjectCmd = &cobra.Command{
- Use: "build",
- Args: cobra.ExactArgs(0),
- Short: "builds Dockerfile for current project",
- Long: "builds a local Dockerfile for the project in the current folder. You can use this Dockerfile to build an image and deploy it to any API server.",
- Run: func(cmd *cobra.Command, args []string) {
- buildProjectDockerfile()
- // config := loadProjectConfig()
- // projectConfig := config.Get("project").(*toml.Tree)
- // projectId := projectConfig.Get("uuid").(string)
- // projectName := config.Get("name").(string)
- // //print next steps
- // fmt.Println("Next steps:")
- // fmt.Println()
- // suggestedDockerTag := fmt.Sprintf("runpod-sls-worker-%s-%s:0.1", projectName, projectId)
- // //docker build
- // fmt.Println("# Build Docker image")
- // fmt.Printf("docker build -t %s .\n", suggestedDockerTag)
- // //dockerhub push
- // fmt.Println("# Push Docker image to a container registry such as Dockerhub")
- // fmt.Printf("docker push %s\n", suggestedDockerTag)
- // //go to runpod url and deploy
- // fmt.Println()
- // fmt.Println("Deploy docker image as a serverless endpoint on Runpod")
- // fmt.Println("https://www.runpod.io/console/serverless")
- },
-}
-
-func init() {
- // Set up flags for the project commands
- NewProjectCmd.Flags().StringVarP(&projectName, "name", "n", "", "Set the project name, a directory with this name will be created in the current path.")
- NewProjectCmd.Flags().BoolVarP(&initCurrentDir, "init", "i", false, "Initialize the project in the current directory instead of creating a new one.")
- StartProjectCmd.Flags().BoolVar(&setDefaultNetworkVolume, "select-volume", false, "Choose a new default network volume for the project.")
-
- NewProjectCmd.Flags().StringVarP(&modelName, "model", "m", "", "Specify the Hugging Face model name for the project.")
- NewProjectCmd.Flags().StringVarP(&modelType, "type", "t", "", "Specify the model type for the project.")
-
- StartProjectCmd.Flags().BoolVar(&showPrefixInPodLogs, "prefix-pod-logs", true, "Include the Pod ID as a prefix in log messages from the project Pod.")
- BuildProjectCmd.Flags().BoolVar(&includeEnvInDockerfile, "include-env", false, "Incorporate environment variables defined in runpod.toml into the generated Dockerfile.")
-}
diff --git a/cmd/project/ssh.go b/cmd/project/ssh.go
deleted file mode 100644
index e7270c2..0000000
--- a/cmd/project/ssh.go
+++ /dev/null
@@ -1,309 +0,0 @@
-package project
-
-import (
- "bufio"
- "bytes"
- "errors"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/runpod/runpodctl/api"
-
- "github.com/fatih/color"
- "golang.org/x/crypto/ssh"
-)
-
-const (
- pollInterval = 1 * time.Second
- maxPollTime = 5 * time.Minute // Adjusted for clarity
-)
-
-func getPodSSHInfo(podID string) (string, int, error) {
- pods, err := api.GetPods()
- if err != nil {
- return "", 0, fmt.Errorf("getting pods: %w", err)
- }
-
- for _, pod := range pods {
- if pod.Id != podID {
- continue
- }
-
- if pod.DesiredStatus != "RUNNING" {
- return "", 0, fmt.Errorf("pod desired status not RUNNING")
- }
- if pod.Runtime == nil {
- return "", 0, fmt.Errorf("pod runtime is missing")
- }
- if pod.Runtime.Ports == nil {
- return "", 0, fmt.Errorf("pod runtime ports are missing")
- }
- for _, port := range pod.Runtime.Ports {
- if port.PrivatePort == 22 {
- return port.Ip, port.PublicPort, nil
- }
- }
-
- }
- return "", 0, fmt.Errorf("no SSH port exposed on pod %s", podID)
-}
-
-type SSHConnection struct {
- podId string
- podIp string
- podPort int
- client *ssh.Client
- sshKeyPath string
-}
-
-func (sshConn *SSHConnection) getSshOptions() []string {
- return []string{
- "-o", "StrictHostKeyChecking=no",
- "-o", "LogLevel=ERROR",
- "-p", fmt.Sprint(sshConn.podPort),
- "-i", sshConn.sshKeyPath,
- }
-}
-
-func (sshConn *SSHConnection) Rsync(localDir string, remoteDir string, quiet bool) error {
- rsyncCmdArgs := []string{"--compress", "--archive", "--verbose", "--no-owner", "--no-group"}
-
- // Retrieve and apply ignore patterns
- patterns, err := GetIgnoreList()
- if err != nil {
- return fmt.Errorf("getting ignore list: %w", err)
- }
- for _, pat := range patterns {
- rsyncCmdArgs = append(rsyncCmdArgs, "--exclude", pat)
- }
-
- // Filter from .runpodignore
- rsyncCmdArgs = append(rsyncCmdArgs, "--filter=:- .runpodignore")
-
- // Prepare SSH options for rsync
- sshOptions := fmt.Sprintf("ssh %s", strings.Join(sshConn.getSshOptions(), " "))
- rsyncCmdArgs = append(rsyncCmdArgs, "-e", sshOptions, localDir, fmt.Sprintf("root@%s:%s", sshConn.podIp, remoteDir))
-
- // Perform a dry run to check if files need syncing
- dryRunArgs := append(rsyncCmdArgs, "--dry-run")
- dryRunCmd := exec.Command("rsync", dryRunArgs...)
- var dryRunBuf bytes.Buffer
- dryRunCmd.Stdout = &dryRunBuf
- dryRunCmd.Stderr = &dryRunBuf
- if err := dryRunCmd.Run(); err != nil {
- return fmt.Errorf("running rsync dry run: %w", err)
- }
- dryRunOutput := dryRunBuf.String()
-
- // Parse the dry run output to determine if files need syncing
- filesNeedSyncing := false
- scanner := bufio.NewScanner(strings.NewReader(dryRunOutput))
- for scanner.Scan() {
- line := scanner.Text()
-
- if line == "" || strings.Contains(line, "sending incremental file list") || strings.Contains(line, "total size is") || strings.Contains(line, "bytes/sec") || strings.Contains(line, "building file list") {
- continue
- }
-
- filename := filepath.Base(line)
- if filename == "" || filename == "." || strings.HasSuffix(line, "/") {
- continue
- }
-
- filesNeedSyncing = true
- break
- }
- if err := scanner.Err(); err != nil {
- return fmt.Errorf("scanning dry run output: %w", err)
- }
-
- // Add quiet flag if requested
- if quiet {
- rsyncCmdArgs = append(rsyncCmdArgs, "--quiet")
- }
-
- if filesNeedSyncing {
- fmt.Println("Syncing files...")
-
- cmd := exec.Command("rsync", rsyncCmdArgs...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("executing rsync command: %w", err)
- }
- }
-
- return nil
-}
-
-// hasChanges checks if there are any modified files in localDir since lastSyncTime.
-func hasChanges(localDir string, lastSyncTime time.Time) (bool, string) {
- var firstModifiedFile string = ""
-
- err := filepath.Walk(localDir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- if os.IsNotExist(err) {
- // Handle the case where a file has been removed
- fmt.Printf("Detected a removed file at: %s\n", path)
- return errors.New("change detected") // Stop walking
- }
- return err
- }
-
- // Check if the file was modified after the last sync time
- if info.ModTime().After(lastSyncTime) {
- firstModifiedFile = path
- return filepath.SkipDir // Skip the rest of the directory if a change is found
- }
-
- return nil
- })
- if err != nil {
- fmt.Printf("Error walking through directory: %v\n", err)
- return false, ""
- }
-
- return firstModifiedFile != "", firstModifiedFile
-}
-
-func (sshConn *SSHConnection) SyncDir(localDir string, remoteDir string) {
- syncFiles := func() {
- // fmt.Println("Syncing files...")
- err := sshConn.Rsync(localDir, remoteDir, true)
- if err != nil {
- fmt.Printf(" error: %v\n", err)
- return
- }
- }
-
- // Start listening for events in a separate goroutine.
- go func() {
- lastSyncTime := time.Now()
- for {
- time.Sleep(100 * time.Millisecond)
- hasChanged, firstModifiedFile := hasChanges(localDir, lastSyncTime)
- if hasChanged {
- fmt.Printf("Local changes detected in %s\n", firstModifiedFile)
- syncFiles()
- lastSyncTime = time.Now()
- }
- }
- }()
-
- <-make(chan struct{})
-}
-
-// RunCommand runs a command on the remote pod.
-func (conn *SSHConnection) RunCommand(command string) error {
- return conn.RunCommands([]string{command})
-}
-
-// RunCommands runs a list of commands on the remote pod.
-func (sshConn *SSHConnection) RunCommands(commands []string) error {
- stdoutColor, stderrColor := color.New(color.FgGreen), color.New(color.FgRed)
-
- for _, command := range commands {
- session, err := sshConn.client.NewSession()
- if err != nil {
- return fmt.Errorf("failed to create SSH session: %w", err)
- }
- defer session.Close()
-
- // Set up pipes for stdout and stderr
- stdout, err := session.StdoutPipe()
- if err != nil {
- return fmt.Errorf("failed to get stdout pipe: %w", err)
- }
- go scanAndPrint(stdout, stdoutColor, sshConn.podId, showPrefixInPodLogs)
-
- stderr, err := session.StderrPipe()
- if err != nil {
- return fmt.Errorf("failed to get stderr pipe: %w", err)
- }
- go scanAndPrint(stderr, stderrColor, sshConn.podId, showPrefixInPodLogs)
-
- // Run the command
- fullCommand := strings.Join([]string{
- "source /root/.bashrc",
- "source /etc/rp_environment",
- "while IFS= read -r -d '' line; do export \"$line\"; done < /proc/1/environ",
- command,
- }, " && ")
-
- if err := session.Run(fullCommand); err != nil {
- return fmt.Errorf("failed to run command %q: %w", command, err)
- }
- }
- return nil
-}
-
-// Utility function to scan and print output from SSH sessions.
-func scanAndPrint(pipe io.Reader, color *color.Color, podID string, showPodIdPrefix bool) {
- scanner := bufio.NewScanner(pipe)
- for scanner.Scan() {
- if showPodIdPrefix {
- color.Printf("[%s] ", podID)
- }
- fmt.Println(scanner.Text())
- }
-}
-
-func PodSSHConnection(podId string) (*SSHConnection, error) {
- homeDir, err := os.UserHomeDir()
- if err != nil {
- return nil, fmt.Errorf("getting user home directory: %w", err)
- }
-
- sshKeyPath := filepath.Join(homeDir, ".runpod", "ssh", "RunPod-Key-Go")
- privateKeyBytes, err := os.ReadFile(sshKeyPath)
- if err != nil {
- return nil, fmt.Errorf("reading private SSH key from %s: %w", sshKeyPath, err)
- }
-
- privateKey, err := ssh.ParsePrivateKey(privateKeyBytes)
- if err != nil {
- return nil, fmt.Errorf("parsing private SSH key: %w", err)
- }
-
- // loop until pod ready
-
- fmt.Print("Waiting for Pod to come online... ")
- // look up ip and ssh port for pod id
- var podIp string
- var podPort int
-
- startTime := time.Now()
- for podIp, podPort, err = getPodSSHInfo(podId); err != nil && time.Since(startTime) < maxPollTime; {
- time.Sleep(pollInterval)
- podIp, podPort, err = getPodSSHInfo(podId)
- }
-
- if err != nil {
- return nil, fmt.Errorf("failed to get SSH info for pod %s: %w", podId, err)
- } else if time.Since(startTime) >= time.Duration(maxPollTime) {
- return nil, fmt.Errorf("timeout waiting for pod %s to come online", podId)
- }
-
- // Configure the SSH client
- config := &ssh.ClientConfig{
- User: "root",
- Auth: []ssh.AuthMethod{
- ssh.PublicKeys(privateKey),
- },
- HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- }
-
- // Connect to the SSH server
- host := fmt.Sprintf("%s:%d", podIp, podPort)
- client, err := ssh.Dial("tcp", host, config)
- if err != nil {
- return nil, fmt.Errorf("establishing SSH connection to %s: %w", host, err)
- }
-
- return &SSHConnection{podId: podId, client: client, podIp: podIp, podPort: podPort, sshKeyPath: sshKeyPath}, nil
-}
diff --git a/cmd/project/starter_examples/Hello_World/.runpodignore b/cmd/project/starter_examples/Hello_World/.runpodignore
deleted file mode 100644
index e4a6598..0000000
--- a/cmd/project/starter_examples/Hello_World/.runpodignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Similar to .gitignore
-# Matches do not sync to the Project Pod or cause the Pod to reload.
-
-Dockerfile
-__pycache__/
-*.pyc
-.*.swp
-.git/
-*.tmp
-*.log
\ No newline at end of file
diff --git a/cmd/project/starter_examples/Hello_World/builder/requirements.txt b/cmd/project/starter_examples/Hello_World/builder/requirements.txt
deleted file mode 100644
index ccdacfa..0000000
--- a/cmd/project/starter_examples/Hello_World/builder/requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-# Required Python packages get listed here, one per line.
-# Lock the version number to avoid unexpected changes.
-
-# You can also install packages from a git repository, e.g.:
-# git+https://github.com/runpod/runpod-python.git
-# To learn more, see https://pip.pypa.io/en/stable/reference/requirements-file-format/
-
-<>
diff --git a/cmd/project/starter_examples/Hello_World/src/handler.py b/cmd/project/starter_examples/Hello_World/src/handler.py
deleted file mode 100644
index bda0040..0000000
--- a/cmd/project/starter_examples/Hello_World/src/handler.py
+++ /dev/null
@@ -1,13 +0,0 @@
-''' A template for a handler file. '''
-
-import runpod
-
-def handler(job):
- '''
- This is the handler function for the job.
- '''
- job_input = job['input']
- name = job_input.get('name', 'World')
- return f"Hello, {name}!"
-
-runpod.serverless.start({"handler": handler})
diff --git a/cmd/project/starter_examples/LLM/.runpodignore b/cmd/project/starter_examples/LLM/.runpodignore
deleted file mode 100644
index e4a6598..0000000
--- a/cmd/project/starter_examples/LLM/.runpodignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Similar to .gitignore
-# Matches do not sync to the Project Pod or cause the Pod to reload.
-
-Dockerfile
-__pycache__/
-*.pyc
-.*.swp
-.git/
-*.tmp
-*.log
\ No newline at end of file
diff --git a/cmd/project/starter_examples/LLM/builder/requirements.txt b/cmd/project/starter_examples/LLM/builder/requirements.txt
deleted file mode 100644
index 087b467..0000000
--- a/cmd/project/starter_examples/LLM/builder/requirements.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-# Required Python packages get listed here, one per line.
-# Lock the version number to avoid unexpected changes.
-
-# You can also install packages from a git repository, e.g.:
-# git+https://github.com/runpod/runpod-python.git
-# To learn more, see https://pip.pypa.io/en/stable/reference/requirements-file-format/
-
-<>
-hf_transfer
-
-torch
-accelerate
-transformers
-sentencepiece
diff --git a/cmd/project/starter_examples/LLM/src/handler.py b/cmd/project/starter_examples/LLM/src/handler.py
deleted file mode 100644
index e75c797..0000000
--- a/cmd/project/starter_examples/LLM/src/handler.py
+++ /dev/null
@@ -1,36 +0,0 @@
-''' A starter handler file using RunPod and a large language model for text generation. '''
-
-import io
-import base64
-from typing import Dict
-
-import runpod
-from transformers import T5Tokenizer, T5ForConditionalGeneration
-
-# Initialize the tokenizer and model
-tokenizer = T5Tokenizer.from_pretrained("<>")
-model = T5ForConditionalGeneration.from_pretrained("<>", device_map="auto").to("cuda")
-
-
-def handler(job: Dict[str, any]) -> str:
- """
- Handler function for processing a job.
-
- Args:
- job (dict): A dictionary containing the job input.
-
- Returns:
- str: The generated text response.
- """
-
- job_input = job['input']
- input_text = job_input['text']
-
- input_ids = tokenizer(input_text, return_tensors="pt").input_ids.to("cuda")
- outputs = model.generate(input_ids)
- response = tokenizer.decode(outputs[0])
-
- return response
-
-
-runpod.serverless.start({"handler": handler})
diff --git a/cmd/project/starter_examples/Stable_Diffusion/.runpodignore b/cmd/project/starter_examples/Stable_Diffusion/.runpodignore
deleted file mode 100644
index e4a6598..0000000
--- a/cmd/project/starter_examples/Stable_Diffusion/.runpodignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Similar to .gitignore
-# Matches do not sync to the Project Pod or cause the Pod to reload.
-
-Dockerfile
-__pycache__/
-*.pyc
-.*.swp
-.git/
-*.tmp
-*.log
\ No newline at end of file
diff --git a/cmd/project/starter_examples/Stable_Diffusion/builder/requirements.txt b/cmd/project/starter_examples/Stable_Diffusion/builder/requirements.txt
deleted file mode 100644
index a28317f..0000000
--- a/cmd/project/starter_examples/Stable_Diffusion/builder/requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# Required Python packages get listed here, one per line.
-# Lock the version number to avoid unexpected changes.
-
-# You can also install packages from a git repository, e.g.:
-# git+https://github.com/runpod/runpod-python.git
-# To learn more, see https://pip.pypa.io/en/stable/reference/requirements-file-format/
-
-<>
-hf_transfer
-
-accelerate
-diffusers
-transformers
diff --git a/cmd/project/starter_examples/Stable_Diffusion/src/handler.py b/cmd/project/starter_examples/Stable_Diffusion/src/handler.py
deleted file mode 100644
index eddbd67..0000000
--- a/cmd/project/starter_examples/Stable_Diffusion/src/handler.py
+++ /dev/null
@@ -1,42 +0,0 @@
-''' A starter handler file using RunPod and diffusers for image generation. '''
-
-import io
-import base64
-from typing import Dict
-
-import runpod
-from diffusers import AutoPipelineForText2Image
-import torch
-
-# Initialize the pipeline
-pipe = AutoPipelineForText2Image.from_pretrained(
- "<>", # model name
- torch_dtype=torch.float16, variant="fp16"
- ).to("cuda")
-
-
-def handler(job: Dict[str, any]) -> str:
- """
- Handler function for processing a job.
-
- Args:
- job (dict): A dictionary containing the job input.
-
- Returns:
- str: A base64 encoded string of the generated image.
- """
-
- job_input = job['input']
- prompt = job_input['prompt']
-
- image = pipe(prompt=prompt, num_inference_steps=1, guidance_scale=0.0).images[0]
-
- with io.BytesIO() as buffer:
- image.save(buffer, format="PNG")
- image_bytes = buffer.getvalue()
- base64_image = base64.b64encode(image_bytes).decode('utf-8')
-
- return f"data:image/png;base64,{base64_image}"
-
-
-runpod.serverless.start({"handler": handler})
diff --git a/cmd/project/starter_examples/Text_to_Audio/.runpodignore b/cmd/project/starter_examples/Text_to_Audio/.runpodignore
deleted file mode 100644
index e4a6598..0000000
--- a/cmd/project/starter_examples/Text_to_Audio/.runpodignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Similar to .gitignore
-# Matches do not sync to the Project Pod or cause the Pod to reload.
-
-Dockerfile
-__pycache__/
-*.pyc
-.*.swp
-.git/
-*.tmp
-*.log
\ No newline at end of file
diff --git a/cmd/project/starter_examples/Text_to_Audio/builder/requirements.txt b/cmd/project/starter_examples/Text_to_Audio/builder/requirements.txt
deleted file mode 100644
index a41fd73..0000000
--- a/cmd/project/starter_examples/Text_to_Audio/builder/requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# Required Python packages get listed here, one per line.
-# Lock the version number to avoid unexpected changes.
-
-# You can also install packages from a git repository, e.g.:
-# git+https://github.com/runpod/runpod-python.git
-# To learn more, see https://pip.pypa.io/en/stable/reference/requirements-file-format/
-
-<>
-hf_transfer
-
-torch
-transformers
-scipy
diff --git a/cmd/project/starter_examples/Text_to_Audio/src/handler.py b/cmd/project/starter_examples/Text_to_Audio/src/handler.py
deleted file mode 100644
index 4ed756d..0000000
--- a/cmd/project/starter_examples/Text_to_Audio/src/handler.py
+++ /dev/null
@@ -1,47 +0,0 @@
-''' A starter handler file using RunPod and transformers for audio generation. '''
-
-import io
-import base64
-from typing import Dict
-
-import scipy.io.wavfile
-from transformers import pipeline
-
-import runpod
-
-
-# Initialize the pipeline
-synthesizer = pipeline("text-to-audio", "<>", device=0)
-
-
-def handler(job):
- """
- Processes a text prompt to generate music, returning the result as a base64-encoded WAV audio.
-
- Args:
- job (dict): Contains 'input' with a 'prompt' key for the music generation text prompt.
-
- Returns:
- str: The generated audio as a base64-encoded string.
- """
- prompt = job['input']['prompt']
- print(f"Received prompt: {prompt}")
-
- result = synthesizer(prompt, forward_params={"do_sample": True, "max_new_tokens":300})
-
- audio_data = result['audio']
- sample_rate = result['sampling_rate']
-
- # Prepare an in-memory bytes buffer to save the audio
- audio_bytes = io.BytesIO()
- scipy.io.wavfile.write(audio_bytes, sample_rate, audio_data)
- audio_bytes.seek(0)
-
- # Encode the WAV file to a base64 string
- base64_audio = base64.b64encode(audio_bytes.read()).decode('utf-8')
-
- # Return the base64 encoded audio with the appropriate data URI scheme
- return f"data:audio/wav;base64,{base64_audio}"
-
-
-runpod.serverless.start({"handler": handler})
diff --git a/cmd/project/tomlBuilder.go b/cmd/project/tomlBuilder.go
deleted file mode 100644
index 752b12a..0000000
--- a/cmd/project/tomlBuilder.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package project
-
-import (
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/google/uuid"
-)
-
-func generateProjectToml(projectFolder, filename, projectName, cudaVersion, pythonVersion string) {
- template := `# RunPod Project Configuration
-
-name = "%s"
-
-[project]
-# uuid - Unique identifier for the project. Automatically generated.
-#
-# volume_mount_path - Default volume mount path in serverless environment. Changing this may affect data persistence.
-#
-# base_image - Base Docker image for the project environment. Includes essential packages and CUDA support.
-# - Use 'runpod/base' as a starting point. Customize only if you need additional packages or configurations.
-#
-# gpu_types - List of GPU types for your development pod. Order the types from most preferred to least preferred.
-# - The pod uses the first available type from this list.
-# - For a full list of supported GPU types, visit: https://docs.runpod.io/references/gpu-types
-#
-# gpu_count - Number of GPUs to allocate for this pod.
-#
-# ports - Ports to expose and their protocols. Configure as needed for your application.
-#
-# container_disk_size_gb - Disk space allocated to the container. Adjust according to your needs.
-
-uuid = "%s"
-base_image = "runpod/base:0.6.1-cuda%s"
-gpu_types = [
- "NVIDIA GeForce RTX 4080", # 16GB
- "NVIDIA RTX A4000", # 16GB
- "NVIDIA RTX A4500", # 20GB
- "NVIDIA RTX A5000", # 24GB
- "NVIDIA GeForce RTX 3090", # 24GB
- "NVIDIA GeForce RTX 4090", # 24GB
- "NVIDIA RTX A6000", # 48GB
- "NVIDIA A100 80GB PCIe", # 80GB
-]
-gpu_count = 1
-volume_mount_path = "/runpod-volume"
-ports = "4040/http, 7270/http, 22/tcp" # FileBrowser, FastAPI, SSH
-container_disk_size_gb = 100
-
-[project.env_vars]
-# Set environment variables for the pod.
-# For a full list of base environment variables, visit: https://github.com/runpod/containers/blob/main/official-templates/base/Dockerfile
-#
-# POD_INACTIVITY_TIMEOUT - Number of seconds before terminating the pod after a session ends. End a session with 'CTRL+C'.
-# - You only pay for the pod until it terminates.
-#
-# RUNPOD_DEBUG_LEVEL - Log level for RunPod. Set to 'debug' for detailed logs.
-#
-# UVICORN_LOG_LEVEL - Log level for Uvicorn. Set to 'warning' for minimal logs.
-
-POD_INACTIVITY_TIMEOUT = "120"
-RUNPOD_DEBUG_LEVEL = "debug"
-UVICORN_LOG_LEVEL = "warning"
-
-[endpoint]
-# Configure the deployed endpoint.
-# For a full list of endpoint configurations, visit: https://docs.runpod.io/serverless/references/endpoint-configurations
-#
-# active_workers - The minimum number of workers your endpoint has running at any given point.
-# - Setting this amount to 1 or more runs that number of "always on" workers.
-# - These workers respond to job requests without any cold start delay.
-#
-# max_workers - The maximum number of workers your endpoint has running at any given point.
-
-active_workers = 0
-max_workers = 3
-flashboot = true
-
-[runtime]
-# python_version - Python version to use for the project.
-#
-# handler_path - Path to the handler file for the project. Adapt example scripts from Hugging Face in this file.
-#
-# requirements_path - Path to the requirements file for the project. Add dependencies from Hugging Face in this file.
-
-python_version = "%s"
-handler_path = "src/handler.py"
-requirements_path = "builder/requirements.txt"
-`
-
- // Format the template with dynamic content
- content := fmt.Sprintf(template, projectName, uuid.New().String()[0:8], cudaVersion, pythonVersion)
-
- // Write the content to a TOML file
- tomlPath := filepath.Join(projectFolder, filename)
- err := os.WriteFile(tomlPath, []byte(content), 0644)
- if err != nil {
- fmt.Printf("Failed to write the TOML file: %s\n", err)
- }
-}
diff --git a/cmd/root.go b/cmd/root.go
index b4e8ebf..8d83c39 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -19,7 +19,7 @@ var version string
var rootCmd = &cobra.Command{
Use: "runpodctl",
Short: "CLI for runpod.io",
- Long: "The RunPod CLI tool to manage resources on runpod.io and develop serverless applications.",
+ Long: "The Runpod CLI tool to manage resources on runpod.io and develop serverless applications.",
}
func GetRootCmd() *cobra.Command {
@@ -41,7 +41,6 @@ func registerCommands() {
rootCmd.AddCommand(startCmd)
rootCmd.AddCommand(stopCmd)
rootCmd.AddCommand(versionCmd)
- rootCmd.AddCommand(projectCmd)
rootCmd.AddCommand(updateCmd)
rootCmd.AddCommand(sshCmd)
diff --git a/cmd/ssh/commands.go b/cmd/ssh/commands.go
index bfa609a..e147884 100644
--- a/cmd/ssh/commands.go
+++ b/cmd/ssh/commands.go
@@ -99,12 +99,12 @@ func confirmAddKey() bool {
}
func promptKeyName() string {
- fmt.Print("Please enter a name for this key (default 'RunPod-Key'): ")
+ fmt.Print("Please enter a name for this key (default 'Runpod-Key'): ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
keyName := scanner.Text()
if keyName == "" {
- return "RunPod-Key"
+ return "Runpod-Key"
}
return strings.ReplaceAll(keyName, " ", "-")
}
diff --git a/cmd/ssh/functions.go b/cmd/ssh/functions.go
index 3e7625c..f5942a2 100644
--- a/cmd/ssh/functions.go
+++ b/cmd/ssh/functions.go
@@ -70,7 +70,7 @@ func GetLocalSSHKey() ([]byte, error) {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
}
- keyPath := filepath.Join(homeDir, ".runpod", "ssh", "RunPod-Key-Go.pub")
+ keyPath := filepath.Join(homeDir, ".runpod", "ssh", "Runpod-Key-Go.pub")
publicKey, err := os.ReadFile(keyPath)
if err != nil {
diff --git a/cmd/ssh/ssh.go b/cmd/ssh/ssh.go
new file mode 100644
index 0000000..b6aa357
--- /dev/null
+++ b/cmd/ssh/ssh.go
@@ -0,0 +1,184 @@
+package ssh
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/runpod/runpodctl/api"
+
+ "golang.org/x/crypto/ssh"
+)
+
+const (
+ pollInterval = 1 * time.Second
+ maxPollTime = 5 * time.Minute
+)
+
+func getPodSSHInfo(podID string) (string, int, error) {
+ pods, err := api.GetPods()
+ if err != nil {
+ return "", 0, fmt.Errorf("getting pods: %w", err)
+ }
+
+ for _, pod := range pods {
+ if pod.Id != podID {
+ continue
+ }
+
+ if pod.DesiredStatus != "RUNNING" {
+ return "", 0, fmt.Errorf("pod desired status not RUNNING")
+ }
+ if pod.Runtime == nil {
+ return "", 0, fmt.Errorf("pod runtime is missing")
+ }
+ if pod.Runtime.Ports == nil {
+ return "", 0, fmt.Errorf("pod runtime ports are missing")
+ }
+ for _, port := range pod.Runtime.Ports {
+ if port.PrivatePort == 22 {
+ return port.Ip, port.PublicPort, nil
+ }
+ }
+ }
+ return "", 0, fmt.Errorf("no SSH port exposed on pod %s", podID)
+}
+
+type SSHConnection struct {
+ podId string
+ podIp string
+ podPort int
+ client *ssh.Client
+ sshKeyPath string
+}
+
+func (sshConn *SSHConnection) getSshOptions() []string {
+ return []string{
+ "-o", "StrictHostKeyChecking=no",
+ "-o", "LogLevel=ERROR",
+ "-p", fmt.Sprint(sshConn.podPort),
+ "-i", sshConn.sshKeyPath,
+ }
+}
+
+func (sshConn *SSHConnection) Rsync(localPath string, remotePath string, quiet bool) error {
+ rsyncArgs := []string{"--compress", "--archive"}
+
+ if quiet {
+ rsyncArgs = append(rsyncArgs, "--quiet")
+ } else {
+ rsyncArgs = append(rsyncArgs, "--verbose")
+ }
+
+ sshOptions := fmt.Sprintf("ssh %s", strings.Join(sshConn.getSshOptions(), " "))
+ rsyncArgs = append(rsyncArgs, "-e", sshOptions, localPath, fmt.Sprintf("root@%s:%s", sshConn.podIp, remotePath))
+
+ cmd := exec.Command("rsync", rsyncArgs...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("executing rsync command: %w", err)
+ }
+
+ return nil
+}
+
+func (conn *SSHConnection) RunCommand(command string) error {
+ return conn.RunCommands([]string{command})
+}
+
+func (sshConn *SSHConnection) RunCommands(commands []string) error {
+ for _, command := range commands {
+ session, err := sshConn.client.NewSession()
+ if err != nil {
+ return fmt.Errorf("failed to create SSH session: %w", err)
+ }
+ defer session.Close()
+
+ stdout, err := session.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("failed to get stdout pipe: %w", err)
+ }
+ go scanAndPrint(stdout)
+
+ stderr, err := session.StderrPipe()
+ if err != nil {
+ return fmt.Errorf("failed to get stderr pipe: %w", err)
+ }
+ go scanAndPrint(stderr)
+
+ fullCommand := strings.Join([]string{
+ "source /root/.bashrc",
+ "source /etc/rp_environment",
+ "while IFS= read -r -d '' line; do export \"$line\"; done < /proc/1/environ",
+ command,
+ }, " && ")
+
+ if err := session.Run(fullCommand); err != nil {
+ return fmt.Errorf("failed to run command %q: %w", command, err)
+ }
+ }
+ return nil
+}
+
+func scanAndPrint(pipe io.Reader) {
+ scanner := bufio.NewScanner(pipe)
+ for scanner.Scan() {
+ fmt.Println(scanner.Text())
+ }
+}
+
+func PodSSHConnection(podId string) (*SSHConnection, error) {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return nil, fmt.Errorf("getting user home directory: %w", err)
+ }
+
+ sshKeyPath := filepath.Join(homeDir, ".runpod", "ssh", "RunPod-Key-Go")
+ privateKeyBytes, err := os.ReadFile(sshKeyPath)
+ if err != nil {
+ return nil, fmt.Errorf("reading private SSH key from %s: %w", sshKeyPath, err)
+ }
+
+ privateKey, err := ssh.ParsePrivateKey(privateKeyBytes)
+ if err != nil {
+ return nil, fmt.Errorf("parsing private SSH key: %w", err)
+ }
+
+ fmt.Print("Waiting for Pod to come online... ")
+ var podIp string
+ var podPort int
+
+ startTime := time.Now()
+ for podIp, podPort, err = getPodSSHInfo(podId); err != nil && time.Since(startTime) < maxPollTime; {
+ time.Sleep(pollInterval)
+ podIp, podPort, err = getPodSSHInfo(podId)
+ }
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to get SSH info for pod %s: %w", podId, err)
+ } else if time.Since(startTime) >= time.Duration(maxPollTime) {
+ return nil, fmt.Errorf("timeout waiting for pod %s to come online", podId)
+ }
+
+ config := &ssh.ClientConfig{
+ User: "root",
+ Auth: []ssh.AuthMethod{
+ ssh.PublicKeys(privateKey),
+ },
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+ }
+
+ host := fmt.Sprintf("%s:%d", podIp, podPort)
+ client, err := ssh.Dial("tcp", host, config)
+ if err != nil {
+ return nil, fmt.Errorf("establishing SSH connection to %s: %w", host, err)
+ }
+
+ return &SSHConnection{podId: podId, client: client, podIp: podIp, podPort: podPort, sshKeyPath: sshKeyPath}, nil
+}
diff --git a/docs/runpodctl.md b/docs/runpodctl.md
index 854e442..bfe38b9 100644
--- a/docs/runpodctl.md
+++ b/docs/runpodctl.md
@@ -4,27 +4,28 @@ CLI for runpod.io
### Synopsis
-CLI tool to manage your pods for runpod.io
+The Runpod CLI tool to manage resources on runpod.io and develop serverless applications.
### Options
```
- -h, --help help for runpodctl
+ -h, --help help for runpodctl
+ -v, --version Print the version of runpodctl
```
### SEE ALSO
-* [runpodctl config](runpodctl_config.md) - CLI Config
+* [runpodctl config](runpodctl_config.md) - Manage CLI configuration
* [runpodctl create](runpodctl_create.md) - create a resource
+* [runpodctl exec](runpodctl_exec.md) - Execute commands in a pod
* [runpodctl get](runpodctl_get.md) - get resource
-* [runpodctl project](runpodctl_project.md) - Manage RunPod projects
* [runpodctl receive](runpodctl_receive.md) - receive file(s), or folder
* [runpodctl remove](runpodctl_remove.md) - remove a resource
+* [runpodctl scp-help](runpodctl_scp-help.md) - help for using scp (secure copy over SSH)
* [runpodctl send](runpodctl_send.md) - send file(s), or folder
* [runpodctl ssh](runpodctl_ssh.md) - SSH keys and commands
* [runpodctl start](runpodctl_start.md) - start a resource
* [runpodctl stop](runpodctl_stop.md) - stop a resource
* [runpodctl update](runpodctl_update.md) - update runpodctl
-* [runpodctl version](runpodctl_version.md) - runpodctl version
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_config.md b/docs/runpodctl_config.md
index 5a04104..2a5ca28 100644
--- a/docs/runpodctl_config.md
+++ b/docs/runpodctl_config.md
@@ -1,10 +1,10 @@
## runpodctl config
-CLI Config
+Manage CLI configuration
### Synopsis
-RunPod CLI Config Settings
+Runpod CLI Config Settings
```
runpodctl config [flags]
@@ -13,8 +13,8 @@ runpodctl config [flags]
### Options
```
- --apiKey string RunPod API key
- --apiUrl string RunPod API URL (default "https://api.runpod.io/graphql")
+ --apiKey string Runpod API key
+ --apiUrl string Runpod API URL (default "https://api.runpod.io/graphql")
-h, --help help for config
```
@@ -22,4 +22,4 @@ runpodctl config [flags]
* [runpodctl](runpodctl.md) - CLI for runpod.io
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_create.md b/docs/runpodctl_create.md
index 8000960..69f20bd 100644
--- a/docs/runpodctl_create.md
+++ b/docs/runpodctl_create.md
@@ -18,4 +18,4 @@ create a resource in runpod.io
* [runpodctl create pod](runpodctl_create_pod.md) - start a pod
* [runpodctl create pods](runpodctl_create_pods.md) - create a group of pods
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_create_pod.md b/docs/runpodctl_create_pod.md
index 7774358..cb565f0 100644
--- a/docs/runpodctl_create_pod.md
+++ b/docs/runpodctl_create_pod.md
@@ -13,28 +13,30 @@ runpodctl create pod [flags]
### Options
```
- --args string container arguments
- --communityCloud create in community cloud
- --containerDiskSize int container disk size in GB (default 20)
- --cost float32 $/hr price ceiling, if not defined, pod will be created with lowest price available
- --env strings container arguments
- --gpuCount int number of GPUs for the pod (default 1)
- --gpuType string gpu type id, e.g. 'NVIDIA GeForce RTX 3090'
- -h, --help help for pod
- --imageName string container image name
- --mem int minimum system memory needed (default 20)
- --name string any pod name for easy reference
- --ports strings ports to expose; max only 1 http and 1 tcp allowed; e.g. '8888/http'
- --secureCloud create in secure cloud
- --templateId string templateId to use with the pod
- --vcpu int minimum vCPUs needed (default 1)
- --volumePath string container volume path (default "/runpod")
- --volumeSize int persistent volume disk size in GB (default 1)
- --networkVolumeId string network volume id
+ --args string container arguments
+ --communityCloud create in community cloud
+ --containerDiskSize int container disk size in GB (default 20)
+ --cost float32 $/hr price ceiling, if not defined, pod will be created with lowest price available
+ --dataCenterId string datacenter id to create in
+ --env strings container arguments
+ --gpuCount int number of GPUs for the pod (default 1)
+ --gpuType string gpu type id, e.g. 'NVIDIA GeForce RTX 3090'
+ -h, --help help for pod
+ --imageName string container image name
+ --mem int minimum system memory needed (default 20)
+ --name string any pod name for easy reference
+ --networkVolumeId string network volume id
+ --ports strings ports to expose; max only 1 http and 1 tcp allowed; e.g. '8888/http'
+ --secureCloud create in secure cloud
+ --startSSH enable SSH login
+ --templateId string templateId to use with the pod
+ --vcpu int minimum vCPUs needed (default 1)
+ --volumePath string container volume path (default "/runpod")
+ --volumeSize int persistent volume disk size in GB (default 1)
```
### SEE ALSO
* [runpodctl create](runpodctl_create.md) - create a resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_create_pods.md b/docs/runpodctl_create_pods.md
index c39899d..ce06059 100644
--- a/docs/runpodctl_create_pods.md
+++ b/docs/runpodctl_create_pods.md
@@ -27,6 +27,7 @@ runpodctl create pods [flags]
--podCount int number of pods to create with the same name (default 1)
--ports strings ports to expose; max only 1 http and 1 tcp allowed; e.g. '8888/http'
--secureCloud create in secure cloud
+ --templateId string templateId to use with the pods
--vcpu int minimum vCPUs needed (default 1)
--volumePath string container volume path (default "/runpod")
--volumeSize int persistent volume disk size in GB (default 1)
@@ -36,4 +37,4 @@ runpodctl create pods [flags]
* [runpodctl create](runpodctl_create.md) - create a resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_exec.md b/docs/runpodctl_exec.md
new file mode 100644
index 0000000..09c9fb0
--- /dev/null
+++ b/docs/runpodctl_exec.md
@@ -0,0 +1,20 @@
+## runpodctl exec
+
+Execute commands in a pod
+
+### Synopsis
+
+Execute a local file remotely in a pod.
+
+### Options
+
+```
+ -h, --help help for exec
+```
+
+### SEE ALSO
+
+* [runpodctl](runpodctl.md) - CLI for runpod.io
+* [runpodctl exec python](runpodctl_exec_python.md) - Runs a remote Python shell
+
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_exec_python.md b/docs/runpodctl_exec_python.md
new file mode 100644
index 0000000..56de8d5
--- /dev/null
+++ b/docs/runpodctl_exec_python.md
@@ -0,0 +1,24 @@
+## runpodctl exec python
+
+Runs a remote Python shell
+
+### Synopsis
+
+Runs a remote Python shell with a local script file.
+
+```
+runpodctl exec python [file] [flags]
+```
+
+### Options
+
+```
+ -h, --help help for python
+ --pod_id string The ID of the pod to run the command on.
+```
+
+### SEE ALSO
+
+* [runpodctl exec](runpodctl_exec.md) - Execute commands in a pod
+
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_get.md b/docs/runpodctl_get.md
index e060e2a..468bd7e 100644
--- a/docs/runpodctl_get.md
+++ b/docs/runpodctl_get.md
@@ -18,4 +18,4 @@ get resources for pods
* [runpodctl get cloud](runpodctl_get_cloud.md) - get all cloud gpus
* [runpodctl get pod](runpodctl_get_pod.md) - get all pods
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_get_cloud.md b/docs/runpodctl_get_cloud.md
index 2c5ebfb..74f1768 100644
--- a/docs/runpodctl_get_cloud.md
+++ b/docs/runpodctl_get_cloud.md
@@ -25,4 +25,4 @@ runpodctl get cloud [gpuCount] [flags]
* [runpodctl get](runpodctl_get.md) - get resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_get_pod.md b/docs/runpodctl_get_pod.md
index c0d6ac6..b9262f3 100644
--- a/docs/runpodctl_get_pod.md
+++ b/docs/runpodctl_get_pod.md
@@ -21,4 +21,4 @@ runpodctl get pod [podId] [flags]
* [runpodctl get](runpodctl_get.md) - get resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_project.md b/docs/runpodctl_project.md
deleted file mode 100644
index daa132c..0000000
--- a/docs/runpodctl_project.md
+++ /dev/null
@@ -1,23 +0,0 @@
-## runpodctl project
-
-Manage RunPod projects
-
-### Synopsis
-
-Develop and deploy projects entirely on RunPod's infrastructure
-
-### Options
-
-```
- -h, --help help for project
-```
-
-### SEE ALSO
-
-* [runpodctl](runpodctl.md) - CLI for runpod.io
-* [runpodctl project build](runpodctl_project_build.md) - builds Dockerfile for current project
-* [runpodctl project create](runpodctl_project_create.md) - creates a new project
-* [runpodctl project deploy](runpodctl_project_deploy.md) - deploys your project as an endpoint
-* [runpodctl project dev](runpodctl_project_dev.md) - starts a development session for the current project
-
-###### Auto generated by spf13/cobra on 6-Feb-2024
diff --git a/docs/runpodctl_project_build.md b/docs/runpodctl_project_build.md
deleted file mode 100644
index 2ddefe4..0000000
--- a/docs/runpodctl_project_build.md
+++ /dev/null
@@ -1,24 +0,0 @@
-## runpodctl project build
-
-builds Dockerfile for current project
-
-### Synopsis
-
-builds a local Dockerfile for the project in the current folder. You can use this Dockerfile to build an image and deploy it to any API server.
-
-```
-runpodctl project build [flags]
-```
-
-### Options
-
-```
- -h, --help help for build
- --include-env include environment variables from runpod.toml in generated Dockerfile
-```
-
-### SEE ALSO
-
-* [runpodctl project](runpodctl_project.md) - Manage RunPod projects
-
-###### Auto generated by spf13/cobra on 6-Feb-2024
diff --git a/docs/runpodctl_project_create.md b/docs/runpodctl_project_create.md
deleted file mode 100644
index f06056f..0000000
--- a/docs/runpodctl_project_create.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## runpodctl project create
-
-creates a new project
-
-### Synopsis
-
-creates a new RunPod project folder on your local machine
-
-```
-runpodctl project create [flags]
-```
-
-### Options
-
-```
- -h, --help help for create
- -i, --init use the current directory as the project directory
- -n, --name string project name
-```
-
-### SEE ALSO
-
-* [runpodctl project](runpodctl_project.md) - Manage RunPod projects
-
-###### Auto generated by spf13/cobra on 6-Feb-2024
diff --git a/docs/runpodctl_project_deploy.md b/docs/runpodctl_project_deploy.md
deleted file mode 100644
index 0737e97..0000000
--- a/docs/runpodctl_project_deploy.md
+++ /dev/null
@@ -1,23 +0,0 @@
-## runpodctl project deploy
-
-deploys your project as an endpoint
-
-### Synopsis
-
-deploys a serverless endpoint for the RunPod project in the current folder
-
-```
-runpodctl project deploy [flags]
-```
-
-### Options
-
-```
- -h, --help help for deploy
-```
-
-### SEE ALSO
-
-* [runpodctl project](runpodctl_project.md) - Manage RunPod projects
-
-###### Auto generated by spf13/cobra on 6-Feb-2024
diff --git a/docs/runpodctl_project_dev.md b/docs/runpodctl_project_dev.md
deleted file mode 100644
index 673e806..0000000
--- a/docs/runpodctl_project_dev.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## runpodctl project dev
-
-starts a development session for the current project
-
-### Synopsis
-
-connects your local environment and the project environment on your Pod. Changes propagate to the project environment in real time.
-
-```
-runpodctl project dev [flags]
-```
-
-### Options
-
-```
- -h, --help help for dev
- --prefix-pod-logs prefix logs from project Pod with Pod ID (default true)
- --select-volume select a new default network volume for current project
-```
-
-### SEE ALSO
-
-* [runpodctl project](runpodctl_project.md) - Manage RunPod projects
-
-###### Auto generated by spf13/cobra on 6-Feb-2024
diff --git a/docs/runpodctl_receive.md b/docs/runpodctl_receive.md
index 888e630..fdbeeef 100644
--- a/docs/runpodctl_receive.md
+++ b/docs/runpodctl_receive.md
@@ -20,4 +20,4 @@ runpodctl receive [code] [flags]
* [runpodctl](runpodctl.md) - CLI for runpod.io
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_remove.md b/docs/runpodctl_remove.md
index 9a8bc7b..a0f921d 100644
--- a/docs/runpodctl_remove.md
+++ b/docs/runpodctl_remove.md
@@ -18,4 +18,4 @@ remove a resource in runpod.io
* [runpodctl remove pod](runpodctl_remove_pod.md) - remove a pod
* [runpodctl remove pods](runpodctl_remove_pods.md) - remove all pods using name
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_remove_pod.md b/docs/runpodctl_remove_pod.md
index d5c9112..4a5dbca 100644
--- a/docs/runpodctl_remove_pod.md
+++ b/docs/runpodctl_remove_pod.md
@@ -20,4 +20,4 @@ runpodctl remove pod [podId] [flags]
* [runpodctl remove](runpodctl_remove.md) - remove a resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_remove_pods.md b/docs/runpodctl_remove_pods.md
index 3d16300..29d5b5d 100644
--- a/docs/runpodctl_remove_pods.md
+++ b/docs/runpodctl_remove_pods.md
@@ -21,4 +21,4 @@ runpodctl remove pods [name] [flags]
* [runpodctl remove](runpodctl_remove.md) - remove a resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_scp-help.md b/docs/runpodctl_scp-help.md
new file mode 100644
index 0000000..35ee8a5
--- /dev/null
+++ b/docs/runpodctl_scp-help.md
@@ -0,0 +1,19 @@
+## runpodctl scp-help
+
+help for using scp (secure copy over SSH)
+
+```
+runpodctl scp-help [flags]
+```
+
+### Options
+
+```
+ -h, --help help for scp-help
+```
+
+### SEE ALSO
+
+* [runpodctl](runpodctl.md) - CLI for runpod.io
+
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_send.md b/docs/runpodctl_send.md
index e7dfcab..f784d0c 100644
--- a/docs/runpodctl_send.md
+++ b/docs/runpodctl_send.md
@@ -7,7 +7,7 @@ send file(s), or folder
send file(s), or folder to pod or any computer
```
-runpodctl send [filename(s) or folder] [flags]
+runpodctl send [file0] [file1] ... [flags]
```
### Options
@@ -21,4 +21,4 @@ runpodctl send [filename(s) or folder] [flags]
* [runpodctl](runpodctl.md) - CLI for runpod.io
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_ssh.md b/docs/runpodctl_ssh.md
index d84fd45..14f0252 100644
--- a/docs/runpodctl_ssh.md
+++ b/docs/runpodctl_ssh.md
@@ -16,6 +16,7 @@ SSH key management and connection to pods
* [runpodctl](runpodctl.md) - CLI for runpod.io
* [runpodctl ssh add-key](runpodctl_ssh_add-key.md) - Adds an SSH key to the current user account
+* [runpodctl ssh connect](runpodctl_ssh_connect.md) - Shows the SSH connect command for pods
* [runpodctl ssh list-keys](runpodctl_ssh_list-keys.md) - List all SSH keys
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_ssh_add-key.md b/docs/runpodctl_ssh_add-key.md
index 5f0a092..d6faf2a 100644
--- a/docs/runpodctl_ssh_add-key.md
+++ b/docs/runpodctl_ssh_add-key.md
@@ -22,4 +22,4 @@ runpodctl ssh add-key [flags]
* [runpodctl ssh](runpodctl_ssh.md) - SSH keys and commands
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_ssh_connect.md b/docs/runpodctl_ssh_connect.md
new file mode 100644
index 0000000..e69e0ca
--- /dev/null
+++ b/docs/runpodctl_ssh_connect.md
@@ -0,0 +1,24 @@
+## runpodctl ssh connect
+
+Shows the SSH connect command for pods
+
+### Synopsis
+
+Shows the full featured SSH connect command for a given pod if a pod ID or name is provided. When no argument is provided, shows the connect information for all pods.
+
+```
+runpodctl ssh connect [podID|name] [flags]
+```
+
+### Options
+
+```
+ -h, --help help for connect
+ -v, --verbose include identifying pod information (name and id)
+```
+
+### SEE ALSO
+
+* [runpodctl ssh](runpodctl_ssh.md) - SSH keys and commands
+
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_ssh_list-keys.md b/docs/runpodctl_ssh_list-keys.md
index 1f31a64..dc4c66e 100644
--- a/docs/runpodctl_ssh_list-keys.md
+++ b/docs/runpodctl_ssh_list-keys.md
@@ -20,4 +20,4 @@ runpodctl ssh list-keys [flags]
* [runpodctl ssh](runpodctl_ssh.md) - SSH keys and commands
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_start.md b/docs/runpodctl_start.md
index 0f251d8..4ef978b 100644
--- a/docs/runpodctl_start.md
+++ b/docs/runpodctl_start.md
@@ -17,4 +17,4 @@ start a resource in runpod.io
* [runpodctl](runpodctl.md) - CLI for runpod.io
* [runpodctl start pod](runpodctl_start_pod.md) - start a pod
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_start_pod.md b/docs/runpodctl_start_pod.md
index 6877b28..0ec7661 100644
--- a/docs/runpodctl_start_pod.md
+++ b/docs/runpodctl_start_pod.md
@@ -13,12 +13,13 @@ runpodctl start pod [podId] [flags]
### Options
```
- --bid float32 bid per gpu for spot price
- -h, --help help for pod
+ --bid float32 bid per gpu for spot price
+ --gpuCount int number of GPUs to request (default 1)
+ -h, --help help for pod
```
### SEE ALSO
* [runpodctl start](runpodctl_start.md) - start a resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_stop.md b/docs/runpodctl_stop.md
index 5cfd3b4..a15af1d 100644
--- a/docs/runpodctl_stop.md
+++ b/docs/runpodctl_stop.md
@@ -17,4 +17,4 @@ stop a resource in runpod.io
* [runpodctl](runpodctl.md) - CLI for runpod.io
* [runpodctl stop pod](runpodctl_stop_pod.md) - stop a pod
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_stop_pod.md b/docs/runpodctl_stop_pod.md
index f7496db..8f6ad48 100644
--- a/docs/runpodctl_stop_pod.md
+++ b/docs/runpodctl_stop_pod.md
@@ -20,4 +20,4 @@ runpodctl stop pod [podId] [flags]
* [runpodctl stop](runpodctl_stop.md) - stop a resource
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_update.md b/docs/runpodctl_update.md
index 6852175..4bb4d00 100644
--- a/docs/runpodctl_update.md
+++ b/docs/runpodctl_update.md
@@ -20,4 +20,4 @@ runpodctl update [flags]
* [runpodctl](runpodctl.md) - CLI for runpod.io
-###### Auto generated by spf13/cobra on 6-Feb-2024
+###### Auto generated by spf13/cobra on 18-Nov-2025
diff --git a/docs/runpodctl_version.md b/docs/runpodctl_version.md
deleted file mode 100644
index 7b5bca9..0000000
--- a/docs/runpodctl_version.md
+++ /dev/null
@@ -1,23 +0,0 @@
-## runpodctl version
-
-runpodctl version
-
-### Synopsis
-
-runpodctl version
-
-```
-runpodctl version [flags]
-```
-
-### Options
-
-```
- -h, --help help for version
-```
-
-### SEE ALSO
-
-* [runpodctl](runpodctl.md) - CLI for runpod.io
-
-###### Auto generated by spf13/cobra on 6-Feb-2024
diff --git a/install.sh b/install.sh
index 1557daa..bce9b9f 100644
--- a/install.sh
+++ b/install.sh
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
-# Unified Installer for RunPod CLI Tool
+# Unified Installer for Runpod CLI Tool
#
-# This script provides a unified approach to installing the RunPod CLI tool.
+# This script provides a unified approach to installing the Runpod CLI tool.
#
# Usage:
# wget -qO- cli.runpod.io | bash
diff --git a/main.go b/main.go
index f73df96..cc715f6 100644
--- a/main.go
+++ b/main.go
@@ -2,7 +2,6 @@ package main
import (
_ "embed"
- "strings"
"github.com/runpod/runpodctl/cmd"
)
@@ -11,5 +10,5 @@ import (
var Version string
func main() {
- cmd.Execute(strings.TrimRight(Version, "\r\n"))
+ cmd.Execute(Version)
}