Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add auto-update #85

Merged
merged 4 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
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", resp.JSON200))
}
}
}
// TODO: more performance tuning
runtime.GOMAXPROCS(1)
err := cmd.Execute()
err = cmd.Execute(version, needsUpgrade)
if err != nil {
return
}
Expand Down
Loading