diff --git a/Makefile b/Makefile index b369bf7a545..a8fa273b29c 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ BUILD_CHANNEL ?= local PATH_WITH_TOOLS="`pwd`/$(TOOL_BIN):`pwd`/node_modules/.bin:${PATH}" GIT_REVISION = $(shell git rev-parse HEAD | tr -d '\n') -TAG_VERSION?=$(shell git tag --points-at | sort -Vr | head -n1) -LDFLAGS = -ldflags "-s -w -extld="$(shell pwd)/etc/ld_wrapper.sh" -X 'go.viam.com/rdk/config.Version=${TAG_VERSION}' -X 'go.viam.com/rdk/config.GitRevision=${GIT_REVISION}'" +TAG_VERSION?=$(shell git tag --points-at | sort -Vr | head -n1 | grep . || echo "(dev)") +DATE_COMPILED?=$(shell date +'%Y-%m-%d') +LDFLAGS = -ldflags "-s -w -extld="$(shell pwd)/etc/ld_wrapper.sh" -X 'go.viam.com/rdk/config.Version=${TAG_VERSION}' -X 'go.viam.com/rdk/config.GitRevision=${GIT_REVISION}' -X 'go.viam.com/rdk/config.DateCompiled=${DATE_COMPILED}'" ifeq ($(shell command -v dpkg >/dev/null && dpkg --print-architecture),armhf) GOFLAGS += -tags=no_tflite endif diff --git a/cli/app.go b/cli/app.go index 767dfd19235..b35b5f5cbdc 100644 --- a/cli/app.go +++ b/cli/app.go @@ -19,6 +19,9 @@ const ( aliasRobotFlag = "robot" partFlag = "part" + // TODO: RSDK-6683. + quietFlag = "quiet" + logsFlagErrors = "errors" logsFlagTail = "tail" @@ -109,6 +112,12 @@ var app = &cli.App{ Aliases: []string{"vvv"}, Usage: "enable debug logging", }, + &cli.BoolFlag{ + Name: quietFlag, + Value: false, + Aliases: []string{"q"}, + Usage: "suppress warnings", + }, }, Commands: []*cli.Command{ { @@ -124,6 +133,7 @@ var app = &cli.App{ }, }, Action: LoginAction, + After: CheckUpdateAction, Subcommands: []*cli.Command{ { Name: "print-access-token", diff --git a/cli/client.go b/cli/client.go index ee2f0e1d39f..5b45e7f0037 100644 --- a/cli/client.go +++ b/cli/client.go @@ -3,9 +3,11 @@ package cli import ( "context" + "encoding/json" "fmt" "io" "net" + "net/http" "net/url" "os" "os/exec" @@ -13,6 +15,7 @@ import ( "strings" "time" + "github.com/Masterminds/semver/v3" "github.com/fullstorydev/grpcurl" "github.com/google/uuid" "github.com/jhump/protoreflect/grpcreflect" @@ -40,6 +43,10 @@ import ( "go.viam.com/rdk/services/shell" ) +const ( + rdkReleaseURL = "https://api.github.com/repos/viamrobotics/rdk/releases/latest" +) + // viamClient wraps a cli.Context and provides all the CLI command functionality // needed to talk to the app and data services but not directly to robot parts. type viamClient struct { @@ -380,6 +387,129 @@ func RobotsPartShellAction(c *cli.Context) error { ) } +// checkUpdateResponse holds the values used to hold release information. +type getLatestReleaseResponse struct { + Name string `json:"name"` + TagName string `json:"tag_name"` + TarballURL string `json:"tarball_url"` +} + +func getLatestReleaseVersion() (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + resp := getLatestReleaseResponse{} + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rdkReleaseURL, nil) + if err != nil { + return "", err + } + + client := http.DefaultClient + res, err := client.Do(req) + if err != nil { + return "", err + } + + err = json.NewDecoder(res.Body).Decode(&resp) + if err != nil { + return "", err + } + + defer utils.UncheckedError(res.Body.Close()) + return resp.TagName, err +} + +// CheckUpdateAction is the corresponding Action for 'check-update'. +func CheckUpdateAction(c *cli.Context) error { + if c.Bool(quietFlag) { + return nil + } + + dateCompiledRaw := rconfig.DateCompiled + + // `go build` will not set the compilation flags needed for this check + if dateCompiledRaw == "" { + return nil + } + + dateCompiled, err := time.Parse("2006-01-02", dateCompiledRaw) + if err != nil { + warningf(c.App.ErrWriter, "CLI Update Check: failed to parse compilation date: %w", err) + return nil + } + + // install is less than six weeks old + if time.Since(dateCompiled) < time.Hour*24*7*6 { + return nil + } + + conf, err := configFromCache() + if err != nil { + if !os.IsNotExist(err) { + utils.UncheckedError(err) + return nil + } + conf = &config{} + } + + var lastCheck time.Time + if conf.LastUpdateCheck == "" { + conf.LastUpdateCheck = time.Now().Format("2006-01-02") + } else { + lastCheck, err = time.Parse("2006-01-02", conf.LastUpdateCheck) + if err != nil { + warningf(c.App.ErrWriter, "CLI Update Check: failed to parse date of last check: %w", err) + return nil + } + } + + // The latest version info is cached to limit api calls to once every three days + if time.Since(lastCheck) < time.Hour*24*3 && conf.LatestVersion != "" { + warningf(c.App.ErrWriter, "CLI Update Check: Your CLI is more than 6 weeks old. "+ + "Consider updating to version: %s", conf.LatestVersion) + return nil + } + + latestRelease, err := getLatestReleaseVersion() + if err != nil { + warningf(c.App.ErrWriter, "CLI Update Check: failed to get latest release information: %w", err) + return nil + } + + latestVersion, err := semver.NewVersion(latestRelease) + if err != nil { + warningf(c.App.ErrWriter, "CLI Update Check: failed to parse latest version: %w", err) + return nil + } + + conf.LatestVersion = latestVersion.String() + + err = storeConfigToCache(conf) + if err != nil { + utils.UncheckedError(err) + } + + appVersion := rconfig.Version + if appVersion == "(dev)" { + warningf(c.App.ErrWriter, "CLI Update Check: Your CLI is more than 6 weeks old. "+ + "Consider updating to version: %s", latestVersion.Original()) + return nil + } + + localVersion, err := semver.NewVersion(appVersion) + if err != nil { + warningf(c.App.ErrWriter, "CLI Update Check: failed to parse compiled version: %w", err) + return nil + } + + if localVersion.LessThan(latestVersion) { + warningf(c.App.ErrWriter, "CLI Update Check: Your CLI is out of date. Consider updating to version %s", latestVersion.Original()) + } + + return nil +} + // VersionAction is the corresponding Action for 'version'. func VersionAction(c *cli.Context) error { info, ok := debug.ReadBuildInfo() @@ -408,10 +538,8 @@ func VersionAction(c *cli.Context) error { if dep, ok := deps["go.viam.com/api"]; ok { apiVersion = dep.Version } + appVersion := rconfig.Version - if appVersion == "" { - appVersion = "(dev)" - } printf(c.App.Writer, "Version %s Git=%s API=%s", appVersion, version, apiVersion) return nil } diff --git a/cli/config.go b/cli/config.go index c5222876831..592b2b48bb5 100644 --- a/cli/config.go +++ b/cli/config.go @@ -51,8 +51,10 @@ func storeConfigToCache(cfg *config) error { } type config struct { - BaseURL string `json:"base_url"` - Auth authMethod `json:"auth"` + BaseURL string `json:"base_url"` + Auth authMethod `json:"auth"` + LastUpdateCheck string `json:"last_update_check"` + LatestVersion string `json:"latest_version"` } func (conf *config) tryUnmarshallWithToken(configBytes []byte) error { diff --git a/cli/print.go b/cli/print.go index e4dcfd119cb..10bf467236f 100644 --- a/cli/print.go +++ b/cli/print.go @@ -38,12 +38,6 @@ func infof(w io.Writer, format string, a ...interface{}) { } // warningf prints a message prefixed with a bold yellow "Warning: ". -// -// NOTE(benjirewis): we disable the unparam linter here. Our usages of warningf -// do not currently make use of the variadic `a` parameter but may in the -// future. unparam will complain until it does. -// -//nolint:unparam func warningf(w io.Writer, format string, a ...interface{}) { if _, err := color.New(color.Bold, color.FgYellow).Fprint(w, "Warning: "); err != nil { log.Fatal(err) diff --git a/config/reader.go b/config/reader.go index e33fbd8c770..36bccce3924 100644 --- a/config/reader.go +++ b/config/reader.go @@ -26,8 +26,9 @@ import ( // RDK versioning variables which are replaced by LD flags. var ( - Version = "" - GitRevision = "" + Version = "" + GitRevision = "" + DateCompiled = "" ) func getAgentInfo() (*apppb.AgentInfo, error) { diff --git a/go.mod b/go.mod index 3f96fa14b33..8193d8c9367 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/AlekSi/gocov-xml v1.0.0 github.com/CPRT/roboclaw v0.0.0-20190825181223-76871438befc + github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/sprig v2.22.0+incompatible github.com/NYTimes/gziphandler v1.1.1 github.com/a8m/envsubst v1.4.2 diff --git a/go.sum b/go.sum index 5e21c720eae..3729a648ff9 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy86 github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=