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
4 changes: 2 additions & 2 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ APACHE 2.0 LICENSED DEPENDENCIES

- github.com/aws/smithy-go
License: Apache-2.0
URL: https://github.com/aws/smithy-go/blob/v1.23.2/LICENSE
URL: https://github.com/aws/smithy-go/blob/v1.24.0/LICENSE

- github.com/cloudposse/atmos
License: Apache-2.0
Expand Down Expand Up @@ -568,7 +568,7 @@ BSD LICENSED DEPENDENCIES

- github.com/aws/smithy-go/internal/sync/singleflight
License: BSD-3-Clause
URL: https://github.com/aws/smithy-go/blob/v1.23.2/internal/sync/singleflight/LICENSE
URL: https://github.com/aws/smithy-go/blob/v1.24.0/internal/sync/singleflight/LICENSE

- github.com/bearsh/hid/hidapi
License: BSD-3-Clause
Expand Down
3 changes: 2 additions & 1 deletion cmd/describe_affected.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ func getRunnableDescribeAffectedCmd(
}

// Get identity from flag and create AuthManager if provided.
// Use the WithAtmosConfig variant to enable stack-level default identity loading.
identityName := GetIdentityFromFlags(cmd, os.Args)
authManager, err := CreateAuthManagerFromIdentity(identityName, &props.CLIConfig.Auth)
authManager, err := CreateAuthManagerFromIdentityWithAtmosConfig(identityName, &props.CLIConfig.Auth, props.CLIConfig)
if err != nil {
return err
}
Expand Down
37 changes: 35 additions & 2 deletions cmd/describe_component.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

errUtils "github.com/cloudposse/atmos/errors"
e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/auth"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
)
Expand Down Expand Up @@ -80,9 +81,41 @@ var describeComponentCmd = &cobra.Command{
return errors.Join(errUtils.ErrFailedToInitConfig, err)
}

// Get identity from flag and create AuthManager if provided.
// Get identity flag value.
identityName := GetIdentityFromFlags(cmd, os.Args)
authManager, err := CreateAuthManagerFromIdentity(identityName, &atmosConfig.Auth)

// Get component-specific auth config and merge with global auth config.
// This follows the same pattern as terraform.go to handle stack-level default identities.
// Start with global config.
mergedAuthConfig := auth.CopyGlobalAuthConfig(&atmosConfig.Auth)

// Get component config to extract auth section (without processing YAML functions to avoid circular dependency).
componentConfig, componentErr := e.ExecuteDescribeComponent(&e.ExecuteDescribeComponentParams{
Component: component,
Stack: stack,
ProcessTemplates: false,
ProcessYamlFunctions: false, // Avoid circular dependency with YAML functions that need auth.
Skip: nil,
AuthManager: nil, // No auth manager yet - we're determining which identity to use.
})
if componentErr != nil {
// If component doesn't exist, exit immediately before attempting authentication.
// This prevents prompting for identity when the component is invalid.
if errors.Is(componentErr, errUtils.ErrInvalidComponent) {
return componentErr
}
// For other errors (e.g., permission issues), continue with global auth config.
} else {
// Merge component-specific auth with global auth.
mergedAuthConfig, err = auth.MergeComponentAuthFromConfig(&atmosConfig.Auth, componentConfig, &atmosConfig, cfg.AuthSectionName)
if err != nil {
return err
}
}

// Create and authenticate AuthManager using merged auth config.
// This enables stack-level default identity to be recognized.
authManager, err := CreateAuthManagerFromIdentity(identityName, mergedAuthConfig)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/describe_dependents.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ func getRunnableDescribeDependentsCmd(
}

// Get identity from flag and create AuthManager if provided.
// Use the WithAtmosConfig variant to enable stack-level default identity loading.
identityName := GetIdentityFromFlags(cmd, os.Args)
authManager, err := CreateAuthManagerFromIdentity(identityName, &atmosConfig.Auth)
authManager, err := CreateAuthManagerFromIdentityWithAtmosConfig(identityName, &atmosConfig.Auth, &atmosConfig)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/describe_stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ func getRunnableDescribeStacksCmd(
}

// Get identity from flag and create AuthManager if provided.
// Use the WithAtmosConfig variant to enable stack-level default identity loading.
identityName := GetIdentityFromFlags(cmd, os.Args)
authManager, err := CreateAuthManagerFromIdentity(identityName, &atmosConfig.Auth)
authManager, err := CreateAuthManagerFromIdentityWithAtmosConfig(identityName, &atmosConfig.Auth, &atmosConfig)
if err != nil {
return err
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/identity_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,32 @@ func extractIdentityFromArgs(args []string) string {
//
// This function delegates to auth.CreateAndAuthenticateManager to ensure consistent
// authentication behavior across CLI commands and internal execution logic.
//
// Note: This function does not load stack configs for default identities.
// Use CreateAuthManagerFromIdentityWithAtmosConfig if you need stack-level default identity resolution.
func CreateAuthManagerFromIdentity(
identityName string,
authConfig *schema.AuthConfig,
) (auth.AuthManager, error) {
return auth.CreateAndAuthenticateManager(identityName, authConfig, IdentityFlagSelectValue)
}

// CreateAuthManagerFromIdentityWithAtmosConfig creates and authenticates an AuthManager from an identity name.
// This version accepts the full atmosConfig to enable loading stack configs for default identities.
//
// When identityName is empty and atmosConfig is provided:
// - Loads stack configuration files for auth identity defaults
// - Applies stack-level defaults on top of atmos.yaml defaults
// - When stack defaults are present, they override atmos.yaml identity defaults (stack > atmos.yaml)
//
// This solves the chicken-and-egg problem where:
// - We need to know the default identity to authenticate
// - But stack configs are only loaded after authentication is configured
// - Stack-level defaults (auth.identities.*.default: true) would otherwise be ignored
func CreateAuthManagerFromIdentityWithAtmosConfig(
identityName string,
authConfig *schema.AuthConfig,
atmosConfig *schema.AtmosConfiguration,
) (auth.AuthManager, error) {
return auth.CreateAndAuthenticateManagerWithAtmosConfig(identityName, authConfig, IdentityFlagSelectValue, atmosConfig)
}
12 changes: 9 additions & 3 deletions cmd/list/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ var componentsCmd = &cobra.Command{
Stack: v.GetString("stack"),
}

output, err := listComponentsWithOptions(opts)
output, err := listComponentsWithOptions(cmd, opts)
if err != nil {
return err
}
Expand Down Expand Up @@ -80,14 +80,20 @@ func init() {
}
}

func listComponentsWithOptions(opts *ComponentsOptions) ([]string, error) {
func listComponentsWithOptions(cmd *cobra.Command, opts *ComponentsOptions) ([]string, error) {
configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true)
if err != nil {
return nil, fmt.Errorf("error initializing CLI config: %v", err)
}

stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil)
// Create AuthManager for authentication support.
authManager, err := createAuthManagerForList(cmd, &atmosConfig)
if err != nil {
return nil, err
}

stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, authManager)
if err != nil {
return nil, fmt.Errorf("error describing stacks: %v", err)
}
Expand Down
25 changes: 24 additions & 1 deletion cmd/list/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"github.com/spf13/viper"

e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/auth"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/flags"
"github.com/cloudposse/atmos/pkg/flags/global"
"github.com/cloudposse/atmos/pkg/list"
"github.com/cloudposse/atmos/pkg/schema"
)

var instancesParser *flags.StandardParser
Expand Down Expand Up @@ -81,5 +84,25 @@ func executeListInstancesCmd(cmd *cobra.Command, args []string, opts *InstancesO
configAndStacksInfo.Command = "list"
configAndStacksInfo.SubCommand = "instances"

return list.ExecuteListInstancesCmd(&configAndStacksInfo, cmd, args)
// Load atmos configuration to get auth config.
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, false)
if err != nil {
return err
}

// Get identity from --identity flag or ATMOS_IDENTITY env var using shared helper.
identityName := getIdentityFromCommand(cmd)

// Create AuthManager with stack-level default identity loading.
authManager, err := auth.CreateAndAuthenticateManagerWithAtmosConfig(
identityName,
&atmosConfig.Auth,
cfg.IdentityFlagSelectValue,
&atmosConfig,
)
if err != nil {
return err
}

return list.ExecuteListInstancesCmd(&configAndStacksInfo, cmd, args, authManager)
}
102 changes: 102 additions & 0 deletions cmd/list/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -168,3 +169,104 @@ func TestInstancesOptions_AllCombinations(t *testing.T) {
})
}
}

// TestInstancesIdentityFlagLogic tests the identity flag/env var logic in instances command.
func TestInstancesIdentityFlagLogic(t *testing.T) {
testCases := []struct {
name string
setupCmd func() *cobra.Command
setupViper func()
expectedIdentity string
}{
{
name: "identity from flag",
setupCmd: func() *cobra.Command {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("identity", "", "Identity flag")
_ = cmd.Flags().Set("identity", "flag-identity")
return cmd
},
setupViper: func() {
viper.Reset()
},
expectedIdentity: "flag-identity",
},
{
name: "identity from viper when flag not changed",
setupCmd: func() *cobra.Command {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("identity", "", "Identity flag")
return cmd
},
setupViper: func() {
viper.Reset()
viper.Set("identity", "env-identity")
},
expectedIdentity: "env-identity",
},
{
name: "empty identity when neither set",
setupCmd: func() *cobra.Command {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("identity", "", "Identity flag")
return cmd
},
setupViper: func() {
viper.Reset()
},
expectedIdentity: "",
},
{
name: "flag takes precedence over viper",
setupCmd: func() *cobra.Command {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("identity", "", "Identity flag")
_ = cmd.Flags().Set("identity", "flag-identity")
return cmd
},
setupViper: func() {
viper.Reset()
viper.Set("identity", "env-identity")
},
expectedIdentity: "flag-identity",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.setupViper()
cmd := tc.setupCmd()

// Simulate the logic from executeListInstancesCmd.
identityName := ""
if cmd.Flags().Changed("identity") {
identityName, _ = cmd.Flags().GetString("identity")
} else if envIdentity := viper.GetString("identity"); envIdentity != "" {
identityName = envIdentity
}

assert.Equal(t, tc.expectedIdentity, identityName)
})
}
}

// TestInstancesParserInit tests that the instances parser is properly initialized.
func TestInstancesParserInit(t *testing.T) {
assert.NotNil(t, instancesParser, "instancesParser should be initialized")

// Verify instancesCmd exists and has the correct Use field.
assert.Equal(t, "instances", instancesCmd.Use)

// The upload flag should be registered - it could be on Flags() or PersistentFlags().
// Check both since the parser might use either.
uploadFlag := instancesCmd.Flags().Lookup("upload")
if uploadFlag == nil {
uploadFlag = instancesCmd.PersistentFlags().Lookup("upload")
}

if uploadFlag != nil {
assert.Equal(t, "false", uploadFlag.DefValue, "upload flag default should be false")
}
// Note: If the flag is not found, that's not necessarily an error - it may be registered
// lazily or through a different mechanism. The important test is that the parser exists.
}
12 changes: 12 additions & 0 deletions cmd/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ var listCmd = &cobra.Command{
}

func init() {
// Add --identity flag to all list commands to enable authentication
// when processing YAML template functions (!terraform.state, !terraform.output).
// This follows the same pattern as the describe commands.
//
// NOTE: NoOptDefVal is NOT used here to avoid Cobra parsing issues with commands
// that have positional arguments. When NoOptDefVal is set and a space-separated value
// is used (--identity value), Cobra misinterprets the value as a subcommand/positional arg.
//
// The ATMOS_IDENTITY environment variable binding is handled centrally by the global
// flag registry in pkg/flags/global_builder.go, so no explicit viper.BindEnv is needed here.
listCmd.PersistentFlags().StringP("identity", "i", "", "Specify the identity to authenticate with")

// Attach all subcommands
listCmd.AddCommand(componentsCmd)
listCmd.AddCommand(stacksCmd)
Expand Down
Loading
Loading