aboutsummaryrefslogtreecommitdiffstats
path: root/client/go/internal/cli/cmd/version.go
blob: aaf2c42cded62189dbf641e9a825281405bfbad5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package cmd

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/fatih/color"
	"github.com/spf13/cobra"
	"github.com/vespa-engine/vespa/client/go/internal/build"
	"github.com/vespa-engine/vespa/client/go/internal/version"
)

func newVersionCmd(cli *CLI) *cobra.Command {
	var skipVersionCheck bool
	cmd := &cobra.Command{
		Use:               "version",
		Short:             "Show current version and check for updates",
		DisableAutoGenTag: true,
		SilenceUsage:      true,
		Args:              cobra.ExactArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			log.Printf("Vespa CLI version %s compiled with %v on %v/%v", build.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
			if !skipVersionCheck && cli.isTerminal() {
				return checkVersion(cli)
			}
			return nil
		},
	}
	cmd.Flags().BoolVarP(&skipVersionCheck, "no-check", "n", false, "Do not check if a new version is available")
	return cmd
}

func checkVersion(cli *CLI) error {
	current, err := version.Parse(build.Version)
	if err != nil {
		return err
	}
	latest, err := latestRelease(cli)
	if err != nil {
		return err
	}
	if !current.Less(latest.Version) {
		return nil
	}
	usingHomebrew := usingHomebrew(cli)
	if usingHomebrew && latest.isRecent() {
		return nil // Allow some time for new release to appear in Homebrew repo
	}
	log.Printf("\nNew release available: %s", color.GreenString(latest.Version.String()))
	log.Printf("https://github.com/vespa-engine/vespa/releases/tag/v%s", latest.Version)
	if usingHomebrew {
		log.Printf("\nUpgrade by running:\n%s", color.CyanString("brew update && brew upgrade vespa-cli"))
	}
	return nil
}

func latestRelease(cli *CLI) (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 := cli.httpClient.Do(req, time.Minute)
	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(cli *CLI) bool {
	selfPath, err := cli.exec.LookPath("vespa")
	if err != nil {
		return false
	}
	brewPrefix, err := cli.exec.Run("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))
}