Skip to content

Commit

Permalink
Merge pull request #85 from algorandfoundation/feat/auto-update
Browse files Browse the repository at this point in the history
feat: add auto-update
  • Loading branch information
PhearZero authored Jan 14, 2025
2 parents b0f93e7 + bf5a770 commit 3968399
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 16 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/CD.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ jobs:
- uses: actions/checkout@v4
- name: Install dependencies
run: go get .
- uses: go-semantic-release/action@v1
name: release
id: semver
with:
dry: true
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Add version to env
run: echo "VERSION=${{ steps.semver.outputs.version }}" >> $GITHUB_ENV
- name: Build
env:
GOOS: ${{matrix.goos}}
GOARCH: ${{matrix.goarch}}
CGO_ENABLED: 0
run: go build -o bin/nodekit-${{matrix.goarch}}-${{matrix.goos}} *.go
run: go build -ldflags "-X main.version=${VERSION}" -o bin/nodekit-${{matrix.goarch}}-${{matrix.goos}} *.go
- uses: actions/upload-artifact@master
with:
name: nodekit-${{matrix.goarch}}-${{matrix.goos}}
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
VERSION ?= dev

.PHONY: all

build:
CGO_ENABLED=0 go build -o bin/nodekit .
CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" -o bin/nodekit .
test:
go test -coverprofile=coverage.out -coverpkg=./... -covermode=atomic ./...
generate:
Expand Down
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
}
18 changes: 10 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"fmt"
"github.com/algorandfoundation/nodekit/api"
"github.com/algorandfoundation/nodekit/cmd/catchup"
"github.com/algorandfoundation/nodekit/cmd/configure"
Expand All @@ -21,12 +22,11 @@ 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

// Version represents the application version string, which is set during build or defaults to "unknown".
Version = ""

// force indicates whether actions should be performed forcefully, bypassing checks or confirmations.
force bool = false

Expand All @@ -45,10 +45,9 @@ var (
)
// RootCmd is the primary command for managing Algorand nodes, providing CLI functionality and TUI for interaction.
RootCmd = utils.WithAlgodFlags(&cobra.Command{
Use: Name,
Version: Version,
Short: short,
Long: long,
Use: Name,
Short: short,
Long: long,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
Expand Down Expand Up @@ -128,6 +127,7 @@ func NeedsToBeStopped(cmd *cobra.Command, args []string) {
// init initializes the application, setting up logging, commands, and version information.
func init() {
log.SetReportTimestamp(false)
RootCmd.SetVersionTemplate(fmt.Sprintf("nodekit-%s-%s@{{.Version}}\n", runtime.GOARCH, runtime.GOOS))
// Add Commands
if runtime.GOOS != "windows" {
RootCmd.AddCommand(bootstrapCmd)
Expand All @@ -143,6 +143,8 @@ func init() {
}

// Execute executes the root command.
func Execute() 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)
}
18 changes: 17 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package main

import (
"fmt"
"github.com/algorandfoundation/nodekit/api"
"github.com/algorandfoundation/nodekit/cmd"
"github.com/charmbracelet/log"
"os"
"runtime"
)

var version = "dev"

func init() {
// TODO: handle log files
// Log as JSON instead of the default ASCII formatter.
Expand All @@ -20,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. Upgrade with \"nodekit upgrade\"", resp.JSON200))
}
}
}
// TODO: more performance tuning
runtime.GOMAXPROCS(1)
err := cmd.Execute()
err = cmd.Execute(version, needsUpgrade)
if err != nil {
return
}
Expand Down

0 comments on commit 3968399

Please sign in to comment.