Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4fe2e86
Fix YQ defaults for terraform.state and terraform.output functions
osterman Dec 4, 2025
942cb89
docs: Add blog post for YQ defaults YAML function fix
osterman Dec 4, 2025
6e36aeb
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 4, 2025
09d09be
Add warning logs when terraform state retries are exhausted
osterman Dec 4, 2025
315ef11
Use exponential backoff for S3 and GCS terraform state retries
osterman Dec 4, 2025
a1fa373
refactor: Use atmos logger wrapper and fix linter warnings
osterman Dec 4, 2025
34f4d11
refactor: Use atmos logger types instead of charmbracelet/log directly
osterman Dec 4, 2025
9eb3533
refactor: Add centralized log field constants
osterman Dec 4, 2025
440afce
fix: Mock terraform state getter in nested auth tests
osterman Dec 4, 2025
14977d9
fix: Restore auth sections in test fixture for propagation tests
osterman Dec 4, 2025
d037300
style: Fix godot linter and improve error handling consistency
osterman Dec 4, 2025
20e0c2b
test: Fix YQ defaults test to use proper error types
osterman Dec 4, 2025
2c7d644
Merge branch 'main' into osterman/yq-defaults-bug-tests
osterman Dec 5, 2025
e134f55
Merge branch 'main' into osterman/yq-defaults-bug-tests
osterman Dec 6, 2025
145e57c
Merge branch 'main' into osterman/yq-defaults-bug-tests
aknysh Dec 9, 2025
8880598
Merge branch 'main' into osterman/yq-defaults-bug-tests
aknysh Dec 9, 2025
d9a52bd
Merge branch 'main' into osterman/yq-defaults-bug-tests
osterman Dec 9, 2025
5dd035d
Merge remote-tracking branch 'origin/osterman/yq-defaults-bug-tests' …
aknysh Dec 9, 2025
9cff0e9
address comments, fix/update docs, add tests
aknysh Dec 9, 2025
8341a33
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 9, 2025
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 @@ -99,7 +99,7 @@ APACHE 2.0 LICENSED DEPENDENCIES

- github.com/aws/aws-sdk-go-v2
License: Apache-2.0
URL: https://github.com/aws/aws-sdk-go-v2/blob/v1.40.1/LICENSE.txt
URL: https://github.com/aws/aws-sdk-go-v2/blob/v1.41.0/LICENSE.txt

- github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream
License: Apache-2.0
Expand Down Expand Up @@ -560,7 +560,7 @@ BSD LICENSED DEPENDENCIES

- github.com/aws/aws-sdk-go-v2/internal/sync/singleflight
License: BSD-3-Clause
URL: https://github.com/aws/aws-sdk-go-v2/blob/v1.40.1/internal/sync/singleflight/LICENSE
URL: https://github.com/aws/aws-sdk-go-v2/blob/v1.41.0/internal/sync/singleflight/LICENSE

- github.com/aws/aws-sdk-go/internal/sync/singleflight
License: BSD-3-Clause
Expand Down
29 changes: 19 additions & 10 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,25 @@ var (
ErrDescribeComponent = errors.New("failed to describe component")
ErrReadTerraformState = errors.New("failed to read Terraform state")
ErrEvaluateTerraformBackendVariable = errors.New("failed to evaluate terraform backend variable")
ErrUnsupportedBackendType = errors.New("unsupported backend type")
ErrProcessTerraformStateFile = errors.New("error processing terraform state file")
ErrLoadAwsConfig = errors.New("failed to load AWS config")
ErrGetObjectFromS3 = errors.New("failed to get object from S3")
ErrReadS3ObjectBody = errors.New("failed to read S3 object body")
ErrCreateGCSClient = errors.New("failed to create GCS client")
ErrGetObjectFromGCS = errors.New("failed to get object from GCS")
ErrReadGCSObjectBody = errors.New("failed to read GCS object body")
ErrGCSBucketRequired = errors.New("bucket is required for gcs backend")
ErrInvalidBackendConfig = errors.New("invalid backend configuration")

// Recoverable YAML function errors - use YQ default if available.
// These errors indicate the data is not available but do not represent API failures.
ErrTerraformStateNotProvisioned = errors.New("terraform state not provisioned")
ErrTerraformOutputNotFound = errors.New("terraform output not found")

// API/infrastructure errors - should cause non-zero exit.
// These errors indicate backend API failures that should not use YQ defaults.
ErrTerraformBackendAPIError = errors.New("terraform backend API error")
ErrUnsupportedBackendType = errors.New("unsupported backend type")
ErrProcessTerraformStateFile = errors.New("error processing terraform state file")
ErrLoadAwsConfig = errors.New("failed to load AWS config")
ErrGetObjectFromS3 = errors.New("failed to get object from S3")
ErrReadS3ObjectBody = errors.New("failed to read S3 object body")
ErrCreateGCSClient = errors.New("failed to create GCS client")
ErrGetObjectFromGCS = errors.New("failed to get object from GCS")
ErrReadGCSObjectBody = errors.New("failed to read GCS object body")
ErrGCSBucketRequired = errors.New("bucket is required for gcs backend")
ErrInvalidBackendConfig = errors.New("invalid backend configuration")

// Azure Blob Storage specific errors.
ErrGetBlobFromAzure = errors.New("failed to get blob from Azure Blob Storage")
Expand Down
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.

4 changes: 2 additions & 2 deletions go.sum

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

117 changes: 110 additions & 7 deletions internal/exec/describe_component_nested_authmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,94 @@ import (
"github.com/cloudposse/atmos/pkg/schema"
)

// setupMockStateGetter configures the global stateGetter to return mock values
// for terraform state lookups. This allows tests to run without actual terraform
// state files. Returns a cleanup function that must be deferred to restore the
// original state getter.
func setupMockStateGetter(t *testing.T, ctrl *gomock.Controller) func() {
t.Helper()

mockStateGetter := NewMockTerraformStateGetter(ctrl)
originalGetter := stateGetter

// Configure mock to return values for all components in the fixture.

// Level 3 components have no dependencies.
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "level3-component", "subnet_id", gomock.Any(), gomock.Any(), gomock.Any()).
Return("subnet-level3-12345", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "level3-component", "cidr_block", gomock.Any(), gomock.Any(), gomock.Any()).
Return("10.0.3.0/24", nil).
AnyTimes()

// Level 2 components depend on level 3.
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "level2-component", "vpc_id", gomock.Any(), gomock.Any(), gomock.Any()).
Return("vpc-level2-67890", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "level2-component", "level3_subnet_id", gomock.Any(), gomock.Any(), gomock.Any()).
Return("subnet-level3-12345", nil).
AnyTimes()

// Auth override scenario components test middle-level auth override.
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "auth-override-level3", "database_host", gomock.Any(), gomock.Any(), gomock.Any()).
Return("db.example.com", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "auth-override-level2", "service_name", gomock.Any(), gomock.Any(), gomock.Any()).
Return("api-service", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "auth-override-level2", "database_config", gomock.Any(), gomock.Any(), gomock.Any()).
Return("db.example.com", nil).
AnyTimes()

// Multi-auth scenario components test multiple auth overrides in chain.
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "multi-auth-level3", "shared_resource_id", gomock.Any(), gomock.Any(), gomock.Any()).
Return("shared-12345", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "multi-auth-level2", "vpc_id", gomock.Any(), gomock.Any(), gomock.Any()).
Return("vpc-account-b", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "multi-auth-level2", "shared_resource", gomock.Any(), gomock.Any(), gomock.Any()).
Return("shared-12345", nil).
AnyTimes()

// Mixed inheritance scenario components test selective auth override.
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "mixed-inherit-component", "config_value", gomock.Any(), gomock.Any(), gomock.Any()).
Return("inherited-auth-config", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "mixed-override-component", "override_value", gomock.Any(), gomock.Any(), gomock.Any()).
Return("override-specific-value", nil).
AnyTimes()

// Deep nesting scenario components test 4-level deep auth override.
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "deep-level4", "data_source", gomock.Any(), gomock.Any(), gomock.Any()).
Return("primary-db", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "deep-level3", "data_ref", gomock.Any(), gomock.Any(), gomock.Any()).
Return("primary-db", nil).
AnyTimes()
mockStateGetter.EXPECT().
GetState(gomock.Any(), gomock.Any(), "test", "deep-level2", "nested_data", gomock.Any(), gomock.Any(), gomock.Any()).
Return("primary-db", nil).
AnyTimes()

stateGetter = mockStateGetter
return func() { stateGetter = originalGetter }
}

// TestNestedAuthManagerPropagation verifies that AuthManager propagates correctly
// through multiple levels of nested !terraform.state YAML functions.
//
Expand All @@ -34,6 +122,10 @@ func TestNestedAuthManagerPropagation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Setup mock state getter to return expected values without actual terraform state.
cleanup := setupMockStateGetter(t, ctrl)
defer cleanup()

// Setup: Create AuthContext with AWS credentials.
expectedAuthContext := &schema.AuthContext{
AWS: &schema.AWSAuthContext{
Expand Down Expand Up @@ -94,6 +186,10 @@ func TestNestedAuthManagerPropagationLevel2(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Setup mock state getter to return expected values without actual terraform state.
cleanup := setupMockStateGetter(t, ctrl)
defer cleanup()

expectedAuthContext := &schema.AuthContext{
AWS: &schema.AWSAuthContext{
Profile: "test-level2-identity",
Expand Down Expand Up @@ -293,12 +389,15 @@ func TestNestedAuthManagerWithNilAuthContext(t *testing.T) {
}

// setupNestedAuthTest creates a mock AuthManager and sets up the test directory.
// Returns the mock controller and AuthManager for use in nested auth tests.
func setupNestedAuthTest(t *testing.T, profile, region string) (*gomock.Controller, types.AuthManager) {
// Returns the mock controller, AuthManager, and cleanup function for use in nested auth tests.
func setupNestedAuthTest(t *testing.T, profile, region string) (*gomock.Controller, types.AuthManager, func()) {
t.Helper()

ctrl := gomock.NewController(t)

// Setup mock state getter to return expected values without actual terraform state.
stateCleanup := setupMockStateGetter(t, ctrl)

parentAuthContext := &schema.AuthContext{
AWS: &schema.AWSAuthContext{
Profile: profile,
Expand All @@ -320,7 +419,7 @@ func setupNestedAuthTest(t *testing.T, profile, region string) (*gomock.Controll
workDir := "../../tests/fixtures/scenarios/authmanager-nested-propagation"
t.Chdir(workDir)

return ctrl, mockAuthManager
return ctrl, mockAuthManager, stateCleanup
}

// TestNestedAuthManagerScenario2_AuthOverrideAtMiddleLevel verifies that
Expand All @@ -340,8 +439,9 @@ func setupNestedAuthTest(t *testing.T, profile, region string) (*gomock.Controll
// - Level 3 inherits Level 2's AuthManager
// - No IMDS timeout errors at any level.
func TestNestedAuthManagerScenario2_AuthOverrideAtMiddleLevel(t *testing.T) {
ctrl, mockAuthManager := setupNestedAuthTest(t, "parent-profile", "us-east-1")
ctrl, mockAuthManager, stateCleanup := setupNestedAuthTest(t, "parent-profile", "us-east-1")
defer ctrl.Finish()
defer stateCleanup()

componentSection, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{
Component: "auth-override-level1",
Expand Down Expand Up @@ -378,8 +478,9 @@ func TestNestedAuthManagerScenario2_AuthOverrideAtMiddleLevel(t *testing.T) {
// - Level 3 uses account 333333333333 (inherited from Level 2)
// - Each level with auth config creates its own AuthManager.
func TestNestedAuthManagerScenario3_MultipleAuthOverrides(t *testing.T) {
ctrl, mockAuthManager := setupNestedAuthTest(t, "global-profile", "us-east-1")
ctrl, mockAuthManager, stateCleanup := setupNestedAuthTest(t, "global-profile", "us-east-1")
defer ctrl.Finish()
defer stateCleanup()

componentSection, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{
Component: "multi-auth-level1",
Expand Down Expand Up @@ -415,8 +516,9 @@ func TestNestedAuthManagerScenario3_MultipleAuthOverrides(t *testing.T) {
// - mixed-override-component uses account 555555555555 (its own auth)
// - When mixed-override calls mixed-inherit, it inherits account 555555555555.
func TestNestedAuthManagerScenario4_MixedInheritance(t *testing.T) {
ctrl, mockAuthManager := setupNestedAuthTest(t, "parent-profile", "us-west-2")
ctrl, mockAuthManager, stateCleanup := setupNestedAuthTest(t, "parent-profile", "us-west-2")
defer ctrl.Finish()
defer stateCleanup()

componentSection, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{
Component: "mixed-top-level",
Expand Down Expand Up @@ -455,8 +557,9 @@ func TestNestedAuthManagerScenario4_MixedInheritance(t *testing.T) {
// - Level 3 switches to account 666666666666 (Level3Access) - its own override
// - Level 4 inherits account 666666666666 from Level 3.
func TestNestedAuthManagerScenario5_DeepNesting(t *testing.T) {
ctrl, mockAuthManager := setupNestedAuthTest(t, "global-profile", "us-east-1")
ctrl, mockAuthManager, stateCleanup := setupNestedAuthTest(t, "global-profile", "us-east-1")
defer ctrl.Finish()
defer stateCleanup()

componentSection, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{
Component: "deep-level1",
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/spinner_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
log "github.com/charmbracelet/log"

"github.com/cloudposse/atmos/internal/tui/templates/term"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/terminal"
"github.com/cloudposse/atmos/pkg/ui/theme"
)
Expand Down
5 changes: 3 additions & 2 deletions internal/exec/terraform_state_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ func GetTerraformState(
// Cache the result.
terraformStateCache.Store(stackSlug, backend)

// If `backend` is `nil`, return `nil` (the component in the stack has not been provisioned yet).
// If `backend` is `nil`, return a recoverable error (the component in the stack has not been provisioned yet).
// This allows callers to use YQ defaults if available.
if backend == nil {
return nil, nil
return nil, fmt.Errorf("%w for component `%s` in stack `%s`", errUtils.ErrTerraformStateNotProvisioned, component, stack)
}

// Get the output.
Expand Down
24 changes: 16 additions & 8 deletions internal/exec/yaml_func_aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,31 +444,35 @@ func TestProcessSimpleTagsWithAWSFunctions(t *testing.T) {
stackInfo := &schema.ConfigAndStacksInfo{}

// Test !aws.account_id through processSimpleTags.
result, handled := processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsAccountID, "", nil, stackInfo)
result, handled, err := processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsAccountID, "", nil, stackInfo)
assert.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "333333333333", result)

// Clear cache for next test.
ClearAWSIdentityCache()

// Test !aws.caller_identity_arn through processSimpleTags.
result, handled = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityArn, "", nil, stackInfo)
result, handled, err = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityArn, "", nil, stackInfo)
assert.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "arn:aws:iam::333333333333:user/integration-test", result)

// Clear cache for next test.
ClearAWSIdentityCache()

// Test !aws.caller_identity_user_id through processSimpleTags.
result, handled = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityUserID, "", nil, stackInfo)
result, handled, err = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityUserID, "", nil, stackInfo)
assert.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "AIDAINTEGRATION", result)

// Clear cache for next test.
ClearAWSIdentityCache()

// Test !aws.region through processSimpleTags.
result, handled = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsRegion, "", nil, stackInfo)
result, handled, err = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsRegion, "", nil, stackInfo)
assert.NoError(t, err)
assert.True(t, handled)
assert.Equal(t, "us-west-2", result)
}
Expand All @@ -479,25 +483,29 @@ func TestProcessSimpleTagsSkipsAWSFunctions(t *testing.T) {

// Test that skipping works for aws.account_id.
skip := []string{"aws.account_id"}
result, handled := processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsAccountID, "", skip, stackInfo)
result, handled, err := processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsAccountID, "", skip, stackInfo)
assert.NoError(t, err)
assert.False(t, handled)
assert.Nil(t, result)

// Test that skipping works for aws.caller_identity_arn.
skip = []string{"aws.caller_identity_arn"}
result, handled = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityArn, "", skip, stackInfo)
result, handled, err = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityArn, "", skip, stackInfo)
assert.NoError(t, err)
assert.False(t, handled)
assert.Nil(t, result)

// Test that skipping works for aws.caller_identity_user_id.
skip = []string{"aws.caller_identity_user_id"}
result, handled = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityUserID, "", skip, stackInfo)
result, handled, err = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsCallerIdentityUserID, "", skip, stackInfo)
assert.NoError(t, err)
assert.False(t, handled)
assert.Nil(t, result)

// Test that skipping works for aws.region.
skip = []string{"aws.region"}
result, handled = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsRegion, "", skip, stackInfo)
result, handled, err = processSimpleTags(atmosConfig, u.AtmosYamlFuncAwsRegion, "", skip, stackInfo)
assert.NoError(t, err)
assert.False(t, handled)
assert.Nil(t, result)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/yaml_func_resolution_context_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func BenchmarkProcessCustomYamlTagsOverhead(b *testing.B) {
b.Run("WithoutCycleDetection", func(b *testing.B) {
// Simulate old behavior (direct processing without context).
for i := 0; i < b.N; i++ {
result := processNodes(atmosConfig, input, "test-stack", nil, nil)
result, _ := processNodes(atmosConfig, input, "test-stack", nil, nil)
_ = result
}
})
Expand Down
Loading
Loading