From bc4412f90093b05fca45934a718c1d8beef217d9 Mon Sep 17 00:00:00 2001 From: Will Robertson Date: Sun, 12 Feb 2023 03:58:13 +0000 Subject: [PATCH 1/6] Add support for update command --- README.md | 2 + rocketpool-cli/rocketpool-cli.go | 2 + rocketpool-cli/update/update.go | 311 +++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+) create mode 100644 rocketpool-cli/update/update.go diff --git a/README.md b/README.md index b9647f262..db6aee706 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ The following commands are available via the Smartnode client: - `rocketpool service resync-eth1` - Deletes the main ETH1 client's chain data and resyncs it from scratch. Only use this as a last resort! - `rocketpool service resync-eth2` - Deletes the ETH2 client's chain data and resyncs it from scratch. Only use this as a last resort! - `rocketpool service terminate, t` - Deletes all of the Rocket Pool Docker containers and volumes, including your ETH1 and ETH2 chain data and your Prometheus database (if metrics are enabled). Only use this if you are cleaning up the Smartnode and want to start over! +- **update**, u - Update Rocket Pool + - `rocketpool update cli, c` - Update the Rocket Pool Client (CLI) - **wallet**, w - Manage the node wallet - `rocketpool wallet status, s` - Get the node wallet status - `rocketpool wallet init, i` - Initialize the node wallet diff --git a/rocketpool-cli/rocketpool-cli.go b/rocketpool-cli/rocketpool-cli.go index 48a387586..4db604a51 100644 --- a/rocketpool-cli/rocketpool-cli.go +++ b/rocketpool-cli/rocketpool-cli.go @@ -15,6 +15,7 @@ import ( "github.com/rocket-pool/smartnode/rocketpool-cli/odao" "github.com/rocket-pool/smartnode/rocketpool-cli/queue" "github.com/rocket-pool/smartnode/rocketpool-cli/service" + "github.com/rocket-pool/smartnode/rocketpool-cli/update" "github.com/rocket-pool/smartnode/rocketpool-cli/wallet" "github.com/rocket-pool/smartnode/shared" "github.com/rocket-pool/smartnode/shared/services/rocketpool" @@ -148,6 +149,7 @@ ______ _ _ ______ _ odao.RegisterCommands(app, "odao", []string{"o"}) queue.RegisterCommands(app, "queue", []string{"q"}) service.RegisterCommands(app, "service", []string{"s"}) + update.RegisterCommands(app, "update", []string{"u"}) wallet.RegisterCommands(app, "wallet", []string{"w"}) app.Before = func(c *cli.Context) error { diff --git a/rocketpool-cli/update/update.go b/rocketpool-cli/update/update.go new file mode 100644 index 000000000..552f0f0a8 --- /dev/null +++ b/rocketpool-cli/update/update.go @@ -0,0 +1,311 @@ +package update + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/blang/semver/v4" + "github.com/urfave/cli" + "golang.org/x/crypto/openpgp" + + "github.com/rocket-pool/smartnode/shared" + "github.com/rocket-pool/smartnode/shared/services/config" + cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" + cliutils "github.com/rocket-pool/smartnode/shared/utils/cli" +) + +// Settings +const ( + ExporterContainerSuffix string = "_exporter" + ValidatorContainerSuffix string = "_validator" + BeaconContainerSuffix string = "_eth2" + ExecutionContainerSuffix string = "_eth1" + NodeContainerSuffix string = "_node" + ApiContainerSuffix string = "_api" + WatchtowerContainerSuffix string = "_watchtower" + PruneProvisionerContainerSuffix string = "_prune_provisioner" + EcMigratorContainerSuffix string = "_ec_migrator" + clientDataVolumeName string = "/ethclient" + dataFolderVolumeName string = "/.rocketpool/data" + + PruneFreeSpaceRequired uint64 = 50 * 1024 * 1024 * 1024 + dockerImageRegex string = ".*/(?P.*):.*" + colorReset string = "\033[0m" + colorBold string = "\033[1m" + colorRed string = "\033[31m" + colorYellow string = "\033[33m" + colorGreen string = "\033[32m" + colorLightBlue string = "\033[36m" + clearLine string = "\033[2K" +) + +// Creates CLI argument flags from the parameters of the configuration struct +func createFlagsFromConfigParams(sectionName string, params []*cfgtypes.Parameter, configFlags []cli.Flag, network cfgtypes.Network) []cli.Flag { + for _, param := range params { + var paramName string + if sectionName == "" { + paramName = param.ID + } else { + paramName = fmt.Sprintf("%s-%s", sectionName, param.ID) + } + + defaultVal, err := param.GetDefault(network) + if err != nil { + panic(fmt.Sprintf("Error getting default value for [%s]: %s\n", paramName, err.Error())) + } + + switch param.Type { + case cfgtypes.ParameterType_Bool: + configFlags = append(configFlags, cli.BoolFlag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: bool\n", param.Description), + }) + case cfgtypes.ParameterType_Int: + configFlags = append(configFlags, cli.IntFlag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: int\n", param.Description), + Value: int(defaultVal.(int64)), + }) + case cfgtypes.ParameterType_Float: + configFlags = append(configFlags, cli.Float64Flag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: float\n", param.Description), + Value: defaultVal.(float64), + }) + case cfgtypes.ParameterType_String: + configFlags = append(configFlags, cli.StringFlag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: string\n", param.Description), + Value: defaultVal.(string), + }) + case cfgtypes.ParameterType_Uint: + configFlags = append(configFlags, cli.UintFlag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: uint\n", param.Description), + Value: uint(defaultVal.(uint64)), + }) + case cfgtypes.ParameterType_Uint16: + configFlags = append(configFlags, cli.UintFlag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: uint16\n", param.Description), + Value: uint(defaultVal.(uint16)), + }) + case cfgtypes.ParameterType_Choice: + optionStrings := []string{} + for _, option := range param.Options { + optionStrings = append(optionStrings, fmt.Sprint(option.Value)) + } + configFlags = append(configFlags, cli.StringFlag{ + Name: paramName, + Usage: fmt.Sprintf("%s\n\tType: choice\n\tOptions: %s\n", param.Description, strings.Join(optionStrings, ", ")), + Value: fmt.Sprint(defaultVal), + }) + } + } + + return configFlags +} + +func getHttpClientWithTimeout() *http.Client { + return &http.Client{ + Timeout: time.Second * 5, + } +} + +func checkSignature(signatureUrl string, pubkeyUrl string, verification_target *os.File) error { + pubkeyResponse, err := http.Get(pubkeyUrl) + if err != nil { + return err + } + defer pubkeyResponse.Body.Close() + if pubkeyResponse.StatusCode != http.StatusOK { + return fmt.Errorf("public key request failed with code %d", pubkeyResponse.StatusCode) + } + keyring, err := openpgp.ReadArmoredKeyRing(pubkeyResponse.Body) + if err != nil { + return fmt.Errorf("error while reading public key: %w", err) + } + + signatureResponse, err := http.Get(signatureUrl) + if err != nil { + return err + } + defer signatureResponse.Body.Close() + if signatureResponse.StatusCode != http.StatusOK { + return fmt.Errorf("signature request failed with code %d", signatureResponse.StatusCode) + } + + entity, err := openpgp.CheckDetachedSignature(keyring, verification_target, signatureResponse.Body) + if err != nil { + return fmt.Errorf("error while verifying signature: %w", err) + } + + for _, v := range entity.Identities { + fmt.Printf("Signed by: %s", v.Name) + } + return nil +} + +// Update the Rocket Pool CLI +func updateCLI(c *cli.Context) error { + // Check the latest version published to the Github repository + client := getHttpClientWithTimeout() + resp, err := client.Get("https://api.github.com/repos/rocket-pool/smartnode-install/releases/latest") + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("request failed with code %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + var apiResponse map[string]interface{} + if err := json.Unmarshal(body, &apiResponse); err != nil { + return fmt.Errorf("could not decode Github API response: %w", err) + } + latestVersion, err := semver.Make(strings.TrimLeft(apiResponse["name"].(string), "v")) + if err != nil { + return fmt.Errorf("could not parse latest Rocket Pool version number from API response '%s': %w", apiResponse["name"].(string), err) + } + + // Check this version against the currently installed version + if !c.Bool("force") { + currentVersion, err := semver.Make(shared.RocketPoolVersion) + if err != nil { + return fmt.Errorf("could not parse local Rocket Pool version number '%s': %w", shared.RocketPoolVersion, err) + } + switch latestVersion.Compare(currentVersion) { + case 1: + fmt.Printf("Newer version avilable online (%s). Downloading...\n", latestVersion.String()) + case 0: + fmt.Printf("Already on latest version (%s). Aborting update\n", latestVersion.String()) + return nil + default: + fmt.Printf("Online version (%s) is lower than running version (%s). Aborting update\n", latestVersion.String(), currentVersion.String()) + return nil + } + } else { + fmt.Printf("Forced update to %s. Downloading...\n", latestVersion.String()) + } + + // Download the new binary to same folder as the running RP binary, as `rocketpool-vX.X.X` + var ClientURL = fmt.Sprintf("https://github.com/rocket-pool/smartnode-install/releases/download/v%s/rocketpool-cli-%s-%s", latestVersion.String(), runtime.GOOS, runtime.GOARCH) + resp, err = http.Get(ClientURL) + if err != nil { + return fmt.Errorf("error while downloading %s: %w", ClientURL, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("request failed with code %d", resp.StatusCode) + } + + ex, err := os.Executable() + if err != nil { + return fmt.Errorf("error while determining running rocketpool location: %w", err) + } + var rpBinDir = filepath.Dir(ex) + var fileName = filepath.Join(rpBinDir, "rocketpool-v"+latestVersion.String()) + output, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("error while creating %s: %w", fileName, err) + } + defer output.Close() + + _, err = io.Copy(output, resp.Body) + if err != nil { + return fmt.Errorf("error while downloading %s: %w", ClientURL, err) + } + + // Verify the signature of the downloaded binary + if !c.Bool("skip-signature-verification") { + var pubkeyUrl = fmt.Sprintf("https://github.com/rocket-pool/smartnode-install/releases/download/v%s/smartnode-signing-key-v3.asc", latestVersion.String()) + output.Seek(0, io.SeekStart) + err = checkSignature(ClientURL+".sig", pubkeyUrl, output) + if err != nil { + return fmt.Errorf("error while verifying GPG signature: %w", err) + } + } + + // Prompt for confirmation + if !(c.Bool("yes") || cliutils.Confirm("Are you sure you want to update? Current Rocketpool Client will be replaced.")) { + fmt.Println("Cancelled.") + return nil + } + + // Do the switcheroo - move `rocketpool-vX.X.X` to the location of the current Rocketpool Client + err = os.Remove(ex) + if err != nil { + return fmt.Errorf("error while removing old rocketpool binary: %w", err) + } + err = os.Rename(fileName, ex) + if err != nil { + return fmt.Errorf("error while writing new rocketpool binary: %w", err) + } + + fmt.Printf("Updated Rocketpool Client to v%s. Please run `rocketpool service install` to finish the installation and update your smartstack.\n", latestVersion.String()) + return nil +} + +// Register commands +func RegisterCommands(app *cli.App, name string, aliases []string) { + + configFlags := []cli.Flag{} + cfgTemplate := config.NewRocketPoolConfig("", false) + network := cfgTemplate.Smartnode.Network.Value.(cfgtypes.Network) + + // Root params + configFlags = createFlagsFromConfigParams("", cfgTemplate.GetParameters(), configFlags, network) + + // Subconfigs + for sectionName, subconfig := range cfgTemplate.GetSubconfigs() { + configFlags = createFlagsFromConfigParams(sectionName, subconfig.GetParameters(), configFlags, network) + } + + app.Commands = append(app.Commands, cli.Command{ + Name: name, + Aliases: aliases, + Subcommands: []cli.Command{ + { + Name: "cli", + Aliases: []string{"c"}, + Usage: "Update the Rocket Pool CLI", + UsageText: "rocketpool update cli [options]", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "Force update, even if same version or lower", + }, + cli.BoolFlag{ + Name: "skip-signature-verification, s", + Usage: "Skip signature verification", + }, + cli.BoolFlag{ + Name: "yes, y", + Usage: "Automatically confirm update", + }, + }, + Action: func(c *cli.Context) error { + + // Validate args + if err := cliutils.ValidateArgCount(c, 0); err != nil { + return err + } + + // Run command + return updateCLI(c) + + }, + }, + }, + }) +} From 7b65b647b2b6c04f10cd57e0e4f6cab76721c94d Mon Sep 17 00:00:00 2001 From: Will Robertson Date: Sun, 12 Feb 2023 10:10:59 +0000 Subject: [PATCH 2/6] Removed unused code --- rocketpool-cli/update/update.go | 121 +++----------------------------- 1 file changed, 11 insertions(+), 110 deletions(-) diff --git a/rocketpool-cli/update/update.go b/rocketpool-cli/update/update.go index 552f0f0a8..02447d2ab 100644 --- a/rocketpool-cli/update/update.go +++ b/rocketpool-cli/update/update.go @@ -16,103 +16,16 @@ import ( "golang.org/x/crypto/openpgp" "github.com/rocket-pool/smartnode/shared" - "github.com/rocket-pool/smartnode/shared/services/config" - cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" cliutils "github.com/rocket-pool/smartnode/shared/utils/cli" ) // Settings const ( - ExporterContainerSuffix string = "_exporter" - ValidatorContainerSuffix string = "_validator" - BeaconContainerSuffix string = "_eth2" - ExecutionContainerSuffix string = "_eth1" - NodeContainerSuffix string = "_node" - ApiContainerSuffix string = "_api" - WatchtowerContainerSuffix string = "_watchtower" - PruneProvisionerContainerSuffix string = "_prune_provisioner" - EcMigratorContainerSuffix string = "_ec_migrator" - clientDataVolumeName string = "/ethclient" - dataFolderVolumeName string = "/.rocketpool/data" - - PruneFreeSpaceRequired uint64 = 50 * 1024 * 1024 * 1024 - dockerImageRegex string = ".*/(?P.*):.*" - colorReset string = "\033[0m" - colorBold string = "\033[1m" - colorRed string = "\033[31m" - colorYellow string = "\033[33m" - colorGreen string = "\033[32m" - colorLightBlue string = "\033[36m" - clearLine string = "\033[2K" + GithubAPIGetLatest string = "https://api.github.com/repos/rocket-pool/smartnode-install/releases/latest" + SigningKeyURL string = "https://github.com/rocket-pool/smartnode-install/releases/download/v%s/smartnode-signing-key-v3.asc" + ReleaseBinaryURL string = "https://github.com/rocket-pool/smartnode-install/releases/download/v%s/rocketpool-cli-%s-%s" ) -// Creates CLI argument flags from the parameters of the configuration struct -func createFlagsFromConfigParams(sectionName string, params []*cfgtypes.Parameter, configFlags []cli.Flag, network cfgtypes.Network) []cli.Flag { - for _, param := range params { - var paramName string - if sectionName == "" { - paramName = param.ID - } else { - paramName = fmt.Sprintf("%s-%s", sectionName, param.ID) - } - - defaultVal, err := param.GetDefault(network) - if err != nil { - panic(fmt.Sprintf("Error getting default value for [%s]: %s\n", paramName, err.Error())) - } - - switch param.Type { - case cfgtypes.ParameterType_Bool: - configFlags = append(configFlags, cli.BoolFlag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: bool\n", param.Description), - }) - case cfgtypes.ParameterType_Int: - configFlags = append(configFlags, cli.IntFlag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: int\n", param.Description), - Value: int(defaultVal.(int64)), - }) - case cfgtypes.ParameterType_Float: - configFlags = append(configFlags, cli.Float64Flag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: float\n", param.Description), - Value: defaultVal.(float64), - }) - case cfgtypes.ParameterType_String: - configFlags = append(configFlags, cli.StringFlag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: string\n", param.Description), - Value: defaultVal.(string), - }) - case cfgtypes.ParameterType_Uint: - configFlags = append(configFlags, cli.UintFlag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: uint\n", param.Description), - Value: uint(defaultVal.(uint64)), - }) - case cfgtypes.ParameterType_Uint16: - configFlags = append(configFlags, cli.UintFlag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: uint16\n", param.Description), - Value: uint(defaultVal.(uint16)), - }) - case cfgtypes.ParameterType_Choice: - optionStrings := []string{} - for _, option := range param.Options { - optionStrings = append(optionStrings, fmt.Sprint(option.Value)) - } - configFlags = append(configFlags, cli.StringFlag{ - Name: paramName, - Usage: fmt.Sprintf("%s\n\tType: choice\n\tOptions: %s\n", param.Description, strings.Join(optionStrings, ", ")), - Value: fmt.Sprint(defaultVal), - }) - } - } - - return configFlags -} - func getHttpClientWithTimeout() *http.Client { return &http.Client{ Timeout: time.Second * 5, @@ -148,7 +61,7 @@ func checkSignature(signatureUrl string, pubkeyUrl string, verification_target * } for _, v := range entity.Identities { - fmt.Printf("Signed by: %s", v.Name) + fmt.Printf("Signature verified. Signed by: %s\n", v.Name) } return nil } @@ -157,7 +70,7 @@ func checkSignature(signatureUrl string, pubkeyUrl string, verification_target * func updateCLI(c *cli.Context) error { // Check the latest version published to the Github repository client := getHttpClientWithTimeout() - resp, err := client.Get("https://api.github.com/repos/rocket-pool/smartnode-install/releases/latest") + resp, err := client.Get(GithubAPIGetLatest) if err != nil { return err } @@ -186,20 +99,20 @@ func updateCLI(c *cli.Context) error { } switch latestVersion.Compare(currentVersion) { case 1: - fmt.Printf("Newer version avilable online (%s). Downloading...\n", latestVersion.String()) + fmt.Printf("Newer version avilable online (v%s). Downloading...\n", latestVersion.String()) case 0: - fmt.Printf("Already on latest version (%s). Aborting update\n", latestVersion.String()) + fmt.Printf("Already on latest version (v%s). Aborting update\n", latestVersion.String()) return nil default: - fmt.Printf("Online version (%s) is lower than running version (%s). Aborting update\n", latestVersion.String(), currentVersion.String()) + fmt.Printf("Online version (v%s) is lower than running version (v%s). Aborting update\n", latestVersion.String(), currentVersion.String()) return nil } } else { - fmt.Printf("Forced update to %s. Downloading...\n", latestVersion.String()) + fmt.Printf("Forced update to v%s. Downloading...\n", latestVersion.String()) } // Download the new binary to same folder as the running RP binary, as `rocketpool-vX.X.X` - var ClientURL = fmt.Sprintf("https://github.com/rocket-pool/smartnode-install/releases/download/v%s/rocketpool-cli-%s-%s", latestVersion.String(), runtime.GOOS, runtime.GOARCH) + var ClientURL = fmt.Sprintf(ReleaseBinaryURL, latestVersion.String(), runtime.GOOS, runtime.GOARCH) resp, err = http.Get(ClientURL) if err != nil { return fmt.Errorf("error while downloading %s: %w", ClientURL, err) @@ -228,7 +141,7 @@ func updateCLI(c *cli.Context) error { // Verify the signature of the downloaded binary if !c.Bool("skip-signature-verification") { - var pubkeyUrl = fmt.Sprintf("https://github.com/rocket-pool/smartnode-install/releases/download/v%s/smartnode-signing-key-v3.asc", latestVersion.String()) + var pubkeyUrl = fmt.Sprintf(SigningKeyURL, latestVersion.String()) output.Seek(0, io.SeekStart) err = checkSignature(ClientURL+".sig", pubkeyUrl, output) if err != nil { @@ -259,18 +172,6 @@ func updateCLI(c *cli.Context) error { // Register commands func RegisterCommands(app *cli.App, name string, aliases []string) { - configFlags := []cli.Flag{} - cfgTemplate := config.NewRocketPoolConfig("", false) - network := cfgTemplate.Smartnode.Network.Value.(cfgtypes.Network) - - // Root params - configFlags = createFlagsFromConfigParams("", cfgTemplate.GetParameters(), configFlags, network) - - // Subconfigs - for sectionName, subconfig := range cfgTemplate.GetSubconfigs() { - configFlags = createFlagsFromConfigParams(sectionName, subconfig.GetParameters(), configFlags, network) - } - app.Commands = append(app.Commands, cli.Command{ Name: name, Aliases: aliases, From 49a337c4a227c459f7da589ddd548ccd9aa5e8f7 Mon Sep 17 00:00:00 2001 From: Will Robertson Date: Sun, 12 Feb 2023 11:33:59 +0000 Subject: [PATCH 3/6] Handle Github API issues gracefully --- rocketpool-cli/update/update.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/rocketpool-cli/update/update.go b/rocketpool-cli/update/update.go index 02447d2ab..f7565b295 100644 --- a/rocketpool-cli/update/update.go +++ b/rocketpool-cli/update/update.go @@ -86,9 +86,19 @@ func updateCLI(c *cli.Context) error { if err := json.Unmarshal(body, &apiResponse); err != nil { return fmt.Errorf("could not decode Github API response: %w", err) } - latestVersion, err := semver.Make(strings.TrimLeft(apiResponse["name"].(string), "v")) - if err != nil { - return fmt.Errorf("could not parse latest Rocket Pool version number from API response '%s': %w", apiResponse["name"].(string), err) + var latestVersion semver.Version + if x, found := apiResponse["url"]; found { + var name string + var ok bool + if name, ok = x.(string); !ok { + return fmt.Errorf("unexpected Github API response format") + } + latestVersion, err = semver.Make(strings.TrimLeft(name, "v")) + if err != nil { + return fmt.Errorf("could not parse version number from release name '%s': %w", name, err) + } + } else { + return fmt.Errorf("unexpected Github API response format") } // Check this version against the currently installed version From 2ec9a9ce2ab2d103969393db9ded07d2f32978da Mon Sep 17 00:00:00 2001 From: Will Robertson Date: Wed, 22 Feb 2023 11:02:01 +0000 Subject: [PATCH 4/6] Address PR comments --- rocketpool-cli/update/update.go | 127 ++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/rocketpool-cli/update/update.go b/rocketpool-cli/update/update.go index f7565b295..e34315cc3 100644 --- a/rocketpool-cli/update/update.go +++ b/rocketpool-cli/update/update.go @@ -26,16 +26,10 @@ const ( ReleaseBinaryURL string = "https://github.com/rocket-pool/smartnode-install/releases/download/v%s/rocketpool-cli-%s-%s" ) -func getHttpClientWithTimeout() *http.Client { - return &http.Client{ - Timeout: time.Second * 5, - } -} - func checkSignature(signatureUrl string, pubkeyUrl string, verification_target *os.File) error { pubkeyResponse, err := http.Get(pubkeyUrl) if err != nil { - return err + return fmt.Errorf("error while fetching public key: %w", err) } defer pubkeyResponse.Body.Close() if pubkeyResponse.StatusCode != http.StatusOK { @@ -48,7 +42,7 @@ func checkSignature(signatureUrl string, pubkeyUrl string, verification_target * signatureResponse, err := http.Get(signatureUrl) if err != nil { - return err + return fmt.Errorf("error while fetching signature: %w", err) } defer signatureResponse.Body.Close() if signatureResponse.StatusCode != http.StatusOK { @@ -66,98 +60,121 @@ func checkSignature(signatureUrl string, pubkeyUrl string, verification_target * return nil } -// Update the Rocket Pool CLI -func updateCLI(c *cli.Context) error { - // Check the latest version published to the Github repository +func getHttpClientWithTimeout() *http.Client { + return &http.Client{ + Timeout: time.Second * 5, + } +} + +func getLatestRelease() (semver.Version, error) { + var latestVersion semver.Version client := getHttpClientWithTimeout() resp, err := client.Get(GithubAPIGetLatest) if err != nil { - return err + return latestVersion, fmt.Errorf("error while fetching latest version: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("request failed with code %d", resp.StatusCode) + return latestVersion, fmt.Errorf("request failed with code %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { - return err + return latestVersion, fmt.Errorf("error while reading Github API response: %w", err) } var apiResponse map[string]interface{} if err := json.Unmarshal(body, &apiResponse); err != nil { - return fmt.Errorf("could not decode Github API response: %w", err) + return latestVersion, fmt.Errorf("could not decode Github API response: %w", err) } - var latestVersion semver.Version - if x, found := apiResponse["url"]; found { + if x, found := apiResponse["name"]; found { var name string var ok bool if name, ok = x.(string); !ok { - return fmt.Errorf("unexpected Github API response format") + return latestVersion, fmt.Errorf("unexpected Github API response format") } latestVersion, err = semver.Make(strings.TrimLeft(name, "v")) if err != nil { - return fmt.Errorf("could not parse version number from release name '%s': %w", name, err) + return latestVersion, fmt.Errorf("could not parse version number from release name '%s': %w", name, err) } } else { - return fmt.Errorf("unexpected Github API response format") - } - - // Check this version against the currently installed version - if !c.Bool("force") { - currentVersion, err := semver.Make(shared.RocketPoolVersion) - if err != nil { - return fmt.Errorf("could not parse local Rocket Pool version number '%s': %w", shared.RocketPoolVersion, err) - } - switch latestVersion.Compare(currentVersion) { - case 1: - fmt.Printf("Newer version avilable online (v%s). Downloading...\n", latestVersion.String()) - case 0: - fmt.Printf("Already on latest version (v%s). Aborting update\n", latestVersion.String()) - return nil - default: - fmt.Printf("Online version (v%s) is lower than running version (v%s). Aborting update\n", latestVersion.String(), currentVersion.String()) - return nil - } - } else { - fmt.Printf("Forced update to v%s. Downloading...\n", latestVersion.String()) + return latestVersion, fmt.Errorf("unexpected Github API response format") } + return latestVersion, nil +} - // Download the new binary to same folder as the running RP binary, as `rocketpool-vX.X.X` - var ClientURL = fmt.Sprintf(ReleaseBinaryURL, latestVersion.String(), runtime.GOOS, runtime.GOARCH) - resp, err = http.Get(ClientURL) +func downloadRelease(version semver.Version, verify bool) (string, string, error) { + var ClientURL = fmt.Sprintf(ReleaseBinaryURL, version.String(), runtime.GOOS, runtime.GOARCH) + resp, err := http.Get(ClientURL) if err != nil { - return fmt.Errorf("error while downloading %s: %w", ClientURL, err) + return "", "", fmt.Errorf("error while downloading %s: %w", ClientURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("request failed with code %d", resp.StatusCode) + return "", "", fmt.Errorf("request failed with code %d", resp.StatusCode) } ex, err := os.Executable() if err != nil { - return fmt.Errorf("error while determining running rocketpool location: %w", err) + return "", "", fmt.Errorf("error while determining running rocketpool location: %w", err) } var rpBinDir = filepath.Dir(ex) - var fileName = filepath.Join(rpBinDir, "rocketpool-v"+latestVersion.String()) + var fileName = filepath.Join(rpBinDir, "rocketpool-v"+version.String()) output, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { - return fmt.Errorf("error while creating %s: %w", fileName, err) + return "", "", fmt.Errorf("error while creating %s: %w", fileName, err) } defer output.Close() _, err = io.Copy(output, resp.Body) if err != nil { - return fmt.Errorf("error while downloading %s: %w", ClientURL, err) + return "", "", fmt.Errorf("error while downloading %s: %w", ClientURL, err) } // Verify the signature of the downloaded binary - if !c.Bool("skip-signature-verification") { - var pubkeyUrl = fmt.Sprintf(SigningKeyURL, latestVersion.String()) + if verify { + var pubkeyUrl = fmt.Sprintf(SigningKeyURL, version.String()) output.Seek(0, io.SeekStart) err = checkSignature(ClientURL+".sig", pubkeyUrl, output) if err != nil { - return fmt.Errorf("error while verifying GPG signature: %w", err) + return "", "", fmt.Errorf("error while verifying GPG signature: %w", err) } } + return fileName, ex, nil +} + +// Update the Rocket Pool CLI +func updateCLI(c *cli.Context) error { + + // Check the latest version published to the Github repository + latestVersion, err := getLatestRelease() + if err != nil { + return fmt.Errorf("could not check latest version: %w", err) + } + + // Check this version against the currently installed version + if !c.Bool("force") { + currentVersion, err := semver.Make(shared.RocketPoolVersion) + if err != nil { + return fmt.Errorf("could not parse local Rocket Pool version number '%s': %w", shared.RocketPoolVersion, err) + } + switch latestVersion.Compare(currentVersion) { + case 1: + fmt.Printf("Newer version avilable online (v%s). Downloading...\n", latestVersion.String()) + case 0: + fmt.Printf("Already on latest version (v%s). Aborting update\n", latestVersion.String()) + return nil + default: + fmt.Printf("Online version (v%s) is lower than running version (v%s). Aborting update\n", latestVersion.String(), currentVersion.String()) + return nil + } + } else { + fmt.Printf("Forced update to v%s. Downloading...\n", latestVersion.String()) + } + + // Download the new binary to same folder as the running RP binary and check signature (unless skipped) + newFile, oldFile, err := downloadRelease(latestVersion, !c.Bool("skip-signature-verification")) + if err != nil { + return fmt.Errorf("error while downloading latest release: %w", err) + } // Prompt for confirmation if !(c.Bool("yes") || cliutils.Confirm("Are you sure you want to update? Current Rocketpool Client will be replaced.")) { @@ -166,11 +183,7 @@ func updateCLI(c *cli.Context) error { } // Do the switcheroo - move `rocketpool-vX.X.X` to the location of the current Rocketpool Client - err = os.Remove(ex) - if err != nil { - return fmt.Errorf("error while removing old rocketpool binary: %w", err) - } - err = os.Rename(fileName, ex) + err = os.Rename(newFile, oldFile) if err != nil { return fmt.Errorf("error while writing new rocketpool binary: %w", err) } From 21ba27bcd4e3474289effbbc7c582c9e80d79885 Mon Sep 17 00:00:00 2001 From: Will Robertson Date: Fri, 24 Feb 2023 22:40:01 +0000 Subject: [PATCH 5/6] Check seek result --- rocketpool-cli/update/update.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocketpool-cli/update/update.go b/rocketpool-cli/update/update.go index e34315cc3..7cdaaf292 100644 --- a/rocketpool-cli/update/update.go +++ b/rocketpool-cli/update/update.go @@ -132,7 +132,10 @@ func downloadRelease(version semver.Version, verify bool) (string, string, error // Verify the signature of the downloaded binary if verify { var pubkeyUrl = fmt.Sprintf(SigningKeyURL, version.String()) - output.Seek(0, io.SeekStart) + _, err = output.Seek(0, io.SeekStart) + if err != nil { + return "", "", fmt.Errorf("error while seeking in %s: %w", fileName, err) + } err = checkSignature(ClientURL+".sig", pubkeyUrl, output) if err != nil { return "", "", fmt.Errorf("error while verifying GPG signature: %w", err) From f11c68cad8bb38de70dd3ac25ca1d38b3e4ae215 Mon Sep 17 00:00:00 2001 From: Will Robertson Date: Sat, 25 Feb 2023 05:13:34 +0000 Subject: [PATCH 6/6] Clean up download if update aborted --- rocketpool-cli/update/update.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rocketpool-cli/update/update.go b/rocketpool-cli/update/update.go index 7cdaaf292..78b9192b1 100644 --- a/rocketpool-cli/update/update.go +++ b/rocketpool-cli/update/update.go @@ -182,6 +182,10 @@ func updateCLI(c *cli.Context) error { // Prompt for confirmation if !(c.Bool("yes") || cliutils.Confirm("Are you sure you want to update? Current Rocketpool Client will be replaced.")) { fmt.Println("Cancelled.") + err = os.Remove(newFile) + if err != nil { + return fmt.Errorf("error while cleaning up downloaded file: %w", err) + } return nil }