Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Listener430 committed Dec 11, 2024
1 parent b144ba2 commit 0f79d1c
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 48 deletions.
6 changes: 6 additions & 0 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,9 @@ settings:
# If the source and destination lists have the same length, all items in the destination lists are
# deep-merged with all items in the source list.
list_merge_strategy: replace

version:
check:
enabled: true
timeout: 1000 # ms
frequency: daily
60 changes: 51 additions & 9 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path"
"strings"
"time"

"github.com/fatih/color"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -428,20 +429,61 @@ func printMessageForMissingAtmosConfig(cliConfig schema.CliConfiguration) {
u.PrintMessage("https://atmos.tools/quick-start\n")
}

// customHelpMessageToUpgradeToAtmosLatestRelease adds Atmos version info at the end of each help commnad
func customHelpMessageToUpgradeToAtmosLatestRelease(cmd *cobra.Command, args []string) {
originalHelpFunc(cmd, args)
// Check for the latest Atmos release on GitHub
// CheckForAtmosUpdateAndPrintMessage checks if a version update is needed and prints a message if a newer version is found.
// It loads the cache, decides if it's time to check for updates, compares the current version to the latest available release,
// and if newer, prints the update message. It also updates the cache's timestamp after printing.
func CheckForAtmosUpdateAndPrintMessage(cliConfig schema.CliConfiguration) {
// If version checking is disabled in the configuration, do nothing
if !cliConfig.Version.Check.Enabled {
return
}

// Load the cache
cacheCfg, err := cfg.LoadCache()
if err != nil {
u.LogWarning(cliConfig, fmt.Sprintf("Could not load cache: %s", err))
return
}

// Determine if it's time to check for updates based on frequency and last_checked
if !cfg.ShouldCheckForUpdates(cacheCfg.LastChecked, cliConfig.Version.Check.Frequency) {
// Not due for another check yet, so return without printing anything
return
}

// Get the latest Atmos release from GitHub
latestReleaseTag, err := u.GetLatestGitHubRepoRelease("cloudposse", "atmos")
if err == nil && latestReleaseTag != "" {
latestRelease := strings.TrimPrefix(latestReleaseTag, "v")
currentRelease := strings.TrimPrefix(version.Version, "v")
if latestRelease != currentRelease {
u.PrintMessageToUpgradeToAtmosLatestRelease(latestRelease)
if err != nil {
u.LogTrace(cliConfig, fmt.Sprintf("Failed to retrieve latest Atmos release info: %s", err))
return
}

if latestReleaseTag == "" {
// No releases found or empty string, return silently
return
}

// Trim "v" prefix to compare versions
latestVersion := strings.TrimPrefix(latestReleaseTag, "v")
currentVersion := strings.TrimPrefix(version.Version, "v")

// If the versions differ, print the update message
if latestVersion != currentVersion {
u.PrintMessageToUpgradeToAtmosLatestRelease(latestVersion)

// Update the cache to mark the current timestamp
cacheCfg.LastChecked = time.Now().Unix()
if saveErr := cfg.SaveCache(cacheCfg); saveErr != nil {
u.LogWarning(cliConfig, fmt.Sprintf("Unable to save cache: %s", saveErr))
}
}
}

func customHelpMessageToUpgradeToAtmosLatestRelease(cmd *cobra.Command, args []string) {
originalHelpFunc(cmd, args)
CheckForAtmosUpdateAndPrintMessage(cliConfig)
}

// Check Atmos is version command
func isVersionCommand() bool {
return len(os.Args) > 1 && os.Args[1] == "version"
Expand Down
15 changes: 2 additions & 13 deletions cmd/helmfile.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package cmd

import (
"strings"

"github.com/samber/lo"
"github.com/spf13/cobra"

e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/cloudposse/atmos/pkg/version"
)

// helmfileCmd represents the base command for all helmfile sub-commands
Expand All @@ -34,18 +31,10 @@ var helmfileCmd = &cobra.Command{
if err != nil {
u.LogErrorAndExit(schema.CliConfiguration{}, err)
}

// Check for the latest Atmos release on GitHub and print update message
latestReleaseTag, err := u.GetLatestGitHubRepoRelease("cloudposse", "atmos")
if err == nil && latestReleaseTag != "" {
latestRelease := strings.TrimPrefix(latestReleaseTag, "v")
currentRelease := strings.TrimPrefix(version.Version, "v")
if latestRelease != currentRelease {
u.PrintMessageToUpgradeToAtmosLatestRelease(latestRelease)
}
}
// Exit on help
if info.NeedHelp {
// Check for the latest Atmos release on GitHub and print update message
CheckForAtmosUpdateAndPrintMessage(cliConfig)
return
}
// Check Atmos configuration
Expand Down
30 changes: 16 additions & 14 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
u "github.com/cloudposse/atmos/pkg/utils"
)

var cliConfig schema.CliConfiguration

// originalHelpFunc holds Cobra's original help function to avoid recursion.
var originalHelpFunc func(*cobra.Command, []string)

Expand Down Expand Up @@ -72,14 +74,6 @@ func Execute() error {
Flags: cc.Bold,
})

// Save the original help function to prevent infinite recursion when overriding it.
// This allows us to call the original help functionality within our custom help function.
originalHelpFunc = RootCmd.HelpFunc()

// Override the help function with a custom one that adds an upgrade message after displaying help.
// This custom help function will call the original help function and then display the bordered message.
RootCmd.SetHelpFunc(customHelpMessageToUpgradeToAtmosLatestRelease)

// Check if the `help` flag is passed and print a styled Atmos logo to the terminal before printing the help
err := RootCmd.ParseFlags(os.Args)
if err != nil && errors.Is(err, pflag.ErrHelp) {
Expand All @@ -89,21 +83,29 @@ func Execute() error {
u.LogErrorAndExit(schema.CliConfiguration{}, err)
}
}

// InitCliConfig finds and merges CLI configurations in the following order:
// system dir, home dir, current dir, ENV vars, command-line arguments
// Here we need the custom commands from the config
cliConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if err != nil && !errors.Is(err, cfg.NotFound) {
var initErr error
cliConfig, initErr = cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if initErr != nil && !errors.Is(initErr, cfg.NotFound) {
if isVersionCommand() {
u.LogTrace(schema.CliConfiguration{}, fmt.Sprintf("warning: CLI configuration 'atmos.yaml' file not found. Error: %s", err))
u.LogTrace(schema.CliConfiguration{}, fmt.Sprintf("warning: CLI configuration 'atmos.yaml' file not found. Error: %s", initErr))
} else {
u.LogErrorAndExit(schema.CliConfiguration{}, err)
u.LogErrorAndExit(schema.CliConfiguration{}, initErr)
}
}

// Save the original help function to prevent infinite recursion when overriding it.
// This allows us to call the original help functionality within our custom help function.
originalHelpFunc = RootCmd.HelpFunc()

// Override the help function with a custom one that adds an upgrade message after displaying help.
// This custom help function will call the original help function and then display the bordered message.
RootCmd.SetHelpFunc(customHelpMessageToUpgradeToAtmosLatestRelease)

// If CLI configuration was found, process its custom commands and command aliases
if err == nil {
if initErr == nil {
err = processCustomCommands(cliConfig, cliConfig.Commands, RootCmd, true)
if err != nil {
u.LogErrorAndExit(schema.CliConfiguration{}, err)
Expand Down
15 changes: 3 additions & 12 deletions cmd/terraform.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package cmd

import (
"strings"

"github.com/samber/lo"
"github.com/spf13/cobra"

e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/cloudposse/atmos/pkg/version"
)

// terraformCmd represents the base command for all terraform sub-commands
Expand All @@ -35,17 +32,11 @@ var terraformCmd = &cobra.Command{
if err != nil {
u.LogErrorAndExit(schema.CliConfiguration{}, err)
}
// Check for the latest Atmos release on GitHub and print update message
latestReleaseTag, err := u.GetLatestGitHubRepoRelease("cloudposse", "atmos")
if err == nil && latestReleaseTag != "" {
latestRelease := strings.TrimPrefix(latestReleaseTag, "v")
currentRelease := strings.TrimPrefix(version.Version, "v")
if latestRelease != currentRelease {
u.PrintMessageToUpgradeToAtmosLatestRelease(latestRelease)
}
}

// Exit on help
if info.NeedHelp {
// Check for the latest Atmos release on GitHub and print update message
CheckForAtmosUpdateAndPrintMessage(cliConfig)
return
}
// Check Atmos configuration
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/elewis787/boa v0.1.2
github.com/fatih/color v1.18.0
github.com/go-git/go-git/v5 v5.12.0
github.com/gofrs/flock v0.12.1
github.com/google/go-containerregistry v0.20.2
github.com/google/go-github/v59 v59.0.0
github.com/google/uuid v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
Expand Down
108 changes: 108 additions & 0 deletions pkg/config/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package config

import (
"os"
"path/filepath"
"time"

"github.com/gofrs/flock"
"github.com/pkg/errors"
"github.com/spf13/viper"
)

type CacheConfig struct {
LastChecked int64 `mapstructure:"last_checked"`
}

func GetCacheFilePath() (string, error) {
xdgCacheHome := os.Getenv("XDG_CACHE_HOME")
var cacheDir string
if xdgCacheHome == "" {
cacheDir = filepath.Join(".", ".atmos")
} else {
cacheDir = filepath.Join(xdgCacheHome, "atmos")
}

if err := os.MkdirAll(cacheDir, 0755); err != nil {
return "", errors.Wrap(err, "error creating cache directory")
}

return filepath.Join(cacheDir, "cache.yaml"), nil
}

func withCacheFileLock(cacheFile string, fn func() error) error {
lock := flock.New(cacheFile)
err := lock.Lock()
if err != nil {
return errors.Wrap(err, "error acquiring file lock")
}
defer lock.Unlock()
return fn()
}

func LoadCache() (CacheConfig, error) {
cacheFile, err := GetCacheFilePath()
if err != nil {
return CacheConfig{}, err
}

var cfg CacheConfig
if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
// No file yet, return default
return cfg, nil
}

v := viper.New()
v.SetConfigFile(cacheFile)
if err := v.ReadInConfig(); err != nil {
return cfg, errors.Wrap(err, "failed to read cache file")
}
if err := v.Unmarshal(&cfg); err != nil {
return cfg, errors.Wrap(err, "failed to unmarshal cache file")
}
return cfg, nil
}

func SaveCache2(cfg CacheConfig) error {
cacheFile, err := GetCacheFilePath()
if err != nil {
return err
}

return withCacheFileLock(cacheFile, func() error {
v := viper.New()
v.Set("last_checked", cfg.LastChecked)
if err := v.WriteConfigAs(cacheFile); err != nil {
return errors.Wrap(err, "failed to write cache file")
}
return nil
})
}

func SaveCache(cfg CacheConfig) error {
cacheFile, err := GetCacheFilePath()
if err != nil {
return err
}

v := viper.New()
v.Set("last_checked", cfg.LastChecked)
if err := v.WriteConfigAs(cacheFile); err != nil {
return errors.Wrap(err, "failed to write cache file")
}
return nil
}

func ShouldCheckForUpdates(lastChecked int64, frequency string) bool {
now := time.Now().Unix()
var interval int64
switch frequency {
case "daily":
interval = 86400
case "weekly":
interval = 604800
default:
interval = 86400 // default daily
}
return now-lastChecked >= interval
}
7 changes: 7 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ var (
},
},
Initialized: true,
Version: schema.Version{
Check: schema.VersionCheck{
Enabled: true,
Timeout: 1000,
Frequency: "daily",
},
},
}
)

Expand Down
12 changes: 12 additions & 0 deletions pkg/config/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,18 @@ func processEnvVars(cliConfig *schema.CliConfiguration) error {
cliConfig.Settings.ListMergeStrategy = listMergeStrategy
}

versionEnabled := os.Getenv("ATMOS_VERSION_CHECK_ENABLED")
if len(versionEnabled) > 0 {
u.LogTrace(*cliConfig, fmt.Sprintf("Found ENV var ATMOS_VERSION_CHECK_ENABLED=%s", versionEnabled))
enabled, err := strconv.ParseBool(versionEnabled)
if err != nil {
u.LogWarning(*cliConfig, fmt.Sprintf("Invalid boolean value '%s' for ATMOS_VERSION_CHECK_ENABLED; using default.", versionEnabled))
} else {
cliConfig.Version.Check.Enabled = enabled
}
cliConfig.Version.Check.Enabled = enabled
}

return nil
}

Expand Down
Loading

0 comments on commit 0f79d1c

Please sign in to comment.