Skip to content

Commit

Permalink
feat: nodekit self update
Browse files Browse the repository at this point in the history
  • Loading branch information
PhearZero committed Jan 14, 2025
1 parent 2f7af0c commit 8418c1d
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 7 deletions.
36 changes: 36 additions & 0 deletions api/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
)

const ChannelNotFoundMsg = "channel not found"
const NodeKitReleaseNotFoundMsg = "nodekit release not found"

type GithubVersionResponse struct {
HTTPResponse *http.Response
Expand Down Expand Up @@ -66,3 +67,38 @@ func GetGoAlgorandReleaseWithResponse(http HttpPkgInterface, channel string) (*G
versions.JSON200 = *versionResponse
return &versions, nil
}

func GetNodeKitReleaseWithResponse(http HttpPkgInterface) (*GithubVersionResponse, error) {
var versions GithubVersionResponse
resp, err := http.Get("https://api.github.com/repos/algorandfoundation/nodekit/releases/latest")
versions.HTTPResponse = resp
if resp == nil || err != nil {
return nil, err
}
// Update Model
versions.ResponseCode = resp.StatusCode
versions.ResponseStatus = resp.Status

// Exit if not 200
if resp.StatusCode != 200 {
return &versions, nil
}

defer resp.Body.Close()

// Parse the versions to a map
var releaseMap map[string]interface{}
if err = json.NewDecoder(resp.Body).Decode(&releaseMap); err != nil {
return &versions, err
}

version := releaseMap["tag_name"]

if version == nil {
return &versions, errors.New(NodeKitReleaseNotFoundMsg)
}

// Update the JSON200 data and return
versions.JSON200 = strings.Replace(version.(string), "v", "", 1)
return &versions, nil
}
5 changes: 4 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
var (
Name = "nodekit"

NeedsUpgrade = false

// algodEndpoint defines the URI address of the Algorand node, including the protocol (http/https), for client communication.
algodData string

Expand Down Expand Up @@ -141,7 +143,8 @@ func init() {
}

// Execute executes the root command.
func Execute(version string) error {
func Execute(version string, needsUpgrade bool) error {
RootCmd.Version = version
NeedsUpgrade = needsUpgrade
return RootCmd.Execute()
}
19 changes: 14 additions & 5 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package cmd

import (
"github.com/algorandfoundation/nodekit/api"
"github.com/algorandfoundation/nodekit/cmd/utils/explanations"
"github.com/algorandfoundation/nodekit/internal/algod"
"github.com/algorandfoundation/nodekit/internal/system"
"github.com/algorandfoundation/nodekit/ui/style"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
Expand Down Expand Up @@ -30,12 +32,19 @@ var upgradeLong = lipgloss.JoinVertical(

// upgradeCmd is a Cobra command used to upgrade Algod, utilizing the OS-specific package manager if applicable.
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: upgradeShort,
Long: upgradeLong,
SilenceUsage: true,
PersistentPreRun: NeedsToBeStopped,
Use: "upgrade",
Short: upgradeShort,
Long: upgradeLong,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
if NeedsUpgrade {
log.Info(style.Green.Render("Upgrading NodeKit"))
err := system.Upgrade(new(api.HttpPkg))
if err != nil {
log.Fatal(err)
}
}

// TODO: get expected version and check if update is required
log.Info(style.Green.Render(UpgradeMsg))
// Warn user for prompt
Expand Down
84 changes: 84 additions & 0 deletions internal/system/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package system

import (
"bytes"
"fmt"
"github.com/algorandfoundation/nodekit/api"
"github.com/charmbracelet/log"
"io"
"os"
"path/filepath"
"runtime"
)

func Upgrade(http api.HttpPkgInterface) error {
// File Permissions
permissions := os.FileMode(0755)

// Fetch the latest binary
var downloadUrlBase = fmt.Sprintf("https://github.com/algorandfoundation/nodekit/releases/latest/download/nodekit-%s-%s", runtime.GOARCH, runtime.GOOS)
log.Debug(fmt.Sprintf("fetching %s", downloadUrlBase))
resp, err := http.Get(downloadUrlBase)
if err != nil {
log.Error(err)
return err
}

// Current Executable Path
pathName, err := os.Executable()
if err != nil {
log.Error(err)
return err
}

// Get Names of Directory and Base
executableDir := filepath.Dir(pathName)
executableName := filepath.Base(pathName)

var programBytes []byte
if programBytes, err = io.ReadAll(resp.Body); err != nil {
log.Error(err)
return err
}

// Create a temporary file to put the binary
tmpPath := filepath.Join(executableDir, fmt.Sprintf(".%s.tmp", executableName))
log.Debug(fmt.Sprintf("writing to %s", tmpPath))
tempFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, permissions)
if err != nil {
return err
}
os.Chmod(tmpPath, permissions)
defer tempFile.Close()
_, err = io.Copy(tempFile, bytes.NewReader(programBytes))
if err != nil {
log.Error(err)
return err
}
tempFile.Sync()
tempFile.Close()

// Backup the exising command
backupPath := filepath.Join(executableDir, fmt.Sprintf(".%s.bak", executableName))
log.Debug(fmt.Sprintf("backing up to %s", tmpPath))
_ = os.Remove(backupPath)
err = os.Rename(pathName, backupPath)
if err != nil {
log.Error(err)
return err
}

// Install new command
log.Debug(fmt.Sprintf("deploying %s to %s", tmpPath, pathName))
err = os.Rename(tmpPath, pathName)
if err != nil {
log.Debug("rolling back installation")
log.Error(err)
// Try to roll back the changes
_ = os.Rename(backupPath, tmpPath)
return err
}

// Cleanup the backup
return os.Remove(backupPath)
}
16 changes: 15 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"fmt"
"github.com/algorandfoundation/nodekit/api"
"github.com/algorandfoundation/nodekit/cmd"
"github.com/charmbracelet/log"
"os"
Expand All @@ -22,9 +24,21 @@ func init() {
log.SetLevel(log.DebugLevel)
}
func main() {
var needsUpgrade = false
resp, err := api.GetNodeKitReleaseWithResponse(new(api.HttpPkg))
if err == nil && resp.ResponseCode >= 200 && resp.ResponseCode < 300 {
if resp.JSON200 != version {
needsUpgrade = true
// Warn on all commands but version
if len(os.Args) > 1 && os.Args[1] != "--version" {
log.Warn(
fmt.Sprintf("nodekit version v%s is available", resp.JSON200))
}
}
}
// TODO: more performance tuning
runtime.GOMAXPROCS(1)
err := cmd.Execute(version)
err = cmd.Execute(version, needsUpgrade)
if err != nil {
return
}
Expand Down

0 comments on commit 8418c1d

Please sign in to comment.