diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 872aa212d..cb849d1a2 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -1,59 +1,97 @@ package list import ( + "errors" "fmt" "sort" "github.com/samber/lo" ) -// getStackComponents extracts Terraform components from the final map of stacks +// Error definitions for component listing. +var ( + // ErrParseStacks is returned when stack data cannot be parsed. + ErrParseStacks = errors.New("could not parse stacks") + // ErrParseComponents is returned when component data cannot be parsed. + ErrParseComponents = errors.New("could not parse components") + // ErrParseTerraformComponents is returned when terraform component data cannot be parsed. + ErrParseTerraformComponents = errors.New("could not parse Terraform components") + // ErrStackNotFound is returned when a requested stack is not found. + ErrStackNotFound = errors.New("stack not found") + // ErrProcessStack is returned when there's an error processing a stack. + ErrProcessStack = errors.New("error processing stack") +) + +// getStackComponents extracts Terraform components from the final map of stacks. func getStackComponents(stackData any) ([]string, error) { stackMap, ok := stackData.(map[string]any) if !ok { - return nil, fmt.Errorf("could not parse stacks") + return nil, ErrParseStacks } componentsMap, ok := stackMap["components"].(map[string]any) if !ok { - return nil, fmt.Errorf("could not parse components") + return nil, ErrParseComponents } terraformComponents, ok := componentsMap["terraform"].(map[string]any) if !ok { - return nil, fmt.Errorf("could not parse Terraform components") + return nil, ErrParseTerraformComponents } return lo.Keys(terraformComponents), nil } -// FilterAndListComponents filters and lists components based on the given stack +// getComponentsForSpecificStack extracts components from a specific stack. +func getComponentsForSpecificStack(stackName string, stacksMap map[string]any) ([]string, error) { + // Verify stack exists. + stackData, ok := stacksMap[stackName] + if !ok { + return nil, fmt.Errorf("%w: %s", ErrStackNotFound, stackName) + } + + // Get components for the specific stack. + stackComponents, err := getStackComponents(stackData) + if err != nil { + return nil, fmt.Errorf("%w: %s: %w", ErrProcessStack, stackName, err) + } + + return stackComponents, nil +} + +// processAllStacks collects components from all valid stacks. +func processAllStacks(stacksMap map[string]any) []string { + var components []string + for _, stackData := range stacksMap { + stackComponents, err := getStackComponents(stackData) + if err != nil { + continue // Skip invalid stacks. + } + components = append(components, stackComponents...) + } + return components +} + +// FilterAndListComponents filters and lists components based on the given stack. func FilterAndListComponents(stackFlag string, stacksMap map[string]any) ([]string, error) { - components := []string{} + var components []string + if stacksMap == nil { + return nil, fmt.Errorf("%w: %s", ErrStackNotFound, stackFlag) + } + // Handle specific stack case. if stackFlag != "" { - // Filter components for the specified stack - if stackData, ok := stacksMap[stackFlag]; ok { - stackComponents, err := getStackComponents(stackData) - if err != nil { - return nil, fmt.Errorf("error processing stack '%s': %w", stackFlag, err) - } - components = append(components, stackComponents...) - } else { - return nil, fmt.Errorf("stack '%s' not found", stackFlag) + stackComponents, err := getComponentsForSpecificStack(stackFlag, stacksMap) + if err != nil { + return nil, err } + components = stackComponents } else { - // Get all components from all stacks - for _, stackData := range stacksMap { - stackComponents, err := getStackComponents(stackData) - if err != nil { - continue // Skip invalid stacks - } - components = append(components, stackComponents...) - } + // Process all stacks. + components = processAllStacks(stacksMap) } - // Remove duplicates and sort components + // Remove duplicates and sort components. components = lo.Uniq(components) sort.Strings(components) diff --git a/pkg/list/list_components_test.go b/pkg/list/list_components_test.go index 6cbfb2500..a9bfef6fd 100644 --- a/pkg/list/list_components_test.go +++ b/pkg/list/list_components_test.go @@ -1,9 +1,11 @@ package list import ( + "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" e "github.com/cloudposse/atmos/internal/exec" cfg "github.com/cloudposse/atmos/pkg/config" @@ -19,16 +21,17 @@ func TestListComponents(t *testing.T) { configAndStacksInfo := schema.ConfigAndStacksInfo{} atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) - assert.Nil(t, err) + require.NoError(t, err) stacksMap, err := e.ExecuteDescribeStacks(atmosConfig, "", nil, nil, nil, false, true, true, false, nil) assert.Nil(t, err) output, err := FilterAndListComponents("", stacksMap) - assert.Nil(t, err) + require.NoError(t, err) dependentsYaml, err := u.ConvertToYAML(output) - assert.Nil(t, err) + require.NoError(t, err) + // Add assertions to validate the output structure assert.NotNil(t, dependentsYaml) assert.Greater(t, len(dependentsYaml), 0) @@ -39,15 +42,248 @@ func TestListComponentsWithStack(t *testing.T) { configAndStacksInfo := schema.ConfigAndStacksInfo{} atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) - assert.Nil(t, err) + require.NoError(t, err) stacksMap, err := e.ExecuteDescribeStacks(atmosConfig, testStack, nil, nil, nil, false, true, true, false, nil) assert.Nil(t, err) output, err := FilterAndListComponents(testStack, stacksMap) - assert.Nil(t, err) + require.NoError(t, err) assert.NotNil(t, output) assert.Greater(t, len(output), 0) - assert.ObjectsAreEqualValues([]string{"infra/vpc", "mixin/test-1", "mixin/test-2", "test/test-component", "test/test-component-override", "test/test-component-override-2", "test/test-component-override-3", "top-level-component1", "vpc", "vpc/new"}, output) + assert.ElementsMatch(t, []string{ + "infra/vpc", "mixin/test-1", "mixin/test-2", "test/test-component", + "test/test-component-override", "test/test-component-override-2", "test/test-component-override-3", + "top-level-component1", "vpc", "vpc/new", + }, output) +} + +// TestGetStackComponents tests the getStackComponents function. +func TestGetStackComponents(t *testing.T) { + // Test successful case + stackData := map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "infra/vpc": map[string]any{}, + }, + }, + } + + components, err := getStackComponents(stackData) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"vpc", "infra/vpc"}, components) + + // Test error cases + t.Run("invalid stack data", func(t *testing.T) { + _, err := getStackComponents("not a map") + require.Error(t, err) + assert.True(t, errors.Is(err, ErrParseStacks)) + }) + + t.Run("missing components", func(t *testing.T) { + _, err := getStackComponents(map[string]any{ + "not-components": map[string]any{}, + }) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrParseComponents)) + }) + + t.Run("missing terraform components", func(t *testing.T) { + _, err := getStackComponents(map[string]any{ + "components": map[string]any{ + "not-terraform": map[string]any{}, + }, + }) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrParseTerraformComponents)) + }) +} + +// TestGetComponentsForSpecificStack tests the getComponentsForSpecificStack function. +func TestGetComponentsForSpecificStack(t *testing.T) { + stacksMap := map[string]any{ + "stack1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "infra/vpc": map[string]any{}, + }, + }, + }, + "stack2": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "eks": map[string]any{}, + "infra/eks": map[string]any{}, + }, + }, + }, + } + + // Test successful case + t.Run("existing stack", func(t *testing.T) { + components, err := getComponentsForSpecificStack("stack1", stacksMap) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"vpc", "infra/vpc"}, components) + }) + + // Test error cases + t.Run("non-existent stack", func(t *testing.T) { + _, err := getComponentsForSpecificStack("non-existent-stack", stacksMap) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrStackNotFound)) + assert.Contains(t, err.Error(), "non-existent-stack") + }) + + t.Run("invalid stack structure", func(t *testing.T) { + invalidStacksMap := map[string]any{ + "invalid-stack": "not a map", + } + _, err := getComponentsForSpecificStack("invalid-stack", invalidStacksMap) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrProcessStack)) + assert.Contains(t, err.Error(), "invalid-stack") + }) +} + +// TestProcessAllStacks tests the processAllStacks function. +func TestProcessAllStacks(t *testing.T) { + // Test with valid stacks + t.Run("valid stacks", func(t *testing.T) { + stacksMap := map[string]any{ + "stack1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + "stack2": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "eks": map[string]any{}, + }, + }, + }, + } + + components := processAllStacks(stacksMap) + assert.ElementsMatch(t, []string{"vpc", "eks"}, components) + }) + + // Test with mix of valid and invalid stacks + t.Run("mixed valid and invalid stacks", func(t *testing.T) { + stacksMap := map[string]any{ + "valid-stack": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + "invalid-stack": "not a map", + "empty-stack": map[string]any{ + "components": map[string]any{}, + }, + } + + components := processAllStacks(stacksMap) + assert.ElementsMatch(t, []string{"vpc"}, components) + }) + + // Test with all invalid stacks + t.Run("all invalid stacks", func(t *testing.T) { + stacksMap := map[string]any{ + "invalid1": "not a map", + "invalid2": map[string]any{ + "not-components": map[string]any{}, + }, + } + + components := processAllStacks(stacksMap) + assert.Empty(t, components) + }) +} + +// TestFilterAndListComponents tests the FilterAndListComponents function. +func TestFilterAndListComponents(t *testing.T) { + stacksMap := map[string]any{ + "stack1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "eks": map[string]any{}, + "rds": map[string]any{}, + "s3": map[string]any{}, + "test": map[string]any{}, + }, + }, + }, + "stack2": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "eks": map[string]any{}, + "elasticache": map[string]any{}, + }, + }, + }, + } + + // Test specific stack case + t.Run("specific stack", func(t *testing.T) { + components, err := FilterAndListComponents("stack1", stacksMap) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"vpc", "eks", "rds", "s3", "test"}, components) + }) + + // Test all stacks case + t.Run("all stacks", func(t *testing.T) { + components, err := FilterAndListComponents("", stacksMap) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"vpc", "eks", "rds", "s3", "test", "elasticache"}, components) + }) + + // Test error cases + t.Run("non-existent stack", func(t *testing.T) { + _, err := FilterAndListComponents("non-existent-stack", stacksMap) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrStackNotFound)) + }) + + // Test with nil stacks map + t.Run("nil stacks map", func(t *testing.T) { + _, err := FilterAndListComponents("stack1", nil) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrStackNotFound)) + }) + + // Test with empty stacks map + t.Run("empty stacks map", func(t *testing.T) { + _, err := FilterAndListComponents("stack1", map[string]any{}) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrStackNotFound)) + }) +} + +// TestFilterAndListComponentsIntegration performs integration tests with actual stack data. +func TestFilterAndListComponentsIntegration(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + require.NoError(t, err) + + // Test with invalid stack name + t.Run("invalid stack name", func(t *testing.T) { + stacksMap, err := e.ExecuteDescribeStacks(atmosConfig, "", nil, nil, + nil, false, false, false, false, nil) + require.NoError(t, err) + + _, err = FilterAndListComponents("non-existent-stack", stacksMap) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrStackNotFound)) + assert.Contains(t, err.Error(), "non-existent-stack") + }) }