From 5c482bea0d5233ae3cdb67171950850181b75184 Mon Sep 17 00:00:00 2001 From: jordanrowe <37838443+jordanrowe@users.noreply.github.com> Date: Mon, 3 Jul 2023 14:53:58 +0100 Subject: [PATCH] BDOG-2580 add -update command which will download and install the latest version of sm2 --- cli/cli.go | 2 + servicemanager/commands.go | 2 + servicemanager/update.go | 166 +++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 servicemanager/update.go diff --git a/cli/cli.go b/cli/cli.go index 256c3fc..5fe42e9 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -40,6 +40,7 @@ type UserOption struct { StatusShort bool // same as --status but is the -s short version of the cmd StopAll bool // stops all the services that are running Stop bool // stops a service, multiple services or profile(s) + Update bool // update sm2 if a newer version is available UpdateConfig bool // pulls the latest copy of service-manager-config Verbose bool // shows extra logging Version bool // prints sm2 version number @@ -182,6 +183,7 @@ func buildFlagSet(opts *UserOption) *flag.FlagSet { flagset.BoolVar(&opts.StatusShort, "s", false, "shows which services are running") flagset.BoolVar(&opts.StopAll, "stop-all", false, "stops all services") flagset.BoolVar(&opts.Stop, "stop", false, "stops one or more services") + flagset.BoolVar(&opts.Update, "update", false, "updates sm2 to the latest available version") flagset.BoolVar(&opts.UpdateConfig, "update-config", false, "pulls the latest version of service-manager-config") flagset.BoolVar(&opts.Verbose, "v", false, "enable verbose output") flagset.BoolVar(&opts.Version, "version", false, "show the version of service-manager") diff --git a/servicemanager/commands.go b/servicemanager/commands.go index 50dc28f..83d3c44 100644 --- a/servicemanager/commands.go +++ b/servicemanager/commands.go @@ -122,6 +122,8 @@ func (sm *ServiceManager) Run() { if !ok { os.Exit(13) } + } else if sm.Commands.Update { + err = update(sm.Config.TmpDir) } else if sm.Commands.AutoComplete { cli.GenerateAutoCompletions() } else { diff --git a/servicemanager/update.go b/servicemanager/update.go new file mode 100644 index 0000000..e4fdd98 --- /dev/null +++ b/servicemanager/update.go @@ -0,0 +1,166 @@ +package servicemanager + +import ( + "archive/zip" + "bytes" + "fmt" + "io/ioutil" + "log" + "net/http" + "os/exec" + "runtime" + "sm2/version" + "strings" +) + +func update(workspaceInstallPath string) error { + currentVersion := version.Version + latestVersion, err := getLatestVersion() + if err != nil { + return err + } + + if currentVersion == latestVersion { + // already on latest, short-circuit + fmt.Printf("Already up to date.") + return nil + } + + installLocation, err := getInstallLocation() + if err != nil { + return err + } + + fmt.Printf("Current Version: %s\n", currentVersion) + fmt.Printf("Latest Version: %s\n", latestVersion) + fmt.Printf("OS: %s\n", runtime.GOOS) + fmt.Printf("CPU: %s\n", runtime.GOARCH) + fmt.Printf("Current Install Location: %s\n", installLocation) + + err = downloadAndInstall(latestVersion, workspaceInstallPath, installLocation) + if err != nil { + return err + } + + return nil +} + +func getLatestVersion() (string, error) { + // create a custom client that doesn't follow redirects + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + // hit releases/latest which will redirect us + resp, err := client.Get("https://github.com/hmrc/sm2/releases/latest") + if err != nil { + return "", err + } + + defer resp.Body.Close() + + // extract the redirect location, should look like https://github.com/hmrc/sm2/releases/tag/v0.0.0 + location := resp.Header.Get("location") + + // split the url and extract the version from the tag + parts := strings.Split(location, "/") + tag := parts[len(parts)-1] + version := strings.TrimPrefix(tag, "v") + + return version, nil +} + +func getInstallLocation() (string, error) { + cmd := exec.Command("which", "sm2") + + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println("Unable to determine the location of the sm2 binary currently installed using `which sm2`. Please ensure sm2 is available on your $PATH.") + } + + return strings.TrimSpace(string(output)), err +} + +func downloadAndInstall(versionToInstall string, workspaceInstallPath string, installLocation string) error { + + // we'll download and inflate the zip into $WORKSPACE/install + downloadLocation := workspaceInstallPath + "/sm2" + + // convert `darwin` to `apple` for download url + var os string + switch runtime.GOOS { + case "darwin": + os = "apple" + case "linux": + os = "linux" + default: + log.Fatalf("unsupported OS: %s", runtime.GOOS) + } + + // convert `amd64` to `intel` for download url + var arch string + switch runtime.GOARCH { + case "amd64": + arch = "intel" + case "arm64": + arch = "arm64" + default: + log.Fatalf("unsupported CPU architecture %s", runtime.GOARCH) + } + + downloadUrl := fmt.Sprintf("https://github.com/hmrc/sm2/releases/download/v%s/sm2-%s-%s-%s.zip", versionToInstall, versionToInstall, os, arch) + + fmt.Printf("Downloading %s...\n", downloadUrl) + + resp, err := http.Get(downloadUrl) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) + if err != nil { + return err + } + + for _, zipFile := range zipReader.File { + if zipFile.Name == "sm2" { + rc, err := zipFile.Open() + if err != nil { + return err + } + defer rc.Close() + + fileData, err := ioutil.ReadAll(rc) + if err != nil { + return err + } + + fmt.Printf("Unzipping into %s...\n", downloadLocation) + + err = ioutil.WriteFile(downloadLocation, fileData, 0755) + if err != nil { + return err + } + } + } + + fmt.Printf("Moving new sm2 binary from %s to %s (you may be prompted for your password)...\n", downloadLocation, installLocation) + + cmd := exec.Command("sudo", "mv", downloadLocation, installLocation) + err = cmd.Run() + if err != nil { + return err + } + + fmt.Printf("Successfully installed v%s!\n", versionToInstall) + + return nil +}