Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: warn when kusion is out of date #157

Merged
merged 1 commit into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ require (
github.com/Azure/go-autorest/autorest/mocks v0.4.1
github.com/aliyun/aliyun-oss-go-sdk v2.1.8+incompatible
github.com/aws/aws-sdk-go v1.42.35
github.com/blang/semver/v4 v4.0.0
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1
github.com/davecgh/go-spew v1.1.1
github.com/didi/gendry v1.7.0
github.com/djherbis/times v1.5.0
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible
github.com/fatih/color v1.13.0 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand Down Expand Up @@ -221,6 +224,8 @@ github.com/didi/gendry v1.7.0 h1:dFR6+TVCnbjvLfNiGN53xInG/C5HqG7u0gfnkF5J/Vo=
github.com/didi/gendry v1.7.0/go.mod h1:cSLuShZ1Zbs1S05RIOLNQv616aBaOQ1BDrXJP9A3J+M=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU=
github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
Expand Down
202 changes: 202 additions & 0 deletions pkg/kusionctl/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package cmd

import (
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/blang/semver/v4"
"github.com/djherbis/times"
"github.com/spf13/cobra"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/kubectl/pkg/util/templates"
Expand All @@ -18,7 +27,12 @@ import (
"kusionstack.io/kusion/pkg/kusionctl/cmd/ls"
"kusionstack.io/kusion/pkg/kusionctl/cmd/preview"
"kusionstack.io/kusion/pkg/kusionctl/cmd/version"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/util/gitutil"
"kusionstack.io/kusion/pkg/util/i18n"
"kusionstack.io/kusion/pkg/util/kfile"
"kusionstack.io/kusion/pkg/util/pretty"
versionInfo "kusionstack.io/kusion/pkg/version"
)

// NewDefaultKusionctlCommand creates the `kusionctl` command with default arguments
Expand Down Expand Up @@ -53,15 +67,45 @@ func NewKusionctlCmd(in io.Reader, out, err io.Writer) *cobra.Command {
// the language, instead of just loading from the LANG env. variable.
_ = i18n.LoadTranslations("kusion", nil)

updateCheckResult := make(chan string)

// Parent command to which all subcommands are added.
cmds := &cobra.Command{
Use: "kusion",
Short: i18n.T(rootShort),
Long: templates.LongDesc(i18n.T(rootLong)),
SilenceErrors: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// If we fail before we start the async update check, go ahead and close the
// channel since we know it will never receive a value.
var waitForUpdateCheck bool
defer func() {
if !waitForUpdateCheck {
close(updateCheckResult)
}
}()

if v := os.Getenv("KUSION_SKIP_UPDATE_CHECK"); v == "true" {
log.Infof("skipping update check")
} else {
// Run the version check in parallel so that it doesn't block executing the command.
// If there is a new version to report, we will do so after the command has finished.
waitForUpdateCheck = true
go func() {
updateCheckResult <- checkForUpdate()
close(updateCheckResult)
}()
}
},
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
checkVersionMsg, ok := <-updateCheckResult
if ok && checkVersionMsg != "" {
fmt.Println(checkVersionMsg)
}
},
}

// From this point and forward we get warnings on flags that contain "_" separators
Expand Down Expand Up @@ -100,3 +144,161 @@ func NewKusionctlCmd(in io.Reader, out, err io.Writer) *cobra.Command {

return cmds
}

// checkForUpdate checks to see if the CLI needs to be updated,
// and if so emits a warning, as well as information as to how it can be upgraded.
func checkForUpdate() string {
curVer, err := semver.ParseTolerant(versionInfo.ReleaseVersion())
if err != nil {
log.Infof("error parsing current version: %s", err)
}

// We don't care about warning for you to update if you have installed a developer version
if isDevVersion(curVer) {
return ""
}

latestVer, err := getLatestVersionInfo()
if err != nil {
log.Infof("error fetching latest version information "+
"(set `KUSION_SKIP_UPDATE_CHECK=true` to skip update checks): %s", err)
}

if latestVer.GT(curVer) {
return pretty.LightYellow("warning: ") + getUpgradeMessage(latestVer, curVer)
}

return ""
}

func isDevVersion(s semver.Version) bool {
if len(s.Build) != 0 {
return true
}

if len(s.Pre) == 0 {
return false
}

devStrings := regexp.MustCompile(`alpha|beta|dev|rc`)
return !s.Pre[0].IsNum && devStrings.MatchString(s.Pre[0].VersionStr)
}

// getLatestVersionInfo returns information about the latest version of the CLI.
// It caches data from the server for a day.
func getLatestVersionInfo() (semver.Version, error) {
cached, err := getCachedVersionInfo()
if err == nil {
return cached, nil
}

latestTag, err := gitutil.GetLatestTag()
if err != nil {
return semver.Version{}, err
}

latest, err := semver.ParseTolerant(latestTag)
if err != nil {
return semver.Version{}, err
}

if err = cacheVersionInfo(latest); err != nil {
log.Infof("failed to cache version info: %s", err)
}

return latest, nil
}

// getCachedVersionInfo reads cached information about the newest CLI version, returning the newest version available.
func getCachedVersionInfo() (semver.Version, error) {
updateCheckFile, err := kfile.GetCachedVersionFilePath()
if err != nil {
return semver.Version{}, err
}

ts, err := times.Stat(updateCheckFile)
if err != nil {
return semver.Version{}, err
}

if time.Now().After(ts.ModTime().Add(24 * time.Hour)) {
return semver.Version{}, errors.New("cached expired")
}

cached, err := os.ReadFile(updateCheckFile)
if err != nil {
return semver.Version{}, err
}

latest, err := semver.ParseTolerant(string(cached))
if err != nil {
return semver.Version{}, err
}

return latest, err
}

// cacheVersionInfo saves version information in a cache file to be looked up later.
func cacheVersionInfo(latest semver.Version) error {
updateCheckFile, err := kfile.GetCachedVersionFilePath()
if err != nil {
return err
}

file, err := os.OpenFile(updateCheckFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0600)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(latest.String())
return err
}

// getUpgradeMessage gets a message to display to a user instructing that
// they are out of date and how to move from current to latest.
func getUpgradeMessage(latest semver.Version, current semver.Version) string {
cmd := getUpgradeCommand()

msg := fmt.Sprintf("A new version of Kusion is available. To upgrade from version '%s' to '%s', ", current, latest)
if cmd != "" {
msg += "run \n " + cmd + "\nor "
}

msg += "visit https://kusionstack.io/docs/user_docs/getting-started/install/ for manual instructions."
return msg
}

// getUpgradeCommand returns a command that will upgrade the CLI to the newest version.
// If we can not determine how the CLI was installed, the empty string is returned.
func getUpgradeCommand() string {
exe, err := os.Executable()
if err != nil {
return ""
}

isKusionup, err := isKusionUpInstall(exe)
if err != nil {
log.Infof("error determining if the running executable was installed with kusionup: %s", err)
}
if isKusionup {
return "$ kusionup install"
}
return ""
}

// isKusionUpInstall returns true if the current running executable is running on linux based and was installed with kusionup.
func isKusionUpInstall(exe string) (bool, error) {
exePath, err := filepath.EvalSymlinks(exe)
if err != nil {
return false, err
}

curUser, err := user.Current()
if err != nil {
return false, err
}

prefix := filepath.Join(curUser.HomeDir, ".kusionup")
return strings.HasPrefix(exePath, prefix), nil
}
23 changes: 23 additions & 0 deletions pkg/kusionctl/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,31 @@ package cmd

import (
"testing"

"github.com/blang/semver/v4"
"github.com/stretchr/testify/assert"
)

func TestNewKusionctlCmd(t *testing.T) {
NewDefaultKusionctlCommand()
}

func TestIsDevVersion(t *testing.T) {
stableVer, _ := semver.ParseTolerant("1.0.0")
assert.False(t, isDevVersion(stableVer))

devVer, _ := semver.ParseTolerant("v1.0.0-dev")
assert.True(t, isDevVersion(devVer))

alphaVer, _ := semver.ParseTolerant("v1.0.0-alpha.1590772212+g4ff08363.dirty")
assert.True(t, isDevVersion(alphaVer))

betaVer, _ := semver.ParseTolerant("v1.0.0-beta.1590772212")
assert.True(t, isDevVersion(betaVer))

rcVer, _ := semver.ParseTolerant("v1.0.0-rc.1")
assert.True(t, isDevVersion(rcVer))

cmVer, _ := semver.ParseTolerant("v0.7.1+3d300d71")
assert.True(t, isDevVersion(cmVer))
}
12 changes: 12 additions & 0 deletions pkg/util/kfile/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

const (
EnvKusionPath = "KUSION_PATH"
// CachedVersionFile is the name of the file we use to store when we last checked if the CLI was out of date
CachedVersionFile = ".cached_version"
)

func Stat(filename string) (fileInfo os.FileInfo, err error) {
Expand Down Expand Up @@ -115,3 +117,13 @@ func GetCredentials() (map[string]interface{}, error) {
}
return credentials, nil
}

// GetCachedVersionFilePath returns the location where the CLI caches information from pulumi.com on the newest
// available version of the CLI
func GetCachedVersionFilePath() (string, error) {
homeDir, err := KusionDataFolder()
if err != nil {
return "", err
}
return filepath.Join(homeDir, CachedVersionFile), nil
}