From b8f480f71a81cc5da8ef678a74bb88825843bd05 Mon Sep 17 00:00:00 2001 From: Sam Tholiya Date: Sat, 18 Jan 2025 19:54:31 +0100 Subject: [PATCH] Standardize Help and Usage content for all commands and sub commands (#914) * Merge atmos specific and terraform help documentation issue: https://linear.app/cloudposse/issue/DEV-2821/atmos-terraform-help-should-also-show-terraform-help * Add help for terraform issue: https://linear.app/cloudposse/issue/DEV-2821/atmos-terraform-help-should-also-show-terraform-help * Updated help content * test fix for auto approve * Update message for invalid command * remove old help code * testing if auto-approve should be with two - * check error after executing old usage func * rebase help.go * Fix terraform subcommand help * Remove unwanted setHelpFunc * terraform,helmfile empty sub command should redirect to help * check errors for usage and help * Added space in between Native commands * Removed unwanted code * Add consistent usage and help for all commands isssue: https://linear.app/cloudposse/issue/DEV-2896/incorrect-output-for-atmos-validate * Removed unwanted help check * Fix atmos help * Help should be in stdout terminal * fix terraform args * fixed terraform command * fix helm command * rearrange the help strings * simplified the code * Updated version with the help and usage as per expectations * fix atmos command usage * Fix atmos usage error assertion * Add SubCommand Aliases: * Updated template to be more dynamic * use cobra.NoArgs instead * fix atmos version command * update atmos test case * Fix help and add test cases * added more test cases * filter commands added * fixed typo * removed unwanted function * remove duplicated test * use existing theme color package * use existing theme color package * Update cmd/cmd_utils.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix error message ux * fix template * fix usage test case for atmos terraform * fix atlantis help * Add more test cases for atlantis * fix atlantis command help and usage * fix atlantis command help and usage * fix test and update golden screenshots * fix tests for atmos help --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- cmd/atlantis.go | 1 + cmd/atlantis_generate.go | 1 + cmd/atlantis_generate_repo_config.go | 5 +- cmd/aws.go | 1 + cmd/aws_eks.go | 1 + cmd/cmd_utils.go | 66 ++- cmd/colored/colored.go | 234 ++++++++++ cmd/completion.go | 52 ++- cmd/describe.go | 2 +- cmd/describe_affected.go | 1 + cmd/describe_config.go | 2 + cmd/describe_stacks.go | 2 + cmd/describe_workflows.go | 1 + cmd/helmfile.go | 48 +- cmd/helmfile_apply.go | 30 ++ cmd/helmfile_destroy.go | 28 ++ cmd/helmfile_diff.go | 30 ++ cmd/helmfile_generate.go | 1 + cmd/helmfile_generate_varfile.go | 1 + cmd/helmfile_sync.go | 28 ++ cmd/list.go | 1 + cmd/list_components.go | 1 + cmd/list_stacks.go | 1 + cmd/pro.go | 1 + cmd/pro_lock.go | 1 + cmd/pro_unlock.go | 1 + cmd/root.go | 66 ++- cmd/terraform.go | 110 +---- cmd/terraform_commands.go | 9 +- cmd/terraform_generate.go | 4 +- cmd/terraform_generate_backend.go | 1 + cmd/terraform_generate_backends.go | 1 + cmd/terraform_generate_varfile.go | 1 + cmd/terraform_generate_varfiles.go | 1 + cmd/validate.go | 1 + cmd/validate_component.go | 1 + cmd/validate_stacks.go | 1 + cmd/vendor.go | 1 + cmd/vendor_diff.go | 3 + cmd/vendor_pull.go | 1 + cmd/version.go | 1 + cmd/workflow.go | 8 +- go.mod | 1 - go.sum | 5 - internal/exec/helmfile.go | 22 - internal/exec/terraform.go | 18 - internal/tui/templates/base_template.go | 57 ++- internal/tui/templates/templater.go | 61 ++- pkg/ui/theme/colors.go | 20 + pkg/utils/log_utils.go | 5 + ...TestCLICommands_atmos_--help.stdout.golden | 11 +- ...ck_atmos_--help_in_empty-dir.stdout.golden | 11 +- tests/test-cases/core.yaml | 17 +- tests/test-cases/empty-dir.yaml | 4 +- tests/test-cases/help-and-usage.yaml | 433 ++++++++++++++++++ tests/test-cases/log-level-validation.yaml | 6 +- 56 files changed, 1134 insertions(+), 288 deletions(-) create mode 100644 cmd/colored/colored.go create mode 100644 cmd/helmfile_apply.go create mode 100644 cmd/helmfile_destroy.go create mode 100644 cmd/helmfile_diff.go create mode 100644 cmd/helmfile_sync.go create mode 100644 tests/test-cases/help-and-usage.yaml diff --git a/cmd/atlantis.go b/cmd/atlantis.go index b81e71194..560dc7da9 100644 --- a/cmd/atlantis.go +++ b/cmd/atlantis.go @@ -10,6 +10,7 @@ var atlantisCmd = &cobra.Command{ Short: "Generate and manage Atlantis configurations", Long: `Generate and manage Atlantis configurations that use Atmos under the hood to run Terraform workflows, bringing the power of Atmos to Atlantis for streamlined infrastructure automation.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/atlantis_generate.go b/cmd/atlantis_generate.go index e5bfa8948..7f23d72fb 100644 --- a/cmd/atlantis_generate.go +++ b/cmd/atlantis_generate.go @@ -10,6 +10,7 @@ var atlantisGenerateCmd = &cobra.Command{ Short: "Generate Atlantis configuration files", Long: "This command generates configuration files to automate and streamline Terraform workflows with Atlantis.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go index f190a1d08..47e29a64b 100644 --- a/cmd/atlantis_generate_repo_config.go +++ b/cmd/atlantis_generate_repo_config.go @@ -15,9 +15,12 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{ Long: "Generate the repository configuration file required for Atlantis to manage Terraform repositories.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) + if len(args) > 0 { + showUsageAndExit(cmd, args) + } // Check Atmos configuration checkAtmosConfig() - err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/aws.go b/cmd/aws.go index 35dd505c4..a65b13fdd 100644 --- a/cmd/aws.go +++ b/cmd/aws.go @@ -10,6 +10,7 @@ var awsCmd = &cobra.Command{ Short: "Run AWS-specific commands for interacting with cloud resources", Long: `This command allows interaction with AWS resources through various CLI commands.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/aws_eks.go b/cmd/aws_eks.go index 1e1a74620..2e6cba71d 100644 --- a/cmd/aws_eks.go +++ b/cmd/aws_eks.go @@ -15,6 +15,7 @@ You can use this command to interact with AWS EKS, including operations like con For a list of available AWS EKS commands, refer to the Atmos documentation: https://atmos.tools/cli/commands/aws/eks-update-kubeconfig`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/cmd_utils.go b/cmd/cmd_utils.go index 64e7c7d1d..66a989d3b 100644 --- a/cmd/cmd_utils.go +++ b/cmd/cmd_utils.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/samber/lo" "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" @@ -441,7 +442,7 @@ func checkAtmosConfig(opts ...AtmosValidateOption) { atmosConfigExists, err := u.IsDirectory(atmosConfig.StacksBaseAbsolutePath) if !atmosConfigExists || err != nil { printMessageForMissingAtmosConfig(atmosConfig) - os.Exit(0) + os.Exit(1) } } } @@ -543,3 +544,66 @@ func CheckForAtmosUpdateAndPrintMessage(atmosConfig schema.AtmosConfiguration) { func isVersionCommand() bool { return len(os.Args) > 1 && os.Args[1] == "version" } + +// handleHelpRequest shows help content and exits only if the first argument is "help" or "--help" or "-h" +func handleHelpRequest(cmd *cobra.Command, args []string) { + if (len(args) > 0 && args[0] == "help") || Contains(args, "--help") || Contains(args, "-h") { + cmd.Help() + os.Exit(0) + } +} + +func showUsageAndExit(cmd *cobra.Command, args []string) { + + var suggestions []string + unknownCommand := fmt.Sprintf("Error: Unknown command: %q\n\n", cmd.CommandPath()) + + if len(args) > 0 { + suggestions = cmd.SuggestionsFor(args[0]) + unknownCommand = fmt.Sprintf("Error: Unknown command %q for %q\n\n", args[0], cmd.CommandPath()) + } + u.PrintErrorInColor(unknownCommand) + if len(suggestions) > 0 { + u.PrintMessage("Did you mean this?") + for _, suggestion := range suggestions { + u.PrintMessage(fmt.Sprintf(" %s\n", suggestion)) + } + } else { + // Retrieve valid subcommands dynamically + validSubcommands := []string{} + for _, subCmd := range cmd.Commands() { + validSubcommands = append(validSubcommands, subCmd.Name()) + } + if len(validSubcommands) > 0 { + u.PrintMessage("Valid subcommands are:") + for _, sub := range validSubcommands { + u.PrintMessage(fmt.Sprintf(" %s", sub)) + } + } else { + u.PrintMessage("No valid subcommands found") + } + } + u.PrintMessage(fmt.Sprintf("\nRun '%s --help' for usage", cmd.CommandPath())) + os.Exit(1) +} + +// getConfigAndStacksInfo gets the +func getConfigAndStacksInfo(commandName string, cmd *cobra.Command, args []string) schema.ConfigAndStacksInfo { + // Check Atmos configuration + checkAtmosConfig() + + var argsAfterDoubleDash []string + var finalArgs = args + + doubleDashIndex := lo.IndexOf(args, "--") + if doubleDashIndex > 0 { + finalArgs = lo.Slice(args, 0, doubleDashIndex) + argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) + } + + info, err := e.ProcessCommandLineArgs(commandName, cmd, finalArgs, argsAfterDoubleDash) + if err != nil { + u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + } + return info +} diff --git a/cmd/colored/colored.go b/cmd/colored/colored.go new file mode 100644 index 000000000..07f447e75 --- /dev/null +++ b/cmd/colored/colored.go @@ -0,0 +1,234 @@ +// ColoredCobra allows you to colorize Cobra's text output, +// making it look better using simple settings to customize +// individual parts of console output. +// +// Usage example: +// +// 1. Insert in cmd/root.go file of your project : +// +// import cc "github.com/ivanpirog/coloredcobra" +// +// 2. Put the following code to the beginning of the Execute() function: +// +// cc.Init(&cc.Config{ +// RootCmd: rootCmd, +// Headings: cc.Bold + cc.Underline, +// Commands: cc.Yellow + cc.Bold, +// ExecName: cc.Bold, +// Flags: cc.Bold, +// }) +// +// 3. Build & execute your code. +// +// Copyright © 2022 Ivan Pirog . +// Released under the MIT license. +// Project home: https://github.com/ivanpirog/coloredcobra +package colored + +import ( + "regexp" + "strings" + + "github.com/cloudposse/atmos/pkg/ui/theme" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +// Config is a settings structure which sets styles for individual parts of Cobra text output. +// +// Note that RootCmd is required. +// +// Example: +// +// c := &cc.Config{ +// RootCmd: rootCmd, +// } +type Config struct { + RootCmd *cobra.Command + NoExtraNewlines bool + NoBottomNewline bool +} + +// Init patches Cobra's usage template with configuration provided. +func Init(cfg *Config) { + + if cfg.RootCmd == nil { + panic("coloredcobra: Root command pointer is missing.") + } + + // Get usage template + tpl := cfg.RootCmd.UsageTemplate() + + // + // Add extra line breaks for headings + // + if !cfg.NoExtraNewlines { + tpl = strings.NewReplacer( + "Use \"", "\nUse \"", + ).Replace(tpl) + } + + // + // Styling headers + // + if theme.Styles.Help.Headings != nil { + ch := theme.Styles.Help.Headings + // Add template function to style the headers + cobra.AddTemplateFunc("HeadingStyle", ch.SprintFunc()) + } + + // + // Styling commands + // + if theme.Styles.Help.Commands != nil { + cc := theme.Styles.Help.Commands + + // Add template function to style commands + cobra.AddTemplateFunc("CommandStyle", cc.SprintFunc()) + cobra.AddTemplateFunc("sum", func(a, b int) int { + return a + b + }) + + // Patch usage template + re := regexp.MustCompile(`(?i){{\s*rpad\s+.Name\s+.NamePadding\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{rpad (CommandStyle .Name) (sum .NamePadding 12)}}") + + re = regexp.MustCompile(`(?i){{\s*rpad\s+.CommandPath\s+.CommandPathPadding\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{rpad (CommandStyle .CommandPath) (sum .CommandPathPadding 12)}}") + } + + // + // Styling a short desription of commands + // + if theme.Styles.Help.CmdShortDescr != nil { + csd := theme.Styles.Help.CmdShortDescr + + cobra.AddTemplateFunc("CmdShortStyle", csd.SprintFunc()) + + re := regexp.MustCompile(`(?ism)({{\s*range\s+.Commands\s*}}.*?){{\s*.Short\s*}}`) + tpl = re.ReplaceAllString(tpl, `$1{{CmdShortStyle .Short}}`) + } + + // + // Styling executable file name + // + if theme.Styles.Help.ExecName != nil { + cen := theme.Styles.Help.ExecName + + // Add template functions + cobra.AddTemplateFunc("ExecStyle", cen.SprintFunc()) + cobra.AddTemplateFunc("UseLineStyle", func(s string) string { + spl := strings.Split(s, " ") + spl[0] = cen.Sprint(spl[0]) + return strings.Join(spl, " ") + }) + + // Patch usage template + re := regexp.MustCompile(`(?i){{\s*.CommandPath\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{ExecStyle .CommandPath}}") + + re = regexp.MustCompile(`(?i){{\s*.UseLine\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{UseLineStyle .UseLine}}") + } + + // + // Styling flags + // + var cf, cfd, cfdt *color.Color + if theme.Styles.Help.Flags != nil { + cf = theme.Styles.Help.Flags + } + if theme.Styles.Help.FlagsDescr != nil { + cfd = theme.Styles.Help.FlagsDescr + } + if theme.Styles.Help.FlagsDataType != nil { + cfdt = theme.Styles.Help.FlagsDataType + } + if cf != nil || cfd != nil || cfdt != nil { + + cobra.AddTemplateFunc("FlagStyle", func(s string) string { + + // Flags info section is multi-line. + // Let's split these lines and iterate them. + lines := strings.Split(s, "\n") + for k := range lines { + + // Styling short and full flags (-f, --flag) + if cf != nil { + re := regexp.MustCompile(`(--?\S+)`) + for _, flag := range re.FindAllString(lines[k], 2) { + lines[k] = strings.Replace(lines[k], flag, cf.Sprint(flag), 1) + } + } + + // If no styles for flag data types and description - continue + if cfd == nil && cfdt == nil { + continue + } + + // Split line into two parts: flag data type and description + // Tip: Use debugger to understand the logic + re := regexp.MustCompile(`\s{2,}`) + spl := re.Split(lines[k], -1) + if len(spl) != 3 { + continue + } + + // Styling the flag description + if cfd != nil { + lines[k] = strings.Replace(lines[k], spl[2], cfd.Sprint(spl[2]), 1) + } + + // Styling flag data type + // Tip: Use debugger to understand the logic + if cfdt != nil { + re = regexp.MustCompile(`\s+(\w+)$`) // the last word after spaces is the flag data type + m := re.FindAllStringSubmatch(spl[1], -1) + if len(m) == 1 && len(m[0]) == 2 { + lines[k] = strings.Replace(lines[k], m[0][1], cfdt.Sprint(m[0][1]), 1) + } + } + + } + s = strings.Join(lines, "\n") + + return s + + }) + + // Patch usage template + re := regexp.MustCompile(`(?i)(\.(InheritedFlags|LocalFlags)\.FlagUsages)`) + tpl = re.ReplaceAllString(tpl, "FlagStyle $1") + } + + // + // Styling aliases + // + if theme.Styles.Help.Aliases != nil { + ca := theme.Styles.Help.Aliases + cobra.AddTemplateFunc("AliasStyle", ca.SprintFunc()) + + re := regexp.MustCompile(`(?i){{\s*.NameAndAliases\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{AliasStyle .NameAndAliases}}") + } + + // + // Styling the example text + // + if theme.Styles.Help.Example != nil { + ce := theme.Styles.Help.Example + cobra.AddTemplateFunc("ExampleStyle", ce.SprintFunc()) + + re := regexp.MustCompile(`(?i){{\s*.Example\s*}}`) + tpl = re.ReplaceAllLiteralString(tpl, "{{ExampleStyle .Example}}") + } + + // Adding a new line to the end + if !cfg.NoBottomNewline { + tpl += "\n" + } + // Apply patched template + cfg.RootCmd.SetUsageTemplate(tpl) + // Debug line, uncomment when needed + // fmt.Println(tpl) +} diff --git a/cmd/completion.go b/cmd/completion.go index 7fe78e9bb..93375e578 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -3,8 +3,6 @@ package cmd import ( "os" - "github.com/cloudposse/atmos/pkg/schema" - "github.com/spf13/cobra" u "github.com/cloudposse/atmos/pkg/utils" @@ -15,28 +13,38 @@ var completionCmd = &cobra.Command{ Short: "Generate autocompletion scripts for Bash, Zsh, Fish, and PowerShell", Long: "This command generates completion scripts for Bash, Zsh, Fish and PowerShell", DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - Run: func(cmd *cobra.Command, args []string) { - var err error - - switch args[0] { - case "bash": - err = cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - err = cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - err = cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - } - - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - }, + Args: cobra.NoArgs, +} + +func runCompletion(cmd *cobra.Command, args []string) { + var err error + + switch cmd.Use { + case "bash": + err = cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + err = cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + err = cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + + if err != nil { + u.LogErrorAndExit(atmosConfig, err) + } } func init() { + shellNames := []string{"bash", "zsh", "fish", "powershell"} + for _, shellName := range shellNames { + completionCmd.AddCommand(&cobra.Command{ + Use: shellName, + Short: "Generate completion script for " + shellName, + Long: "This command generates completion scripts for " + shellName, + Run: runCompletion, + Args: cobra.NoArgs, + }) + } RootCmd.AddCommand(completionCmd) } diff --git a/cmd/describe.go b/cmd/describe.go index 20ad9110d..8de4c0c1e 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -10,10 +10,10 @@ var describeCmd = &cobra.Command{ Short: "Show details about Atmos configurations and components", Long: `Display configuration details for Atmos CLI, stacks, and components.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { describeCmd.PersistentFlags().StringP("query", "q", "", "Query the results of an 'atmos describe' command using 'yq' expressions: atmos describe --query ") - RootCmd.AddCommand(describeCmd) } diff --git a/cmd/describe_affected.go b/cmd/describe_affected.go index 1a95ca535..c32337923 100644 --- a/cmd/describe_affected.go +++ b/cmd/describe_affected.go @@ -14,6 +14,7 @@ var describeAffectedCmd = &cobra.Command{ Short: "List Atmos components and stacks affected by two Git commits", Long: "Identify and list Atmos components and stacks impacted by changes between two Git commits.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_config.go b/cmd/describe_config.go index 4a7e4dba2..e258e7d4c 100644 --- a/cmd/describe_config.go +++ b/cmd/describe_config.go @@ -14,7 +14,9 @@ var describeConfigCmd = &cobra.Command{ Short: "Display the final merged CLI configuration", Long: "This command displays the final, deep-merged CLI configuration after combining all relevant configuration files.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { + err := e.ExecuteDescribeConfigCmd(cmd, args) if err != nil { u.LogErrorAndExit(schema.AtmosConfiguration{}, err) diff --git a/cmd/describe_stacks.go b/cmd/describe_stacks.go index 248cc948f..001d6ab72 100644 --- a/cmd/describe_stacks.go +++ b/cmd/describe_stacks.go @@ -14,7 +14,9 @@ var describeStacksCmd = &cobra.Command{ Short: "Display configuration for Atmos stacks and their components", Long: "This command shows the configuration details for Atmos stacks and the components within those stacks.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { + // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/describe_workflows.go b/cmd/describe_workflows.go index ea660dfa4..855af276f 100644 --- a/cmd/describe_workflows.go +++ b/cmd/describe_workflows.go @@ -21,6 +21,7 @@ var describeWorkflowsCmd = &cobra.Command{ "describe workflows -o map\n" + "describe workflows -o all", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteDescribeWorkflowsCmd(cmd, args) if err != nil { diff --git a/cmd/helmfile.go b/cmd/helmfile.go index 4455c14d8..0c62ba872 100644 --- a/cmd/helmfile.go +++ b/cmd/helmfile.go @@ -1,12 +1,9 @@ package cmd import ( - "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/spf13/cobra" ) // helmfileCmd represents the base command for all helmfile sub-commands @@ -16,37 +13,7 @@ var helmfileCmd = &cobra.Command{ Short: "Manage Helmfile-based Kubernetes deployments", Long: `This command runs Helmfile commands to manage Kubernetes deployments using Helmfile.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - Run: func(cmd *cobra.Command, args []string) { - - var argsAfterDoubleDash []string - var finalArgs = args - - doubleDashIndex := lo.IndexOf(args, "--") - if doubleDashIndex > 0 { - finalArgs = lo.Slice(args, 0, doubleDashIndex) - argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) - } - - info, err := e.ProcessCommandLineArgs("helmfile", cmd, finalArgs, argsAfterDoubleDash) - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - // Exit on help - if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { - // Check for the latest Atmos release on GitHub and print update message - if err := cmd.Help(); err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - return - } - // Check Atmos configuration - checkAtmosConfig() - - err = e.ExecuteHelmfile(info) - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - }, + Args: cobra.NoArgs, } func init() { @@ -55,3 +22,14 @@ func init() { helmfileCmd.PersistentFlags().StringP("stack", "s", "", "atmos helmfile -s ") RootCmd.AddCommand(helmfileCmd) } + +func helmfileRun(cmd *cobra.Command, commandName string, args []string) { + handleHelpRequest(cmd, args) + diffArgs := []string{commandName} + diffArgs = append(diffArgs, args...) + info := getConfigAndStacksInfo("helmfile", cmd, diffArgs) + err := e.ExecuteHelmfile(info) + if err != nil { + u.LogErrorAndExit(atmosConfig, err) + } +} diff --git a/cmd/helmfile_apply.go b/cmd/helmfile_apply.go new file mode 100644 index 000000000..fad4c8b9d --- /dev/null +++ b/cmd/helmfile_apply.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// Command: atmos helmfile apply +var helmfileApplyShort = "Apply changes to align the actual state of Helm releases with the desired state." +var helmfileApplyLong = `This command reconciles the actual state of Helm releases in the cluster with the desired state +defined in your configurations by applying the necessary changes. + +Example usage: + atmos helmfile apply echo-server -s tenant1-ue2-dev + atmos helmfile apply echo-server -s tenant1-ue2-dev --redirect-stderr /dev/stdout` + +// helmfileApplyCmd represents the base command for all helmfile sub-commands +var helmfileApplyCmd = &cobra.Command{ + Use: "apply", + Aliases: []string{}, + Short: helmfileApplyShort, + Long: helmfileApplyLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "apply", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileApplyCmd) +} diff --git a/cmd/helmfile_destroy.go b/cmd/helmfile_destroy.go new file mode 100644 index 000000000..8f92953bd --- /dev/null +++ b/cmd/helmfile_destroy.go @@ -0,0 +1,28 @@ +package cmd + +import "github.com/spf13/cobra" + +// Command: atmos helmfile destroy +var helmfileDestroyShort = "Destroy the Helm releases for the specified stack." +var helmfileDestroyLong = `This command removes the specified Helm releases from the cluster, ensuring a clean state for +the given stack. + +Example usage: + atmos helmfile destroy echo-server --stack=tenant1-ue2-dev + atmos helmfile destroy echo-server --stack=tenant1-ue2-dev --redirect-stderr /dev/stdout` + +// helmfileDestroyCmd represents the base command for all helmfile sub-commands +var helmfileDestroyCmd = &cobra.Command{ + Use: "destroy", + Aliases: []string{}, + Short: helmfileDestroyShort, + Long: helmfileDestroyLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "destroy", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileDestroyCmd) +} diff --git a/cmd/helmfile_diff.go b/cmd/helmfile_diff.go new file mode 100644 index 000000000..13280ddd0 --- /dev/null +++ b/cmd/helmfile_diff.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// Command: atmos helmfile diff +var helmfileDiffShort = "Show differences between the desired and actual state of Helm releases." +var helmfileDiffLong = `This command calculates and displays the differences between the desired state of Helm releases +defined in your configurations and the actual state deployed in the cluster. + +Example usage: + atmos helmfile diff echo-server -s tenant1-ue2-dev + atmos helmfile diff echo-server -s tenant1-ue2-dev --redirect-stderr /dev/null` + +// helmfileDiffCmd represents the base command for all helmfile sub-commands +var helmfileDiffCmd = &cobra.Command{ + Use: "diff", + Aliases: []string{}, + Short: helmfileDiffShort, + Long: helmfileDiffLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "diff", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileDiffCmd) +} diff --git a/cmd/helmfile_generate.go b/cmd/helmfile_generate.go index 781f92bb1..7aba7df13 100644 --- a/cmd/helmfile_generate.go +++ b/cmd/helmfile_generate.go @@ -10,6 +10,7 @@ var helmfileGenerateCmd = &cobra.Command{ Short: "Generate configurations for Helmfile components", Long: "This command generates various configuration files for Helmfile components in Atmos.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/helmfile_generate_varfile.go b/cmd/helmfile_generate_varfile.go index 5299ef44f..acaac887e 100644 --- a/cmd/helmfile_generate_varfile.go +++ b/cmd/helmfile_generate_varfile.go @@ -15,6 +15,7 @@ var helmfileGenerateVarfileCmd = &cobra.Command{ Long: "This command generates a values file for a specified Helmfile component.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/helmfile_sync.go b/cmd/helmfile_sync.go new file mode 100644 index 000000000..8a836af59 --- /dev/null +++ b/cmd/helmfile_sync.go @@ -0,0 +1,28 @@ +package cmd + +import "github.com/spf13/cobra" + +// Command: atmos helmfile sync +var helmfileSyncShort = "Synchronize the state of Helm releases with the desired state without making changes." +var helmfileSyncLong = `This command ensures that the actual state of Helm releases in the cluster matches the desired +state defined in your configurations without performing destructive actions. + +Example usage: + atmos helmfile sync echo-server --stack tenant1-ue2-dev + atmos helmfile sync echo-server --stack tenant1-ue2-dev --redirect-stderr ./errors.txt` + +// helmfileSyncCmd represents the base command for all helmfile sub-commands +var helmfileSyncCmd = &cobra.Command{ + Use: "sync", + Aliases: []string{}, + Short: helmfileSyncShort, + Long: helmfileSyncLong, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + helmfileRun(cmd, "sync", args) + }, +} + +func init() { + helmfileCmd.AddCommand(helmfileSyncCmd) +} diff --git a/cmd/list.go b/cmd/list.go index 7f84e57a2..3e95c5791 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -10,6 +10,7 @@ var listCmd = &cobra.Command{ Short: "List available stacks and components", Long: `Display a list of all available stacks and components defined in your project.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/list_components.go b/cmd/list_components.go index 4180315da..3800a590e 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -21,6 +21,7 @@ var listComponentsCmd = &cobra.Command{ Long: "List Atmos components, with options to filter results by specific stacks.", Example: "atmos list components\n" + "atmos list components -s ", + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/list_stacks.go b/cmd/list_stacks.go index 655d83a3c..9dcd8ac91 100644 --- a/cmd/list_stacks.go +++ b/cmd/list_stacks.go @@ -21,6 +21,7 @@ var listStacksCmd = &cobra.Command{ Example: "atmos list stacks\n" + "atmos list stacks -c ", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro.go b/cmd/pro.go index 39baf220c..0e0262d38 100644 --- a/cmd/pro.go +++ b/cmd/pro.go @@ -10,6 +10,7 @@ var proCmd = &cobra.Command{ Short: "Access premium features integrated with app.cloudposse.com", Long: `This command allows you to manage and configure premium features available through app.cloudposse.com.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/pro_lock.go b/cmd/pro_lock.go index fc37aaa90..68191669f 100644 --- a/cmd/pro_lock.go +++ b/cmd/pro_lock.go @@ -14,6 +14,7 @@ var proLockCmd = &cobra.Command{ Short: "Lock a stack", Long: `This command calls the atmos pro API and locks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/pro_unlock.go b/cmd/pro_unlock.go index b612306cd..feb625a60 100644 --- a/cmd/pro_unlock.go +++ b/cmd/pro_unlock.go @@ -14,6 +14,7 @@ var proUnlockCmd = &cobra.Command{ Short: "Unlock a stack", Long: `This command calls the atmos pro API and unlocks a stack`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/root.go b/cmd/root.go index 84eff8c9a..40c0c53ea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,11 +4,13 @@ import ( "errors" "fmt" "os" + "regexp" + "strings" "github.com/elewis787/boa" - cc "github.com/ivanpirog/coloredcobra" "github.com/spf13/cobra" + "github.com/cloudposse/atmos/cmd/colored" e "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/internal/tui/templates" tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" @@ -17,13 +19,15 @@ import ( u "github.com/cloudposse/atmos/pkg/utils" ) +// atmosConfig This is initialized before everything in the Execute function. So we can directly use this. var atmosConfig schema.AtmosConfiguration // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ - Use: "atmos", - Short: "Universal Tool for DevOps and Cloud Automation", - Long: `Atmos is a universal tool for DevOps and cloud automation used for provisioning, managing and orchestrating workflows across various toolchains`, + Use: "atmos", + Short: "Universal Tool for DevOps and Cloud Automation", + Long: `Atmos is a universal tool for DevOps and cloud automation used for provisioning, managing and orchestrating workflows across various toolchains`, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Determine if the command is a help command or if the help flag is set isHelpCommand := cmd.Name() == "help" @@ -84,13 +88,8 @@ var RootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the RootCmd. func Execute() error { - cc.Init(&cc.Config{ - RootCmd: RootCmd, - Headings: cc.HiCyan + cc.Bold + cc.Underline, - Commands: cc.HiGreen + cc.Bold, - Example: cc.Italic, - ExecName: cc.Bold, - Flags: cc.Bold, + colored.Init(&colored.Config{ + RootCmd: RootCmd, }) // InitCliConfig finds and merges CLI configurations in the following order: @@ -119,7 +118,31 @@ func Execute() error { } } - return RootCmd.Execute() + // Cobra for some reason handles root command in such a way that custom usage and help command don't work as per expectations + RootCmd.SilenceErrors = true + err = RootCmd.Execute() + if err != nil { + if strings.Contains(err.Error(), "unknown command") { + command := getInvalidCommandName(err.Error()) + showUsageAndExit(RootCmd, []string{command}) + } + } + return err +} + +func getInvalidCommandName(input string) string { + // Regular expression to match the command name inside quotes + re := regexp.MustCompile(`unknown command "([^"]+)"`) + + // Find the match + match := re.FindStringSubmatch(input) + + // Check if a match is found + if len(match) > 1 { + command := match[1] // The first capturing group contains the command + return command + } + return "" } func init() { @@ -146,9 +169,22 @@ func initCobraConfig() { styles := boa.DefaultStyles() b := boa.New(boa.WithStyles(styles)) oldUsageFunc := RootCmd.UsageFunc() - RootCmd.SetUsageFunc(b.UsageFunc) + RootCmd.SetUsageFunc(func(c *cobra.Command) error { + if c.Use == "atmos" { + return b.UsageFunc(c) + } + showUsageAndExit(c, c.Flags().Args()) + return nil + }) + RootCmd.SetHelpFunc(func(command *cobra.Command, args []string) { - RootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + if !(Contains(os.Args, "help") || Contains(os.Args, "--help") || Contains(os.Args, "-h")) { + arguments := os.Args[len(strings.Split(command.CommandPath(), " ")):] + if len(command.Flags().Args()) > 0 { + arguments = command.Flags().Args() + } + showUsageAndExit(command, arguments) + } // Print a styled Atmos logo to the terminal fmt.Println() if command.Use != "atmos" || command.Flags().Changed("help") { @@ -164,7 +200,7 @@ func initCobraConfig() { if err != nil { u.LogErrorAndExit(atmosConfig, err) } - b.HelpFunc(command, strings) + b.HelpFunc(command, args) if err := command.Usage(); err != nil { u.LogErrorAndExit(atmosConfig, err) } diff --git a/cmd/terraform.go b/cmd/terraform.go index 9fc9f8e22..452956b8c 100644 --- a/cmd/terraform.go +++ b/cmd/terraform.go @@ -1,18 +1,10 @@ package cmd import ( - "context" - "fmt" - - "github.com/samber/lo" - "github.com/spf13/cobra" - e "github.com/cloudposse/atmos/internal/exec" - "github.com/cloudposse/atmos/internal/tui/templates" "github.com/cloudposse/atmos/pkg/hooks" - "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" - cc "github.com/ivanpirog/coloredcobra" + "github.com/spf13/cobra" ) type contextKey string @@ -26,36 +18,8 @@ var terraformCmd = &cobra.Command{ Short: "Execute Terraform commands (e.g., plan, apply, destroy) using Atmos stack configurations", Long: `This command allows you to execute Terraform commands, such as plan, apply, and destroy, using Atmos stack configurations for consistent infrastructure management.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, - RunE: func(cmd *cobra.Command, args []string) error { - return terraformRun(cmd, cmd, args) - }, - PreRun: func(cmd *cobra.Command, args []string) { - var argsAfterDoubleDash []string - var finalArgs = args - - doubleDashIndex := lo.IndexOf(args, "--") - if doubleDashIndex > 0 { - finalArgs = lo.Slice(args, 0, doubleDashIndex) - argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) - } - - info, err := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) - if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - - ctx := context.WithValue(context.Background(), contextKey(atmosInfoKey), info) - RootCmd.SetContext(ctx) - - // Check Atmos configuration - checkAtmosConfig() - }, PostRunE: func(cmd *cobra.Command, args []string) error { - info, ok := RootCmd.Context().Value(atmosInfoKey).(schema.ConfigAndStacksInfo) - if !ok { - return fmt.Errorf("failed to retrieve atmos info from context") - } - + info := getConfigAndStacksInfo("terraform", cmd, args) return hooks.RunE(cmd, args, &info) }, } @@ -70,74 +34,16 @@ func Contains(slice []string, target string) bool { return false } -func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) error { - var argsAfterDoubleDash []string - var finalArgs = args - - doubleDashIndex := lo.IndexOf(args, "--") - if doubleDashIndex > 0 { - finalArgs = lo.Slice(args, 0, doubleDashIndex) - argsAfterDoubleDash = lo.Slice(args, doubleDashIndex+1, len(args)) +func terraformRun(cmd *cobra.Command, actualCmd *cobra.Command, args []string) { + info := getConfigAndStacksInfo("terraform", cmd, args) + if info.NeedHelp { + actualCmd.Usage() + return } - - info, _ := e.ProcessCommandLineArgs("terraform", cmd, finalArgs, argsAfterDoubleDash) - // Exit on help - if info.NeedHelp || (info.SubCommand == "" && info.SubCommand2 == "") { - if info.SubCommand != "" && info.SubCommand != "--help" && info.SubCommand != "help" { - suggestions := cmd.SuggestionsFor(args[0]) - if !Contains(suggestions, args[0]) { - if len(suggestions) > 0 { - fmt.Printf("Unknown command: '%s'\n\nDid you mean this?\n", args[0]) - for _, suggestion := range suggestions { - fmt.Printf(" %s\n", suggestion) - } - } else { - fmt.Printf(`Error: Unknkown command %q for %q`+"\n", args[0], cmd.CommandPath()) - } - fmt.Printf(`Run '%s --help' for usage`+"\n", cmd.CommandPath()) - return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) - } - } - // check if this is terraform --help command. TODO: check if this is the best way to do - if cmd == actualCmd { - template := templates.GenerateFromBaseTemplate(actualCmd.Use, []templates.HelpTemplateSections{ - templates.LongDescription, - templates.Usage, - templates.Aliases, - templates.Examples, - templates.AvailableCommands, - templates.Flags, - templates.GlobalFlags, - templates.NativeCommands, - templates.DoubleDashHelp, - templates.Footer, - }) - actualCmd.SetUsageTemplate(template) - cc.Init(&cc.Config{ - RootCmd: actualCmd, - Headings: cc.HiCyan + cc.Bold + cc.Underline, - Commands: cc.HiGreen + cc.Bold, - Example: cc.Italic, - ExecName: cc.Bold, - Flags: cc.Bold, - }) - } - - err := actualCmd.Help() - if err != nil { - return err - } - - return nil - } - // Check Atmos configuration - checkAtmosConfig() - err := e.ExecuteTerraform(info) if err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) + u.LogErrorAndExit(atmosConfig, err) } - return nil } func init() { diff --git a/cmd/terraform_commands.go b/cmd/terraform_commands.go index d84c84f4b..1bb077145 100644 --- a/cmd/terraform_commands.go +++ b/cmd/terraform_commands.go @@ -257,11 +257,14 @@ func attachTerraformCommands(parentCmd *cobra.Command) { if setFlags, ok := commandMaps[cmd.Use]; ok { setFlags(cmd) } - cmd.RunE = func(cmd_ *cobra.Command, args []string) error { - if len(os.Args) > 3 { + cmd.Run = func(cmd_ *cobra.Command, args []string) { + // Because we disable flag parsing we require manual handle help Request + handleHelpRequest(cmd, args) + if len(os.Args) > 2 { args = os.Args[2:] } - return terraformRun(parentCmd, cmd_, args) + + terraformRun(parentCmd, cmd_, args) } parentCmd.AddCommand(cmd) } diff --git a/cmd/terraform_generate.go b/cmd/terraform_generate.go index f59b063e8..609f8d7d2 100644 --- a/cmd/terraform_generate.go +++ b/cmd/terraform_generate.go @@ -16,10 +16,8 @@ This command supports the following subcommands: - 'backends' to generate backend configuration files for all Atmos components in all stacks. - 'varfile' to generate a variable file (varfile) for an Atmos component in a stack. - 'varfiles' to generate varfiles for all Atmos components in all stacks.`, + Args: cobra.NoArgs, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, - RunE: func(cmd *cobra.Command, args []string) error { - return terraformRun(cmd, cmd, args) - }, } func init() { diff --git a/cmd/terraform_generate_backend.go b/cmd/terraform_generate_backend.go index c47b11999..d65f9ddd4 100644 --- a/cmd/terraform_generate_backend.go +++ b/cmd/terraform_generate_backend.go @@ -16,6 +16,7 @@ var terraformGenerateBackendCmd = &cobra.Command{ Example: `atmos terraform generate backend -s `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_backends.go b/cmd/terraform_generate_backends.go index c81257830..8f9f11f6d 100644 --- a/cmd/terraform_generate_backends.go +++ b/cmd/terraform_generate_backends.go @@ -14,6 +14,7 @@ var terraformGenerateBackendsCmd = &cobra.Command{ Short: "Generate backend configurations for all Terraform components", Long: "This command generates the backend configuration files for all Terraform components in the Atmos environment.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfile.go b/cmd/terraform_generate_varfile.go index dc2be2dd1..6d1fa2327 100644 --- a/cmd/terraform_generate_varfile.go +++ b/cmd/terraform_generate_varfile.go @@ -15,6 +15,7 @@ var terraformGenerateVarfileCmd = &cobra.Command{ Long: "This command generates a `varfile` for a specified Atmos Terraform component.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/terraform_generate_varfiles.go b/cmd/terraform_generate_varfiles.go index c83980033..25e756454 100644 --- a/cmd/terraform_generate_varfiles.go +++ b/cmd/terraform_generate_varfiles.go @@ -14,6 +14,7 @@ var terraformGenerateVarfilesCmd = &cobra.Command{ Short: "Generate varfiles for all Terraform components in all stacks", Long: "This command generates varfiles for all Atmos Terraform components across all stacks.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate.go b/cmd/validate.go index bbaca0cca..5588be093 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -10,6 +10,7 @@ var validateCmd = &cobra.Command{ Short: "Validate configurations against OPA policies and JSON schemas", Long: `This command validates stacks and components by checking their configurations against Open Policy Agent (OPA) policies and JSON schemas.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/validate_component.go b/cmd/validate_component.go index 3b9fd4e3d..ece24430d 100644 --- a/cmd/validate_component.go +++ b/cmd/validate_component.go @@ -21,6 +21,7 @@ var validateComponentCmd = &cobra.Command{ "atmos validate component -s --schema-path --schema-type opa --module-paths catalog", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/validate_stacks.go b/cmd/validate_stacks.go index 872ad802d..ef8cdb210 100644 --- a/cmd/validate_stacks.go +++ b/cmd/validate_stacks.go @@ -16,6 +16,7 @@ var ValidateStacksCmd = &cobra.Command{ Long: "This command validates the configuration of stack manifests in Atmos to ensure proper setup and compliance.", Example: "validate stacks", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/vendor.go b/cmd/vendor.go index f069a9725..9ff2d4da6 100644 --- a/cmd/vendor.go +++ b/cmd/vendor.go @@ -10,6 +10,7 @@ var vendorCmd = &cobra.Command{ Short: "Manage external dependencies for components or stacks", Long: `This command manages external dependencies for Atmos components or stacks by vendoring them. Vendoring involves copying and locking required dependencies locally, ensuring consistency, reliability, and alignment with the principles of immutable infrastructure.`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, } func init() { diff --git a/cmd/vendor_diff.go b/cmd/vendor_diff.go index 7a65950bf..f208c98d8 100644 --- a/cmd/vendor_diff.go +++ b/cmd/vendor_diff.go @@ -15,6 +15,9 @@ var vendorDiffCmd = &cobra.Command{ Long: "This command compares and displays the differences in vendor-specific configurations or dependencies.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) + // TODO: There was no documentation here:https://atmos.tools/cli/commands/vendor we need to know what this command requires to check if we should add usage help + // Check Atmos configuration checkAtmosConfig() diff --git a/cmd/vendor_pull.go b/cmd/vendor_pull.go index 57db43242..f860c556f 100644 --- a/cmd/vendor_pull.go +++ b/cmd/vendor_pull.go @@ -14,6 +14,7 @@ var vendorPullCmd = &cobra.Command{ Short: "Pull the latest vendor configurations or dependencies", Long: "Pull and update vendor-specific configurations or dependencies to ensure the project has the latest required resources.", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // WithStackValidation is a functional option that enables/disables stack configuration validation // based on whether the --stack flag is provided diff --git a/cmd/version.go b/cmd/version.go index 7c205bb63..4007acaf8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -20,6 +20,7 @@ var versionCmd = &cobra.Command{ Short: "Display the version of Atmos you are running and check for updates", Long: `This command shows the version of the Atmos CLI you are currently running and checks if a newer version is available. Use this command to verify your installation and ensure you are up to date.`, Example: "atmos version", + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Print a styled Atmos logo to the terminal fmt.Println() diff --git a/cmd/workflow.go b/cmd/workflow.go index 413cd0bf6..434287417 100644 --- a/cmd/workflow.go +++ b/cmd/workflow.go @@ -83,6 +83,7 @@ var workflowCmd = &cobra.Command{ "For more details refer to https://atmos.tools/cli/commands/workflow/", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { + handleHelpRequest(cmd, args) // If no arguments are provided, start the workflow UI if len(args) == 0 { err := e.ExecuteWorkflowCmd(cmd, args) @@ -92,13 +93,6 @@ var workflowCmd = &cobra.Command{ return } - if args[0] == "help" { - if err := cmd.Help(); err != nil { - u.LogErrorAndExit(schema.AtmosConfiguration{}, err) - } - return - } - // Get the --file flag value workflowFile, _ := cmd.Flags().GetString("file") diff --git a/go.mod b/go.mod index 55cb90289..fdc0a6594 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,6 @@ require ( github.com/hashicorp/terraform-config-inspect v0.0.0-20241129133400-c404f8227ea6 github.com/hashicorp/terraform-exec v0.21.0 github.com/hexops/gotextdiff v1.0.3 - github.com/ivanpirog/coloredcobra v1.0.1 github.com/jfrog/jfrog-client-go v1.49.0 github.com/json-iterator/go v1.1.12 github.com/jwalton/go-supportscolor v1.2.0 diff --git a/go.sum b/go.sum index 92ef88391..f21cc00b0 100644 --- a/go.sum +++ b/go.sum @@ -918,7 +918,6 @@ github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1368,11 +1367,8 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ivanpirog/coloredcobra v1.0.1 h1:aURSdEmlR90/tSiWS0dMjdwOvCVUeYLfltLfbgNxrN4= -github.com/ivanpirog/coloredcobra v1.0.1/go.mod h1:iho4nEKcnwZFiniGSdcgdvRgZNjxm+h20acv8vqmN6Q= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -1736,7 +1732,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/internal/exec/helmfile.go b/internal/exec/helmfile.go index 734cbdf68..ed5c9668a 100644 --- a/internal/exec/helmfile.go +++ b/internal/exec/helmfile.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" @@ -33,27 +32,6 @@ func ExecuteHelmfile(info schema.ConfigAndStacksInfo) error { return err } - if info.NeedHelp { - return nil - } - - // If the user just types `atmos helmfile`, print Atmos logo and show helmfile help - if info.SubCommand == "" { - fmt.Println() - err = tuiUtils.PrintStyledText("ATMOS") - if err != nil { - return err - } - - err = processHelp(atmosConfig, "helmfile", "") - if err != nil { - return err - } - - fmt.Println() - return nil - } - if info.SubCommand == "version" { return ExecuteShellCommand(atmosConfig, "helmfile", []string{info.SubCommand}, "", nil, false, info.RedirectStdErr) } diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index feeffa823..ea712c1f2 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - tuiUtils "github.com/cloudposse/atmos/internal/tui/utils" cfg "github.com/cloudposse/atmos/pkg/config" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" @@ -104,23 +103,6 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { return nil } - // If the user just types `atmos terraform`, print Atmos logo and show terraform help - if info.SubCommand == "" { - fmt.Println() - err = tuiUtils.PrintStyledText("ATMOS") - if err != nil { - return err - } - - err = processHelp(atmosConfig, "terraform", "") - if err != nil { - return err - } - - fmt.Println() - return nil - } - if info.SubCommand == "version" { return ExecuteShellCommand(atmosConfig, "terraform", diff --git a/internal/tui/templates/base_template.go b/internal/tui/templates/base_template.go index 5e3ce2269..a4d648f2e 100644 --- a/internal/tui/templates/base_template.go +++ b/internal/tui/templates/base_template.go @@ -1,13 +1,12 @@ package templates -import "fmt" - type HelpTemplateSections int const ( LongDescription HelpTemplateSections = iota Usage Aliases + SubCommandAliases Examples AvailableCommands Flags @@ -18,15 +17,15 @@ const ( Footer ) -func GenerateFromBaseTemplate(commandName string, parts []HelpTemplateSections) string { +func GenerateFromBaseTemplate(parts []HelpTemplateSections) string { template := "" for _, value := range parts { - template += getSection(commandName, value) + template += getSection(value) } return template } -func getSection(commandName string, section HelpTemplateSections) string { +func getSection(section HelpTemplateSections) string { switch section { case LongDescription: return `{{ .Long }} @@ -34,56 +33,74 @@ func getSection(commandName string, section HelpTemplateSections) string { case AdditionalHelpTopics: return `{{if .HasHelpSubCommands}} -Additional help topics: + +{{HeadingStyle "Additional help topics:"}} + {{formatCommands .Commands "additionalHelpTopics"}}{{end}}` case Aliases: return `{{if gt (len .Aliases) 0}} -Aliases: +{{HeadingStyle "Aliases:"}} + {{.NameAndAliases}}{{end}}` + case SubCommandAliases: + return `{{if (isAliasesPresent .Commands)}} + +{{HeadingStyle "SubCommand Aliases:"}} + +{{formatCommands .Commands "subcommandAliases"}}{{end}}` case AvailableCommands: return `{{if .HasAvailableSubCommands}} -Available Commands: + +{{HeadingStyle "Available Commands:"}} + {{formatCommands .Commands "availableCommands"}}{{end}}` case Examples: return `{{if .HasExample}} -Examples: + +{{HeadingStyle "Examples:"}} + {{.Example}}{{end}}` case Flags: return `{{if .HasAvailableLocalFlags}} -Flags: + +{{HeadingStyle "Flags:"}} + {{wrappedFlagUsages .LocalFlags | trimTrailingWhitespaces}}{{end}}` case GlobalFlags: return `{{if .HasAvailableInheritedFlags}} -Global Flags: + +{{HeadingStyle "Global Flags:"}} + {{wrappedFlagUsages .InheritedFlags | trimTrailingWhitespaces}}{{end}}` case NativeCommands: - return fmt.Sprintf(` + return `{{if (isNativeCommandsAvailable .Commands)}} -{{HeadingStyle "Native %s Commands:"}} +{{HeadingStyle "Native "}}{{HeadingStyle .Use}}{{HeadingStyle " Commands:"}} -{{formatCommands .Commands "native"}} -`, commandName) +{{formatCommands .Commands "native"}}{{end}}` case Usage: - return `Usage:{{if .Runnable}} + return ` +{{HeadingStyle "Usage:"}} +{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} - {{.CommandPath}} [command]{{end}}` + {{.CommandPath}} [sub-command] [flags]{{end}}` case DoubleDashHelp: - return fmt.Sprintf(` + return ` The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. Example: - atmos %s -s -- `, commandName) + atmos {{.CommandPath}} {{if gt (len .Commands) 0}}[subcommand]{{end}} -s -- ` case Footer: return `{{if .HasAvailableSubCommands}} -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}` +Use "{{.CommandPath}} {{if gt (len .Commands) 0}}[subcommand]{{end}} --help" for more information about a command.{{end}}` default: return "" } diff --git a/internal/tui/templates/templater.go b/internal/tui/templates/templater.go index 468f71329..f96ea3245 100644 --- a/internal/tui/templates/templater.go +++ b/internal/tui/templates/templater.go @@ -45,7 +45,43 @@ func formatCommand(name string, desc string, padding int, IsNotSupported bool) s var customHelpShortMessage = map[string]string{ "help": "Display help information for Atmos commands", - "tf": "Alias for `terraform` commands", +} + +// filterCommands returns only commands or aliases based on returnOnlyAliases boolean +func filterCommands(commands []*cobra.Command, returnOnlyAliases bool) []*cobra.Command { + if !returnOnlyAliases { + return commands + } + filtered := []*cobra.Command{} + cmdMap := make(map[string]struct{}) + for _, cmd := range commands { + cmdMap[cmd.Use] = struct{}{} + } + for _, cmd := range commands { + for _, alias := range cmd.Aliases { + if _, ok := cmdMap[alias]; ok { + continue + } + copyCmd := *cmd + copyCmd.Use = alias + copyCmd.Short = fmt.Sprintf("Alias of %q command", cmd.CommandPath()) + filtered = append(filtered, ©Cmd) + } + } + return filtered +} + +func isNativeCommandsAvailable(cmds []*cobra.Command) bool { + for _, cmd := range cmds { + if cmd.Annotations["nativeCommand"] == "true" { + return true + } + } + return false +} + +func isAliasesPresent(cmds []*cobra.Command) bool { + return len(filterCommands(cmds, true)) > 0 } // formatCommands formats a slice of cobra commands with proper styling @@ -54,7 +90,11 @@ func formatCommands(cmds []*cobra.Command, listType string) string { availableCmds := make([]*cobra.Command, 0) // First pass: collect available commands and find max length + cmds = filterCommands(cmds, listType == "subcommandAliases") for _, cmd := range cmds { + if v, ok := customHelpShortMessage[cmd.Name()]; ok { + cmd.Short = v + } switch listType { case "additionalHelpTopics": if cmd.IsAdditionalHelpTopicCommand() { @@ -72,6 +112,16 @@ func formatCommands(cmds []*cobra.Command, listType string) string { } continue } + case "subcommandAliases": + if cmd.Annotations["nativeCommand"] == "true" { + continue + } + if cmd.IsAvailableCommand() || cmd.Name() == "help" { + availableCmds = append(availableCmds, cmd) + if len(cmd.Name()) > maxLen { + maxLen = len(cmd.Name()) + } + } default: if cmd.Annotations["nativeCommand"] == "true" { continue @@ -83,9 +133,6 @@ func formatCommands(cmds []*cobra.Command, listType string) string { } } } - if v, ok := customHelpShortMessage[cmd.Name()]; ok { - cmd.Short = v - } } var lines []string @@ -119,11 +166,13 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { return fmt.Errorf("command cannot be nil") } t := &Templater{ - UsageTemplate: GenerateFromBaseTemplate(cmd.Use, []HelpTemplateSections{ + UsageTemplate: GenerateFromBaseTemplate([]HelpTemplateSections{ Usage, Aliases, Examples, AvailableCommands, + NativeCommands, + SubCommandAliases, Flags, GlobalFlags, AdditionalHelpTopics, @@ -133,6 +182,8 @@ func SetCustomUsageFunc(cmd *cobra.Command) error { } cmd.SetUsageTemplate(t.UsageTemplate) + cobra.AddTemplateFunc("isAliasesPresent", isAliasesPresent) + cobra.AddTemplateFunc("isNativeCommandsAvailable", isNativeCommandsAvailable) cobra.AddTemplateFunc("formatCommands", formatCommands) return nil } diff --git a/pkg/ui/theme/colors.go b/pkg/ui/theme/colors.go index aba957436..1e7fe1115 100644 --- a/pkg/ui/theme/colors.go +++ b/pkg/ui/theme/colors.go @@ -21,6 +21,18 @@ const ( ColorBorder = "#5F5FD7" // UI borders ) +type HelpStyle struct { + Headings *color.Color + Commands *color.Color + Example *color.Color + ExecName *color.Color + Flags *color.Color + CmdShortDescr *color.Color + FlagsDescr *color.Color + FlagsDataType *color.Color + Aliases *color.Color +} + // Styles provides pre-configured lipgloss styles for common UI elements var Styles = struct { VersionNumber lipgloss.Style @@ -34,6 +46,7 @@ var Styles = struct { CommandName lipgloss.Style Description lipgloss.Style Border lipgloss.Style + Help HelpStyle }{ VersionNumber: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGray)), NewVersion: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGreen)), @@ -46,6 +59,13 @@ var Styles = struct { CommandName: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorGreen)), Description: lipgloss.NewStyle().Foreground(lipgloss.Color(ColorWhite)), Border: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorBorder)), + Help: HelpStyle{ + Headings: color.New(color.FgHiCyan).Add(color.Bold).Add(color.Underline), + Commands: color.New(color.FgHiGreen).Add(color.Bold), + Example: color.New(color.Italic), + ExecName: color.New(color.Bold), + Flags: color.New(color.Bold), + }, } // Colors provides color.Attribute mappings for the old color.New style diff --git a/pkg/utils/log_utils.go b/pkg/utils/log_utils.go index bdfc4d026..f0b665827 100644 --- a/pkg/utils/log_utils.go +++ b/pkg/utils/log_utils.go @@ -29,6 +29,11 @@ func PrintMessageInColor(message string, messageColor *color.Color) { _, _ = messageColor.Fprint(os.Stdout, message) } +func PrintErrorInColor(message string) { + messageColor := theme.Colors.Error + _, _ = messageColor.Fprint(os.Stderr, message) +} + // LogErrorAndExit logs errors to std.Error and exits with an error code func LogErrorAndExit(atmosConfig schema.AtmosConfiguration, err error) { if err != nil { diff --git a/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden b/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden index 485f99f73..49c614a05 100644 --- a/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden +++ b/tests/snapshots/TestCLICommands_atmos_--help.stdout.golden @@ -3,7 +3,7 @@ Usage: atmos [flags] - atmos [command] + atmos [sub-command] [flags] Available Commands: @@ -25,6 +25,11 @@ Available Commands: version Display the version of Atmos you are running and check for updates workflow Run predefined tasks using workflows +SubCommand Aliases: + + hf Alias of "atmos helmfile" command + tf Alias of "atmos terraform" command + Flags: @@ -51,7 +56,7 @@ The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. Example: - atmos atmos -s -- + atmos atmos [subcommand] -s -- -Use "atmos [command] --help" for more information about a command. +Use "atmos [subcommand] --help" for more information about a command. diff --git a/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden b/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden index 485f99f73..49c614a05 100644 --- a/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden +++ b/tests/snapshots/TestCLICommands_check_atmos_--help_in_empty-dir.stdout.golden @@ -3,7 +3,7 @@ Usage: atmos [flags] - atmos [command] + atmos [sub-command] [flags] Available Commands: @@ -25,6 +25,11 @@ Available Commands: version Display the version of Atmos you are running and check for updates workflow Run predefined tasks using workflows +SubCommand Aliases: + + hf Alias of "atmos helmfile" command + tf Alias of "atmos terraform" command + Flags: @@ -51,7 +56,7 @@ The '--' (double-dash) can be used to signify the end of Atmos-specific options and the beginning of additional native arguments and flags for the specific command being run. Example: - atmos atmos -s -- + atmos atmos [subcommand] -s -- -Use "atmos [command] --help" for more information about a command. +Use "atmos [subcommand] --help" for more information about a command. diff --git a/tests/test-cases/core.yaml b/tests/test-cases/core.yaml index ce6288a33..cdbc9bb7c 100644 --- a/tests/test-cases/core.yaml +++ b/tests/test-cases/core.yaml @@ -62,7 +62,7 @@ tests: - "but the directory does not exist." stderr: - "^$" - exit_code: 0 + exit_code: 1 - name: atmos docs enabled: true @@ -77,21 +77,6 @@ tests: exit_code: 0 stderr: - "^$" - - - name: atmos non-existent - enabled: true - snapshot: true - description: "Ensure atmos CLI returns an error for a non-existent command." - workdir: "../" - command: "atmos" - args: - - "non-existent" - expect: - diff: [] - stderr: - - 'unknown command "non-existent" for "atmos"' - exit_code: 1 - - name: atmos terraform non-existent enabled: false snapshot: true diff --git a/tests/test-cases/empty-dir.yaml b/tests/test-cases/empty-dir.yaml index f0e1eabaa..29c051d51 100644 --- a/tests/test-cases/empty-dir.yaml +++ b/tests/test-cases/empty-dir.yaml @@ -28,7 +28,7 @@ tests: - "https://atmos.tools/cli/configuration" stderr: - "^$" - exit_code: 0 + exit_code: 1 - name: check atmos --help in empty-dir enabled: true @@ -42,7 +42,7 @@ tests: stdout: - "Available Commands:" - "Flags:" - - 'Use "atmos \[command\] --help" for more information about a command\.' + - 'Use "atmos \[subcommand\] --help" for more information about a command\.' stderr: - "^$" exit_code: 0 diff --git a/tests/test-cases/help-and-usage.yaml b/tests/test-cases/help-and-usage.yaml new file mode 100644 index 000000000..c209133b8 --- /dev/null +++ b/tests/test-cases/help-and-usage.yaml @@ -0,0 +1,433 @@ +tests: + - name: atmos non-existent + enabled: true + description: "Ensure atmos CLI returns an error for a non-existent command." + workdir: "../" + command: "atmos" + args: + - "non-existent" + expect: + stderr: + - 'Error: Unknown command \"non-existent\" for \"atmos\"' + exit_code: 1 + - name: atmos --help + enabled: true + description: "Ensure atmos CLI help command lists available commands." + workdir: "./" + command: "atmos" + args: + - "--help" + expect: + stdout: + - "Usage:" + - "Available Commands:" + - "\\batlantis\\b" + - "\\baws\\b" + - "\\bcompletion\\b" + - "\\bdescribe\\b" + - "\\bdocs\\b" + - "\\bhelmfile\\b" + - "\\bhelp\\b" + - "\\blist\\b" + - "\\bpro\\b" + - "\\bterraform\\b" + - "\\bvalidate\\b" + - "\\bvendor\\b" + - "\\bversion\\b" + - "\\bworkflow\\b" + - "SubCommand Aliases:" + - "\\bhf\\b" + - "Flags:" + - "\\batmos \\[subcommand\\] [<]component[>] -s [<]stack[>] -- [<]native-flags[>]" + - "for more information about a command" + stderr: + - "^$" + exit_code: 0 + - name: atmos + enabled: true + description: "Ensure atmos CLI help command shows atmos config " + workdir: "./" + command: "atmos" + expect: + stdout: + - "atmos.yaml CLI config file was not found" + - "The default Atmos stacks directory is set to stacks," + stderr: + - "^$" + exit_code: 1 + - name: atmos terraform + enabled: true + description: "Ensure atmos terraform cli shows usage command" + workdir: "./" + command: "atmos" + args: + - "terraform" + expect: + stdout: + - "Valid subcommands are:" + - "apply" + - "clean" + - "console" + stderr: + - "Error: Unknown command: \"atmos terraform\"" + exit_code: 1 + - name: atmos terraform help + enabled: true + description: "Ensure 'atmos terraform help' shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "help" + expect: + stdout: + - "\\bapply\\b" + - "\\bgenerate\\b" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform --help + enabled: true + description: "Ensure atmos terraform --help shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "--help" + expect: + stdout: + - "\\bapply\\b" + - "\\bgenerate\\b" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform apply --help + enabled: true + description: "Ensure atmos terraform apply --help shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "apply" + - "--help" + expect: + stdout: + - "\\bUsage\\b" + - "Flags:" + - "and the beginning of additional native arguments and flags for the specific command being run" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform apply help + enabled: true + description: "Ensure atmos terraform help shows help output" + workdir: "./" + command: "atmos" + args: + - "terraform" + - "apply" + - "--help" + expect: + stdout: + - "\\bUsage\\b" + - "Flags:" + - "and the beginning of additional native arguments and flags for the specific command being run" + stderr: + - "^$" + exit_code: 0 + - name: atmos terraform non-existent + enabled: true + description: "Ensure atmos terraform CLI returns an error for a non-existent command." + workdir: "../" + command: "atmos" + args: + - "terraform" + - "non-existent" + expect: + stderr: + - 'Error: Unknown command \"non-existent\" for \"atmos terraform\"' + exit_code: 1 + - name: atmos terraform plan non-existent in non workspace + enabled: true + description: "Ensure atmos terraform CLI returns an error for a non-existent command in non existing workspace." + workdir: "../" + command: "atmos" + args: + - "terraform" + - "plan" + - "non-existent" + expect: + stdout: + - "CLI config file specifies the directory for Atmos stack" + - "Quick Start" + exit_code: 1 + - name: atmos terraform plan non-existent in workspace + enabled: true + description: "Ensure atmos terraform CLI returns an error for a non-existent command in existing workspace." + workdir: "../examples/quick-start-simple" + command: "atmos" + args: + - "terraform" + - "plan" + - "non-existent" + expect: + stderr: + - "'stack' is required. Usage: atmos terraform -s " + exit_code: 1 + - name: atmos helmfile + enabled: true + description: "Should show usage for helmfile" + workdir: "../" + command: "atmos" + args: + - "helmfile" + expect: + stderr: + - "Error: Unknown command: \"atmos helmfile\"" + stdout: + - "Valid subcommands are:" + - "apply" + - "destroy" + exit_code: 1 + - name: atmos helmfile non-existant + enabled: true + description: "Should show usage for helmfile and non-existant subcommand" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "non-existant" + expect: + stderr: + - "Error: Unknown command \"non-existant\" for \"atmos helmfile\"" + stdout: + - "Valid subcommands are:" + - "apply" + - "destroy" + exit_code: 1 + - name: atmos helmfile help + enabled: true + description: "Should show help for helmfile" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "help" + expect: + stdout: + - "Available Commands:" + - "apply" + - "destroy" + exit_code: 0 + - name: atmos helmfile --help + enabled: true + description: "Should show help for helmfile when using help flag" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "help" + expect: + stdout: + - "Available Commands:" + - "apply" + - "destroy" + exit_code: 0 + - name: atmos helmfile apply non-existant + enabled: true + description: "Should show error in non atmos workspace" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "non-existent" + expect: + stdout: + - "CLI config file specifies the directory for Atmos stack" + - "Quick Start" + exit_code: 1 + - name: atmos helmfile apply + enabled: true + description: "Should show error in atmos workspace" + workdir: "../examples/demo-helmfile" + command: "atmos" + args: + - "helmfile" + - "apply" + expect: + stderr: + - "'stack' is required. Usage: atmos helmfile -s " + exit_code: 1 + - name: atmos helmfile apply help + enabled: true + description: "Should show help for atmos helmfile apply" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "help" + expect: + stdout: + - "Flags:" + - "--logs-file" + exit_code: 0 + - name: atmos helmfile apply --help + enabled: true + description: "Should show help for atmos helmfile apply --help" + workdir: "../" + command: "atmos" + args: + - "helmfile" + - "apply" + - "--help" + expect: + stdout: + - "Flags:" + - "--logs-file" + exit_code: 0 + - name: atmos atlantis + enabled: true + description: "Should show usage atmos atlantis" + workdir: "../" + command: "atmos" + args: + - "atlantis" + expect: + stderr: + - "Error: Unknown command: \"atmos atlantis\"" + exit_code: 1 + - name: atmos atlantis help + enabled: true + description: "Should show help 'atmos atlantis help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "help" + expect: + stdout: + - "Available Commands:" + - "generate" + exit_code: 0 + - name: atmos atlantis --help + enabled: true + description: "Should show help 'atmos atlantis --help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "--help" + expect: + stdout: + - "Available Commands:" + - "generate" + exit_code: 0 + + - name: atmos atlantis generate + enabled: true + description: "Should show usage atmos atlantis" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + expect: + stderr: + - "Error: Unknown command: \"atmos atlantis generate\"" + exit_code: 1 + - name: atmos atlantis generate help + enabled: true + description: "Should show help 'atmos atlantis generate help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "help" + expect: + stdout: + - "Available Commands:" + - "repo-config" + exit_code: 0 + - name: atmos atlantis generate --help + enabled: true + description: "Should show help 'atmos atlantis --help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "--help" + expect: + stdout: + - "Available Commands:" + - "repo-config" + exit_code: 0 + - name: atmos atlantis generate repo-config + enabled: true + description: "Should show config missing in non atmos workspace 'atmos atlantis generate repo-config'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + expect: + stdout: + - "atmos.yaml CLI config file specifies the directory for Atmos stacks as stacks," + - "To configure and start using Atmos, refer to the following documents:" + stderr: + - "^$" + exit_code: 1 + - name: atmos atlantis generate repo-config help + enabled: true + description: "Should show help for 'atmos atlantis generate repo-config help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "help" + expect: + stdout: + - "Flags:" + - "--affected-only" + - "--config-template" + stderr: + - "^$" + exit_code: 0 + - name: atmos atlantis generate repo-config --help + enabled: true + description: "Should show help for 'atmos atlantis generate repo-config --help'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "--help" + expect: + stdout: + - "Flags:" + - "--affected-only" + - "--config-template" + stderr: + - "^$" + exit_code: 0 + - name: atmos atlantis generate repo-config non-existant + enabled: true + description: "Should show usage for 'atmos atlantis generate repo-config non-existant'" + workdir: "../" + command: "atmos" + args: + - "atlantis" + - "generate" + - "repo-config" + - "non-existant" + expect: + stderr: + - "Error: Unknown command \"non-existant\" for \"atmos atlantis generate repo-config\"" + exit_code: 1 diff --git a/tests/test-cases/log-level-validation.yaml b/tests/test-cases/log-level-validation.yaml index e2742dc6d..7d5b12e9c 100644 --- a/tests/test-cases/log-level-validation.yaml +++ b/tests/test-cases/log-level-validation.yaml @@ -45,7 +45,7 @@ tests: - -s - test expect: - exit_code: 0 + exit_code: 1 - name: "Valid Log Level in Environment Variable" enabled: true @@ -61,7 +61,7 @@ tests: env: ATMOS_LOGS_LEVEL: Debug expect: - exit_code: 0 + exit_code: 1 - name: "Valid Log Level in Command Line Flag" enabled: true @@ -77,4 +77,4 @@ tests: - -s - test expect: - exit_code: 0 + exit_code: 1