Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/go-git/go-git/v5"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

errUtils "github.com/cloudposse/atmos/errors"
e "github.com/cloudposse/atmos/internal/exec"
Expand Down Expand Up @@ -64,6 +65,49 @@ func WithStackValidation(check bool) AtmosValidateOption {
}
}

// getGlobalFlagNames returns a set of all global persistent flag names and shorthands
// by querying RootCmd.PersistentFlags() at runtime. This ensures we always have the
// current set of reserved flags without maintaining a static list.
func getGlobalFlagNames() map[string]bool {
return getReservedFlagNamesFor(RootCmd)
}

// getReservedFlagNamesFor returns a set of all reserved flag names and shorthands
// for a given parent command. This includes:
// 1. The parent command's own persistent flags
// 2. Flags inherited from ancestor commands (via InheritedFlags)
// 3. The hardcoded "identity" flag that gets added to every custom command
//
// This function is used to validate that child commands don't define flags
// that would conflict with their parent's flags at any level of nesting.
func getReservedFlagNamesFor(parent *cobra.Command) map[string]bool {
reserved := make(map[string]bool)

// Query persistent flags from the parent command.
parent.PersistentFlags().VisitAll(func(f *pflag.Flag) {
reserved[f.Name] = true
if f.Shorthand != "" {
reserved[f.Shorthand] = true
}
})

// Query inherited flags from ancestor commands.
// InheritedFlags() returns flags that are inherited from parent commands
// but not defined on this command itself.
parent.InheritedFlags().VisitAll(func(f *pflag.Flag) {
reserved[f.Name] = true
if f.Shorthand != "" {
reserved[f.Shorthand] = true
}
})

// Also include the hardcoded "identity" flag that gets added to every custom command.
// This prevents user-defined flags from conflicting with it.
reserved["identity"] = true

return reserved
}

// processCustomCommands registers custom commands defined in the Atmos configuration onto the given parent Cobra command.
//
// It reads the provided command definitions, reuses any existing top-level commands when appropriate, and adds new Cobra
Expand Down Expand Up @@ -112,6 +156,30 @@ func processCustomCommands(
customCommand.PersistentFlags().String("identity", "", "Identity to use for authentication (overrides identity in command config)")
AddIdentityCompletion(customCommand)

// Get reserved flag names by querying the parent command's persistent and inherited flags.
// This ensures we detect conflicts with both global flags and parent custom command flags.
reservedFlags := getReservedFlagNamesFor(parentCommand)

// Validate flags don't conflict with global/reserved flags or parent command flags.
for _, flag := range commandConfig.Flags {
if reservedFlags[flag.Name] {
return errUtils.Build(errUtils.ErrReservedFlagName).
WithExplanation(fmt.Sprintf("Custom command '%s' defines flag '--%s' which conflicts with a reserved or parent command flag", commandConfig.Name, flag.Name)).
WithHint("Rename the flag in your atmos.yaml to avoid conflicts with reserved flag names").
WithContext("command", commandConfig.Name).
WithContext("flag", flag.Name).
Err()
}
if flag.Shorthand != "" && reservedFlags[flag.Shorthand] {
return errUtils.Build(errUtils.ErrReservedFlagName).
WithExplanation(fmt.Sprintf("Custom command '%s' defines flag shorthand '-%s' which conflicts with a reserved or parent command flag shorthand", commandConfig.Name, flag.Shorthand)).
WithHint("Change the shorthand in your atmos.yaml to avoid conflicts with reserved flag shorthands").
WithContext("command", commandConfig.Name).
WithContext("shorthand", flag.Shorthand).
Err()
}
}

// Process and add flags to the command.
for _, flag := range commandConfig.Flags {
if flag.Type == "bool" {
Expand Down
Loading
Loading