Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 scanning.
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 scanning.
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 scanning.
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 scan 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 scanning stack configs for default identities.
//
// When identityName is empty and atmosConfig is provided:
// - Scans stack configuration files for auth identity defaults
// - Merges stack-level defaults with atmos.yaml defaults
// - Stack defaults have lower priority than atmos.yaml defaults
//
// 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)
}
29 changes: 28 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,29 @@ 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 flag (if --identity flag is added to this command).
identityName := ""
if cmd.Flags().Changed("identity") {
identityName, _ = cmd.Flags().GetString("identity")
}

// Create AuthManager with stack-level default identity scanning.
// This enables stack-level auth.identities.*.default to be recognized.
authManager, err := auth.CreateAndAuthenticateManagerWithAtmosConfig(
identityName,
&atmosConfig.Auth,
cfg.IdentityFlagSelectValue,
&atmosConfig,
)
if err != nil {
return err
}

return list.ExecuteListInstancesCmd(&configAndStacksInfo, cmd, args, authManager)
}
210 changes: 210 additions & 0 deletions docs/fixes/stack-level-default-auth-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Stack-Level Default Auth Identity Not Recognized

## Issue Summary

When a default identity is configured only in stack config (e.g., `stacks/orgs/acme/_defaults`),
the identity is not passed to `describe component` and other commands. Users are prompted to
select an identity even though one is configured as default in their stack configuration.

## Symptoms

```bash
❯ atmos describe component <component> -s <stack>
┃ No default identity configured. Please choose an identity:
┃ > identity-1
┃ identity-2
┃ ...
```

Even when stack config has:
```yaml
# stacks/orgs/acme/_defaults.yaml
auth:
identities:
identity-1:
default: true
```

## Workaround

Setting the default in the profile config works:
```yaml
# profiles/managers/atmos.yaml
auth:
identities:
identity-1:
kind: aws/assume-role
default: true
via:
identity: identity-1/permission-set
principal:
assume_role: arn:aws:iam::123456789012:role/managers
```

## Root Cause Analysis

### Different Root Causes for Different Commands

The issue manifests differently depending on the command type:

#### For `atmos describe component` - Missing Implementation

The `atmos describe component` command was **not following the same pattern** as `atmos terraform *` commands. While terraform commands already implemented Component Auth Merge (Approach 1), `describe component` was simply using global auth config directly:

**Old (broken) code in `cmd/describe_component.go`:**
```go
// Load atmos configuration - processStacks = FALSE
atmosConfig, err := cfg.InitCliConfig(..., false)

// Create AuthManager using ONLY atmos.yaml + profile auth config
// Stack-level defaults were completely ignored!
authManager, err := CreateAuthManagerFromIdentity(identityName, &atmosConfig.Auth)
```

The fix was to update `describe component` to follow the same Approach 1 pattern that `terraform` commands already used.

#### For Multi-Stack Commands - Timing Problem (Chicken-and-Egg)

For commands like `describe stacks`, `describe affected`, and `list instances` that operate on multiple stacks/components, there's a genuine **chicken-and-egg problem**:

1. **`InitCliConfig(processStacks=false)`** only loads:
- System atmos config
- User atmos config (`~/.atmos/atmos.yaml`)
- Project atmos config (`atmos.yaml`)
- Profile configs (e.g., `profiles/managers/atmos.yaml`)

2. **Stack configs are NOT loaded** at this point because:
- Processing stacks may require authentication (for YAML functions)
- Authentication requires knowing the identity
- Identity resolution happens before stack processing

3. These commands cannot use Approach 1 (Component Auth Merge) because they don't have a specific component+stack pair to query upfront.

### Why Profile Config Works

Profile configs are loaded during `InitCliConfig` **before** stacks are processed:
- Profiles are part of the atmos configuration layer
- They're merged into `atmosConfig.Auth` immediately
- The `AuthManager` sees them when checking for defaults

### Why Stack Config Didn't Work

For `describe component`: The command simply wasn't merging stack-level auth config.

For multi-stack commands: Stack configs are only processed when `processStacks=true`, creating a timing issue that required Approach 2 (Stack Scanning) to solve.

## Solution

Two approaches are used depending on whether a specific component+stack pair is available:

### Approach 1: Component Auth Merge (for commands with specific component+stack)

For commands like `describe component` and `terraform *` where both component and stack are known,
we leverage the **existing stack inheritance and merge functionality**:

1. Call `ExecuteDescribeComponent()` with `ProcessTemplates=false, ProcessYamlFunctions=false, AuthManager=nil`
2. The component config output includes the merged auth section from stack inheritance
3. Merge with global auth using `auth.MergeComponentAuthFromConfig()`
4. Create auth manager with the merged config

This approach is preferred because:
- It uses the existing stack merge logic (no duplication)
- The auth section includes all inherited defaults from stack hierarchy
- Component-level auth overrides are respected

**Example flow in `terraform.go` and `describe_component.go`:**
```go
// 1. Start with global auth config
mergedAuthConfig := auth.CopyGlobalAuthConfig(&atmosConfig.Auth)

// 2. Get component config (includes stack-level auth with default flag)
componentConfig, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{
Component: component,
Stack: stack,
ProcessTemplates: false,
ProcessYamlFunctions: false, // Avoid circular dependency
AuthManager: nil, // No auth manager yet
})

// 3. Merge component auth (including stack defaults) with global auth
if err == nil {
mergedAuthConfig, err = auth.MergeComponentAuthFromConfig(...)
}

// 4. Create auth manager with fully merged config
authManager, err := CreateAuthManagerFromIdentity(identityName, mergedAuthConfig)
```

### Approach 2: Stack Scanning (for commands without specific component)

For commands that operate on multiple stacks/components (e.g., `describe stacks`, `describe affected`),
we perform a lightweight pre-scan of stack configurations to extract auth identity defaults:

1. **Stack auth scanner** (`pkg/config/stack_auth_scanner.go`):
- Scans stack manifest files for `auth.identities.*.default: true`
- Uses minimal YAML parsing without template/function processing
- Returns a map of identity names to their default status

2. **Merge into auth config** before creating an auth manager:
- Stack defaults take **precedence** over atmos.yaml defaults
- Follows Atmos inheritance model (more specific config overrides global)

### Precedence Order (lowest to highest priority)

1. Atmos config defaults (`atmos.yaml`)
2. Stack config defaults (scanned or from component merge)
3. CLI flag (`--identity`) / environment variable (`ATMOS_IDENTITY`)

### Key Files Changed

**New Files:**
- `pkg/config/stack_auth_scanner.go` - Scanner for stack-level auth defaults
- `pkg/config/stack_auth_scanner_test.go` - Unit tests for scanner

**Commands Using Component Auth Merge (Approach 1):**
- `cmd/describe_component.go` - Uses terraform.go pattern with `ExecuteDescribeComponent` + `MergeComponentAuthFromConfig`
- `internal/exec/terraform.go` - Original implementation of this pattern

**Commands Using Stack Scanning (Approach 2):**
- `cmd/describe_stacks.go` - Uses `CreateAuthManagerFromIdentityWithAtmosConfig`
- `cmd/describe_affected.go` - Uses `CreateAuthManagerFromIdentityWithAtmosConfig`
- `cmd/describe_dependents.go` - Uses `CreateAuthManagerFromIdentityWithAtmosConfig`
- `cmd/list/instances.go` - Uses `CreateAndAuthenticateManagerWithAtmosConfig`
- `internal/exec/workflow_utils.go` - Scans stack defaults when creating AuthManager

**Updated Internal Execution:**
- `internal/exec/terraform_nested_auth_helper.go` - Resolves AuthManager for nested component references in YAML functions using Approach 1

**Updated Auth Helpers:**
- `pkg/auth/manager_helpers.go` - New `CreateAndAuthenticateManagerWithAtmosConfig` function
- `cmd/identity_flag.go` - New `CreateAuthManagerFromIdentityWithAtmosConfig` wrapper

## Testing

### Unit Tests
- `pkg/config/stack_auth_scanner_test.go` - Scanner logic tests
- `pkg/auth/manager_helpers_test.go` - Integration with auth manager

### Integration Tests
- Test fixture in `tests/fixtures/scenarios/stack-auth-defaults/`
- CLI test verifying stack-level defaults work without prompting

## Commands/Features Now Supporting Stack-Level Auth Defaults

### CLI Commands Using Component Auth Merge (Approach 1)
- `atmos describe component` - Has specific component+stack
- `atmos terraform *` (all terraform subcommands) - Has specific component+stack

### CLI Commands Using Stack Scanning (Approach 2)
- `atmos describe stacks` - Operates on multiple stacks/components
- `atmos describe affected` - Operates on all affected components
- `atmos describe dependents` - Operates on multiple stacks
- `atmos list instances` - Lists all instances across stacks

### YAML Functions
- `!terraform.state` - Inherits AuthManager from parent command context
- `!terraform.output` - Inherits AuthManager from parent command context
- For nested component references, uses Approach 1 (Component Auth Merge) via `resolveAuthManagerForNestedComponent()`

### Workflows
- Workflow execution scans for stack-level defaults when no explicit identity is specified (uses Approach 2)
2 changes: 1 addition & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading