From 873a2bf4cc87553610a79ccf712cb45b9aef5c5b Mon Sep 17 00:00:00 2001 From: Dj Isaac Date: Tue, 18 Nov 2025 17:45:21 -0600 Subject: [PATCH] chore: remove runpod projects, regen docs, update branding --- .gitignore | 2 + README.md | 14 +- api/query.go | 4 +- api/volume.go | 2 +- cmd/config/config.go | 8 +- cmd/exec/functions.go | 4 +- cmd/project.go | 20 - cmd/project/defaults.go | 15 - cmd/project/exampleDockerfile | 28 - cmd/project/functions.go | 653 ------------------ cmd/project/ignore.go | 92 --- cmd/project/project.go | 351 ---------- cmd/project/ssh.go | 309 --------- .../Hello_World/.runpodignore | 10 - .../Hello_World/builder/requirements.txt | 8 - .../Hello_World/src/handler.py | 13 - .../starter_examples/LLM/.runpodignore | 10 - .../LLM/builder/requirements.txt | 14 - .../starter_examples/LLM/src/handler.py | 36 - .../Stable_Diffusion/.runpodignore | 10 - .../Stable_Diffusion/builder/requirements.txt | 13 - .../Stable_Diffusion/src/handler.py | 42 -- .../Text_to_Audio/.runpodignore | 10 - .../Text_to_Audio/builder/requirements.txt | 13 - .../Text_to_Audio/src/handler.py | 47 -- cmd/project/tomlBuilder.go | 101 --- cmd/root.go | 3 +- cmd/ssh/commands.go | 4 +- cmd/ssh/functions.go | 2 +- cmd/ssh/ssh.go | 184 +++++ docs/runpodctl.md | 13 +- docs/runpodctl_config.md | 10 +- docs/runpodctl_create.md | 2 +- docs/runpodctl_create_pod.md | 40 +- docs/runpodctl_create_pods.md | 3 +- docs/runpodctl_exec.md | 20 + docs/runpodctl_exec_python.md | 24 + docs/runpodctl_get.md | 2 +- docs/runpodctl_get_cloud.md | 2 +- docs/runpodctl_get_pod.md | 2 +- docs/runpodctl_project.md | 23 - docs/runpodctl_project_build.md | 24 - docs/runpodctl_project_create.md | 25 - docs/runpodctl_project_deploy.md | 23 - docs/runpodctl_project_dev.md | 25 - docs/runpodctl_receive.md | 2 +- docs/runpodctl_remove.md | 2 +- docs/runpodctl_remove_pod.md | 2 +- docs/runpodctl_remove_pods.md | 2 +- docs/runpodctl_scp-help.md | 19 + docs/runpodctl_send.md | 4 +- docs/runpodctl_ssh.md | 3 +- docs/runpodctl_ssh_add-key.md | 2 +- docs/runpodctl_ssh_connect.md | 24 + docs/runpodctl_ssh_list-keys.md | 2 +- docs/runpodctl_start.md | 2 +- docs/runpodctl_start_pod.md | 7 +- docs/runpodctl_stop.md | 2 +- docs/runpodctl_stop_pod.md | 2 +- docs/runpodctl_update.md | 2 +- docs/runpodctl_version.md | 23 - install.sh | 4 +- main.go | 3 +- 63 files changed, 352 insertions(+), 2015 deletions(-) delete mode 100644 cmd/project.go delete mode 100644 cmd/project/defaults.go delete mode 100644 cmd/project/exampleDockerfile delete mode 100644 cmd/project/functions.go delete mode 100644 cmd/project/ignore.go delete mode 100644 cmd/project/project.go delete mode 100644 cmd/project/ssh.go delete mode 100644 cmd/project/starter_examples/Hello_World/.runpodignore delete mode 100644 cmd/project/starter_examples/Hello_World/builder/requirements.txt delete mode 100644 cmd/project/starter_examples/Hello_World/src/handler.py delete mode 100644 cmd/project/starter_examples/LLM/.runpodignore delete mode 100644 cmd/project/starter_examples/LLM/builder/requirements.txt delete mode 100644 cmd/project/starter_examples/LLM/src/handler.py delete mode 100644 cmd/project/starter_examples/Stable_Diffusion/.runpodignore delete mode 100644 cmd/project/starter_examples/Stable_Diffusion/builder/requirements.txt delete mode 100644 cmd/project/starter_examples/Stable_Diffusion/src/handler.py delete mode 100644 cmd/project/starter_examples/Text_to_Audio/.runpodignore delete mode 100644 cmd/project/starter_examples/Text_to_Audio/builder/requirements.txt delete mode 100644 cmd/project/starter_examples/Text_to_Audio/src/handler.py delete mode 100644 cmd/project/tomlBuilder.go create mode 100644 cmd/ssh/ssh.go create mode 100644 docs/runpodctl_exec.md create mode 100644 docs/runpodctl_exec_python.md delete mode 100644 docs/runpodctl_project.md delete mode 100644 docs/runpodctl_project_build.md delete mode 100644 docs/runpodctl_project_create.md delete mode 100644 docs/runpodctl_project_deploy.md delete mode 100644 docs/runpodctl_project_dev.md create mode 100644 docs/runpodctl_scp-help.md create mode 100644 docs/runpodctl_ssh_connect.md delete mode 100644 docs/runpodctl_version.md 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) }