Skip to content

Commit

Permalink
Merge pull request #173 from planetscale/fatih/check-for-new-version
Browse files Browse the repository at this point in the history
cli: check for latest version and inform the user
  • Loading branch information
fatih authored Apr 23, 2021
2 parents 26cd655 + ec8c4e2 commit 760c85c
Show file tree
Hide file tree
Showing 17 changed files with 1,446 additions and 8 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/frankban/quicktest v1.12.0
github.com/gocarina/gocsv v0.0.0-20210326111627-0340a0229e98
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-version v1.3.0
github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 // indirect
github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7
github.com/mattn/go-isatty v0.0.12
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
Expand Down
4 changes: 1 addition & 3 deletions internal/cmd/connect/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ func ConnectCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
localAddr string
remoteAddr string
debug bool
}

cmd := &cobra.Command{
Expand Down Expand Up @@ -97,7 +96,7 @@ argument:
LocalAddr: localAddr,
RemoteAddr: flags.remoteAddr,
Instance: fmt.Sprintf("%s/%s/%s", ch.Config.Organization, database, branch),
Logger: cmdutil.NewZapLogger(flags.debug),
Logger: cmdutil.NewZapLogger(ch.Debug),
}

err = runProxy(proxyOpts, database, branch)
Expand All @@ -119,7 +118,6 @@ argument:
"Local address to bind and listen for connections")
cmd.PersistentFlags().StringVar(&flags.remoteAddr, "remote-addr", "",
"PlanetScale Database remote network address. By default the remote address is populated automatically from the PlanetScale API.")
cmd.PersistentFlags().BoolVar(&flags.debug, "debug", false, "enable debug mode")
cmd.MarkPersistentFlagRequired("org") // nolint:errcheck

return cmd
Expand Down
15 changes: 15 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/planetscale/cli/internal/cmdutil"
"github.com/planetscale/cli/internal/config"
"github.com/planetscale/cli/internal/printer"
"github.com/planetscale/cli/internal/update"

ps "github.com/planetscale/planetscale-go/planetscale"

Expand Down Expand Up @@ -100,12 +101,19 @@ func Execute(ver, commit, buildDate string) error {
return err
}

var debug bool
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug mode")
if err := viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")); err != nil {
return err
}

ch := &cmdutil.Helper{
Printer: printer.NewPrinter(&format),
Config: cfg,
Client: func() (*ps.Client, error) {
return cfg.NewClientFromConfig()
},
Debug: debug,
}

// service token flags. they are hidden for now.
Expand Down Expand Up @@ -148,6 +156,13 @@ func Execute(ver, commit, buildDate string) error {
}
}

if format == printer.Human {
err := update.CheckVersion(ver)
if err != nil && debug {
return err
}
}

return nil
}

Expand Down
4 changes: 1 addition & 3 deletions internal/cmd/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ func ShellCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
localAddr string
remoteAddr string
debug bool
}

cmd := &cobra.Command{
Expand Down Expand Up @@ -109,7 +108,7 @@ second argument:
LocalAddr: localAddr,
RemoteAddr: flags.remoteAddr,
Instance: fmt.Sprintf("%s/%s/%s", ch.Config.Organization, database, branch),
Logger: cmdutil.NewZapLogger(flags.debug),
Logger: cmdutil.NewZapLogger(ch.Debug),
}

p, err := proxy.NewClient(proxyOpts)
Expand Down Expand Up @@ -186,7 +185,6 @@ second argument:
"", "Local address to bind and listen for connections. By default the proxy binds to 127.0.0.1 with a random port.")
cmd.PersistentFlags().StringVar(&flags.remoteAddr, "remote-addr", "",
"PlanetScale Database remote network address. By default the remote address is populated automatically from the PlanetScale API.")
cmd.PersistentFlags().BoolVar(&flags.debug, "debug", false, "enable debug mode")
cmd.MarkPersistentFlagRequired("org") // nolint:errcheck

return cmd
Expand Down
5 changes: 3 additions & 2 deletions internal/cmd/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/planetscale/cli/internal/cmdutil"
"github.com/planetscale/cli/internal/printer"

"github.com/spf13/cobra"
)

Expand All @@ -15,10 +16,10 @@ func VersionCmd(ch *cmdutil.Helper, ver, commit, buildDate string) *cobra.Comman
Use: "version <command>",
// we can also show the version via `--version`, hence this doesn't
// need to be displayed.
Hidden: true, //
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
if ch.Printer.Format() == printer.Human {
ch.Printer.Print(Format(ver, commit, buildDate))
ch.Printer.Println(Format(ver, commit, buildDate))
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions internal/cmdutil/cmdutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type Helper struct {

// Printer is used to print output of a command to stdout.
Printer *printer.Printer

// Debug defines the debug mode
Debug bool
}

// RequiredArgs returns a short and actionable error message if the given
Expand Down
238 changes: 238 additions & 0 deletions internal/update/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Package update is checking for a new version of Pscale and informs the user
// to update. Most of the logic is copied from cli/cli:
// https://github.com/cli/cli/blob/trunk/internal/update/update.go and updated
// to our own needs.
package update

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"

"github.com/cli/safeexec"
"github.com/fatih/color"
"github.com/hashicorp/go-version"
"github.com/planetscale/cli/internal/config"
"gopkg.in/yaml.v2"
)

type UpdateInfo struct {
Update bool
Reason string
ReleaseInfo *ReleaseInfo
}

// ReleaseInfo stores information about a release
type ReleaseInfo struct {
Version string `json:"tag_name"`
URL string `json:"html_url"`
PublishedAt time.Time `json:"published_at"`
}

// StateEntry stores the information we have checked for a new version. It's
// used to decide whether to check for a new version or not.
type StateEntry struct {
CheckedForUpdateAt time.Time `yaml:"checked_for_update_at"`
LatestRelease ReleaseInfo `yaml:"latest_release"`
}

// CheckVersion checks for the given build version whether there is a new
// version of the CLI or not.
func CheckVersion(buildVersion string) error {
path, err := stateFilePath()
if err != nil {
return err
}

updateInfo, err := checkVersion(
buildVersion,
path,
latestVersion,
)
if err != nil {
return fmt.Errorf("skipping update, error: %s", err)
}

if !updateInfo.Update {
return fmt.Errorf("skipping update, reason: %s", updateInfo.Reason)
}

fmt.Fprintf(os.Stderr, "\n\n%s %s → %s\n",
color.BlueString("A new release of pscale is available:"),
color.CyanString(buildVersion),
color.CyanString(updateInfo.ReleaseInfo.Version))

if isUnderHomebrew() {
fmt.Fprintf(os.Stderr, "To upgrade, run: %s\n", "brew update && brew upgrade pscale")
}
fmt.Fprintf(os.Stderr, "%s\n", color.YellowString(updateInfo.ReleaseInfo.URL))
return nil
}

func checkVersion(buildVersion, path string, latestVersionFn func(addr string) (*ReleaseInfo, error)) (*UpdateInfo, error) {
if os.Getenv("PSCALE_NO_UPDATE_NOTIFIER") != "" {
return &UpdateInfo{
Update: false,
Reason: "PSCALE_NO_UPDATE_NOTIFIER is set",
}, nil
}

stateEntry, _ := getStateEntry(path)
if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 {
return &UpdateInfo{
Update: false,
Reason: "Latest version was already checked",
}, nil
}

addr := "https://api.github.com/repos/planetscale/cli/releases/latest"
info, err := latestVersionFn(addr)
if err != nil {
return nil, err
}

err = setStateEntry(path, time.Now(), *info)
if err != nil {
return nil, err
}

v1, err := version.NewVersion(info.Version)
if err != nil {
return nil, err
}

v2, err := version.NewVersion(buildVersion)
if err != nil {
return nil, err
}

if v1.LessThanOrEqual(v2) {
return &UpdateInfo{
Update: false,
Reason: fmt.Sprintf("Latest version (%s) is less than or equal to current build version (%s)",
info.Version, buildVersion),
ReleaseInfo: info,
}, nil
}

return &UpdateInfo{
Update: true,
Reason: fmt.Sprintf("Latest version (%s) is greater than the current build version (%s)",
info.Version, buildVersion),
ReleaseInfo: info,
}, nil

}

func latestVersion(addr string) (*ReleaseInfo, error) {
req, err := http.NewRequest("GET", addr, nil)
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/vnd.github.v3+json")

getToken := func() string {
if t := os.Getenv("GH_TOKEN"); t != "" {
return t
}
return os.Getenv("GITHUB_TOKEN")
}

if token := getToken(); token != "" {
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
}

client := &http.Client{Timeout: time.Second * 15}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

out, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
return nil, fmt.Errorf("error fetching latest release: %v", string(out))
}

var info *ReleaseInfo
err = json.Unmarshal(out, &info)
if err != nil {
return nil, err
}

return info, nil
}

// copied from: https://github.com/cli/cli/blob/trunk/cmd/gh/main.go#L298
func isUnderHomebrew() bool {
binary := "pscale"
if exe, err := os.Executable(); err == nil {
binary = exe
}

brewExe, err := safeexec.LookPath("brew")
if err != nil {
return false
}

brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
if err != nil {
return false
}

brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator)
return strings.HasPrefix(binary, brewBinPrefix)
}

func getStateEntry(stateFilePath string) (*StateEntry, error) {
content, err := ioutil.ReadFile(stateFilePath)
if err != nil {
return nil, err
}

var stateEntry StateEntry
err = yaml.Unmarshal(content, &stateEntry)
if err != nil {
return nil, err
}

return &stateEntry, nil
}

func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error {
data := StateEntry{
CheckedForUpdateAt: t,
LatestRelease: r,
}

content, err := yaml.Marshal(data)
if err != nil {
return err
}
_ = ioutil.WriteFile(stateFilePath, content, 0600)

return nil
}

func stateFilePath() (string, error) {
dir, err := config.ConfigDir()
if err != nil {
return "", err
}

return path.Join(dir, "state.yml"), nil
}
Loading

0 comments on commit 760c85c

Please sign in to comment.