From b177e7f0e94a0ca8cb5b5d786c64e269211345a1 Mon Sep 17 00:00:00 2001 From: Guy Wilks Date: Mon, 17 Jul 2023 00:59:34 -0700 Subject: [PATCH] Added utility version check (FSOC-24) (#145) --------- Co-authored-by: GDW1 --- cmd/root.go | 51 ++++++++++++++++++++++++-- cmd/version/version_check.go | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 cmd/version/version_check.go diff --git a/cmd/root.go b/cmd/root.go index a9147da8..1e4e4aa0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,10 @@ import ( "fmt" "os" "path" + "strconv" + "time" + "github.com/Masterminds/semver/v3" "github.com/apex/log" "github.com/apex/log/handlers/json" "github.com/apex/log/handlers/multi" @@ -39,10 +42,18 @@ var cfgProfile string var outputFormat string const ( - FSOC_CONFIG_ENVVAR = "FSOC_CONFIG" - FSOC_PROFILE_ENVVAR = "FSOC_PROFILE" + FSOC_CONFIG_ENVVAR = "FSOC_CONFIG" + FSOC_PROFILE_ENVVAR = "FSOC_PROFILE" + FSOC_NO_VERSION_CHECK = "FSOC_NO_VERSION_CHECK" ) +const ( + secondInDay = time.Hour * 24 + timestampFileName = "fsoc.timestamp" +) + +var updateChannel chan *semver.Version + // rootCmd represents the base command when called without any subcommands // TODO: replace github link "for more info" with Cisco DevNet link for fsoc once published var rootCmd = &cobra.Command{ @@ -69,6 +80,7 @@ For more information, see https://github.com/cisco-open/fsoc NOTE: fsoc is in alpha; breaking changes may occur`, PersistentPreRun: preExecHook, + PersistentPostRun: postExecHook, TraverseChildren: true, DisableAutoGenTag: true, } @@ -93,6 +105,7 @@ func init() { rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable detailed output") rootCmd.PersistentFlags().Bool("curl", false, "Log curl equivalent for platform API calls (implies --verbose)") rootCmd.PersistentFlags().String("log", path.Join(os.TempDir(), "fsoc.log"), "determines the location of the fsoc log file") + rootCmd.PersistentFlags().Bool("no-version-check", false, "Removes daily check for new versions of FSOC") rootCmd.SetOut(os.Stdout) rootCmd.SetErr(os.Stderr) rootCmd.SetIn(os.Stdin) @@ -222,6 +235,40 @@ func preExecHook(cmd *cobra.Command, args []string) { log.Fatalf("fsoc is not configured, please use \"fsoc config set\" to configure an initial context") } } + + // Do version checking + noVerCheck, _ := cmd.Flags().GetBool("no-version-check") + envNoVerCheck, err := strconv.ParseBool(os.Getenv(FSOC_NO_VERSION_CHECK)) + if err != nil { + envNoVerCheck = false + } + noVerCheck = noVerCheck || envNoVerCheck + updateChecked := int(time.Now().Unix())-getLastVersionCheckTime() > int(secondInDay) && !noVerCheck + if updateChecked { + updateChannel = make(chan *semver.Version) + go version.CheckForUpdate(updateChannel) + } + // Create new timestamp file + _ = os.Remove(os.TempDir() + timestampFileName) + _, err = os.Create(os.TempDir() + timestampFileName) + if err != nil { + log.Errorf("failed to create version check timestamp file: %v", err.Error()) + } +} + +func getLastVersionCheckTime() int { + fInfo, err := os.Stat(os.TempDir() + timestampFileName) + if err != nil { + return 0 + } + return int(fInfo.ModTime().Unix()) +} + +func postExecHook(cmd *cobra.Command, args []string) { + if updateChannel != nil { + var updateSemVar = <-updateChannel + version.CompareAndLogVersions(updateSemVar) + } } func bypassConfig(cmd *cobra.Command) bool { diff --git a/cmd/version/version_check.go b/cmd/version/version_check.go new file mode 100644 index 00000000..f63a4f2b --- /dev/null +++ b/cmd/version/version_check.go @@ -0,0 +1,69 @@ +package version + +import ( + "fmt" + "net/http" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/apex/log" +) + +const ( + versionLatestURL = "https://github.com/cisco-open/fsoc/releases/latest" +) + +func GetLatestVersion() (string, error) { + // Open HTTP client which will not follow redirect + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { // no redirect + return http.ErrUseLastResponse + }, + } + resp, err := client.Get(versionLatestURL) + if err != nil { + return "", err + } + // Redirected link should look like https://github.com/cisco-open/fsoc/releases/tag/{VERSION} + split := strings.Split(resp.Header.Get("Location"), "/") + if len(split) < 1 { + return "", fmt.Errorf("version request did not return a version") + } + return split[len(split)-1], nil +} + +func CheckForUpdate(versionChannel chan *semver.Version) { + log.Infof("Checking for newer version of FSOC") + newestVersion, err := GetLatestVersion() + log.WithField("Latest FSOC version", newestVersion).Infof("Latest fsoc version available: %s", newestVersion) + if err != nil { + log.Warnf(err.Error()) + } + newestVersionSemVar, err := semver.NewVersion(newestVersion) + if err != nil { + log.WithField("unparseable version", newestVersion).Warnf("Could not parse version string: %v", err.Error()) + } + versionChannel <- newestVersionSemVar +} + +func CompareAndLogVersions(newestVersionSemVar *semver.Version) { + currentVersion := GetVersion() + currentVersionSemVer := ConvertVerToSemVar(currentVersion) + newerVersionAvailable := currentVersionSemVer.Compare(newestVersionSemVar) == -1 + var debugFields = log.Fields{"IsLatestVersionDifferent": newerVersionAvailable, "CurrentVersion": currentVersionSemVer.String(), "LatestVersion": newestVersionSemVar.String()} + + if newerVersionAvailable { + log.WithFields(debugFields).Warnf("There is a newer version of FSOC available, please upgrade from version") + } else { + log.WithFields(debugFields) + } + +} + +func ConvertVerToSemVar(data VersionData) *semver.Version { + return semver.New( + uint64(data.VersionMajor), + uint64(data.VersionMajor), + uint64(data.VersionMajor), + data.VersionMeta, "") +}