From 8418c1d7e720afc52e951f9204d0d3f3532830d2 Mon Sep 17 00:00:00 2001 From: Michael Feher Date: Mon, 13 Jan 2025 21:44:49 -0500 Subject: [PATCH] feat: nodekit self update --- api/github.go | 36 ++++++++++++++++ cmd/root.go | 5 ++- cmd/upgrade.go | 19 ++++++--- internal/system/upgrade.go | 84 ++++++++++++++++++++++++++++++++++++++ main.go | 16 +++++++- 5 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 internal/system/upgrade.go diff --git a/api/github.go b/api/github.go index 89292f06..351b9957 100644 --- a/api/github.go +++ b/api/github.go @@ -8,6 +8,7 @@ import ( ) const ChannelNotFoundMsg = "channel not found" +const NodeKitReleaseNotFoundMsg = "nodekit release not found" type GithubVersionResponse struct { HTTPResponse *http.Response @@ -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 +} diff --git a/cmd/root.go b/cmd/root.go index 6dce0e46..6caee3f8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 @@ -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() } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 9c7ceb30..f6006246 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -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" @@ -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 diff --git a/internal/system/upgrade.go b/internal/system/upgrade.go new file mode 100644 index 00000000..76ff4a08 --- /dev/null +++ b/internal/system/upgrade.go @@ -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) +} diff --git a/main.go b/main.go index 2600d6a7..83be01fc 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/algorandfoundation/nodekit/api" "github.com/algorandfoundation/nodekit/cmd" "github.com/charmbracelet/log" "os" @@ -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 }