Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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