From 5681e544be48eda5341508468d640b30e135efa3 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:42:20 -0500 Subject: [PATCH 1/6] feat: add pagination and searching to `atmos docs` command (#848) * feat: add pagination and search to docs command * docs: update documentation with pagination settings * fix: cleanup documentation and capitalization --------- Co-authored-by: Andriy Knysh --- cmd/docs.go | 5 ++- pkg/schema/schema.go | 3 +- pkg/utils/doc_utils.go | 39 +++++++++++++++++++ .../docs/cli/configuration/configuration.mdx | 21 +++++++++- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 pkg/utils/doc_utils.go diff --git a/cmd/docs.go b/cmd/docs.go index 547cd018a..129e58591 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -97,7 +97,10 @@ var docsCmd = &cobra.Command{ u.LogErrorAndExit(schema.CliConfiguration{}, err) } - fmt.Println(componentDocs) + if err := u.DisplayDocs(componentDocs, cliConfig.Settings.Docs.Pagination); err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, fmt.Errorf("failed to display documentation: %w", err)) + } + return } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 7eacc76ff..ff05ed6dc 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -34,7 +34,8 @@ type CliSettings struct { } type Docs struct { - MaxWidth int `yaml:"max-width" json:"max_width" mapstructure:"max-width"` + MaxWidth int `yaml:"max-width" json:"max_width" mapstructure:"max-width"` + Pagination bool `yaml:"pagination" json:"pagination" mapstructure:"pagination"` } type Templates struct { diff --git a/pkg/utils/doc_utils.go b/pkg/utils/doc_utils.go new file mode 100644 index 000000000..a8d302acc --- /dev/null +++ b/pkg/utils/doc_utils.go @@ -0,0 +1,39 @@ +package utils + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// DisplayDocs displays component documentation directly through the terminal or +// through a pager (like less). The use of a pager is determined by the pagination value +// set in the CLI Settings for Atmos +func DisplayDocs(componentDocs string, usePager bool) error { + if !usePager { + fmt.Println(componentDocs) + return nil + } + + pagerCmd := os.Getenv("PAGER") + if pagerCmd == "" { + pagerCmd = "less -r" + } + + args := strings.Fields(pagerCmd) + if len(args) == 0 { + return fmt.Errorf("invalid pager command") + } + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = strings.NewReader(componentDocs) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to execute pager: %w", err) + } + + return nil +} diff --git a/website/docs/cli/configuration/configuration.mdx b/website/docs/cli/configuration/configuration.mdx index bcc4761e7..4e95a6850 100644 --- a/website/docs/cli/configuration/configuration.mdx +++ b/website/docs/cli/configuration/configuration.mdx @@ -151,6 +151,13 @@ The `settings` section configures Atmos global 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 + # `docs` specifies how component documentation is displayed in the terminal. + # The following documentation display settings are supported: + # `max-width`: The maximum width for displaying component documentation in the terminal. + # 'pagination`: When enabled, displays component documentation in a pager instead of directly in the terminal. + docs: + max-width: 80 + pagination: true ``` @@ -171,9 +178,21 @@ The `settings` section configures Atmos global settings.
The items in the destination list are deep-merged with the items in the source list. The items in the source list take precedence. The items are processed starting from the first up to the length of the source list (the remaining items are not processed). 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.
- + +
`settings.docs`
+
+ Specifies how component documentation is displayed in the terminal. + The following settings are supported: +
+
`max-width`
+
The maximum width for displaying component documentation in the terminal.
+
`pagination`
+
When enabled, displays component documentation in a pager instead of directly in the terminal.
+
+
+ ## Workflows From ed93da1ba7c9c5111c5b2998155f99331175db4c Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 14 Dec 2024 22:57:07 +0100 Subject: [PATCH 2/6] Exclude abstract components from list (#851) issue: https://linear.app/cloudposse/issue/DEV-2808/atmos-tui-should-not-include-abstract-components Co-authored-by: Andriy Knysh --- internal/exec/atmos.go | 2 +- internal/exec/describe_component.go | 30 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/internal/exec/atmos.go b/internal/exec/atmos.go index 446c42ca1..a7f5f6f7d 100644 --- a/internal/exec/atmos.go +++ b/internal/exec/atmos.go @@ -51,7 +51,7 @@ func ExecuteAtmosCmd() error { if v2, ok := v.(map[string]any); ok { if v3, ok := v2["components"].(map[string]any); ok { if v4, ok := v3["terraform"].(map[string]any); ok { - return k, lo.Keys(v4) + return k, FilterAbstractComponents(v4) } // TODO: process 'helmfile' components and stacks. // This will require checking the list of commands and filtering the stacks and components depending on the selected command. diff --git a/internal/exec/describe_component.go b/internal/exec/describe_component.go index 2fabc2611..5afea0dd7 100644 --- a/internal/exec/describe_component.go +++ b/internal/exec/describe_component.go @@ -2,6 +2,7 @@ package exec import ( "github.com/pkg/errors" + "github.com/samber/lo" "github.com/spf13/cobra" cfg "github.com/cloudposse/atmos/pkg/config" @@ -83,3 +84,32 @@ func ExecuteDescribeComponent( return configAndStacksInfo.ComponentSection, nil } + +// FilterAbstractComponents This function removes abstract components and returns the list of components +func FilterAbstractComponents(componentsMap map[string]any) []string { + if componentsMap == nil { + return []string{} + } + components := make([]string, 0) + for _, k := range lo.Keys(componentsMap) { + componentMap, ok := componentsMap[k].(map[string]any) + if !ok { + components = append(components, k) + continue + } + + metadata, ok := componentMap["metadata"].(map[string]any) + if !ok { + components = append(components, k) + continue + } + if componentType, ok := metadata["type"].(string); ok && componentType == "abstract" { + continue + } + if componentEnabled, ok := metadata["enabled"].(bool); ok && !componentEnabled { + continue + } + components = append(components, k) + } + return components +} From 58b46f5dc26288ba9c8dce1579dd84d367f6b8fb Mon Sep 17 00:00:00 2001 From: Josh Simmonds Date: Sat, 14 Dec 2024 17:37:14 -0500 Subject: [PATCH 3/6] Append new lines to end of json files before writing (#849) * Append new lines to end of json files before writing * Address readability concerns from the robot --------- Co-authored-by: Andriy Knysh --- pkg/utils/json_utils.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/utils/json_utils.go b/pkg/utils/json_utils.go index aeb8ad4b6..d9e9c53fc 100644 --- a/pkg/utils/json_utils.go +++ b/pkg/utils/json_utils.go @@ -51,6 +51,13 @@ func WriteToFileAsJSON(filePath string, data any, fileMode os.FileMode) error { return err } + const newlineByte = '\n' + + // Ensure that the JSON content ends with a newline + if len(indentedJSON) == 0 || indentedJSON[len(indentedJSON)-1] != newlineByte { + indentedJSON = append(indentedJSON, newlineByte) + } + err = os.WriteFile(filePath, indentedJSON, fileMode) if err != nil { return err From 11ec388215e672217428d8dcbf4d05b886bf6061 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 14 Dec 2024 23:51:14 +0100 Subject: [PATCH 4/6] Add support for helmfile version command (#854) issue: https://linear.app/cloudposse/issue/DEV-2844/atmos-helmfile-version-does-not-work Co-authored-by: Andriy Knysh --- internal/exec/helmfile.go | 4 ++++ internal/exec/utils.go | 13 +++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/exec/helmfile.go b/internal/exec/helmfile.go index bfcfde41a..fe36f7e9f 100644 --- a/internal/exec/helmfile.go +++ b/internal/exec/helmfile.go @@ -54,6 +54,10 @@ func ExecuteHelmfile(info schema.ConfigAndStacksInfo) error { return nil } + if info.SubCommand == "version" { + return ExecuteShellCommand(cliConfig, "helmfile", []string{info.SubCommand}, "", nil, false, info.RedirectStdErr) + } + info, err = ProcessStacks(cliConfig, info, true, true) if err != nil { return err diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 6fefa53ba..46693a4ba 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -661,22 +661,19 @@ func processArgsAndFlags(componentType string, inputArgsAndFlags []string) (sche var indexesToRemove []int // For commands like `atmos terraform clean` and `atmos terraform plan`, show the command help - if len(inputArgsAndFlags) == 1 { + if len(inputArgsAndFlags) == 1 && inputArgsAndFlags[0] != "version" { info.SubCommand = inputArgsAndFlags[0] info.NeedHelp = true return info, nil } - - // https://github.com/roboll/helmfile#cli-reference - var globalOptionsFlagIndex int - - // For commands like `atmos terraform clean` and `atmos terraform plan`, show the command help - if len(inputArgsAndFlags) == 1 { + if len(inputArgsAndFlags) == 1 && inputArgsAndFlags[0] == "version" { info.SubCommand = inputArgsAndFlags[0] - info.NeedHelp = true return info, nil } + // https://github.com/roboll/helmfile#cli-reference + var globalOptionsFlagIndex int + for i, arg := range inputArgsAndFlags { if arg == cfg.GlobalOptionsFlag { globalOptionsFlagIndex = i + 1 From e9a610ebfef0a800f0a126ca46411b917d28786a Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sun, 15 Dec 2024 01:35:11 +0100 Subject: [PATCH 5/6] add support for terraform version (#846) * add support for terraform version * add support for terraform version issue: https://linear.app/cloudposse/issue/DEV-2820/atmos-terraform-version-does-not-work --------- Co-authored-by: Andriy Knysh --- internal/exec/terraform.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index d6d891060..88fd45b04 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -61,6 +61,15 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { fmt.Println() return nil } + if info.SubCommand == "version" { + return ExecuteShellCommand(cliConfig, + "terraform", + []string{info.SubCommand}, + "", + nil, + false, + info.RedirectStdErr) + } shouldProcessStacks := true shouldCheckStack := true From b205a0d92580f539ff3a447691e36b607b7acdcf Mon Sep 17 00:00:00 2001 From: Listener430 <95291462+Listener430@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:19:07 +0500 Subject: [PATCH 6/6] Implement Atmos Version Check Configuration (#844) * DEV-2815 * version * docs update * Update website/docs/cli/commands/help.mdx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update website/docs/cli/commands/version.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * Update pkg/config/cache.go Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * Update website/docs/cli/commands/version.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * Update website/docs/cli/commands/version.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * help wording fix as per coderabbit suggestion * more update frequencies and respective update to the dox * help wording update * reviewchanges * versiondoc * Update website/docs/cli/commands/help.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * Update website/docs/cli/commands/version.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * Update website/docs/cli/commands/version.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * updates * atmos.yaml * updates --------- Co-authored-by: Erik Osterman (CEO @ Cloud Posse) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: aknysh --- .gitignore | 2 + atmos.yaml | 6 + cmd/cmd_utils.go | 63 +++++-- cmd/helmfile.go | 15 +- cmd/root.go | 30 ++-- cmd/terraform.go | 15 +- cmd/version.go | 31 +++- examples/quick-start-advanced/Dockerfile | 2 +- go.mod | 1 + go.sum | 2 + pkg/config/cache.go | 164 ++++++++++++++++++ pkg/config/config.go | 7 + pkg/config/utils.go | 11 ++ pkg/schema/schema.go | 11 ++ website/docs/cli/commands/help.mdx | 4 + website/docs/cli/commands/version.mdx | 22 ++- .../docs/cli/configuration/configuration.mdx | 1 + website/docs/integrations/atlantis.mdx | 2 +- 18 files changed, 330 insertions(+), 59 deletions(-) create mode 100644 pkg/config/cache.go diff --git a/.gitignore b/.gitignore index f231eff31..7eeb05d79 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ # Nix .envrc .direnv/ + +.atmos/cache.yaml diff --git a/atmos.yaml b/atmos.yaml index adfd5176b..e9793b62f 100644 --- a/atmos.yaml +++ b/atmos.yaml @@ -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: 1h diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 3d55bf630..149969cf6 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -7,6 +7,7 @@ import ( "os" "path" "strings" + "time" "github.com/fatih/color" "github.com/spf13/cobra" @@ -428,18 +429,60 @@ 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.LogWarning(cliConfig, fmt.Sprintf("Failed to retrieve latest Atmos release info: %s", err)) + return } + + if latestReleaseTag == "" { + u.LogWarning(cliConfig, "No release information available") + 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 diff --git a/cmd/helmfile.go b/cmd/helmfile.go index ae7280fe7..d2236e835 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -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 @@ -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 diff --git a/cmd/root.go b/cmd/root.go index 0027776c1..c7b043a3f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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) @@ -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) { @@ -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) diff --git a/cmd/terraform.go b/cmd/terraform.go index 3b5ec3783..586e79d22 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -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 @@ -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 diff --git a/cmd/version.go b/cmd/version.go index 5146f4ff3..c865d81d0 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -13,6 +13,8 @@ import ( "github.com/cloudposse/atmos/pkg/version" ) +var checkFlag bool + var versionCmd = &cobra.Command{ Use: "version", Short: "Print the CLI version", @@ -29,18 +31,33 @@ var versionCmd = &cobra.Command{ u.PrintMessage(fmt.Sprintf("\U0001F47D Atmos %s on %s/%s", version.Version, runtime.GOOS, runtime.GOARCH)) fmt.Println() - // Check for the latest Atmos release on 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 checkFlag { + // Check for the latest Atmos release on GitHub + latestReleaseTag, err := u.GetLatestGitHubRepoRelease("cloudposse", "atmos") + if err == nil && latestReleaseTag != "" { + if err != nil { + u.LogWarning(schema.CliConfiguration{}, fmt.Sprintf("Failed to check for updates: %v", err)) + return + } + if latestReleaseTag == "" { + u.LogWarning(schema.CliConfiguration{}, "No release information available") + return + } + latestRelease := strings.TrimPrefix(latestReleaseTag, "v") + currentRelease := strings.TrimPrefix(version.Version, "v") + if latestRelease != currentRelease { + u.PrintMessageToUpgradeToAtmosLatestRelease(latestRelease) + } } + return } + + // Check for the cache and print update message + CheckForAtmosUpdateAndPrintMessage(cliConfig) }, } func init() { + versionCmd.Flags().BoolVarP(&checkFlag, "check", "c", false, "Run additional checks after displaying version info") RootCmd.AddCommand(versionCmd) } diff --git a/examples/quick-start-advanced/Dockerfile b/examples/quick-start-advanced/Dockerfile index 8ad376f7e..895f09c56 100644 --- a/examples/quick-start-advanced/Dockerfile +++ b/examples/quick-start-advanced/Dockerfile @@ -6,7 +6,7 @@ ARG GEODESIC_OS=debian # https://atmos.tools/ # https://github.com/cloudposse/atmos # https://github.com/cloudposse/atmos/releases -ARG ATMOS_VERSION=1.122.0 +ARG ATMOS_VERSION=1.127.0 # Terraform: https://github.com/hashicorp/terraform/releases ARG TF_VERSION=1.5.7 diff --git a/go.mod b/go.mod index ef51bcbdf..1c01ef148 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f557e9bc6..90b091ecb 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/config/cache.go b/pkg/config/cache.go new file mode 100644 index 000000000..6833e67b5 --- /dev/null +++ b/pkg/config/cache.go @@ -0,0 +1,164 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/gofrs/flock" + "github.com/pkg/errors" + "github.com/spf13/viper" + + "github.com/cloudposse/atmos/pkg/schema" + u "github.com/cloudposse/atmos/pkg/utils" +) + +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() + + interval, err := parseFrequency(frequency) + if err != nil { + // Log warning and default to daily if we can’t parse + u.LogWarning(schema.CliConfiguration{}, fmt.Sprintf("Unsupported frequency '%s' encountered. Defaulting to daily.", frequency)) + interval = 86400 // daily + } + return now-lastChecked >= interval +} + +// parseFrequency attempts to parse the frequency string in three ways: +// 1. As an integer (seconds) +// 2. As a duration with a suffix (e.g., "1h", "5m", "30s") +// 3. As one of the predefined keywords (daily, hourly, etc.) +func parseFrequency(frequency string) (int64, error) { + freq := strings.TrimSpace(frequency) + + if intVal, err := strconv.ParseInt(freq, 10, 64); err == nil { + if intVal > 0 { + return intVal, nil + } + } + + // Parse duration with suffix + if len(freq) > 1 { + unit := freq[len(freq)-1] + valPart := freq[:len(freq)-1] + if valInt, err := strconv.ParseInt(valPart, 10, 64); err == nil && valInt > 0 { + switch unit { + case 's': + return valInt, nil + case 'm': + return valInt * 60, nil + case 'h': + return valInt * 3600, nil + case 'd': + return valInt * 86400, nil + default: + return 0, fmt.Errorf("unrecognized duration unit: %s", string(unit)) + } + } + } + + // Handle predefined keywords + switch freq { + case "minute": + return 60, nil + case "hourly": + return 3600, nil + case "daily": + return 86400, nil + case "weekly": + return 604800, nil + case "monthly": + return 2592000, nil + case "yearly": + return 31536000, nil + default: + return 0, fmt.Errorf("unrecognized frequency: %s", freq) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 8cbfb8c36..a0a1f0167 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -82,6 +82,13 @@ var ( }, }, Initialized: true, + Version: schema.Version{ + Check: schema.VersionCheck{ + Enabled: true, + Timeout: 1000, + Frequency: "daily", + }, + }, } ) diff --git a/pkg/config/utils.go b/pkg/config/utils.go index 929243c3f..060a1cf2a 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -371,6 +371,17 @@ 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 + } + } + return nil } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index ff05ed6dc..e44396a38 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -26,6 +26,7 @@ type CliConfiguration struct { StackConfigFilesAbsolutePaths []string `yaml:"stackConfigFilesAbsolutePaths,omitempty" json:"stackConfigFilesAbsolutePaths,omitempty" mapstructure:"stackConfigFilesAbsolutePaths"` StackType string `yaml:"stackType,omitempty" json:"StackType,omitempty" mapstructure:"stackType"` Default bool `yaml:"default" json:"default" mapstructure:"default"` + Version Version `yaml:"version,omitempty" json:"version,omitempty" mapstructure:"version"` } type CliSettings struct { @@ -128,6 +129,16 @@ type Context struct { TerraformWorkspace string `yaml:"terraform_workspace" json:"terraform_workspace" mapstructure:"terraform_workspace"` } +type VersionCheck struct { + Enabled bool `yaml:"enabled,omitempty" mapstructure:"enabled"` + Timeout int `yaml:"timeout,omitempty" mapstructure:"timeout"` + Frequency string `yaml:"frequency,omitempty" mapstructure:"frequency"` +} + +type Version struct { + Check VersionCheck `yaml:"check,omitempty" mapstructure:"check"` +} + type ArgsAndFlagsInfo struct { AdditionalArgsAndFlags []string SubCommand string diff --git a/website/docs/cli/commands/help.mdx b/website/docs/cli/commands/help.mdx index fcbc1c2e3..1df2ef3a1 100644 --- a/website/docs/cli/commands/help.mdx +++ b/website/docs/cli/commands/help.mdx @@ -13,6 +13,10 @@ import Terminal from '@site/src/components/Terminal' The `atmos --help` and `atmos -h` commands show help for all Atmos CLI commands. +From time to time, Atmos will check for a newer release and let you know if one is available. +Please see the [`atmos version`](/cli/commands/version) documentation to configure this behavior. + + ```shell atmos help atmos --help diff --git a/website/docs/cli/commands/version.mdx b/website/docs/cli/commands/version.mdx index a8aabaf56..2deb20a4f 100644 --- a/website/docs/cli/commands/version.mdx +++ b/website/docs/cli/commands/version.mdx @@ -20,7 +20,27 @@ Execute the `atmos version` command like this: atmos version ``` -This will show the CLI version. +This will show the CLI version. + +From time to time, Atmos will check for updates. The frequency of these checks is configured in the `atmos.yaml` file. + +Atmos supports three ways to specify the update check frequency: + +1. As an integer: Specify the number of seconds between checks (for example, 3600 for hourly checks). +2. As a duration with a suffix: Use a time suffix to indicate the interval (for example, `1m` for one minute, `5h` for five hours, or `2d` for two days). +3. As one of the predefined keywords: Choose from the following options: minute, hourly, daily, weekly, monthly, and yearly. The default is daily. +The default is to check `daily`, and if any unsupported values are passed this default will be used. + +It is also possible to turn off version checks in `atmos.yaml` by setting `version.check.enabled` to `false`, +or by setting the `ATMOS_VERSION_CHECK_ENABLED` environment variable to `false`, which overrides +the `version.check.enabled` settings in `atmos.yaml`. + +```shell +atmos version --check +``` + +The version command supports a `--check` flag. +This will force Atmos to check for a new version, irrespective of the configuration settings. :::tip To find the latest version of Atmos, go to the [releases](https://github.com/cloudposse/atmos/releases) page on GitHub. diff --git a/website/docs/cli/configuration/configuration.mdx b/website/docs/cli/configuration/configuration.mdx index 4e95a6850..9268c0cb3 100644 --- a/website/docs/cli/configuration/configuration.mdx +++ b/website/docs/cli/configuration/configuration.mdx @@ -652,6 +652,7 @@ setting `ATMOS_STACKS_BASE_PATH` to a path in `/localhost` to your local develop | ATMOS_LOGS_FILE | logs.file | The file to write Atmos logs to. Logs can be written to any file or any standard file descriptor, including `/dev/stdout`, `/dev/stderr` and `/dev/null`). If omitted, `/dev/stdout` will be used | | ATMOS_LOGS_LEVEL | logs.level | Logs level. Supported log levels are `Trace`, `Debug`, `Info`, `Warning`, `Off`. If the log level is set to `Off`, Atmos will not log any messages (note that this does not prevent other tools like Terraform from logging) | | ATMOS_SETTINGS_LIST_MERGE_STRATEGY | settings.list_merge_strategy | Specifies how lists are merged in Atmos stack manifests. The following strategies are supported: `replace`, `append`, `merge` | +| ATMOS_VERSION_CHECK_ENABLED | version.check.enabled | Enable/disable Atmos version checks for updates to the newest release | ### Context diff --git a/website/docs/integrations/atlantis.mdx b/website/docs/integrations/atlantis.mdx index 842404516..e3a544497 100644 --- a/website/docs/integrations/atlantis.mdx +++ b/website/docs/integrations/atlantis.mdx @@ -673,7 +673,7 @@ on: branches: [ main ] env: - ATMOS_VERSION: 1.122.0 + ATMOS_VERSION: 1.127.0 ATMOS_CLI_CONFIG_PATH: ./ jobs: