summaryrefslogtreecommitdiffstats
path: root/client/go
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-09-20 12:10:30 +0200
committerMartin Polden <mpolden@mpolden.no>2021-09-20 14:54:39 +0200
commit9889680a4e1ce02e210bf3f09f320380b4991b41 (patch)
tree27cc70054a22a82a450cb78a8836a855e564e009 /client/go
parent0c9276b0935dbaa2979b75ecc53a6f1d54e0b27d (diff)
Implement version check
Diffstat (limited to 'client/go')
-rw-r--r--client/go/cmd/root.go13
-rw-r--r--client/go/cmd/version.go123
-rw-r--r--client/go/cmd/version_test.go42
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 }