Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go 1.25.7
- name: Setup Go 1.25.8
uses: actions/setup-go@v5
with:
go-version: 1.25.7
go-version: 1.25.8
# You can test your matrix by printing the current Go version
- name: Display Go version
run: go version
Expand Down
17 changes: 17 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ builds:
ldflags:
- -s -w -X main.Version={{ .Version }}-{{ .ShortCommit }}

# standalone per-arch binaries so older runpodctl versions can self-update
# (the old update command expects raw binaries named runpodctl-{os}-{arch})
- id: legacy
binary: runpodctl
goos: [darwin, linux, windows]
goarch: [amd64, arm64]
env: [CGO_ENABLED=0]
flags: [-mod=mod]
ldflags:
- -s -w -X main.Version={{ .Version }}-{{ .ShortCommit }}

- id: linux_amd64_upx
binary: runpodctl
goos: [linux]
Expand Down Expand Up @@ -40,6 +51,12 @@ archives:
formats: [zip]
name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}"

# raw binaries for backward compat with old runpodctl update command
- id: legacy
ids: [legacy]
formats: [binary]
name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}"

# UPX linux/amd64 artifact, also archived for internal Runpod use
- id: upx
ids: [linux_amd64_upx]
Expand Down
197 changes: 150 additions & 47 deletions cmd/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cmd

import (
"archive/tar"
"archive/zip"
"compress/gzip"
"encoding/json"
"fmt"
"io"
Expand All @@ -9,6 +12,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/spf13/cobra"
"golang.org/x/mod/semver"
Expand Down Expand Up @@ -62,7 +66,6 @@ func GetJson(url string) (*GithubApiResponse, error) {
if err != nil {
return nil, err
}
// fmt.Println(string(body))
var result GithubApiResponse
err = json.Unmarshal(body, &result)
if err != nil {
Expand All @@ -72,6 +75,88 @@ func GetJson(url string) (*GithubApiResponse, error) {
return &result, nil
}

// assetName returns the expected release asset name for the current platform.
// darwin uses a universal binary ("all"), others use the specific arch.
// release assets are archives: .tar.gz for unix, .zip for windows.
func assetName() string {
arch := runtime.GOARCH
if runtime.GOOS == "darwin" {
arch = "all"
}
ext := ".tar.gz"
if runtime.GOOS == "windows" {
ext = ".zip"
}
return fmt.Sprintf("runpodctl-%s-%s%s", runtime.GOOS, arch, ext)
}

// extractBinaryFromTarGz extracts the "runpodctl" binary from a .tar.gz archive.
func extractBinaryFromTarGz(archivePath, destPath string) error {
f, err := os.Open(archivePath)
if err != nil {
return err
}
defer f.Close()

gr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gr.Close()

tr := tar.NewReader(gr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "runpodctl" {
out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, tr); err != nil {
return err
}
return nil
}
}
return fmt.Errorf("runpodctl binary not found in archive")
}

// extractBinaryFromZip extracts the "runpodctl.exe" binary from a .zip archive.
func extractBinaryFromZip(archivePath, destPath string) error {
r, err := zip.OpenReader(archivePath)
if err != nil {
return err
}
defer r.Close()

for _, f := range r.File {
if strings.EqualFold(filepath.Base(f.Name), "runpodctl.exe") {
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, rc); err != nil {
return err
}
return nil
}
}
return fmt.Errorf("runpodctl.exe not found in archive")
}

var updateCmd = &cobra.Command{
Use: "update",
Short: "update runpodctl cli",
Expand All @@ -84,58 +169,76 @@ var updateCmd = &cobra.Command{
fmt.Println("error fetching latest version info for runpodctl", err)
return
}
//find download link for current platform
latestVersion := apiResp.Version
if semver.Compare("v"+version, latestVersion) == -1 {
//version < latest
newBinaryName := fmt.Sprintf("runpodctl-%s-%s", runtime.GOOS, runtime.GOARCH)
foundNewBinary := false
var downloadLink string
for _, asset := range apiResp.Assets {
if asset.Name == newBinaryName {
foundNewBinary = true
downloadLink = asset.Url
}
if semver.Compare("v"+version, latestVersion) >= 0 {
fmt.Printf("runpodctl %s is already up to date\n", version)
return
}

// find download link for current platform
expectedAsset := assetName()
var downloadLink string
for _, asset := range apiResp.Assets {
if asset.Name == expectedAsset {
downloadLink = asset.Url
break
}
if !foundNewBinary {
fmt.Printf("platform %s-%s not supported in latest version\n", runtime.GOOS, runtime.GOARCH)
}
if downloadLink == "" {
fmt.Printf("platform %s-%s not supported in latest version\n", runtime.GOOS, runtime.GOARCH)
return
}

ex, err := os.Executable()
if err != nil {
fmt.Println("error finding current executable:", err)
return
}
exPath := filepath.Dir(ex)

destFilename := "runpodctl"
if runtime.GOOS == "windows" {
destFilename = "runpodctl.exe"
}
destPath := filepath.Join(exPath, destFilename)

// download archive to a temp file
tmpFile, err := os.CreateTemp("", "runpodctl-update-*")
if err != nil {
fmt.Println("error creating temp file:", err)
return
}
archivePath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(archivePath)

fmt.Printf("downloading runpodctl %s\n", latestVersion)
file, err := DownloadFile(downloadLink, archivePath)
if err != nil {
fmt.Println("error fetching the latest version of runpodctl:", err)
return
}
file.Close()

// extract binary from archive to a temp location next to the destination
extractedPath := destPath + ".new"
defer os.Remove(extractedPath)

if runtime.GOOS == "windows" {
if err := extractBinaryFromZip(archivePath, extractedPath); err != nil {
fmt.Println("error extracting update:", err)
return
}
ex, err := os.Executable()
if err != nil {
panic(err)
}
exPath := filepath.Dir(ex)
downloadPath := newBinaryName
destFilename := "runpodctl"
if runtime.GOOS == "windows" {
destFilename = "runpodctl.exe"
}
destPath := filepath.Join(exPath, destFilename)
if runtime.GOOS == "windows" {
fmt.Println("to get the newest version, run this command:")
fmt.Printf("wget https://github.com/runpod/runpodctl/releases/download/%s/%s -O runpodctl.exe\n", latestVersion, newBinaryName)
}
fmt.Printf("downloading runpodctl %s to %s\n", latestVersion, downloadPath)
file, err := DownloadFile(downloadLink, downloadPath)
defer file.Close()
if err != nil {
fmt.Println("error fetching the latest version of runpodctl", err)
fmt.Println("to complete the update, run this command:")
fmt.Printf("move /Y \"%s\" \"%s\"\n", extractedPath, destPath)
} else {
if err := extractBinaryFromTarGz(archivePath, extractedPath); err != nil {
fmt.Println("error extracting update:", err)
return
}
//chmod +x
err = file.Chmod(0755)
if err != nil {
fmt.Println("error setting permissions on new binary", err)
}
fmt.Printf("moving %s to %s\n", downloadPath, destPath)
if runtime.GOOS == "windows" {
//if I do this, windows antivirus considers the program to be a trojan
// exec.Command("cmd", "/C", "rm", destPath, ";", "move", downloadPath, destPath).Run()
} else {
exec.Command("mv", downloadPath, destPath).Run() //need to run externally to current process because we're updating the running executable
}
fmt.Printf("installing runpodctl %s to %s\n", latestVersion, destPath)
// need to run externally to current process because we're updating the running executable
exec.Command("mv", extractedPath, destPath).Run()
}

},
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/runpod/runpodctl

go 1.25.7
go 1.25.8

require (
github.com/denisbrodbeck/machineid v1.0.1
Expand Down
Loading