diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-09-20 12:10:30 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2021-09-20 14:54:39 +0200 |
commit | 9889680a4e1ce02e210bf3f09f320380b4991b41 (patch) | |
tree | 27cc70054a22a82a450cb78a8836a855e564e009 /client/go | |
parent | 0c9276b0935dbaa2979b75ecc53a6f1d54e0b27d (diff) |
Implement version check
Diffstat (limited to 'client/go')
-rw-r--r-- | client/go/cmd/root.go | 13 | ||||
-rw-r--r-- | client/go/cmd/version.go | 123 | ||||
-rw-r--r-- | client/go/cmd/version_test.go | 42 |
3 files changed, 172 insertions, 6 deletions
diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go index 4202035af92..6bfca7fd613 100644 --- a/client/go/cmd/root.go +++ b/client/go/cmd/root.go @@ -49,6 +49,14 @@ const ( colorFlag = "color" ) +func isTerminal() bool { + file, ok := stdout.(*os.File) + if ok { + return isatty.IsTerminal(file.Fd()) + } + return false +} + func configureOutput() { log.SetFlags(0) // No timestamps log.SetOutput(stdout) @@ -65,10 +73,7 @@ func configureOutput() { colorize := false switch colorValue { case "auto": - file, ok := stdout.(*os.File) - if ok { - colorize = isatty.IsTerminal(file.Fd()) - } + colorize = isTerminal() case "always": colorize = true case "never": diff --git a/client/go/cmd/version.go b/client/go/cmd/version.go index 05820f4e34b..749d17a41d9 100644 --- a/client/go/cmd/version.go +++ b/client/go/cmd/version.go @@ -1,23 +1,144 @@ package cmd import ( + "encoding/json" "log" + "net/http" + "os" + "os/exec" + "path/filepath" "runtime" + "sort" + "strings" + "time" "github.com/spf13/cobra" "github.com/vespa-engine/vespa/client/go/build" + "github.com/vespa-engine/vespa/client/go/util" + "github.com/vespa-engine/vespa/client/go/version" ) +var skipVersionCheck bool + +var sp subprocess = &execSubprocess{} + +type subprocess interface { + pathOf(name string) (string, error) + outputOf(name string, args ...string) ([]byte, error) + isTerminal() bool +} + +type execSubprocess struct{} + +func (c *execSubprocess) pathOf(name string) (string, error) { return exec.LookPath(name) } +func (c *execSubprocess) isTerminal() bool { return isTerminal() } +func (c *execSubprocess) outputOf(name string, args ...string) ([]byte, error) { + return exec.Command(name, args...).Output() +} + func init() { rootCmd.AddCommand(versionCmd) + versionCmd.Flags().BoolVarP(&skipVersionCheck, "no-check", "n", false, "Do not check if a new version is available") } var versionCmd = &cobra.Command{ Use: "version", - Short: "Show version number", + Short: "Show current version and check for updates", DisableAutoGenTag: true, Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { log.Printf("vespa version %s compiled with %v on %v/%v", build.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) + if !skipVersionCheck && sp.isTerminal() { + if err := checkVersion(); err != nil { + fatalErr(err) + } + } }, } + +func checkVersion() error { + current, err := version.Parse(build.Version) + if err != nil { + return err + } + latest, err := latestRelease() + if err != nil { + return err + } + if !current.Less(latest.Version) { + return nil + } + usingHomebrew := usingHomebrew() + if usingHomebrew && latest.isRecent() { + return nil // Allow some time for new release to appear in Homebrew repo + } + log.Printf("\nNew release available: %s", color.Green(latest.Version)) + log.Printf("https://github.com/vespa-engine/vespa/releases/tag/v%s", latest.Version) + if usingHomebrew { + log.Printf("\nUpgrade by running:\n%s", color.Cyan("brew update && brew upgrade vespa-cli")) + } + return nil +} + +func latestRelease() (release, error) { + req, err := http.NewRequest("GET", "https://api.github.com/repos/vespa-engine/vespa/releases", nil) + if err != nil { + return release{}, err + } + response, err := util.HttpDo(req, time.Minute, "GitHub") + if err != nil { + return release{}, err + } + defer response.Body.Close() + + var ghReleases []githubRelease + dec := json.NewDecoder(response.Body) + if err := dec.Decode(&ghReleases); err != nil { + return release{}, err + } + if len(ghReleases) == 0 { + return release{}, nil // No releases found + } + + var releases []release + for _, r := range ghReleases { + v, err := version.Parse(r.TagName) + if err != nil { + return release{}, err + } + publishedAt, err := time.Parse(time.RFC3339, r.PublishedAt) + if err != nil { + return release{}, err + } + releases = append(releases, release{Version: v, PublishedAt: publishedAt}) + } + sort.Slice(releases, func(i, j int) bool { return releases[i].Version.Less(releases[j].Version) }) + return releases[len(releases)-1], nil +} + +func usingHomebrew() bool { + selfPath, err := sp.pathOf("vespa") + if err != nil { + return false + } + brewPrefix, err := sp.outputOf("brew", "--prefix") + if err != nil { + return false + } + brewBin := filepath.Join(strings.TrimSpace(string(brewPrefix)), "bin") + string(os.PathSeparator) + return strings.HasPrefix(selfPath, brewBin) +} + +type githubRelease struct { + TagName string `json:"tag_name"` + PublishedAt string `json:"published_at"` +} + +type release struct { + Version version.Version + PublishedAt time.Time +} + +func (r release) isRecent() bool { + return time.Now().Before(r.PublishedAt.Add(time.Hour * 24)) +} diff --git a/client/go/cmd/version_test.go b/client/go/cmd/version_test.go index fc977c47938..3b0f73de408 100644 --- a/client/go/cmd/version_test.go +++ b/client/go/cmd/version_test.go @@ -1,11 +1,51 @@ package cmd import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/vespa-engine/vespa/client/go/util" ) func TestVersion(t *testing.T) { - assert.Contains(t, execute(command{args: []string{"version"}}, t, nil), "vespa version 0.0.0-devel compiled with") + c := &mockHttpClient{} + c.NextResponse(200, `[{"tag_name": "v1.2.3", "published_at": "2021-09-10T12:00:00Z"}]`) + util.ActiveHttpClient = c + + sp = &mockSubprocess{} + out := execute(command{args: []string{"version"}}, t, nil) + assert.Contains(t, out, "vespa version 0.0.0-devel compiled with") + assert.Contains(t, out, "New release available: 1.2.3\nhttps://github.com/vespa-engine/vespa/releases/tag/v1.2.3") +} + +func TestVersionCheckHomebrew(t *testing.T) { + c := &mockHttpClient{} + c.NextResponse(200, `[{"tag_name": "v1.2.3", "published_at": "2021-09-10T12:00:00Z"}]`) + util.ActiveHttpClient = c + + sp = &mockSubprocess{programPath: "/usr/local/bin/vespa", output: "/usr/local"} + out := execute(command{args: []string{"version"}}, t, nil) + assert.Contains(t, out, "vespa version 0.0.0-devel compiled with") + assert.Contains(t, out, "New release available: 1.2.3\n"+ + "https://github.com/vespa-engine/vespa/releases/tag/v1.2.3\n"+ + "\nUpgrade by running:\nbrew update && brew upgrade vespa-cli\n") +} + +type mockSubprocess struct { + programPath string + output string } + +func (c *mockSubprocess) pathOf(name string) (string, error) { + if c.programPath == "" { + return "", fmt.Errorf("no program path set in this mock") + } + return c.programPath, nil +} + +func (c *mockSubprocess) outputOf(name string, args ...string) ([]byte, error) { + return []byte(c.output), nil +} + +func (c *mockSubprocess) isTerminal() bool { return true } |