Skip to content

Commit 3ad84d7

Browse files
committed
Add a rocketpool update command
1 parent 9211b67 commit 3ad84d7

4 files changed

Lines changed: 224 additions & 5 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ ${BIN_DIR}/rocketpool-cli-$1-$2: ${bin_deps}
2727
ifndef NO_DOCKER
2828
docker run --rm -v ./:/src --user $(shell id -u):$(shell id -g) -e CGO_ENABLED=0 \
2929
-e GOARCH=$2 -e GOOS=$1 --workdir /src -v ~/.cache:/.cache rocketpool/smartnode-builder:${VERSION} \
30-
go build -o $$@ rocketpool-cli/rocketpool-cli.go
30+
go build -o $$@ ./rocketpool-cli/
3131
else
32-
CGO_ENABLED=0 GOOS=$1 GOARCH=$2 go build -o $$@ ./rocketpool-cli/rocketpool-cli.go
32+
CGO_ENABLED=0 GOOS=$1 GOARCH=$2 go build -o $$@ ./rocketpool-cli/
3333
endif
3434
endef
3535

rocketpool-cli/rocketpool-cli.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import (
2626

2727
const (
2828
colorReset string = "\033[0m"
29+
colorRed string = "\033[31m"
2930
colorYellow string = "\033[33m"
31+
colorGreen string = "\033[32m"
3032
maxAlertItems int = 3
3133
)
3234

@@ -110,6 +112,23 @@ A special thanks to the Rocket Pool community for all their contributions.
110112
service.RegisterCommands(app, "service", []string{"s"})
111113
wallet.RegisterCommands(app, "wallet", []string{"w"})
112114

115+
// Add a command that updates the smart node cli.
116+
app.Commands = append(app.Commands, cli.Command{
117+
Name: "update",
118+
Usage: "Update the cli binary",
119+
Flags: []cli.Flag{
120+
cli.BoolFlag{
121+
Name: "yes, y",
122+
Usage: "Automatically confirm the update",
123+
},
124+
cli.BoolFlag{
125+
Name: "force, f",
126+
Usage: "Force the update even if the current version is the latest",
127+
},
128+
},
129+
Action: update,
130+
})
131+
113132
app.Before = func(c *cli.Context) error {
114133
// Check user ID
115134
if os.Getuid() == 0 && !c.GlobalBool("allow-root") {

rocketpool-cli/update.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"runtime"
11+
"strings"
12+
13+
"github.com/rocket-pool/smartnode/shared"
14+
"github.com/rocket-pool/smartnode/shared/utils/cli/prompt"
15+
"github.com/urfave/cli"
16+
)
17+
18+
const (
19+
downloadUrlFormatString = "https://github.com/rocket-pool/smartnode/releases/latest/download/rocketpool-cli-%s-%s"
20+
)
21+
22+
func validateOsArch() error {
23+
24+
switch runtime.GOARCH {
25+
case "amd64":
26+
case "arm64":
27+
default:
28+
return fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
29+
}
30+
31+
switch runtime.GOOS {
32+
case "linux":
33+
case "darwin":
34+
default:
35+
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
36+
}
37+
38+
return nil
39+
}
40+
41+
func errorPartialSuccess(err error) {
42+
fmt.Fprintln(os.Stderr, "An error occurred after the cli binary was updated, but before the service was.")
43+
fmt.Fprintln(os.Stderr, "The error was:")
44+
fmt.Fprintf(os.Stderr, "%s %s%s\n", colorRed, err.Error(), colorReset)
45+
fmt.Fprintln(os.Stderr)
46+
printPartialSuccessNextSteps()
47+
os.Exit(1)
48+
}
49+
50+
func printPartialSuccessNextSteps() {
51+
fmt.Println("Please complete the following steps to complete the update:")
52+
fmt.Println(" Run `rocketpool service stop` to stop the service")
53+
fmt.Println(" Run `rocketpool service install -d` to upgrade the service")
54+
fmt.Println(" Run `rocketpool service start` to start the service")
55+
}
56+
57+
func forkCommand(binaryPath string, yes bool, args ...string) *exec.Cmd {
58+
cmd := exec.Command(binaryPath, args...)
59+
cmd.Stdout = os.Stdout
60+
cmd.Stderr = os.Stderr
61+
cmd.Stdin = os.Stdin
62+
if yes {
63+
cmd.Args = append(cmd.Args, "--yes")
64+
}
65+
return cmd
66+
}
67+
68+
func update(c *cli.Context) error {
69+
// Get the pwd and argv[0]
70+
pwd, err := os.Getwd()
71+
if err != nil {
72+
return err
73+
}
74+
argv0 := os.Args[0]
75+
76+
oldBinaryPath := filepath.Join(pwd, argv0)
77+
78+
// Validate the OS and architecture
79+
err = validateOsArch()
80+
if err != nil {
81+
return err
82+
}
83+
84+
fmt.Printf("Your detected os/architecture is %s%s/%s%s.\n", colorGreen, runtime.GOOS, runtime.GOARCH, colorReset)
85+
fmt.Println()
86+
87+
if !c.Bool("yes") {
88+
ok := prompt.Confirm("The cli at %s%s%s will be replaced. Continue?", colorYellow, oldBinaryPath, colorReset)
89+
if !ok {
90+
return nil
91+
}
92+
}
93+
fmt.Printf("Replacing the cli at %s with the latest version...\n", oldBinaryPath)
94+
95+
// Create a temporary directory to download the new binary to
96+
tempDir, err := os.MkdirTemp("", "rocketpool-cli-update-")
97+
if err != nil {
98+
return fmt.Errorf("error creating temporary directory: %w", err)
99+
}
100+
defer os.RemoveAll(tempDir)
101+
102+
// Create a file that is executable and has the correct permissions
103+
tempFile, err := os.CreateTemp(tempDir, "rocketpool-cli-update-*.bin")
104+
if err != nil {
105+
return fmt.Errorf("error creating temporary file: %w", err)
106+
}
107+
tempFile.Chmod(0755)
108+
109+
// Download the new binary
110+
downloadUrl := fmt.Sprintf(downloadUrlFormatString, runtime.GOOS, runtime.GOARCH)
111+
fmt.Printf("Downloading the new binary from %s%s%s\n", colorGreen, downloadUrl, colorReset)
112+
fmt.Println()
113+
response, err := http.Get(downloadUrl)
114+
if err != nil {
115+
return fmt.Errorf("error downloading new binary: %w", err)
116+
}
117+
if response.StatusCode != 200 {
118+
return fmt.Errorf("error downloading new binary: %s", response.Status)
119+
}
120+
defer response.Body.Close()
121+
_, err = io.Copy(tempFile, response.Body)
122+
if err != nil {
123+
return fmt.Errorf("error copying new binary to temporary file: %w", err)
124+
}
125+
tempFile.Close()
126+
127+
// Fork off a process to get the new binary's version and compare
128+
// it with the current version
129+
cmd := exec.Command(tempFile.Name(), "--version")
130+
output, err := cmd.Output()
131+
if err != nil {
132+
return fmt.Errorf("error getting new binary's version: %w", err)
133+
}
134+
newVersion := strings.TrimSpace(string(output))
135+
newVersion = strings.TrimPrefix(newVersion, "rocketpool version ")
136+
137+
if strings.EqualFold(shared.RocketPoolVersion(), newVersion) && !c.Bool("force") {
138+
fmt.Printf("%sYou are already on the latest version of smartnode: %s.%s\n", colorGreen, newVersion, colorReset)
139+
return nil
140+
}
141+
142+
fmt.Printf("Updating from %s%s%s to %s%s%s\n", colorYellow, shared.RocketPoolVersion(), colorReset, colorGreen, newVersion, colorReset)
143+
144+
// Rename the temporary file to the actual binary
145+
err = os.Rename(tempFile.Name(), oldBinaryPath)
146+
if err != nil {
147+
return fmt.Errorf("error replacing binary: %w", err)
148+
}
149+
150+
fmt.Println()
151+
fmt.Printf("%sThe cli has been updated.%s\n", colorGreen, colorReset)
152+
fmt.Println()
153+
154+
if !c.Bool("yes") {
155+
if !prompt.Confirm("Would you like to automatically stop, upgrade, and restart the service to complete the update?") {
156+
printPartialSuccessNextSteps()
157+
return nil
158+
}
159+
}
160+
161+
fmt.Println("=========================================")
162+
fmt.Println("========= Stopping service... ===========")
163+
fmt.Println("=========================================")
164+
stopCmd := []string{"service", "stop"}
165+
cmd = forkCommand(oldBinaryPath, c.Bool("yes"), stopCmd...)
166+
err = cmd.Run()
167+
if err != nil {
168+
errorPartialSuccess(err)
169+
return nil
170+
}
171+
172+
fmt.Println("=========================================")
173+
fmt.Println("========= Upgrading service... ==========")
174+
fmt.Println("=========================================")
175+
cmd = forkCommand(oldBinaryPath, c.Bool("yes"), "service", "install", "-d")
176+
err = cmd.Run()
177+
if err != nil {
178+
errorPartialSuccess(err)
179+
return nil
180+
}
181+
182+
fmt.Println("=========================================")
183+
fmt.Println("========= Starting service... ===========")
184+
fmt.Println("=========================================")
185+
cmd = forkCommand(oldBinaryPath, c.Bool("yes"), "service", "start")
186+
err = cmd.Run()
187+
if err != nil {
188+
errorPartialSuccess(err)
189+
return nil
190+
}
191+
192+
fmt.Printf("%sThe upgrade to Smart Node %s has been completed.%s\n", colorGreen, newVersion, colorReset)
193+
fmt.Println()
194+
fmt.Printf("%sPlease monitor your validators for a few minutes for issues.%s\n", colorYellow, colorReset)
195+
fmt.Println()
196+
197+
return nil
198+
}

shared/utils/cli/prompt/prompt.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ func Prompt(initialPrompt string, expectedFormat string, incorrectFormatPrompt s
3737
}
3838

3939
// Prompt for confirmation
40-
func Confirm(initialPrompt string) bool {
40+
func Confirm(fmtStr string, args ...any) bool {
41+
initialPrompt := fmt.Sprintf(fmtStr, args...)
4142
response := Prompt(fmt.Sprintf("%s [y/n]", initialPrompt), "(?i)^(y|yes|n|no)$", "Please answer 'y' or 'n'")
4243
return (strings.ToLower(response[:1]) == "y")
4344
}
4445

4546
// Prompt for 'I agree' confirmation (used on important questions to avoid a quick 'y' response from the user)
46-
func ConfirmWithIAgree(initialPrompt string) bool {
47+
func ConfirmWithIAgree(fmtStr string, args ...any) bool {
48+
initialPrompt := fmt.Sprintf(fmtStr, args...)
4749
response := Prompt(fmt.Sprintf("%s [Type 'I agree' or 'n']", initialPrompt), "(?i)^(i agree|n|no)$", "Please answer 'I agree' or 'n'")
4850
return (len(response) == 7 && strings.ToLower(response[:7]) == "i agree")
4951
}
@@ -79,7 +81,7 @@ func Select(initialPrompt string, options []string) (int, string) {
7981

8082
// Prompts the user to verify that there is nobody looking over their shoulder before printing sensitive information.
8183
func ConfirmSecureSession(warning string) bool {
82-
if !Confirm(fmt.Sprintf("%s%s%s\nAre you sure you want to continue?", colorYellow, warning, colorReset)) {
84+
if !Confirm("%s%s%s\nAre you sure you want to continue?", colorYellow, warning, colorReset) {
8385
fmt.Println("Cancelled.")
8486
return false
8587
}

0 commit comments

Comments
 (0)