Skip to content

Commit

Permalink
Merge branch 'main' into DEV-2802
Browse files Browse the repository at this point in the history
  • Loading branch information
Cerebrovinny authored Feb 13, 2025
2 parents 72a8e76 + 5a55781 commit b6b19b2
Show file tree
Hide file tree
Showing 201 changed files with 3,412 additions and 977 deletions.
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Always check-out / check-in files with LF line endings.
* text=auto eol=lf

docs linguist-documentation=true
docs/** linguist-documentation
website/** linguist-documentation

# Screengrabs are binary HTML files that are automatically generated
website/src/components/Screengrabs/**/*.html linguist-generated=true binary
Expand Down
6 changes: 3 additions & 3 deletions .github/mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ pull_request_rules:
actions:
comment:
message: |
Triggering the workflow dispatch for preview build...
Triggering the workflow dispatch for tests & preview build on `{{ head }}`...
github_actions:
workflow:
dispatch:
- workflow: website-preview-build.yml
ref: "{{ pull_request.head.ref }}"
ref: "{{ head }}"
- workflow: test.yml
ref: "{{ pull_request.head.ref }}"
ref: "{{ head }}"
33 changes: 6 additions & 27 deletions cmd/about.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package cmd

import (
_ "embed"
"fmt"
"os"

"github.com/charmbracelet/glamour"
"github.com/cloudposse/atmos/pkg/utils"
"github.com/spf13/cobra"
)

Expand All @@ -14,31 +12,12 @@ var aboutMarkdown string

// aboutCmd represents the about command
var aboutCmd = &cobra.Command{
Use: "about",
Short: "Learn about Atmos",
Long: `Display information about Atmos, its features, and benefits.`,
Args: cobra.NoArgs,
DisableSuggestions: true,
SilenceUsage: true,
SilenceErrors: true,
Use: "about",
Short: "Learn about Atmos",
Long: `Display information about Atmos, its features, and benefits.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(80),
)
if err != nil {
return fmt.Errorf("failed to create markdown renderer: %w", err)
}

out, err := renderer.Render(aboutMarkdown)
if err != nil {
return fmt.Errorf("failed to render about documentation: %w", err)
}

_, err = fmt.Fprint(os.Stdout, out)
if err != nil {
return err
}
utils.PrintfMarkdown(aboutMarkdown)
return nil
},
}
Expand Down
1 change: 1 addition & 0 deletions cmd/atlantis.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ var atlantisCmd = &cobra.Command{
}

func init() {
atlantisCmd.PersistentFlags().Bool("", false, doubleDashHint)
RootCmd.AddCommand(atlantisCmd)
}
4 changes: 2 additions & 2 deletions cmd/atlantis_generate_repo_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"github.com/spf13/cobra"

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

// atlantisGenerateRepoConfigCmd generates repository configuration for Atlantis
Expand All @@ -22,7 +22,7 @@ var atlantisGenerateRepoConfigCmd = &cobra.Command{
checkAtmosConfig()
err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args)
if err != nil {
u.LogErrorAndExit(err)
utils.PrintErrorMarkdownAndExit("", err, "")
}
},
}
Expand Down
1 change: 1 addition & 0 deletions cmd/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ var awsCmd = &cobra.Command{
}

func init() {
awsCmd.PersistentFlags().Bool("", false, doubleDashHint)
RootCmd.AddCommand(awsCmd)
}
4 changes: 3 additions & 1 deletion cmd/aws_eks_update_kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ See https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html
Run: func(cmd *cobra.Command, args []string) {
err := e.ExecuteAwsEksUpdateKubeconfigCommand(cmd, args)
if err != nil {
u.LogErrorAndExit(err)
u.PrintErrorMarkdownAndExit("", err, "")
}
},
ValidArgsFunction: ComponentsArgCompletion,
}

// https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html
func init() {
awsEksCmdUpdateKubeconfigCmd.DisableFlagParsing = false
awsEksCmdUpdateKubeconfigCmd.PersistentFlags().StringP("stack", "s", "", "atmos aws eks update-kubeconfig <component> -s <stack>")
AddStackCompletion(awsEksCmdUpdateKubeconfigCmd)
awsEksCmdUpdateKubeconfigCmd.PersistentFlags().String("profile", "", "atmos aws eks update-kubeconfig --profile <profile>")
awsEksCmdUpdateKubeconfigCmd.PersistentFlags().String("name", "", "atmos aws eks update-kubeconfig --name <cluster name>")
awsEksCmdUpdateKubeconfigCmd.PersistentFlags().String("region", "", "atmos aws eks update-kubeconfig --region <region>")
Expand Down
198 changes: 166 additions & 32 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ func processCustomCommands(
var command *cobra.Command
existingTopLevelCommands := make(map[string]*cobra.Command)

// Build commands and their hierarchy from the alias map
for alias, fullCmd := range atmosConfig.CommandAliases {
parts := strings.Fields(fullCmd)
addCommandWithAlias(RootCmd, alias, parts)
}

if topLevel {
existingTopLevelCommands = getTopLevelCommands()
}
Expand Down Expand Up @@ -73,7 +79,8 @@ func processCustomCommands(
executeCustomCommand(atmosConfig, cmd, args, parentCommand, commandConfig)
},
}

// TODO: we need to update this post https://github.com/cloudposse/atmos/pull/959 gets merged
customCommand.PersistentFlags().Bool("", false, "Use double dashes to separate Atmos-specific options from native arguments and flags for the command.")
// Process and add flags to the command
for _, flag := range commandConfig.Flags {
if flag.Type == "bool" {
Expand Down Expand Up @@ -112,6 +119,35 @@ func processCustomCommands(
return nil
}

// addCommandWithAlias adds a command hierarchy based on the full command
func addCommandWithAlias(parentCmd *cobra.Command, alias string, parts []string) {
if len(parts) == 0 {
return
}

// Check if a command with the current part already exists
var cmd *cobra.Command
for _, c := range parentCmd.Commands() {
if c.Use == parts[0] {
cmd = c
break
}
}

// If the command doesn't exist, create it
if cmd == nil {
u.LogErrorAndExit(fmt.Errorf("subcommand `%s` not found for alias `%s`", parts[0], alias))
}

// If there are more parts, recurse for the next level
if len(parts) > 1 {
addCommandWithAlias(cmd, alias, parts[1:])
} else if !Contains(cmd.Aliases, alias) {
// This is the last part of the command, add the alias
cmd.Aliases = append(cmd.Aliases, alias)
}
}

// processCommandAliases processes the command aliases
func processCommandAliases(
atmosConfig schema.AtmosConfiguration,
Expand Down Expand Up @@ -186,10 +222,9 @@ func preCustomCommand(
os.Exit(1)
} else {
// truly invalid, nothing to do
u.LogError(errors.New(
"invalid command: no args, no steps, no sub-commands",
))
os.Exit(1)
u.PrintErrorMarkdownAndExit("Invalid command", errors.New(
fmt.Sprintf("The `%s` command has no steps or subcommands configured.", cmd.CommandPath()),
), "https://atmos.tools/cli/configuration/commands")
}
}

Expand Down Expand Up @@ -270,7 +305,7 @@ func executeCustomCommand(
commandConfig *schema.Command,
) {
var err error

args, trailingArgs := extractTrailingArgs(args, os.Args)
if commandConfig.Verbose {
atmosConfig.Logs.Level = u.LogLevelTrace
}
Expand Down Expand Up @@ -311,8 +346,9 @@ func executeCustomCommand(

// Prepare template data
data := map[string]any{
"Arguments": argumentsData,
"Flags": flagsData,
"Arguments": argumentsData,
"Flags": flagsData,
"TrailingArgs": trailingArgs,
}

// If the custom command defines 'component_config' section with 'component' and 'stack' attributes,
Expand Down Expand Up @@ -407,6 +443,35 @@ func executeCustomCommand(
}
}

// Extracts native arguments (everything after "--") signifying the end of Atmos-specific arguments.
// Because of the flag hint for double dash, args is already consumed by Cobra.
// So we need to perform manual parsing of os.Args to extract the "trailing args" after the "--" end of args marker.
func extractTrailingArgs(args []string, osArgs []string) ([]string, string) {
doubleDashIndex := lo.IndexOf(osArgs, "--")
mainArgs := args
trailingArgs := ""
if doubleDashIndex > 0 {
mainArgs = lo.Slice(osArgs, 0, doubleDashIndex)
trailingArgs = strings.Join(lo.Slice(osArgs, doubleDashIndex+1, len(osArgs)), " ")
result := []string{}
lookup := make(map[string]bool)

// Populate a lookup map for quick existence check
for _, val := range mainArgs {
lookup[val] = true
}

// Iterate over leftArr and collect matching elements in order
for _, val := range args {
if lookup[val] {
result = append(result, val)
}
}
mainArgs = result
}
return mainArgs, trailingArgs
}

// cloneCommand clones a custom command config into a new struct
func cloneCommand(orig *schema.Command) (*schema.Command, error) {
origJSON, err := json.Marshal(orig)
Expand Down Expand Up @@ -552,37 +617,31 @@ func handleHelpRequest(cmd *cobra.Command, args []string) {
}
}

// showUsageAndExit we display the markdown usage or fallback to our custom usage
// Markdown usage is not compatible with all outputs. We should therefore have fallback option.
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 {
showErrorExampleFromMarkdown(cmd, "")
}
if len(args) > 0 {
suggestions = cmd.SuggestionsFor(args[0])
unknownCommand = fmt.Sprintf("Error: Unknown command %q for %q\n\n", args[0], cmd.CommandPath())
showErrorExampleFromMarkdown(cmd, args[0])
}
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))
}
os.Exit(1)
}

func showFlagUsageAndExit(cmd *cobra.Command, err error) error {
unknownCommand := fmt.Sprintf("%v for command `%s`\n\n", err.Error(), cmd.CommandPath())
args := strings.Split(err.Error(), ": ")
if len(args) == 2 {
if strings.Contains(args[0], "flag needs an argument") {
unknownCommand = fmt.Sprintf("`%s` %s for command `%s`\n\n", args[1], args[0], cmd.CommandPath())
} else {
u.PrintMessage("No valid subcommands found")
unknownCommand = fmt.Sprintf("%s `%s` for command `%s`\n\n", args[0], args[1], cmd.CommandPath())
}
}
u.PrintMessage(fmt.Sprintf("\nRun '%s --help' for usage", cmd.CommandPath()))
showUsageExample(cmd, unknownCommand)
os.Exit(1)
return nil
}

// getConfigAndStacksInfo processes the CLI config and stacks
Expand All @@ -606,6 +665,81 @@ func getConfigAndStacksInfo(commandName string, cmd *cobra.Command, args []strin
return info
}

func showErrorExampleFromMarkdown(cmd *cobra.Command, arg string) {
commandPath := cmd.CommandPath()
suggestions := []string{}
details := fmt.Sprintf("The command `%s` is not valid usage\n", commandPath)
if len(arg) > 0 {
details = fmt.Sprintf("Unknown command `%s` for `%s`\n", arg, commandPath)
} else if len(cmd.Commands()) != 0 && arg == "" {
details = fmt.Sprintf("The command `%s` requires a subcommand\n", commandPath)
}
if len(arg) > 0 {
suggestions = cmd.SuggestionsFor(arg)
}
if len(suggestions) > 0 {
details = details + "Did you mean this?\n"
for _, suggestion := range suggestions {
details += "* " + suggestion + "\n"
}
} else {
if len(cmd.Commands()) > 0 {
details += "\nValid subcommands are:\n"
}
// Retrieve valid subcommands dynamically
for _, subCmd := range cmd.Commands() {
details = details + "* " + subCmd.Name() + "\n"
}
}
showUsageExample(cmd, details)
}

func showUsageExample(cmd *cobra.Command, details string) {
contentName := strings.ReplaceAll(cmd.CommandPath(), " ", "_")
suggestion := fmt.Sprintf("\n\nRun `%s --help` for usage", cmd.CommandPath())
if exampleContent, ok := examples[contentName]; ok {
suggestion = exampleContent.Suggestion
details += "\n## Usage Examples:\n" + exampleContent.Content
}
u.PrintInvalidUsageErrorAndExit(errors.New(details), suggestion)
}

func stackFlagCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
output, err := listStacks(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return output, cobra.ShellCompDirectiveNoFileComp
}

func AddStackCompletion(cmd *cobra.Command) {
cmd.RegisterFlagCompletionFunc("stack", stackFlagCompletion)
}

func ComponentsArgCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
output, err := listComponents(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return output, cobra.ShellCompDirectiveNoFileComp
}
if len(args) > 0 {
flagName := args[len(args)-1]
if strings.HasPrefix(flagName, "--") {
flagName = strings.ReplaceAll(flagName, "--", "")
}
if strings.HasPrefix(toComplete, "--") {
flagName = strings.ReplaceAll(toComplete, "--", "")
}
flagName = strings.ReplaceAll(flagName, "=", "")
if option, ok := cmd.GetFlagCompletionFunc(flagName); ok {
return option(cmd, args, toComplete)
}
}
return nil, cobra.ShellCompDirectiveNoFileComp
}

// Contains checks if a slice of strings contains an exact match for the target string.
func Contains(slice []string, target string) bool {
for _, item := range slice {
Expand Down
Loading

0 comments on commit b6b19b2

Please sign in to comment.