Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,11 @@ commands:
# steps support Go templates
steps:
- atmos terraform plan {{ .Arguments.component }} -s {{ .Flags.stack }}
- name: terraform
description: Execute 'terraform' commands
# Note: Using 'tf-custom' instead of 'terraform' to avoid conflict with the built-in
# terraform command which already has a --stack flag. Custom subcommands cannot
# redefine flags that exist on their parent command.
- name: tf-custom
description: Execute custom 'terraform' commands
# subcommands
commands:
- name: provision
Expand Down
92 changes: 92 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.
// It also includes reserved names like "identity".
// It queries RootCmd at runtime so the set stays current 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,54 @@ 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,
// and detect duplicate flag names/shorthands within the same command config.
seen := make(map[string]bool)
for _, flag := range commandConfig.Flags {
// Detect duplicates within the same command config early.
if seen[flag.Name] {
return errUtils.Build(errUtils.ErrDuplicateFlagRegistration).
WithExplanation(fmt.Sprintf("Custom command '%s' defines duplicate flag '--%s'", commandConfig.Name, flag.Name)).
WithHint("Remove or rename the duplicate flag in your atmos.yaml").
WithContext("command", commandConfig.Name).
WithContext("flag", flag.Name).
Err()
}
seen[flag.Name] = true

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()
}
if flag.Shorthand != "" {
if seen[flag.Shorthand] {
return errUtils.Build(errUtils.ErrDuplicateFlagRegistration).
WithExplanation(fmt.Sprintf("Custom command '%s' defines duplicate flag shorthand '-%s'", commandConfig.Name, flag.Shorthand)).
WithHint("Remove or change the duplicate shorthand in your atmos.yaml").
WithContext("command", commandConfig.Name).
WithContext("shorthand", flag.Shorthand).
Err()
}
seen[flag.Shorthand] = true
}
}

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