From b1e142e18507b44ab65b765d3ab9938866035907 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Fri, 16 Jan 2026 09:23:07 -0600 Subject: [PATCH 1/6] fix: Filter out abstract and disabled components from interactive menus and shell completion When running 'atmos terraform plan -s stack-name' without specifying a component, the interactive "Choose a component" menu now correctly filters out: - Abstract components (metadata.type: abstract) that cannot be deployed - Disabled components (metadata.enabled: false) that cannot be deployed - Components not in the specified stack when --stack is provided This filtering also applies to shell tab completion. Added helper functions isComponentDeployable() and filterDeployableComponents() to centralize the filtering logic. Updated PromptForComponent() to accept a stack parameter for stack-scoped filtering. All callers updated to pass the stack parameter. Co-Authored-By: Claude Haiku 4.5 --- cmd/terraform/clean.go | 2 +- cmd/terraform/generate/backend.go | 9 +- cmd/terraform/generate/planfile.go | 9 +- cmd/terraform/generate/varfile.go | 9 +- cmd/terraform/shared/prompt.go | 149 ++++++++++++- cmd/terraform/shared/prompt_test.go | 200 ++++++++++++++++++ cmd/terraform/shell.go | 9 +- cmd/terraform/utils.go | 8 +- ...26-01-16-component-selection-filtering.mdx | 30 +++ 9 files changed, 399 insertions(+), 26 deletions(-) create mode 100644 website/blog/2026-01-16-component-selection-filtering.mdx diff --git a/cmd/terraform/clean.go b/cmd/terraform/clean.go index 10eb5a0dfd..c3634db4ba 100644 --- a/cmd/terraform/clean.go +++ b/cmd/terraform/clean.go @@ -57,7 +57,7 @@ Common use cases: // Prompt for component/stack if neither is provided. if component == "" && stack == "" { - prompted, err := promptForComponent(cmd) + prompted, err := promptForComponent(cmd, stack) // stack is empty here. if err == nil && prompted != "" { component = prompted } diff --git a/cmd/terraform/generate/backend.go b/cmd/terraform/generate/backend.go index c1545abbd5..f001785ced 100644 --- a/cmd/terraform/generate/backend.go +++ b/cmd/terraform/generate/backend.go @@ -36,9 +36,13 @@ var backendCmd = &cobra.Command{ return err } + // Get stack early so we can use it to filter component selection. + stack := v.GetString("stack") + // Prompt for component if missing. + // If stack is already provided (via --stack flag), filter components to that stack. if component == "" { - prompted, err := shared.PromptForComponent(cmd) + prompted, err := shared.PromptForComponent(cmd, stack) if err = shared.HandlePromptError(err, "component"); err != nil { return err } @@ -50,8 +54,7 @@ var backendCmd = &cobra.Command{ return errUtils.ErrMissingComponent } - // Get flag values from Viper. - stack := v.GetString("stack") + // Get remaining flag values from Viper. processTemplates := v.GetBool("process-templates") processFunctions := v.GetBool("process-functions") skip := v.GetStringSlice("skip") diff --git a/cmd/terraform/generate/planfile.go b/cmd/terraform/generate/planfile.go index 51364ffa5b..8ffd4d2d34 100644 --- a/cmd/terraform/generate/planfile.go +++ b/cmd/terraform/generate/planfile.go @@ -36,9 +36,13 @@ var planfileCmd = &cobra.Command{ return err } + // Get stack early so we can use it to filter component selection. + stack := v.GetString("stack") + // Prompt for component if missing. + // If stack is already provided (via --stack flag), filter components to that stack. if component == "" { - prompted, err := shared.PromptForComponent(cmd) + prompted, err := shared.PromptForComponent(cmd, stack) if err = shared.HandlePromptError(err, "component"); err != nil { return err } @@ -50,8 +54,7 @@ var planfileCmd = &cobra.Command{ return errUtils.ErrMissingComponent } - // Get flag values from Viper. - stack := v.GetString("stack") + // Get remaining flag values from Viper. file := v.GetString("file") format := v.GetString("format") processTemplates := v.GetBool("process-templates") diff --git a/cmd/terraform/generate/varfile.go b/cmd/terraform/generate/varfile.go index 3e13df88e7..26b88c1cad 100644 --- a/cmd/terraform/generate/varfile.go +++ b/cmd/terraform/generate/varfile.go @@ -36,9 +36,13 @@ var varfileCmd = &cobra.Command{ return err } + // Get stack early so we can use it to filter component selection. + stack := v.GetString("stack") + // Prompt for component if missing. + // If stack is already provided (via --stack flag), filter components to that stack. if component == "" { - prompted, err := shared.PromptForComponent(cmd) + prompted, err := shared.PromptForComponent(cmd, stack) if err = shared.HandlePromptError(err, "component"); err != nil { return err } @@ -50,8 +54,7 @@ var varfileCmd = &cobra.Command{ return errUtils.ErrMissingComponent } - // Get flag values from Viper. - stack := v.GetString("stack") + // Get remaining flag values from Viper. file := v.GetString("file") processTemplates := v.GetBool("process-templates") processFunctions := v.GetBool("process-functions") diff --git a/cmd/terraform/shared/prompt.go b/cmd/terraform/shared/prompt.go index 3d3866d30f..ec7251f226 100644 --- a/cmd/terraform/shared/prompt.go +++ b/cmd/terraform/shared/prompt.go @@ -16,11 +16,17 @@ import ( ) // PromptForComponent shows an interactive selector for component selection. -func PromptForComponent(cmd *cobra.Command) (string, error) { +// If stack is provided, filters components to only those in that stack. +func PromptForComponent(cmd *cobra.Command, stack string) (string, error) { + // Create a completion function that respects the stack filter. + completionFunc := func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return componentsArgCompletionWithStack(cmd, args, toComplete, stack) + } + return flags.PromptForPositionalArg( "component", "Choose a component", - ComponentsArgCompletion, + completionFunc, cmd, nil, ) @@ -59,17 +65,46 @@ func HandlePromptError(err error, name string) error { } // ComponentsArgCompletion provides shell completion for component positional arguments. +// Checks for --stack flag and filters components accordingly. func ComponentsArgCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { - output, err := listTerraformComponents() - if err != nil { - return nil, cobra.ShellCompDirectiveNoFileComp + // Check if --stack flag was provided. + stack := "" + if cmd != nil { + if stackFlag := cmd.Flag("stack"); stackFlag != nil { + stack = stackFlag.Value.String() + } } - return output, cobra.ShellCompDirectiveNoFileComp + return componentsArgCompletionWithStack(cmd, args, toComplete, stack) } return nil, cobra.ShellCompDirectiveNoFileComp } +// componentsArgCompletionWithStack provides shell completion for component arguments with optional stack filtering. +func componentsArgCompletionWithStack(cmd *cobra.Command, args []string, toComplete string, stack string) ([]string, cobra.ShellCompDirective) { + // cmd and toComplete kept for Cobra completion function signature compatibility. + _ = cmd + _ = toComplete + + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var output []string + var err error + + if stack != "" { + output, err = listTerraformComponentsForStack(stack) + } else { + output, err = listTerraformComponents() + } + + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return output, cobra.ShellCompDirectiveNoFileComp +} + // StackFlagCompletion provides shell completion for the --stack flag. // If a component was provided as the first positional argument, it filters stacks // to only those containing that component. @@ -91,7 +126,55 @@ func StackFlagCompletion(cmd *cobra.Command, args []string, toComplete string) ( return output, cobra.ShellCompDirectiveNoFileComp } -// listTerraformComponents lists all terraform components. +// isComponentDeployable checks if a component can be deployed (not abstract, not disabled). +// Returns false for components with metadata.type: abstract or metadata.enabled: false. +func isComponentDeployable(componentConfig any) bool { + // Handle nil or non-map configs - assume deployable. + configMap, ok := componentConfig.(map[string]any) + if !ok { + return true + } + + // Check metadata section. + metadata, ok := configMap["metadata"].(map[string]any) + if !ok { + return true // No metadata means deployable. + } + + // Check if component is abstract. + if componentType, ok := metadata["type"].(string); ok && componentType == "abstract" { + return false + } + + // Check if component is disabled. + if enabled, ok := metadata["enabled"].(bool); ok && !enabled { + return false + } + + return true +} + +// filterDeployableComponents returns only components that can be deployed. +// Filters out abstract and disabled components from the terraform components map. +// Returns a sorted slice of deployable component names. +func filterDeployableComponents(terraformComponents map[string]any) []string { + if len(terraformComponents) == 0 { + return []string{} + } + + var components []string + for name, config := range terraformComponents { + if isComponentDeployable(config) { + components = append(components, name) + } + } + + sort.Strings(components) + return components +} + +// listTerraformComponents lists all deployable terraform components across all stacks. +// Filters out abstract and disabled components. func listTerraformComponents() ([]string, error) { configAndStacksInfo := schema.ConfigAndStacksInfo{} atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) @@ -104,14 +187,16 @@ func listTerraformComponents() ([]string, error) { return nil, err } - // Collect unique component names from all stacks. + // Collect unique deployable component names from all stacks. componentSet := make(map[string]struct{}) for _, stackData := range stacksMap { if stackMap, ok := stackData.(map[string]any); ok { if components, ok := stackMap["components"].(map[string]any); ok { if terraform, ok := components["terraform"].(map[string]any); ok { - for componentName := range terraform { - componentSet[componentName] = struct{}{} + // Filter to only deployable components. + deployable := filterDeployableComponents(terraform) + for _, name := range deployable { + componentSet[name] = struct{}{} } } } @@ -126,6 +211,50 @@ func listTerraformComponents() ([]string, error) { return components, nil } +// listTerraformComponentsForStack lists deployable terraform components for a specific stack. +// Filters out abstract and disabled components. +// If stack is empty, returns components from all stacks. +func listTerraformComponentsForStack(stack string) ([]string, error) { + if stack == "" { + return listTerraformComponents() + } + + configAndStacksInfo := schema.ConfigAndStacksInfo{} + atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + if err != nil { + return nil, err + } + + stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, stack, nil, nil, nil, false, false, false, false, nil, nil) + if err != nil { + return nil, err + } + + // Get components from the specified stack only. + stackData, exists := stacksMap[stack] + if !exists { + return []string{}, nil + } + + stackMap, ok := stackData.(map[string]any) + if !ok { + return []string{}, nil + } + + components, ok := stackMap["components"].(map[string]any) + if !ok { + return []string{}, nil + } + + terraform, ok := components["terraform"].(map[string]any) + if !ok { + return []string{}, nil + } + + // Filter to only deployable components and return sorted. + return filterDeployableComponents(terraform), nil +} + // listStacksForComponent returns stacks that contain the specified component. func listStacksForComponent(component string) ([]string, error) { configAndStacksInfo := schema.ConfigAndStacksInfo{} diff --git a/cmd/terraform/shared/prompt_test.go b/cmd/terraform/shared/prompt_test.go index 4da7b15f3d..6e6511c663 100644 --- a/cmd/terraform/shared/prompt_test.go +++ b/cmd/terraform/shared/prompt_test.go @@ -319,3 +319,203 @@ func TestStackFlagCompletion_ArgsHandling(t *testing.T) { }) } } + +// TestIsComponentDeployable tests the helper that checks if a component can be deployed. +func TestIsComponentDeployable(t *testing.T) { + tests := []struct { + name string + componentConfig any + expected bool + }{ + { + name: "nil config is deployable", + componentConfig: nil, + expected: true, + }, + { + name: "non-map config is deployable", + componentConfig: "string", + expected: true, + }, + { + name: "empty map is deployable", + componentConfig: map[string]any{}, + expected: true, + }, + { + name: "component without metadata is deployable", + componentConfig: map[string]any{ + "vars": map[string]any{"foo": "bar"}, + }, + expected: true, + }, + { + name: "component with invalid metadata type is deployable", + componentConfig: map[string]any{ + "metadata": "invalid", + }, + expected: true, + }, + { + name: "real component is deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "type": "real", + }, + }, + expected: true, + }, + { + name: "abstract component is not deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "type": "abstract", + }, + }, + expected: false, + }, + { + name: "enabled component is deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "enabled": true, + }, + }, + expected: true, + }, + { + name: "disabled component is not deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "enabled": false, + }, + }, + expected: false, + }, + { + name: "abstract and disabled component is not deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "type": "abstract", + "enabled": false, + }, + }, + expected: false, + }, + { + name: "real and enabled component is deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "type": "real", + "enabled": true, + }, + }, + expected: true, + }, + { + name: "real but disabled component is not deployable", + componentConfig: map[string]any{ + "metadata": map[string]any{ + "type": "real", + "enabled": false, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isComponentDeployable(tt.componentConfig) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestFilterDeployableComponents tests filtering a map of components to only deployable ones. +func TestFilterDeployableComponents(t *testing.T) { + tests := []struct { + name string + components map[string]any + expected []string + }{ + { + name: "nil map returns empty slice", + components: nil, + expected: []string{}, + }, + { + name: "empty map returns empty slice", + components: map[string]any{}, + expected: []string{}, + }, + { + name: "all real components are returned", + components: map[string]any{ + "vpc": map[string]any{ + "metadata": map[string]any{"type": "real"}, + }, + "eks": map[string]any{ + "metadata": map[string]any{"type": "real"}, + }, + }, + expected: []string{"eks", "vpc"}, + }, + { + name: "abstract components are filtered out", + components: map[string]any{ + "vpc": map[string]any{ + "metadata": map[string]any{"type": "real"}, + }, + "base-vpc": map[string]any{ + "metadata": map[string]any{"type": "abstract"}, + }, + }, + expected: []string{"vpc"}, + }, + { + name: "disabled components are filtered out", + components: map[string]any{ + "vpc": map[string]any{ + "metadata": map[string]any{"enabled": true}, + }, + "old-vpc": map[string]any{ + "metadata": map[string]any{"enabled": false}, + }, + }, + expected: []string{"vpc"}, + }, + { + name: "mixed filtering", + components: map[string]any{ + "vpc": map[string]any{ + "metadata": map[string]any{"type": "real", "enabled": true}, + }, + "base-vpc": map[string]any{ + "metadata": map[string]any{"type": "abstract"}, + }, + "disabled-vpc": map[string]any{ + "metadata": map[string]any{"enabled": false}, + }, + "eks": map[string]any{}, // No metadata - should be deployable. + }, + expected: []string{"eks", "vpc"}, + }, + { + name: "components without metadata are deployable", + components: map[string]any{ + "simple-component": map[string]any{ + "vars": map[string]any{"foo": "bar"}, + }, + }, + expected: []string{"simple-component"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterDeployableComponents(tt.components) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/cmd/terraform/shell.go b/cmd/terraform/shell.go index bbbfbbe99d..9c9b67f57d 100644 --- a/cmd/terraform/shell.go +++ b/cmd/terraform/shell.go @@ -44,9 +44,13 @@ as you would in a typical setup, but within the configured Atmos environment.`, return err } + // Get stack early so we can use it to filter component selection. + stack := v.GetString("stack") + // Prompt for component if missing. + // If stack is already provided (via --stack flag), filter components to that stack. if component == "" { - prompted, err := promptForComponent(cmd) + prompted, err := promptForComponent(cmd, stack) if err = handlePromptError(err, "component"); err != nil { return err } @@ -58,8 +62,7 @@ as you would in a typical setup, but within the configured Atmos environment.`, return errUtils.ErrMissingComponent } - // Get flag values from Viper - stack := v.GetString("stack") + // Get remaining flag values from Viper. processTemplates := v.GetBool("process-templates") processFunctions := v.GetBool("process-functions") skip := v.GetStringSlice("skip") diff --git a/cmd/terraform/utils.go b/cmd/terraform/utils.go index 1bf401d0e3..41f38a7710 100644 --- a/cmd/terraform/utils.go +++ b/cmd/terraform/utils.go @@ -365,8 +365,9 @@ func handleInteractiveComponentStackSelection(info *schema.ConfigAndStacksInfo, } // Prompt for component if missing. + // If stack is already provided (via --stack flag), filter components to that stack. if info.ComponentFromArg == "" { - component, err := promptForComponent(cmd) + component, err := promptForComponent(cmd, info.Stack) if err = handlePromptError(err, "component"); err != nil { return err } @@ -391,8 +392,9 @@ func handlePromptError(err error, name string) error { } // promptForComponent delegates to shared.PromptForComponent. -func promptForComponent(cmd *cobra.Command) (string, error) { - return shared.PromptForComponent(cmd) +// If stack is provided, filters components to only those in that stack. +func promptForComponent(cmd *cobra.Command, stack string) (string, error) { + return shared.PromptForComponent(cmd, stack) } // promptForStack delegates to shared.PromptForStack. diff --git a/website/blog/2026-01-16-component-selection-filtering.mdx b/website/blog/2026-01-16-component-selection-filtering.mdx new file mode 100644 index 0000000000..171ef9b241 --- /dev/null +++ b/website/blog/2026-01-16-component-selection-filtering.mdx @@ -0,0 +1,30 @@ +--- +slug: component-selection-filtering +title: "Smarter Component Selection in Interactive Prompts" +authors: [osterman] +tags: [bugfix] +--- + +Interactive component selection now filters out non-deployable components. + +{/* truncate */} + +## What Changed + +When using `atmos terraform plan -s stack-name` without specifying a component, the interactive +"Choose a component" menu now correctly filters the component list: + +- **Abstract components** (`metadata.type: abstract`) are hidden - they're templates, not deployable +- **Disabled components** (`metadata.enabled: false`) are hidden - they can't be deployed +- **Stack-scoped filtering** - only components in the specified stack appear + +## Why This Matters + +Previously, users would see all components from all stacks, including abstract base components +that serve as templates. This was confusing and could lead to errors when selecting a component +that couldn't actually be deployed. + +## Shell Completion Too + +Tab completion for component arguments also uses the same filtering logic, so you'll only see +valid, deployable components when completing `atmos terraform plan -s stack `. From bb12d453c182efd79f52fbbfb4b9dc504565eb54 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Fri, 16 Jan 2026 17:44:25 -0600 Subject: [PATCH 2/6] test: Add coverage for terraform command files - Add DI variables to cmd/terraform/shared/prompt.go for testability - Add comprehensive tests for listTerraformComponents, listTerraformComponentsForStack, listStacksForComponent, listAllStacks, and componentsArgCompletionWithStack - Add tests for utils.go helper functions: isMultiComponentExecution, hasMultiComponentFlags, hasNonAffectedMultiFlags, hasSingleComponentFlags, handlePathResolutionError, handleInteractiveComponentStackSelection - Extract common mock setup to helper function to avoid duplication - Increases cmd/terraform/shared coverage from 30% to 87.9% Co-Authored-By: Claude Opus 4.5 --- cmd/terraform/shared/prompt.go | 22 +- cmd/terraform/shared/prompt_test.go | 706 ++++++++++++++++++++++++++++ cmd/terraform/utils_test.go | 363 ++++++++++++++ website/src/data/roadmap.js | 1 + 4 files changed, 1084 insertions(+), 8 deletions(-) diff --git a/cmd/terraform/shared/prompt.go b/cmd/terraform/shared/prompt.go index ec7251f226..b4d9c818cb 100644 --- a/cmd/terraform/shared/prompt.go +++ b/cmd/terraform/shared/prompt.go @@ -15,6 +15,12 @@ import ( "github.com/cloudposse/atmos/pkg/schema" ) +// Package-level variables for dependency injection (enables testing). +var ( + initCliConfig = cfg.InitCliConfig + executeDescribeStacks = e.ExecuteDescribeStacks +) + // PromptForComponent shows an interactive selector for component selection. // If stack is provided, filters components to only those in that stack. func PromptForComponent(cmd *cobra.Command, stack string) (string, error) { @@ -177,12 +183,12 @@ func filterDeployableComponents(terraformComponents map[string]any) []string { // Filters out abstract and disabled components. func listTerraformComponents() ([]string, error) { configAndStacksInfo := schema.ConfigAndStacksInfo{} - atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err } - stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil) + stacksMap, err := executeDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil) if err != nil { return nil, err } @@ -220,12 +226,12 @@ func listTerraformComponentsForStack(stack string) ([]string, error) { } configAndStacksInfo := schema.ConfigAndStacksInfo{} - atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err } - stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, stack, nil, nil, nil, false, false, false, false, nil, nil) + stacksMap, err := executeDescribeStacks(&atmosConfig, stack, nil, nil, nil, false, false, false, false, nil, nil) if err != nil { return nil, err } @@ -258,12 +264,12 @@ func listTerraformComponentsForStack(stack string) ([]string, error) { // listStacksForComponent returns stacks that contain the specified component. func listStacksForComponent(component string) ([]string, error) { configAndStacksInfo := schema.ConfigAndStacksInfo{} - atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err } - stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil) + stacksMap, err := executeDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil) if err != nil { return nil, err } @@ -300,12 +306,12 @@ func stackContainsComponent(stackData any, component string) bool { // listAllStacks returns all stacks. func listAllStacks() ([]string, error) { configAndStacksInfo := schema.ConfigAndStacksInfo{} - atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err } - stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil) + stacksMap, err := executeDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil) if err != nil { return nil, err } diff --git a/cmd/terraform/shared/prompt_test.go b/cmd/terraform/shared/prompt_test.go index 6e6511c663..5dc4399d2f 100644 --- a/cmd/terraform/shared/prompt_test.go +++ b/cmd/terraform/shared/prompt_test.go @@ -8,6 +8,9 @@ import ( "github.com/stretchr/testify/assert" errUtils "github.com/cloudposse/atmos/errors" + "github.com/cloudposse/atmos/pkg/auth" + cfg "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/schema" ) func TestHandlePromptError(t *testing.T) { @@ -519,3 +522,706 @@ func TestFilterDeployableComponents(t *testing.T) { }) } } + +// mockSetup holds the common mock configuration for tests. +type mockSetup struct { + configError error + stacksError error + stacksMap map[string]any +} + +// setupMocksWithCleanup sets up the mocks with the given configuration and returns a cleanup function. +func setupMocksWithCleanup(t *testing.T) (func(ms mockSetup), func()) { + t.Helper() + originalInitCliConfig := initCliConfig + originalExecuteDescribeStacks := executeDescribeStacks + + setMocks := func(ms mockSetup) { + initCliConfig = func(_ schema.ConfigAndStacksInfo, _ bool) (schema.AtmosConfiguration, error) { + if ms.configError != nil { + return schema.AtmosConfiguration{}, ms.configError + } + return schema.AtmosConfiguration{}, nil + } + + executeDescribeStacks = func(_ *schema.AtmosConfiguration, _ string, _ []string, _ []string, _ []string, _, _, _, _ bool, _ []string, _ auth.AuthManager) (map[string]any, error) { + if ms.stacksError != nil { + return nil, ms.stacksError + } + return ms.stacksMap, nil + } + } + + cleanup := func() { + initCliConfig = originalInitCliConfig + executeDescribeStacks = originalExecuteDescribeStacks + } + + return setMocks, cleanup +} + +// TestListTerraformComponents tests the listTerraformComponents function. +func TestListTerraformComponents(t *testing.T) { + setMocks, cleanup := setupMocksWithCleanup(t) + defer cleanup() + + tests := []struct { + name string + mockConfigError error + mockStacksError error + mockStacksMap map[string]any + expectedComponents []string + expectedError bool + }{ + { + name: "success with multiple components across stacks", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "eks": map[string]any{}, + }, + }, + }, + "prod-us-west-2": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "aurora": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{"aurora", "eks", "vpc"}, + expectedError: false, + }, + { + name: "returns error when config init fails", + mockConfigError: errors.New("config init failed"), + mockStacksError: nil, + mockStacksMap: nil, + expectedComponents: nil, + expectedError: true, + }, + { + name: "returns error when describe stacks fails", + mockConfigError: nil, + mockStacksError: errors.New("describe stacks failed"), + mockStacksMap: nil, + expectedComponents: nil, + expectedError: true, + }, + { + name: "returns empty slice for empty stacks", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{}, + expectedComponents: []string{}, + expectedError: false, + }, + { + name: "filters out abstract components", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{ + "metadata": map[string]any{"type": "real"}, + }, + "base-component": map[string]any{ + "metadata": map[string]any{"type": "abstract"}, + }, + }, + }, + }, + }, + expectedComponents: []string{"vpc"}, + expectedError: false, + }, + { + name: "deduplicates components across stacks", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: 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{ + "vpc": map[string]any{}, + }, + }, + }, + "stack3": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{"vpc"}, + expectedError: false, + }, + { + name: "handles stacks without terraform components", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "helmfile": map[string]any{ + "chart1": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{}, + expectedError: false, + }, + { + name: "handles invalid stack data type", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": "invalid-stack-data", + }, + expectedComponents: []string{}, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setMocks(mockSetup{ + configError: tt.mockConfigError, + stacksError: tt.mockStacksError, + stacksMap: tt.mockStacksMap, + }) + + result, err := listTerraformComponents() + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedComponents, result) + } + }) + } +} + +// TestListTerraformComponentsForStack tests the listTerraformComponentsForStack function. +func TestListTerraformComponentsForStack(t *testing.T) { + setMocks, cleanup := setupMocksWithCleanup(t) + defer cleanup() + + tests := []struct { + name string + stack string + mockConfigError error + mockStacksError error + mockStacksMap map[string]any + expectedComponents []string + expectedError bool + }{ + { + name: "success with specific stack", + stack: "dev-us-east-1", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "eks": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{"eks", "vpc"}, + expectedError: false, + }, + { + name: "returns empty when stack not found", + stack: "nonexistent", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{}, + expectedError: false, + }, + { + name: "returns error when config init fails", + stack: "dev", + mockConfigError: errors.New("config init failed"), + mockStacksError: nil, + mockStacksMap: nil, + expectedComponents: nil, + expectedError: true, + }, + { + name: "returns error when describe stacks fails", + stack: "dev", + mockConfigError: nil, + mockStacksError: errors.New("describe stacks failed"), + mockStacksMap: nil, + expectedComponents: nil, + expectedError: true, + }, + { + name: "handles invalid stack data type", + stack: "dev", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": "invalid-data", + }, + expectedComponents: []string{}, + expectedError: false, + }, + { + name: "handles stack without components key", + stack: "dev", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "settings": map[string]any{}, + }, + }, + expectedComponents: []string{}, + expectedError: false, + }, + { + name: "handles stack without terraform components", + stack: "dev", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "helmfile": map[string]any{}, + }, + }, + }, + expectedComponents: []string{}, + expectedError: false, + }, + { + name: "filters out abstract components", + stack: "dev", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "base-vpc": map[string]any{ + "metadata": map[string]any{"type": "abstract"}, + }, + }, + }, + }, + }, + expectedComponents: []string{"vpc"}, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setMocks(mockSetup{ + configError: tt.mockConfigError, + stacksError: tt.mockStacksError, + stacksMap: tt.mockStacksMap, + }) + + result, err := listTerraformComponentsForStack(tt.stack) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedComponents, result) + } + }) + } +} + +// TestListTerraformComponentsForStack_EmptyStackDelegation tests that empty stack delegates to listTerraformComponents. +func TestListTerraformComponentsForStack_EmptyStackDelegation(t *testing.T) { + // This test needs custom mock setup to track calls, so we keep it separate. + originalInitCliConfig := initCliConfig + originalExecuteDescribeStacks := executeDescribeStacks + defer func() { + initCliConfig = originalInitCliConfig + executeDescribeStacks = originalExecuteDescribeStacks + }() + + describeStacksCalled := false + initCliConfig = func(_ schema.ConfigAndStacksInfo, _ bool) (schema.AtmosConfiguration, error) { + return schema.AtmosConfiguration{}, nil + } + executeDescribeStacks = func(_ *schema.AtmosConfiguration, filterStack string, _ []string, _ []string, _ []string, _, _, _, _ bool, _ []string, _ auth.AuthManager) (map[string]any, error) { + describeStacksCalled = true + // When stack is empty, it should call listTerraformComponents which passes empty string. + assert.Equal(t, "", filterStack) + return map[string]any{ + "stack1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + }, nil + } + + result, err := listTerraformComponentsForStack("") + + assert.NoError(t, err) + assert.True(t, describeStacksCalled) + assert.Equal(t, []string{"vpc"}, result) +} + +// TestListStacksForComponent tests the listStacksForComponent function. +func TestListStacksForComponent(t *testing.T) { + setMocks, cleanup := setupMocksWithCleanup(t) + defer cleanup() + + tests := []struct { + name string + component string + mockConfigError error + mockStacksError error + mockStacksMap map[string]any + expectedStacks []string + expectedError bool + }{ + { + name: "success with matching stacks", + component: "vpc", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "eks": map[string]any{}, + }, + }, + }, + "prod-us-west-2": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + "staging": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "rds": map[string]any{}, + }, + }, + }, + }, + expectedStacks: []string{"dev-us-east-1", "prod-us-west-2"}, + expectedError: false, + }, + { + name: "no matching stacks", + component: "nonexistent", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + }, + expectedStacks: nil, + expectedError: false, + }, + { + name: "returns error when config init fails", + component: "vpc", + mockConfigError: errors.New("config init failed"), + mockStacksError: nil, + mockStacksMap: nil, + expectedStacks: nil, + expectedError: true, + }, + { + name: "returns error when describe stacks fails", + component: "vpc", + mockConfigError: nil, + mockStacksError: errors.New("describe stacks failed"), + mockStacksMap: nil, + expectedStacks: nil, + expectedError: true, + }, + { + name: "returns empty for empty stacks map", + component: "vpc", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{}, + expectedStacks: nil, + expectedError: false, + }, + { + name: "results are sorted alphabetically", + component: "vpc", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "z-stack": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{"vpc": map[string]any{}}, + }, + }, + "a-stack": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{"vpc": map[string]any{}}, + }, + }, + "m-stack": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{"vpc": map[string]any{}}, + }, + }, + }, + expectedStacks: []string{"a-stack", "m-stack", "z-stack"}, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setMocks(mockSetup{ + configError: tt.mockConfigError, + stacksError: tt.mockStacksError, + stacksMap: tt.mockStacksMap, + }) + + result, err := listStacksForComponent(tt.component) + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedStacks, result) + } + }) + } +} + +// TestListAllStacks tests the listAllStacks function. +func TestListAllStacks(t *testing.T) { + setMocks, cleanup := setupMocksWithCleanup(t) + defer cleanup() + + tests := []struct { + name string + mockConfigError error + mockStacksError error + mockStacksMap map[string]any + expectedStacks []string + expectedError bool + }{ + { + name: "success with multiple stacks", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{}, + "prod-us-west-2": map[string]any{}, + "staging": map[string]any{}, + }, + expectedStacks: []string{"dev-us-east-1", "prod-us-west-2", "staging"}, + expectedError: false, + }, + { + name: "returns error when config init fails", + mockConfigError: errors.New("config init failed"), + mockStacksError: nil, + mockStacksMap: nil, + expectedStacks: nil, + expectedError: true, + }, + { + name: "returns error when describe stacks fails", + mockConfigError: nil, + mockStacksError: errors.New("describe stacks failed"), + mockStacksMap: nil, + expectedStacks: nil, + expectedError: true, + }, + { + name: "returns empty for empty stacks map", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{}, + expectedStacks: []string{}, + expectedError: false, + }, + { + name: "results are sorted alphabetically", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "z-stack": map[string]any{}, + "a-stack": map[string]any{}, + "m-stack": map[string]any{}, + }, + expectedStacks: []string{"a-stack", "m-stack", "z-stack"}, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setMocks(mockSetup{ + configError: tt.mockConfigError, + stacksError: tt.mockStacksError, + stacksMap: tt.mockStacksMap, + }) + + result, err := listAllStacks() + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedStacks, result) + } + }) + } +} + +// TestComponentsArgCompletionWithStack tests the componentsArgCompletionWithStack function. +func TestComponentsArgCompletionWithStack(t *testing.T) { + setMocks, cleanup := setupMocksWithCleanup(t) + defer cleanup() + + tests := []struct { + name string + args []string + stack string + mockConfigError error + mockStacksError error + mockStacksMap map[string]any + expectedComponents []string + expectedDirective cobra.ShellCompDirective + }{ + { + name: "with stack filter returns filtered components", + args: []string{}, + stack: "dev", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + "eks": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{"eks", "vpc"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + name: "without stack filter returns all components", + args: []string{}, + stack: "", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "vpc": map[string]any{}, + }, + }, + }, + "prod": map[string]any{ + "components": map[string]any{ + "terraform": map[string]any{ + "rds": map[string]any{}, + }, + }, + }, + }, + expectedComponents: []string{"rds", "vpc"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + name: "with args returns nil", + args: []string{"existing-component"}, + stack: "", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: nil, + expectedComponents: nil, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + name: "returns NoFileComp on error", + args: []string{}, + stack: "", + mockConfigError: errors.New("config error"), + mockStacksError: nil, + mockStacksMap: nil, + expectedComponents: nil, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setMocks(mockSetup{ + configError: tt.mockConfigError, + stacksError: tt.mockStacksError, + stacksMap: tt.mockStacksMap, + }) + + cmd := &cobra.Command{Use: "test"} + result, directive := componentsArgCompletionWithStack(cmd, tt.args, "", tt.stack) + + assert.Equal(t, tt.expectedComponents, result) + assert.Equal(t, tt.expectedDirective, directive) + }) + } +} + +// Ensure cfg import is used. +var _ = cfg.InitCliConfig diff --git a/cmd/terraform/utils_test.go b/cmd/terraform/utils_test.go index e50c57b677..a90ab5f1af 100644 --- a/cmd/terraform/utils_test.go +++ b/cmd/terraform/utils_test.go @@ -341,3 +341,366 @@ func TestStackFlagCompletion_WithComponent(t *testing.T) { assert.NotNil(t, stacks) assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) } + +// TestIsMultiComponentExecution tests the isMultiComponentExecution function. +func TestIsMultiComponentExecution(t *testing.T) { + tests := []struct { + name string + info *schema.ConfigAndStacksInfo + expected bool + }{ + { + name: "all flag set", + info: &schema.ConfigAndStacksInfo{All: true}, + expected: true, + }, + { + name: "components set", + info: &schema.ConfigAndStacksInfo{Components: []string{"comp1", "comp2"}}, + expected: true, + }, + { + name: "query set", + info: &schema.ConfigAndStacksInfo{Query: ".components.test"}, + expected: true, + }, + { + name: "stack set without component", + info: &schema.ConfigAndStacksInfo{Stack: "dev-us-east-1"}, + expected: true, + }, + { + name: "stack and component both set - single component mode", + info: &schema.ConfigAndStacksInfo{Stack: "dev-us-east-1", ComponentFromArg: "vpc"}, + expected: false, + }, + { + name: "no flags - single component mode", + info: &schema.ConfigAndStacksInfo{}, + expected: false, + }, + { + name: "only component - single component mode", + info: &schema.ConfigAndStacksInfo{ComponentFromArg: "vpc"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isMultiComponentExecution(tt.info) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestHasMultiComponentFlags tests the hasMultiComponentFlags function. +func TestHasMultiComponentFlags(t *testing.T) { + tests := []struct { + name string + info *schema.ConfigAndStacksInfo + expected bool + }{ + { + name: "all flag", + info: &schema.ConfigAndStacksInfo{All: true}, + expected: true, + }, + { + name: "affected flag", + info: &schema.ConfigAndStacksInfo{Affected: true}, + expected: true, + }, + { + name: "components set", + info: &schema.ConfigAndStacksInfo{Components: []string{"comp1"}}, + expected: true, + }, + { + name: "query set", + info: &schema.ConfigAndStacksInfo{Query: ".test"}, + expected: true, + }, + { + name: "no flags", + info: &schema.ConfigAndStacksInfo{}, + expected: false, + }, + { + name: "empty components slice", + info: &schema.ConfigAndStacksInfo{Components: []string{}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := hasMultiComponentFlags(tt.info) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestHasNonAffectedMultiFlags tests the hasNonAffectedMultiFlags function. +func TestHasNonAffectedMultiFlags(t *testing.T) { + tests := []struct { + name string + info *schema.ConfigAndStacksInfo + expected bool + }{ + { + name: "all flag", + info: &schema.ConfigAndStacksInfo{All: true}, + expected: true, + }, + { + name: "components set", + info: &schema.ConfigAndStacksInfo{Components: []string{"comp1"}}, + expected: true, + }, + { + name: "query set", + info: &schema.ConfigAndStacksInfo{Query: ".test"}, + expected: true, + }, + { + name: "affected only - should return false", + info: &schema.ConfigAndStacksInfo{Affected: true}, + expected: false, + }, + { + name: "no flags", + info: &schema.ConfigAndStacksInfo{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := hasNonAffectedMultiFlags(tt.info) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestHasSingleComponentFlags tests the hasSingleComponentFlags function. +func TestHasSingleComponentFlags(t *testing.T) { + tests := []struct { + name string + info *schema.ConfigAndStacksInfo + expected bool + }{ + { + name: "plan-file set", + info: &schema.ConfigAndStacksInfo{PlanFile: "plan.tfplan"}, + expected: true, + }, + { + name: "use-terraform-plan set", + info: &schema.ConfigAndStacksInfo{UseTerraformPlan: true}, + expected: true, + }, + { + name: "both set", + info: &schema.ConfigAndStacksInfo{PlanFile: "plan.tfplan", UseTerraformPlan: true}, + expected: true, + }, + { + name: "neither set", + info: &schema.ConfigAndStacksInfo{}, + expected: false, + }, + { + name: "empty plan-file", + info: &schema.ConfigAndStacksInfo{PlanFile: ""}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := hasSingleComponentFlags(tt.info) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestHandlePathResolutionError tests the handlePathResolutionError function. +func TestHandlePathResolutionError(t *testing.T) { + tests := []struct { + name string + err error + expectedErr error + checkIs bool // Use errors.Is to check + }{ + { + name: "ambiguous path error passes through", + err: errUtils.ErrAmbiguousComponentPath, + expectedErr: errUtils.ErrAmbiguousComponentPath, + checkIs: true, + }, + { + name: "component not in stack passes through", + err: errUtils.ErrComponentNotInStack, + expectedErr: errUtils.ErrComponentNotInStack, + checkIs: true, + }, + { + name: "stack not found passes through", + err: errUtils.ErrStackNotFound, + expectedErr: errUtils.ErrStackNotFound, + checkIs: true, + }, + { + name: "user aborted passes through", + err: errUtils.ErrUserAborted, + expectedErr: errUtils.ErrUserAborted, + checkIs: true, + }, + { + name: "generic error gets wrapped with ErrPathResolutionFailed", + err: errors.New("some generic error"), + expectedErr: errUtils.ErrPathResolutionFailed, + checkIs: true, + }, + { + name: "wrapped ambiguous path error passes through", + err: errUtils.Build(errUtils.ErrAmbiguousComponentPath).WithExplanation("test").Err(), + expectedErr: errUtils.ErrAmbiguousComponentPath, + checkIs: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := handlePathResolutionError(tt.err) + assert.Error(t, result) + if tt.checkIs { + assert.ErrorIs(t, result, tt.expectedErr) + } + }) + } +} + +// TestHandleInteractiveComponentStackSelection tests the handleInteractiveComponentStackSelection function. +func TestHandleInteractiveComponentStackSelection(t *testing.T) { + tests := []struct { + name string + info *schema.ConfigAndStacksInfo + expectComponentPrompt bool + expectStackPrompt bool + shouldSkip bool + }{ + { + name: "skip when all flag is set", + info: &schema.ConfigAndStacksInfo{All: true}, + shouldSkip: true, + }, + { + name: "skip when affected flag is set", + info: &schema.ConfigAndStacksInfo{Affected: true}, + shouldSkip: true, + }, + { + name: "skip when components set", + info: &schema.ConfigAndStacksInfo{Components: []string{"comp1"}}, + shouldSkip: true, + }, + { + name: "skip when query set", + info: &schema.ConfigAndStacksInfo{Query: ".test"}, + shouldSkip: true, + }, + { + name: "skip when need help", + info: &schema.ConfigAndStacksInfo{NeedHelp: true}, + shouldSkip: true, + }, + { + name: "skip when both component and stack provided", + info: &schema.ConfigAndStacksInfo{ComponentFromArg: "vpc", Stack: "dev"}, + shouldSkip: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + err := handleInteractiveComponentStackSelection(tt.info, cmd) + + if tt.shouldSkip { + assert.NoError(t, err) + // Info should not be modified. + } + }) + } +} + +// TestIdentityFlagCompletion tests the identityFlagCompletion function. +func TestIdentityFlagCompletion(t *testing.T) { + t.Chdir("../../examples/demo-stacks") + + cmd := &cobra.Command{Use: "test"} + + // Test identity completion (will return empty if no identities configured). + identities, directive := identityFlagCompletion(cmd, []string{}, "") + + // Directive should always be NoFileComp. + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + + // Identities may be nil or empty depending on config. + // We just verify no panic and correct directive. + _ = identities +} + +// TestAddIdentityCompletion tests the addIdentityCompletion function. +func TestAddIdentityCompletion(t *testing.T) { + t.Run("flag exists", func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().StringP("identity", "i", "", "Identity flag") + + // Should not panic. + addIdentityCompletion(cmd) + }) + + t.Run("flag does not exist", func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + + // Should not panic when flag doesn't exist. + addIdentityCompletion(cmd) + }) + + t.Run("inherited flag", func(t *testing.T) { + parent := &cobra.Command{Use: "parent"} + parent.PersistentFlags().StringP("identity", "i", "", "Identity flag") + + child := &cobra.Command{Use: "child"} + parent.AddCommand(child) + + // Should find inherited flag. + addIdentityCompletion(child) + }) +} + +// TestComponentsArgCompletion tests the componentsArgCompletion function. +func TestComponentsArgCompletion(t *testing.T) { + t.Chdir("../../examples/demo-stacks") + + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("stack", "", "Stack flag") + + // Test with no args. + components, directive := componentsArgCompletion(cmd, []string{}, "") + assert.NotNil(t, components) + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) +} + +// TestComponentsArgCompletion_WithExistingArgs tests completion when args already exist. +func TestComponentsArgCompletion_WithExistingArgs(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + + // Test with existing args - should return nil. + components, directive := componentsArgCompletion(cmd, []string{"existing-component"}, "") + assert.Nil(t, components) + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) +} diff --git a/website/src/data/roadmap.js b/website/src/data/roadmap.js index f7274846ef..51229f80da 100644 --- a/website/src/data/roadmap.js +++ b/website/src/data/roadmap.js @@ -190,6 +190,7 @@ export const roadmapConfig = { { label: 'Theme-aware help text', status: 'shipped', quarter: 'q4-2025', changelog: 'theme-aware-help', version: 'v1.200.0', description: 'Help text respects your terminal theme settings for consistent styling.', benefits: 'Help output matches your theme. Consistent look across all Atmos commands.' }, { label: 'Helpful error messages with hints', status: 'shipped', quarter: 'q4-2025', changelog: 'helpful-errors', version: 'v1.199.0', description: 'Error messages include actionable hints, rich context, and optional Sentry integration for enterprise error tracking.', benefits: 'Understand what went wrong and how to fix it. Enterprise teams get centralized error tracking.' }, { label: 'Interactive Terraform prompts', status: 'shipped', quarter: 'q4-2025', changelog: 'interactive-terraform-prompts', description: 'Interactive component and stack selection when running Terraform commands without arguments.', benefits: 'Discover and select components interactively. No need to remember exact names.' }, + { label: 'Smarter component selection filtering', status: 'shipped', quarter: 'q1-2026', pr: 1977, changelog: 'component-selection-filtering', description: 'Interactive component menus and shell completion now filter out abstract and disabled components, showing only deployable options.', benefits: 'Component selection shows only what can actually be deployed. No more confusion from seeing abstract templates or disabled components.' }, { label: 'Seamless first login with provider fallback', status: 'shipped', quarter: 'q4-2025', pr: 1918, changelog: 'auth-login-provider-fallback', description: 'Auth login automatically falls back to provider when no identities exist, enabling zero-friction first-time login.', benefits: 'New users just run atmos auth login. No need to understand --provider flag on first use.' }, { label: 'Configuration provenance tracking', status: 'shipped', quarter: 'q4-2025', changelog: 'provenance-tracking', version: 'v1.195.0', description: 'Track where every configuration value comes from—which file, which import, which override.', benefits: 'Debug complex inheritance chains. Understand exactly how configuration was assembled.' }, { label: 'Global environment variables', status: 'shipped', quarter: 'q4-2025', changelog: 'global-env-section', version: 'v1.202.0', description: 'Define environment variables globally in atmos.yaml that apply to all commands.', benefits: 'Set common environment variables once. No repetition across components.' }, From 026a63fb5a9d968f37129a14a31bac33c9cab6c9 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Fri, 16 Jan 2026 21:15:48 -0600 Subject: [PATCH 3/6] fix: Update roadmap quarters for Q1 2026 - Mark Q4 2025 as completed - Mark Q1 2026 as current Addresses CodeRabbit feedback about quarter status alignment with shipped Q1 2026 milestone. Co-Authored-By: Claude Opus 4.5 --- website/src/data/roadmap.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/data/roadmap.js b/website/src/data/roadmap.js index 51229f80da..bcf26dcfa5 100644 --- a/website/src/data/roadmap.js +++ b/website/src/data/roadmap.js @@ -31,8 +31,8 @@ export const roadmapConfig = { { id: 'q1-2025', label: 'Q1 2025', status: 'completed' }, { id: 'q2-2025', label: 'Q2 2025', status: 'completed' }, { id: 'q3-2025', label: 'Q3 2025', status: 'completed' }, - { id: 'q4-2025', label: 'Q4 2025', status: 'current' }, - { id: 'q1-2026', label: 'Q1 2026', status: 'planned' }, + { id: 'q4-2025', label: 'Q4 2025', status: 'completed' }, + { id: 'q1-2026', label: 'Q1 2026', status: 'current' }, { id: 'q2-2026', label: 'Q2 2026', status: 'planned' }, ], From 3836ca8d36c137169e5ac8e1a46993f8b5b7c2bd Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Fri, 16 Jan 2026 22:27:09 -0600 Subject: [PATCH 4/6] fix: Validate stack name and show helpful error for invalid stacks When running `atmos terraform plan --stack ` without a component, the command would exit silently. Now it validates the stack exists and shows a helpful error with available stacks. Changes: - Add ValidateStackExists() to cmd/terraform/shared/prompt.go - Add stack validation in handleInteractiveComponentStackSelection() - Add WithCausef() method to ErrorBuilder for formatted cause messages - Add comprehensive tests for both features Example error output: Error: invalid stack: stack `demo` does not exist Explanation: The specified stack was not found in the configuration Hint: Available stacks: acme-west-dev, acme-west-prod, acme-west-staging Co-Authored-By: Claude Opus 4.5 --- cmd/terraform/shared/prompt.go | 24 ++++++++ cmd/terraform/shared/prompt_test.go | 89 +++++++++++++++++++++++++++++ cmd/terraform/utils.go | 7 +++ errors/builder.go | 15 +++++ errors/builder_test.go | 40 +++++++++++++ 5 files changed, 175 insertions(+) diff --git a/cmd/terraform/shared/prompt.go b/cmd/terraform/shared/prompt.go index b4d9c818cb..26d8b49018 100644 --- a/cmd/terraform/shared/prompt.go +++ b/cmd/terraform/shared/prompt.go @@ -4,6 +4,7 @@ package shared import ( "errors" "sort" + "strings" "github.com/spf13/cobra" @@ -323,3 +324,26 @@ func listAllStacks() ([]string, error) { sort.Strings(stacks) return stacks, nil } + +// ValidateStackExists checks if the provided stack name exists and returns +// an error with suggestions if it doesn't. +func ValidateStackExists(stack string) error { + stacks, err := listAllStacks() + if err != nil { + return err + } + + for _, s := range stacks { + if s == stack { + return nil // Stack exists. + } + } + + // Stack not found - use ErrorBuilder pattern with sentinel error. + return errUtils.Build(errUtils.ErrInvalidStack). + WithCausef("stack `%s` does not exist", stack). + WithExplanation("The specified stack was not found in the configuration"). + WithHintf("Available stacks: %s", strings.Join(stacks, ", ")). + WithContext("stack", stack). + Err() +} diff --git a/cmd/terraform/shared/prompt_test.go b/cmd/terraform/shared/prompt_test.go index 5dc4399d2f..60ba5122f1 100644 --- a/cmd/terraform/shared/prompt_test.go +++ b/cmd/terraform/shared/prompt_test.go @@ -1223,5 +1223,94 @@ func TestComponentsArgCompletionWithStack(t *testing.T) { } } +// TestValidateStackExists tests the ValidateStackExists function. +func TestValidateStackExists(t *testing.T) { + setMocks, cleanup := setupMocksWithCleanup(t) + defer cleanup() + + tests := []struct { + name string + stack string + mockConfigError error + mockStacksError error + mockStacksMap map[string]any + expectError bool + errorIs error + }{ + { + name: "valid stack returns nil", + stack: "dev-us-east-1", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{}, + "prod-us-west-2": map[string]any{}, + }, + expectError: false, + errorIs: nil, + }, + { + name: "invalid stack returns error", + stack: "nonexistent-stack", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{ + "dev-us-east-1": map[string]any{}, + "prod-us-west-2": map[string]any{}, + }, + expectError: true, + errorIs: errUtils.ErrInvalidStack, + }, + { + name: "config error propagates", + stack: "any-stack", + mockConfigError: errors.New("config init failed"), + mockStacksError: nil, + mockStacksMap: nil, + expectError: true, + errorIs: nil, // Not a sentinel error. + }, + { + name: "describe stacks error propagates", + stack: "any-stack", + mockConfigError: nil, + mockStacksError: errors.New("describe stacks failed"), + mockStacksMap: nil, + expectError: true, + errorIs: nil, // Not a sentinel error. + }, + { + name: "empty stacks map returns error", + stack: "any-stack", + mockConfigError: nil, + mockStacksError: nil, + mockStacksMap: map[string]any{}, + expectError: true, + errorIs: errUtils.ErrInvalidStack, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setMocks(mockSetup{ + configError: tt.mockConfigError, + stacksError: tt.mockStacksError, + stacksMap: tt.mockStacksMap, + }) + + err := ValidateStackExists(tt.stack) + + if tt.expectError { + assert.Error(t, err) + if tt.errorIs != nil { + assert.ErrorIs(t, err, tt.errorIs) + } + } else { + assert.NoError(t, err) + } + }) + } +} + // Ensure cfg import is used. var _ = cfg.InitCliConfig diff --git a/cmd/terraform/utils.go b/cmd/terraform/utils.go index 41f38a7710..302110b130 100644 --- a/cmd/terraform/utils.go +++ b/cmd/terraform/utils.go @@ -359,6 +359,13 @@ func handleInteractiveComponentStackSelection(info *schema.ConfigAndStacksInfo, return nil } + // Validate stack exists if provided via flag (fail fast before prompting or execution). + if info.Stack != "" && info.ComponentFromArg == "" { + if err := shared.ValidateStackExists(info.Stack); err != nil { + return err + } + } + // Both provided - nothing to do. if info.ComponentFromArg != "" && info.Stack != "" { return nil diff --git a/errors/builder.go b/errors/builder.go index f3006f5c37..369f2156c9 100644 --- a/errors/builder.go +++ b/errors/builder.go @@ -140,6 +140,21 @@ func (b *ErrorBuilder) WithCause(cause error) *ErrorBuilder { return b } +// WithCausef wraps the builder's error with a formatted cause message. +// This is a convenience method that creates a new error from the format string +// and passes it to WithCause. +// +// Example: +// +// return errUtils.Build(errUtils.ErrInvalidStack). +// WithCausef("stack '%s' does not exist", stackName). +// WithHint("Check your stack configuration"). +// Err() +func (b *ErrorBuilder) WithCausef(format string, args ...interface{}) *ErrorBuilder { + //nolint:err113 // WithCausef intentionally creates dynamic errors for cause messages. + return b.WithCause(fmt.Errorf(format, args...)) +} + // Err finalizes and returns the enriched error. func (b *ErrorBuilder) Err() error { if b.err == nil { diff --git a/errors/builder_test.go b/errors/builder_test.go index 716f68f4f0..329aacda1b 100644 --- a/errors/builder_test.go +++ b/errors/builder_test.go @@ -469,3 +469,43 @@ func TestErrorBuilder_WithCause(t *testing.T) { assert.True(t, errors.Is(err, causeErr)) }) } + +func TestErrorBuilder_WithCausef(t *testing.T) { + t.Run("creates formatted cause error", func(t *testing.T) { + stackName := "dev-us-east-1" + + err := Build(ErrInvalidStack). + WithCausef("stack `%s` does not exist", stackName). + Err() + + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidStack)) + assert.Contains(t, err.Error(), "stack `dev-us-east-1` does not exist") + }) + + t.Run("works with multiple format arguments", func(t *testing.T) { + err := Build(ErrInvalidComponent). + WithCausef("component `%s` not found in stack `%s`", "vpc", "prod"). + Err() + + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidComponent)) + assert.Contains(t, err.Error(), "component `vpc` not found in stack `prod`") + }) + + t.Run("chains with other builder methods", func(t *testing.T) { + err := Build(ErrInvalidStack). + WithCausef("stack `%s` does not exist", "demo"). + WithExplanation("The stack was not found"). + WithHint("Check your configuration"). + Err() + + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrInvalidStack)) + assert.Contains(t, err.Error(), "stack `demo` does not exist") + + // Verify hint is present. + hints := errors.GetAllHints(err) + assert.Contains(t, hints, "Check your configuration") + }) +} From 1ef003f2162b9a5948bebe8203bfac89dc0d1ecb Mon Sep 17 00:00:00 2001 From: aknysh Date: Sat, 17 Jan 2026 16:09:35 -0500 Subject: [PATCH 5/6] add test fixtures, add tests, address comments --- cmd/terraform/clean_test.go | 29 ++ cmd/terraform/generate/generate_test.go | 68 +++ cmd/terraform/shared/prompt.go | 69 ++- cmd/terraform/shared/prompt_test.go | 55 +- cmd/terraform/shell_test.go | 109 ++++ cmd/terraform/utils.go | 2 +- cmd/terraform/utils_test.go | 143 ++++++ errors/builder_test.go | 97 ++-- go.mod | 50 +- go.sum | 100 ++-- pkg/telemetry/mock/mock_posthog_client.go | 47 +- tests/cli_component_menu_filtering_test.go | 471 ++++++++++++++++++ .../component-menu-filtering/README.md | 157 ++++++ .../component-menu-filtering/atmos.yaml | 25 + .../components/terraform/eks/main.tf | 29 ++ .../components/terraform/rds/main.tf | 29 ++ .../components/terraform/vpc/main.tf | 29 ++ .../stacks/catalog/eks.yaml | 13 + .../stacks/catalog/rds.yaml | 13 + .../stacks/catalog/vpc.yaml | 24 + .../stacks/deploy/dev.yaml | 28 ++ .../stacks/deploy/prod.yaml | 32 ++ .../stacks/deploy/staging.yaml | 25 + 23 files changed, 1473 insertions(+), 171 deletions(-) create mode 100644 cmd/terraform/shell_test.go create mode 100644 tests/cli_component_menu_filtering_test.go create mode 100644 tests/fixtures/scenarios/component-menu-filtering/README.md create mode 100644 tests/fixtures/scenarios/component-menu-filtering/atmos.yaml create mode 100644 tests/fixtures/scenarios/component-menu-filtering/components/terraform/eks/main.tf create mode 100644 tests/fixtures/scenarios/component-menu-filtering/components/terraform/rds/main.tf create mode 100644 tests/fixtures/scenarios/component-menu-filtering/components/terraform/vpc/main.tf create mode 100644 tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/eks.yaml create mode 100644 tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/rds.yaml create mode 100644 tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/vpc.yaml create mode 100644 tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/dev.yaml create mode 100644 tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/prod.yaml create mode 100644 tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/staging.yaml diff --git a/cmd/terraform/clean_test.go b/cmd/terraform/clean_test.go index 099a9b138a..4ba06daaf0 100644 --- a/cmd/terraform/clean_test.go +++ b/cmd/terraform/clean_test.go @@ -3,6 +3,7 @@ package terraform import ( "testing" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -191,3 +192,31 @@ func TestCleanCommandIsSubcommand(t *testing.T) { assert.NotNil(t, parent) assert.Equal(t, "terraform", parent.Name()) } + +// TestCleanPromptHandling tests the prompt handling in the clean command. +// When neither component nor stack is provided, the command prompts for both. +// In non-TTY environment, the prompts return errors which are swallowed. +func TestCleanPromptHandling(t *testing.T) { + t.Run("prompts are triggered when component and stack are empty", func(t *testing.T) { + // Reset viper to avoid state pollution. + v := viper.New() + + // Bind the parser to the fresh viper instance. + err := cleanParser.BindToViper(v) + require.NoError(t, err) + + // Create a test command. + cmd := &cobra.Command{Use: "clean"} + cleanParser.RegisterPersistentFlags(cmd) + + // Execute RunE with no component or stack. + // In non-TTY environment, the prompts return ErrInteractiveModeNotAvailable. + // The errors are swallowed, and the command continues with empty component/stack. + // This test just verifies the code path doesn't panic. + err = cleanCmd.RunE(cmd, []string{}) + + // The command will fail at config initialization because we're not in a valid atmos project. + // This is expected behavior - we just want to verify the prompt handling path is exercised. + assert.Error(t, err) + }) +} diff --git a/cmd/terraform/generate/generate_test.go b/cmd/terraform/generate/generate_test.go index 1d9b88e3f6..2de8673aaa 100644 --- a/cmd/terraform/generate/generate_test.go +++ b/cmd/terraform/generate/generate_test.go @@ -4,8 +4,11 @@ import ( "testing" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + errUtils "github.com/cloudposse/atmos/errors" ) func TestGenerateCmd(t *testing.T) { @@ -376,3 +379,68 @@ func TestFilesParserFlags(t *testing.T) { }) } } + +// TestPlanfileValidation tests validation errors in the planfile command. +func TestPlanfileValidation(t *testing.T) { + t.Run("missing component returns error", func(t *testing.T) { + // Reset viper to avoid state pollution. + v := viper.New() + v.Set("stack", "test-stack") + + // Bind the parser to the fresh viper instance. + err := planfileParser.BindToViper(v) + require.NoError(t, err) + + // Create a test command to execute RunE directly. + cmd := &cobra.Command{Use: "planfile"} + planfileParser.RegisterFlags(cmd) + + // Execute RunE with no component argument. + // In non-TTY environment, the prompt returns ErrInteractiveModeNotAvailable + // which is swallowed, leaving component empty and triggering ErrMissingComponent. + err = planfileCmd.RunE(cmd, []string{}) + assert.ErrorIs(t, err, errUtils.ErrMissingComponent) + }) +} + +// TestBackendValidation tests validation errors in the backend command. +func TestBackendValidation(t *testing.T) { + t.Run("missing component returns error", func(t *testing.T) { + // Reset viper to avoid state pollution. + v := viper.New() + v.Set("stack", "test-stack") + + // Bind the parser to the fresh viper instance. + err := backendParser.BindToViper(v) + require.NoError(t, err) + + // Create a test command to execute RunE directly. + cmd := &cobra.Command{Use: "backend"} + backendParser.RegisterFlags(cmd) + + // Execute RunE with no component argument. + err = backendCmd.RunE(cmd, []string{}) + assert.ErrorIs(t, err, errUtils.ErrMissingComponent) + }) +} + +// TestVarfileValidation tests validation errors in the varfile command. +func TestVarfileValidation(t *testing.T) { + t.Run("missing component returns error", func(t *testing.T) { + // Reset viper to avoid state pollution. + v := viper.New() + v.Set("stack", "test-stack") + + // Bind the parser to the fresh viper instance. + err := varfileParser.BindToViper(v) + require.NoError(t, err) + + // Create a test command to execute RunE directly. + cmd := &cobra.Command{Use: "varfile"} + varfileParser.RegisterFlags(cmd) + + // Execute RunE with no component argument. + err = varfileCmd.RunE(cmd, []string{}) + assert.ErrorIs(t, err, errUtils.ErrMissingComponent) + }) +} diff --git a/cmd/terraform/shared/prompt.go b/cmd/terraform/shared/prompt.go index 26d8b49018..ea2663ee9f 100644 --- a/cmd/terraform/shared/prompt.go +++ b/cmd/terraform/shared/prompt.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" errUtils "github.com/cloudposse/atmos/errors" e "github.com/cloudposse/atmos/internal/exec" @@ -22,6 +23,22 @@ var ( executeDescribeStacks = e.ExecuteDescribeStacks ) +// buildConfigAndStacksInfo creates a ConfigAndStacksInfo populated with global CLI flags. +// This ensures --base-path, --config, --config-path, and --profile flags are respected. +func buildConfigAndStacksInfo(cmd *cobra.Command) schema.ConfigAndStacksInfo { + if cmd == nil { + return schema.ConfigAndStacksInfo{} + } + v := viper.GetViper() + globalFlags := flags.ParseGlobalFlags(cmd, v) + return schema.ConfigAndStacksInfo{ + AtmosBasePath: globalFlags.BasePath, + AtmosConfigFilesFromArg: globalFlags.Config, + AtmosConfigDirsFromArg: globalFlags.ConfigPath, + ProfilesFromArg: globalFlags.Profile, + } +} + // PromptForComponent shows an interactive selector for component selection. // If stack is provided, filters components to only those in that stack. func PromptForComponent(cmd *cobra.Command, stack string) (string, error) { @@ -89,8 +106,7 @@ func ComponentsArgCompletion(cmd *cobra.Command, args []string, toComplete strin // componentsArgCompletionWithStack provides shell completion for component arguments with optional stack filtering. func componentsArgCompletionWithStack(cmd *cobra.Command, args []string, toComplete string, stack string) ([]string, cobra.ShellCompDirective) { - // cmd and toComplete kept for Cobra completion function signature compatibility. - _ = cmd + // toComplete kept for Cobra completion function signature compatibility. _ = toComplete if len(args) > 0 { @@ -101,9 +117,9 @@ func componentsArgCompletionWithStack(cmd *cobra.Command, args []string, toCompl var err error if stack != "" { - output, err = listTerraformComponentsForStack(stack) + output, err = listTerraformComponentsForStack(cmd, stack) } else { - output, err = listTerraformComponents() + output, err = listTerraformComponents(cmd) } if err != nil { @@ -118,7 +134,7 @@ func componentsArgCompletionWithStack(cmd *cobra.Command, args []string, toCompl func StackFlagCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // If a component was provided as the first argument, filter stacks by that component. if len(args) > 0 && args[0] != "" { - output, err := listStacksForComponent(args[0]) + output, err := listStacksForComponent(cmd, args[0]) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } @@ -126,16 +142,16 @@ func StackFlagCompletion(cmd *cobra.Command, args []string, toComplete string) ( } // Otherwise, list all stacks. - output, err := listAllStacks() + output, err := listAllStacks(cmd) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } return output, cobra.ShellCompDirectiveNoFileComp } -// isComponentDeployable checks if a component can be deployed (not abstract, not disabled). +// IsComponentDeployable checks if a component can be deployed (not abstract, not disabled). // Returns false for components with metadata.type: abstract or metadata.enabled: false. -func isComponentDeployable(componentConfig any) bool { +func IsComponentDeployable(componentConfig any) bool { // Handle nil or non-map configs - assume deployable. configMap, ok := componentConfig.(map[string]any) if !ok { @@ -161,17 +177,17 @@ func isComponentDeployable(componentConfig any) bool { return true } -// filterDeployableComponents returns only components that can be deployed. +// FilterDeployableComponents returns only components that can be deployed. // Filters out abstract and disabled components from the terraform components map. // Returns a sorted slice of deployable component names. -func filterDeployableComponents(terraformComponents map[string]any) []string { +func FilterDeployableComponents(terraformComponents map[string]any) []string { if len(terraformComponents) == 0 { return []string{} } var components []string for name, config := range terraformComponents { - if isComponentDeployable(config) { + if IsComponentDeployable(config) { components = append(components, name) } } @@ -182,8 +198,9 @@ func filterDeployableComponents(terraformComponents map[string]any) []string { // listTerraformComponents lists all deployable terraform components across all stacks. // Filters out abstract and disabled components. -func listTerraformComponents() ([]string, error) { - configAndStacksInfo := schema.ConfigAndStacksInfo{} +// The cmd parameter is used to respect global CLI flags (--base-path, --config, --config-path, --profile). +func listTerraformComponents(cmd *cobra.Command) ([]string, error) { + configAndStacksInfo := buildConfigAndStacksInfo(cmd) atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err @@ -201,7 +218,7 @@ func listTerraformComponents() ([]string, error) { if components, ok := stackMap["components"].(map[string]any); ok { if terraform, ok := components["terraform"].(map[string]any); ok { // Filter to only deployable components. - deployable := filterDeployableComponents(terraform) + deployable := FilterDeployableComponents(terraform) for _, name := range deployable { componentSet[name] = struct{}{} } @@ -221,12 +238,13 @@ func listTerraformComponents() ([]string, error) { // listTerraformComponentsForStack lists deployable terraform components for a specific stack. // Filters out abstract and disabled components. // If stack is empty, returns components from all stacks. -func listTerraformComponentsForStack(stack string) ([]string, error) { +// The cmd parameter is used to respect global CLI flags (--base-path, --config, --config-path, --profile). +func listTerraformComponentsForStack(cmd *cobra.Command, stack string) ([]string, error) { if stack == "" { - return listTerraformComponents() + return listTerraformComponents(cmd) } - configAndStacksInfo := schema.ConfigAndStacksInfo{} + configAndStacksInfo := buildConfigAndStacksInfo(cmd) atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err @@ -259,12 +277,13 @@ func listTerraformComponentsForStack(stack string) ([]string, error) { } // Filter to only deployable components and return sorted. - return filterDeployableComponents(terraform), nil + return FilterDeployableComponents(terraform), nil } // listStacksForComponent returns stacks that contain the specified component. -func listStacksForComponent(component string) ([]string, error) { - configAndStacksInfo := schema.ConfigAndStacksInfo{} +// The cmd parameter is used to respect global CLI flags (--base-path, --config, --config-path, --profile). +func listStacksForComponent(cmd *cobra.Command, component string) ([]string, error) { + configAndStacksInfo := buildConfigAndStacksInfo(cmd) atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err @@ -305,8 +324,9 @@ func stackContainsComponent(stackData any, component string) bool { } // listAllStacks returns all stacks. -func listAllStacks() ([]string, error) { - configAndStacksInfo := schema.ConfigAndStacksInfo{} +// The cmd parameter is used to respect global CLI flags (--base-path, --config, --config-path, --profile). +func listAllStacks(cmd *cobra.Command) ([]string, error) { + configAndStacksInfo := buildConfigAndStacksInfo(cmd) atmosConfig, err := initCliConfig(configAndStacksInfo, true) if err != nil { return nil, err @@ -327,8 +347,9 @@ func listAllStacks() ([]string, error) { // ValidateStackExists checks if the provided stack name exists and returns // an error with suggestions if it doesn't. -func ValidateStackExists(stack string) error { - stacks, err := listAllStacks() +// The cmd parameter is used to respect global CLI flags (--base-path, --config, --config-path, --profile). +func ValidateStackExists(cmd *cobra.Command, stack string) error { + stacks, err := listAllStacks(cmd) if err != nil { return err } diff --git a/cmd/terraform/shared/prompt_test.go b/cmd/terraform/shared/prompt_test.go index 60ba5122f1..607caed0a2 100644 --- a/cmd/terraform/shared/prompt_test.go +++ b/cmd/terraform/shared/prompt_test.go @@ -323,8 +323,8 @@ func TestStackFlagCompletion_ArgsHandling(t *testing.T) { } } -// TestIsComponentDeployable tests the helper that checks if a component can be deployed. -func TestIsComponentDeployable(t *testing.T) { +// TestIsComponentDeployableHelper tests the helper that checks if a component can be deployed. +func TestIsComponentDeployableHelper(t *testing.T) { tests := []struct { name string componentConfig any @@ -429,14 +429,14 @@ func TestIsComponentDeployable(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := isComponentDeployable(tt.componentConfig) + result := IsComponentDeployable(tt.componentConfig) assert.Equal(t, tt.expected, result) }) } } -// TestFilterDeployableComponents tests filtering a map of components to only deployable ones. -func TestFilterDeployableComponents(t *testing.T) { +// TestFilterDeployableComponentsHelper tests filtering a map of components to only deployable ones. +func TestFilterDeployableComponentsHelper(t *testing.T) { tests := []struct { name string components map[string]any @@ -517,7 +517,7 @@ func TestFilterDeployableComponents(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := filterDeployableComponents(tt.components) + result := FilterDeployableComponents(tt.components) assert.Equal(t, tt.expected, result) }) } @@ -709,7 +709,7 @@ func TestListTerraformComponents(t *testing.T) { stacksMap: tt.mockStacksMap, }) - result, err := listTerraformComponents() + result, err := listTerraformComponents(nil) if tt.expectedError { assert.Error(t, err) @@ -857,7 +857,7 @@ func TestListTerraformComponentsForStack(t *testing.T) { stacksMap: tt.mockStacksMap, }) - result, err := listTerraformComponentsForStack(tt.stack) + result, err := listTerraformComponentsForStack(nil, tt.stack) if tt.expectedError { assert.Error(t, err) @@ -898,7 +898,7 @@ func TestListTerraformComponentsForStack_EmptyStackDelegation(t *testing.T) { }, nil } - result, err := listTerraformComponentsForStack("") + result, err := listTerraformComponentsForStack(nil, "") assert.NoError(t, err) assert.True(t, describeStacksCalled) @@ -1030,7 +1030,7 @@ func TestListStacksForComponent(t *testing.T) { stacksMap: tt.mockStacksMap, }) - result, err := listStacksForComponent(tt.component) + result, err := listStacksForComponent(nil, tt.component) if tt.expectedError { assert.Error(t, err) @@ -1113,7 +1113,7 @@ func TestListAllStacks(t *testing.T) { stacksMap: tt.mockStacksMap, }) - result, err := listAllStacks() + result, err := listAllStacks(nil) if tt.expectedError { assert.Error(t, err) @@ -1298,7 +1298,7 @@ func TestValidateStackExists(t *testing.T) { stacksMap: tt.mockStacksMap, }) - err := ValidateStackExists(tt.stack) + err := ValidateStackExists(nil, tt.stack) if tt.expectError { assert.Error(t, err) @@ -1312,5 +1312,36 @@ func TestValidateStackExists(t *testing.T) { } } +// TestBuildConfigAndStacksInfo tests the buildConfigAndStacksInfo function. +func TestBuildConfigAndStacksInfo(t *testing.T) { + t.Run("nil command returns empty struct", func(t *testing.T) { + result := buildConfigAndStacksInfo(nil) + assert.Equal(t, schema.ConfigAndStacksInfo{}, result) + }) + + t.Run("command without flags returns empty values", func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + result := buildConfigAndStacksInfo(cmd) + // When no flags are set, the result should have empty values. + assert.Empty(t, result.AtmosBasePath) + assert.Empty(t, result.AtmosConfigFilesFromArg) + assert.Empty(t, result.AtmosConfigDirsFromArg) + assert.Empty(t, result.ProfilesFromArg) + }) + + t.Run("command with flags defined does not panic", func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("base-path", "", "Base path") + cmd.Flags().StringSlice("config", nil, "Config files") + cmd.Flags().StringSlice("config-path", nil, "Config paths") + cmd.Flags().StringSlice("profile", nil, "Profiles") + // Note: The function reads from viper, not directly from flags. + // This test verifies the function doesn't panic with a real command. + result := buildConfigAndStacksInfo(cmd) + // Result may have empty values since we didn't bind to viper. + _ = result + }) +} + // Ensure cfg import is used. var _ = cfg.InitCliConfig diff --git a/cmd/terraform/shell_test.go b/cmd/terraform/shell_test.go new file mode 100644 index 0000000000..72ae9da731 --- /dev/null +++ b/cmd/terraform/shell_test.go @@ -0,0 +1,109 @@ +package terraform + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + errUtils "github.com/cloudposse/atmos/errors" +) + +// TestShellCommandSetup verifies that the shell command is properly configured. +func TestShellCommandSetup(t *testing.T) { + // Verify command is registered. + require.NotNil(t, shellCmd) + + // Verify it's attached to terraformCmd. + found := false + for _, cmd := range terraformCmd.Commands() { + if cmd.Name() == "shell" { + found = true + break + } + } + assert.True(t, found, "shell should be registered as a subcommand of terraformCmd") + + // Verify command short and long descriptions. + assert.Contains(t, shellCmd.Short, "environment") + assert.Contains(t, shellCmd.Long, "Terraform") +} + +// TestShellParserSetup verifies that the shell parser is properly configured. +func TestShellParserSetup(t *testing.T) { + require.NotNil(t, shellParser, "shellParser should be initialized") + + // Verify the parser has the shell-specific flags. + registry := shellParser.Registry() + + expectedFlags := []string{ + "process-templates", + "process-functions", + "skip", + } + + for _, flagName := range expectedFlags { + assert.True(t, registry.Has(flagName), "shellParser should have %s flag registered", flagName) + } +} + +// TestShellFlagDefaults verifies that shell command flags have correct default values. +func TestShellFlagDefaults(t *testing.T) { + v := viper.New() + + // Bind parser to fresh viper instance. + err := shellParser.BindToViper(v) + require.NoError(t, err) + + // Verify default values. + assert.True(t, v.GetBool("process-templates"), "process-templates should default to true") + assert.True(t, v.GetBool("process-functions"), "process-functions should default to true") +} + +// TestShellValidation tests validation errors in the shell command. +func TestShellValidation(t *testing.T) { + t.Run("missing component returns error", func(t *testing.T) { + // Reset viper to avoid state pollution. + v := viper.New() + v.Set("stack", "test-stack") + + // Bind the parser to the fresh viper instance. + err := shellParser.BindToViper(v) + require.NoError(t, err) + + // Create a test command to execute RunE directly. + cmd := &cobra.Command{Use: "shell"} + shellParser.RegisterFlags(cmd) + + // Execute RunE with no component argument. + // In non-TTY environment, the prompt returns ErrInteractiveModeNotAvailable + // which is swallowed, leaving component empty and triggering ErrMissingComponent. + err = shellCmd.RunE(cmd, []string{}) + assert.ErrorIs(t, err, errUtils.ErrMissingComponent) + }) +} + +// TestShellCommandArgs verifies that shell command accepts the correct number of arguments. +func TestShellCommandArgs(t *testing.T) { + // The command should accept 0 or 1 argument (component name is optional). + require.NotNil(t, shellCmd.Args) + + // Verify with no args. + err := shellCmd.Args(shellCmd, []string{}) + assert.NoError(t, err, "shell command should accept 0 arguments") + + // Verify with one arg. + err = shellCmd.Args(shellCmd, []string{"my-component"}) + assert.NoError(t, err, "shell command should accept 1 argument") + + // Verify with two args (should fail). + err = shellCmd.Args(shellCmd, []string{"arg1", "arg2"}) + assert.Error(t, err, "shell command should reject more than 1 argument") +} + +// TestShellCommandUsage verifies the command usage string. +func TestShellCommandUsage(t *testing.T) { + assert.Equal(t, "shell [component]", shellCmd.Use) +} diff --git a/cmd/terraform/utils.go b/cmd/terraform/utils.go index 302110b130..ce26b85805 100644 --- a/cmd/terraform/utils.go +++ b/cmd/terraform/utils.go @@ -361,7 +361,7 @@ func handleInteractiveComponentStackSelection(info *schema.ConfigAndStacksInfo, // Validate stack exists if provided via flag (fail fast before prompting or execution). if info.Stack != "" && info.ComponentFromArg == "" { - if err := shared.ValidateStackExists(info.Stack); err != nil { + if err := shared.ValidateStackExists(cmd, info.Stack); err != nil { return err } } diff --git a/cmd/terraform/utils_test.go b/cmd/terraform/utils_test.go index a90ab5f1af..9278004e54 100644 --- a/cmd/terraform/utils_test.go +++ b/cmd/terraform/utils_test.go @@ -704,3 +704,146 @@ func TestComponentsArgCompletion_WithExistingArgs(t *testing.T) { assert.Nil(t, components) assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) } + +// TestHandleInteractiveComponentStackSelection_ValidateStackExists tests the ValidateStackExists path. +func TestHandleInteractiveComponentStackSelection_ValidateStackExists(t *testing.T) { + t.Chdir("../../examples/demo-stacks") + + tests := []struct { + name string + info *schema.ConfigAndStacksInfo + expectError bool + }{ + { + name: "valid stack with no component passes validation", + info: &schema.ConfigAndStacksInfo{ + Stack: "dev", + ComponentFromArg: "", + }, + // Note: In non-TTY environment, this won't prompt but will return nil + // since interactive mode isn't available. + expectError: false, + }, + { + name: "invalid stack with no component - validation returns error", + info: &schema.ConfigAndStacksInfo{ + Stack: "nonexistent-stack-xyz", + ComponentFromArg: "", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + err := handleInteractiveComponentStackSelection(tt.info, cmd) + + if tt.expectError { + assert.Error(t, err) + } else { + // In non-TTY environment, it should return nil (interactive mode not available). + assert.NoError(t, err) + } + }) + } +} + +// TestHandlePromptErrorDelegate tests the handlePromptError delegate function. +func TestHandlePromptErrorDelegate(t *testing.T) { + // Save original OsExit and restore it after tests. + originalOsExit := errUtils.OsExit + defer func() { + errUtils.OsExit = originalOsExit + }() + + tests := []struct { + name string + err error + promptName string + expectExit bool + expectedExitCode int + expectedReturn error + }{ + { + name: "nil error returns nil", + err: nil, + promptName: "component", + expectExit: false, + expectedReturn: nil, + }, + { + name: "ErrInteractiveModeNotAvailable returns nil", + err: errUtils.ErrInteractiveModeNotAvailable, + promptName: "stack", + expectExit: false, + expectedReturn: nil, + }, + { + name: "generic error returns the error", + err: errors.New("some error"), + promptName: "component", + expectExit: false, + expectedReturn: errors.New("some error"), + }, + { + name: "ErrUserAborted triggers exit with SIGINT code", + err: errUtils.ErrUserAborted, + promptName: "component", + expectExit: true, + expectedExitCode: errUtils.ExitCodeSIGINT, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var exitCalled bool + var exitCode int + errUtils.OsExit = func(code int) { + exitCalled = true + exitCode = code + } + + result := handlePromptError(tt.err, tt.promptName) + + if tt.expectExit { + assert.True(t, exitCalled, "OsExit should be called") + assert.Equal(t, tt.expectedExitCode, exitCode, "Exit code should match") + } else { + assert.False(t, exitCalled, "OsExit should not be called") + if tt.expectedReturn == nil { + assert.NoError(t, result) + } else { + assert.Error(t, result) + assert.Equal(t, tt.expectedReturn.Error(), result.Error()) + } + } + }) + } +} + +// TestPromptForComponentDelegate tests the promptForComponent delegate function. +func TestPromptForComponentDelegate(t *testing.T) { + // Test that it delegates to shared.PromptForComponent. + // In non-TTY environment, it should return ErrInteractiveModeNotAvailable. + cmd := &cobra.Command{Use: "test"} + _, err := promptForComponent(cmd, "") + // The function should return an error in non-TTY environment. + // This is expected behavior - in CI, interactive mode is not available. + if err != nil { + assert.ErrorIs(t, err, errUtils.ErrInteractiveModeNotAvailable) + } +} + +// TestPromptForStackDelegate tests the promptForStack delegate function. +func TestPromptForStackDelegate(t *testing.T) { + // Test that it delegates to shared.PromptForStack. + // In non-TTY environment, it should return ErrInteractiveModeNotAvailable. + cmd := &cobra.Command{Use: "test"} + _, err := promptForStack(cmd, "") + // The function should return an error in non-TTY environment. + // This is expected behavior - in CI, interactive mode is not available. + if err != nil { + assert.ErrorIs(t, err, errUtils.ErrInteractiveModeNotAvailable) + } +} diff --git a/errors/builder_test.go b/errors/builder_test.go index 329aacda1b..1087408936 100644 --- a/errors/builder_test.go +++ b/errors/builder_test.go @@ -471,41 +471,66 @@ func TestErrorBuilder_WithCause(t *testing.T) { } func TestErrorBuilder_WithCausef(t *testing.T) { - t.Run("creates formatted cause error", func(t *testing.T) { - stackName := "dev-us-east-1" - - err := Build(ErrInvalidStack). - WithCausef("stack `%s` does not exist", stackName). - Err() - - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrInvalidStack)) - assert.Contains(t, err.Error(), "stack `dev-us-east-1` does not exist") - }) - - t.Run("works with multiple format arguments", func(t *testing.T) { - err := Build(ErrInvalidComponent). - WithCausef("component `%s` not found in stack `%s`", "vpc", "prod"). - Err() - - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrInvalidComponent)) - assert.Contains(t, err.Error(), "component `vpc` not found in stack `prod`") - }) - - t.Run("chains with other builder methods", func(t *testing.T) { - err := Build(ErrInvalidStack). - WithCausef("stack `%s` does not exist", "demo"). - WithExplanation("The stack was not found"). - WithHint("Check your configuration"). - Err() - - assert.Error(t, err) - assert.True(t, errors.Is(err, ErrInvalidStack)) - assert.Contains(t, err.Error(), "stack `demo` does not exist") + tests := []struct { + name string + build func() error + wantIs error + wantContains string + assertExtra func(t *testing.T, err error) + }{ + { + name: "creates formatted cause error", + build: func() error { + stackName := "dev-us-east-1" + return Build(ErrInvalidStack). + WithCausef("stack `%s` does not exist", stackName). + Err() + }, + wantIs: ErrInvalidStack, + wantContains: "stack `dev-us-east-1` does not exist", + }, + { + name: "works with multiple format arguments", + build: func() error { + return Build(ErrInvalidComponent). + WithCausef("component `%s` not found in stack `%s`", "vpc", "prod"). + Err() + }, + wantIs: ErrInvalidComponent, + wantContains: "component `vpc` not found in stack `prod`", + }, + { + name: "chains with other builder methods", + build: func() error { + return Build(ErrInvalidStack). + WithCausef("stack `%s` does not exist", "demo"). + WithExplanation("The stack was not found"). + WithHint("Check your configuration"). + Err() + }, + wantIs: ErrInvalidStack, + wantContains: "stack `demo` does not exist", + assertExtra: func(t *testing.T, err error) { + // Verify hint is present. + hints := errors.GetAllHints(err) + assert.Contains(t, hints, "Check your configuration") + }, + }, + } - // Verify hint is present. - hints := errors.GetAllHints(err) - assert.Contains(t, hints, "Check your configuration") - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.build() + assert.Error(t, err) + if tt.wantIs != nil { + assert.True(t, errors.Is(err, tt.wantIs)) + } + if tt.wantContains != "" { + assert.Contains(t, err.Error(), tt.wantContains) + } + if tt.assertExtra != nil { + tt.assertExtra(t, err) + } + }) + } } diff --git a/go.mod b/go.mod index 1bc7a7b03a..55f99c0a0c 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( cloud.google.com/go/storage v1.58.0 dario.cat/mergo v1.0.2 github.com/99designs/keyring v1.2.2 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 github.com/HdrHistogram/hdrhistogram-go v1.2.0 github.com/Masterminds/semver/v3 v3.4.0 @@ -23,15 +23,15 @@ require ( github.com/arsham/figurine v1.3.0 github.com/atotto/clipboard v0.1.4 github.com/aws/aws-sdk-go-v2 v1.41.1 - github.com/aws/aws-sdk-go-v2/config v1.32.6 - github.com/aws/aws-sdk-go-v2/credentials v1.19.6 + github.com/aws/aws-sdk-go-v2/config v1.32.7 + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18 - github.com/aws/aws-sdk-go-v2/service/ecr v1.55.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 - github.com/aws/aws-sdk-go-v2/service/ssm v1.67.7 - github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 - github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 + github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 + github.com/aws/aws-sdk-go-v2/service/ssm v1.67.8 + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 github.com/aws/smithy-go v1.24.0 github.com/bmatcuk/doublestar/v4 v4.9.1 github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 @@ -49,7 +49,7 @@ require ( github.com/elewis787/boa v0.1.3 github.com/fatih/color v1.18.0 github.com/gabriel-vasile/mimetype v1.4.12 - github.com/getsentry/sentry-go v0.40.0 + github.com/getsentry/sentry-go v0.41.0 github.com/go-git/go-git/v5 v5.16.4 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/gobwas/glob v0.2.3 @@ -88,7 +88,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/otiai10/copy v1.14.1 github.com/pkg/errors v0.9.1 - github.com/posthog/posthog-go v1.8.2 + github.com/posthog/posthog-go v1.9.0 github.com/redis/go-redis/v9 v9.12.1 github.com/samber/lo v1.52.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 @@ -105,8 +105,8 @@ require ( go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/oauth2 v0.34.0 - golang.org/x/term v0.38.0 - golang.org/x/text v0.32.0 + golang.org/x/term v0.39.0 + golang.org/x/text v0.33.0 google.golang.org/api v0.258.0 google.golang.org/grpc v1.78.0 gopkg.in/ini.v1 v1.67.0 @@ -156,17 +156,17 @@ require ( github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.4 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bearsh/hid v1.6.0 // indirect @@ -396,12 +396,12 @@ require ( go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect gocloud.dev v0.41.0 // indirect - golang.org/x/crypto v0.46.0 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect diff --git a/go.sum b/go.sum index 5d17ba097b..6b7065755b 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= @@ -64,8 +64,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= @@ -171,46 +171,46 @@ github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6ce github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= -github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= -github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18 h1:9vWXHtaepwoAl/UuKzxwgOoJDXPCC3hvgNMfcmdS2Tk= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18/go.mod h1:sKuUZ+MwUTuJbYvZ8pK0x10LvgcJK3Y4rmh63YBekwk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= -github.com/aws/aws-sdk-go-v2/service/ecr v1.55.0 h1:Mz6rvVhqmqGPzZNDLolW9IwPzhL/V+QS+dvX+vm/zh8= -github.com/aws/aws-sdk-go-v2/service/ecr v1.55.0/go.mod h1:8n8vVvu7LzveA0or4iWQwNndJStpKOX4HiVHM5jax2U= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= +github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 h1:B7f9R99lCF83XlolTg6d6Lvghyto+/VU83ZrneAVfK8= +github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1/go.mod h1:cpYRXx5BkmS3mwWRKPbWSPKmyAUNL7aLWAPiiinwk/U= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 h1:C2dUPSnEpy4voWFIq3JNd8gN0Y5vYGDo44eUE58a/p8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.4 h1:EKXYJ8kgz4fiqef8xApu7eH0eae2SrVG+oHCLFybMRI= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.4/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/ssm v1.67.7 h1:0q42w8/mywPCzQD1IoWIBUCYfBJc5+fLwtZNpHffBSM= -github.com/aws/aws-sdk-go-v2/service/ssm v1.67.7/go.mod h1:urlU9nfKJEfi0+8T9luB3f3Y0UnomH/yxI7tTrfH9es= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/ssm v1.67.8 h1:31Llf5VfrZ78YvYs7sWcS7L2m3waikzRc6q1nYenVS4= +github.com/aws/aws-sdk-go-v2/service/ssm v1.67.8/go.mod h1:/jgaDlU1UImoxTxhRNxXHvBAPqPZQ8oCjcPbbkR6kac= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -431,8 +431,8 @@ github.com/fsouza/fake-gcs-server v1.52.2 h1:j6ne83nqHrlX5EEor7WWVIKdBsztGtwJ1J2 github.com/fsouza/fake-gcs-server v1.52.2/go.mod h1:47HKyIkz6oLTes1R8vEaHLwXfzYsGfmDUk1ViHHAUsA= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo= -github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= +github.com/getsentry/sentry-go v0.41.0 h1:q/dQZOlEIb4lhxQSjJhQqtRr3vwrJ6Ahe1C9zv+ryRo= +github.com/getsentry/sentry-go v0.41.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= @@ -911,8 +911,8 @@ github.com/playwright-community/playwright-go v0.4702.0/go.mod h1:bpArn5TqNzmP0j github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v1.8.2 h1:v/ajsM8lq+2Z3OlQbTVWqiHI+hyh9Cd4uiQt1wFlehE= -github.com/posthog/posthog-go v1.8.2/go.mod h1:ueZiJCmHezyDHI/swIR1RmOfktLehnahJnFxEvQ9mnQ= +github.com/posthog/posthog-go v1.9.0 h1:7tRfnaHqPNrBNTnSnFLQwJ5aVz6LOBngiwl15lD8bHU= +github.com/posthog/posthog-go v1.9.0/go.mod h1:0i1H2BlsK9mHvHGc9Kp6oenUlHUqPl45hWzRtR/2PVI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -1188,8 +1188,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= @@ -1232,8 +1232,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= @@ -1291,8 +1291,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1302,8 +1302,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1314,8 +1314,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= diff --git a/pkg/telemetry/mock/mock_posthog_client.go b/pkg/telemetry/mock/mock_posthog_client.go index 72d3c59f63..71f616a7ae 100644 --- a/pkg/telemetry/mock/mock_posthog_client.go +++ b/pkg/telemetry/mock/mock_posthog_client.go @@ -3,16 +3,17 @@ // // Generated by this command: // -// mockgen github.com/posthog/posthog-go Client +// mockgen -destination=pkg/telemetry/mock/mock_posthog_client.go -package=mock_telemetry github.com/posthog/posthog-go Client // // Package mock_telemetry is a generated GoMock package. package mock_telemetry import ( + context "context" reflect "reflect" - posthog_go "github.com/posthog/posthog-go" + posthog "github.com/posthog/posthog-go" gomock "go.uber.org/mock/gomock" ) @@ -54,8 +55,22 @@ func (mr *MockClientMockRecorder) Close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close)) } +// CloseWithContext mocks base method. +func (m *MockClient) CloseWithContext(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloseWithContext", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CloseWithContext indicates an expected call of CloseWithContext. +func (mr *MockClientMockRecorder) CloseWithContext(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseWithContext", reflect.TypeOf((*MockClient)(nil).CloseWithContext), arg0) +} + // Enqueue mocks base method. -func (m *MockClient) Enqueue(arg0 posthog_go.Message) error { +func (m *MockClient) Enqueue(arg0 posthog.Message) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Enqueue", arg0) ret0, _ := ret[0].(error) @@ -69,7 +84,7 @@ func (mr *MockClientMockRecorder) Enqueue(arg0 any) *gomock.Call { } // GetAllFlags mocks base method. -func (m *MockClient) GetAllFlags(arg0 posthog_go.FeatureFlagPayloadNoKey) (map[string]any, error) { +func (m *MockClient) GetAllFlags(arg0 posthog.FeatureFlagPayloadNoKey) (map[string]any, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAllFlags", arg0) ret0, _ := ret[0].(map[string]any) @@ -84,7 +99,7 @@ func (mr *MockClientMockRecorder) GetAllFlags(arg0 any) *gomock.Call { } // GetFeatureFlag mocks base method. -func (m *MockClient) GetFeatureFlag(arg0 posthog_go.FeatureFlagPayload) (any, error) { +func (m *MockClient) GetFeatureFlag(arg0 posthog.FeatureFlagPayload) (any, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFeatureFlag", arg0) ret0, _ := ret[0].(any) @@ -99,7 +114,7 @@ func (mr *MockClientMockRecorder) GetFeatureFlag(arg0 any) *gomock.Call { } // GetFeatureFlagPayload mocks base method. -func (m *MockClient) GetFeatureFlagPayload(arg0 posthog_go.FeatureFlagPayload) (string, error) { +func (m *MockClient) GetFeatureFlagPayload(arg0 posthog.FeatureFlagPayload) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFeatureFlagPayload", arg0) ret0, _ := ret[0].(string) @@ -114,10 +129,10 @@ func (mr *MockClientMockRecorder) GetFeatureFlagPayload(arg0 any) *gomock.Call { } // GetFeatureFlags mocks base method. -func (m *MockClient) GetFeatureFlags() ([]posthog_go.FeatureFlag, error) { +func (m *MockClient) GetFeatureFlags() ([]posthog.FeatureFlag, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFeatureFlags") - ret0, _ := ret[0].([]posthog_go.FeatureFlag) + ret0, _ := ret[0].([]posthog.FeatureFlag) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -128,20 +143,6 @@ func (mr *MockClientMockRecorder) GetFeatureFlags() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeatureFlags", reflect.TypeOf((*MockClient)(nil).GetFeatureFlags)) } -// GetLastCapturedEvent mocks base method. -func (m *MockClient) GetLastCapturedEvent() *posthog_go.Capture { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLastCapturedEvent") - ret0, _ := ret[0].(*posthog_go.Capture) - return ret0 -} - -// GetLastCapturedEvent indicates an expected call of GetLastCapturedEvent. -func (mr *MockClientMockRecorder) GetLastCapturedEvent() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastCapturedEvent", reflect.TypeOf((*MockClient)(nil).GetLastCapturedEvent)) -} - // GetRemoteConfigPayload mocks base method. func (m *MockClient) GetRemoteConfigPayload(arg0 string) (string, error) { m.ctrl.T.Helper() @@ -158,7 +159,7 @@ func (mr *MockClientMockRecorder) GetRemoteConfigPayload(arg0 any) *gomock.Call } // IsFeatureEnabled mocks base method. -func (m *MockClient) IsFeatureEnabled(arg0 posthog_go.FeatureFlagPayload) (any, error) { +func (m *MockClient) IsFeatureEnabled(arg0 posthog.FeatureFlagPayload) (any, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsFeatureEnabled", arg0) ret0, _ := ret[0].(any) diff --git a/tests/cli_component_menu_filtering_test.go b/tests/cli_component_menu_filtering_test.go new file mode 100644 index 0000000000..fde931164a --- /dev/null +++ b/tests/cli_component_menu_filtering_test.go @@ -0,0 +1,471 @@ +package tests + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudposse/atmos/cmd/terraform/shared" + "github.com/cloudposse/atmos/internal/exec" + "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/schema" +) + +// TestComponentMenuFilteringAbstractComponents tests that abstract components +// (metadata.type: abstract) are filtered out from component listings. +// This tests the fix for PR #1977. +func TestComponentMenuFilteringAbstractComponents(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + // Get all stacks configuration. + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + "", // filterByStack - get all stacks + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err) + require.NotNil(t, stacksMap) + + // Verify vpc-base is marked as abstract in the dev stack. + devStack, ok := stacksMap["dev"].(map[string]any) + require.True(t, ok, "dev stack should exist") + + components, ok := devStack["components"].(map[string]any) + require.True(t, ok, "dev stack should have components") + + terraform, ok := components["terraform"].(map[string]any) + require.True(t, ok, "dev stack should have terraform components") + + // vpc-base should exist and be marked as abstract. + vpcBase, ok := terraform["vpc-base"].(map[string]any) + require.True(t, ok, "vpc-base should exist in dev stack") + + metadata, ok := vpcBase["metadata"].(map[string]any) + require.True(t, ok, "vpc-base should have metadata") + + metadataType, ok := metadata["type"].(string) + require.True(t, ok, "vpc-base metadata should have type") + assert.Equal(t, "abstract", metadataType, "vpc-base should be abstract") + + // Verify that isComponentDeployable correctly identifies abstract components. + // Test the helper function directly. + assert.False(t, shared.IsComponentDeployable(vpcBase), + "vpc-base (abstract) should NOT be deployable") + + // Regular vpc component should be deployable. + vpc, ok := terraform["vpc"].(map[string]any) + require.True(t, ok, "vpc should exist in dev stack") + assert.True(t, shared.IsComponentDeployable(vpc), + "vpc should be deployable") +} + +// TestComponentMenuFilteringDisabledComponents tests that disabled components +// (metadata.enabled: false) are filtered out from component listings. +// This tests the fix for PR #1977. +func TestComponentMenuFilteringDisabledComponents(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + // Get all stacks configuration. + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + "", // filterByStack - get all stacks + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err) + require.NotNil(t, stacksMap) + + // Verify eks is disabled in the dev stack. + devStack, ok := stacksMap["dev"].(map[string]any) + require.True(t, ok, "dev stack should exist") + + components, ok := devStack["components"].(map[string]any) + require.True(t, ok, "dev stack should have components") + + terraform, ok := components["terraform"].(map[string]any) + require.True(t, ok, "dev stack should have terraform components") + + // eks should exist in dev stack and be disabled. + eks, ok := terraform["eks"].(map[string]any) + require.True(t, ok, "eks should exist in dev stack") + + metadata, ok := eks["metadata"].(map[string]any) + require.True(t, ok, "eks should have metadata in dev stack") + + enabled, ok := metadata["enabled"].(bool) + require.True(t, ok, "eks metadata should have enabled field") + assert.False(t, enabled, "eks should be disabled in dev stack") + + // Verify that isComponentDeployable correctly identifies disabled components. + assert.False(t, shared.IsComponentDeployable(eks), + "eks (disabled in dev) should NOT be deployable") + + // Verify eks is NOT disabled in prod stack. + prodStack, ok := stacksMap["prod"].(map[string]any) + require.True(t, ok, "prod stack should exist") + + prodComponents, ok := prodStack["components"].(map[string]any) + require.True(t, ok, "prod stack should have components") + + prodTerraform, ok := prodComponents["terraform"].(map[string]any) + require.True(t, ok, "prod stack should have terraform components") + + prodEks, ok := prodTerraform["eks"].(map[string]any) + require.True(t, ok, "eks should exist in prod stack") + + // eks in prod should be deployable (not disabled). + assert.True(t, shared.IsComponentDeployable(prodEks), + "eks in prod stack should be deployable") +} + +// TestComponentMenuFilteringStackScoped tests that when a stack is specified, +// only components deployed in that specific stack appear. +// This tests the fix for PR #1977. +func TestComponentMenuFilteringStackScoped(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + tests := []struct { + name string + stack string + expectedComponents []string + description string + }{ + { + name: "dev stack has vpc only (eks disabled, no rds)", + stack: "dev", + expectedComponents: []string{"vpc"}, + description: "dev stack should only show vpc (eks is disabled, rds not present)", + }, + { + name: "staging stack has vpc and rds (no eks)", + stack: "staging", + expectedComponents: []string{"rds", "vpc"}, + description: "staging stack should show rds and vpc (eks not present in this stack)", + }, + { + name: "prod stack has all components", + stack: "prod", + expectedComponents: []string{"eks", "rds", "vpc"}, + description: "prod stack should show all deployable components", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Get stack-specific configuration. + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + tt.stack, // filterByStack + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err, tt.description) + require.NotNil(t, stacksMap, tt.description) + + // Get the specific stack. + stackData, ok := stacksMap[tt.stack].(map[string]any) + require.True(t, ok, "stack %s should exist", tt.stack) + + components, ok := stackData["components"].(map[string]any) + require.True(t, ok, "stack %s should have components", tt.stack) + + terraform, ok := components["terraform"].(map[string]any) + require.True(t, ok, "stack %s should have terraform components", tt.stack) + + // Filter to only deployable components. + deployable := shared.FilterDeployableComponents(terraform) + sort.Strings(deployable) + + assert.Equal(t, tt.expectedComponents, deployable, tt.description) + }) + } +} + +// TestComponentMenuFilteringAllStacks tests that when no stack is specified, +// all deployable components across all stacks are returned. +// This tests the fix for PR #1977. +func TestComponentMenuFilteringAllStacks(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + // Get all stacks configuration. + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + "", // filterByStack - get all stacks + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err) + require.NotNil(t, stacksMap) + + // Collect unique deployable component names from all stacks. + componentSet := make(map[string]struct{}) + for _, stackData := range stacksMap { + if stackMap, ok := stackData.(map[string]any); ok { + if components, ok := stackMap["components"].(map[string]any); ok { + if terraform, ok := components["terraform"].(map[string]any); ok { + // Filter to only deployable components. + deployable := shared.FilterDeployableComponents(terraform) + for _, name := range deployable { + componentSet[name] = struct{}{} + } + } + } + } + } + + // Convert to sorted slice. + var allComponents []string + for name := range componentSet { + allComponents = append(allComponents, name) + } + sort.Strings(allComponents) + + // Expected: eks, rds, vpc (but NOT vpc-base which is abstract). + expected := []string{"eks", "rds", "vpc"} + assert.Equal(t, expected, allComponents, + "all stacks should have eks, rds, vpc as deployable components (vpc-base filtered out)") +} + +// TestComponentMenuFilteringVerifyStackStructure verifies the test fixture structure +// matches what was tested manually via shell commands. +func TestComponentMenuFilteringVerifyStackStructure(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + // Get all stacks configuration. + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + "", // filterByStack - get all stacks + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err) + require.NotNil(t, stacksMap) + + // Verify the fixture has exactly 3 stacks: dev, staging, prod. + var stackNames []string + for stackName := range stacksMap { + stackNames = append(stackNames, stackName) + } + sort.Strings(stackNames) + assert.Equal(t, []string{"dev", "prod", "staging"}, stackNames, + "fixture should have exactly dev, prod, staging stacks") + + // Verify component presence matches documentation. + componentPresence := map[string]map[string]struct{}{ + "dev": {}, + "staging": {}, + "prod": {}, + } + + for stackName, stackData := range stacksMap { + if stackMap, ok := stackData.(map[string]any); ok { + if components, ok := stackMap["components"].(map[string]any); ok { + if terraform, ok := components["terraform"].(map[string]any); ok { + for componentName := range terraform { + componentPresence[stackName][componentName] = struct{}{} + } + } + } + } + } + + // Verify dev stack has: vpc-base, vpc, eks (but eks is disabled). + _, hasVpcBase := componentPresence["dev"]["vpc-base"] + _, hasVpc := componentPresence["dev"]["vpc"] + _, hasEks := componentPresence["dev"]["eks"] + _, hasRds := componentPresence["dev"]["rds"] + assert.True(t, hasVpcBase, "dev should have vpc-base (abstract)") + assert.True(t, hasVpc, "dev should have vpc") + assert.True(t, hasEks, "dev should have eks (disabled)") + assert.False(t, hasRds, "dev should NOT have rds") + + // Verify staging stack has: vpc-base, vpc, rds (no eks). + _, hasVpcBase = componentPresence["staging"]["vpc-base"] + _, hasVpc = componentPresence["staging"]["vpc"] + _, hasRds = componentPresence["staging"]["rds"] + _, hasEks = componentPresence["staging"]["eks"] + assert.True(t, hasVpcBase, "staging should have vpc-base (abstract)") + assert.True(t, hasVpc, "staging should have vpc") + assert.True(t, hasRds, "staging should have rds") + assert.False(t, hasEks, "staging should NOT have eks") + + // Verify prod stack has: vpc-base, vpc, eks, rds. + _, hasVpcBase = componentPresence["prod"]["vpc-base"] + _, hasVpc = componentPresence["prod"]["vpc"] + _, hasEks = componentPresence["prod"]["eks"] + _, hasRds = componentPresence["prod"]["rds"] + assert.True(t, hasVpcBase, "prod should have vpc-base (abstract)") + assert.True(t, hasVpc, "prod should have vpc") + assert.True(t, hasEks, "prod should have eks") + assert.True(t, hasRds, "prod should have rds") +} + +// TestFilterDeployableComponentsFixture tests the FilterDeployableComponents helper +// using the actual fixture data. +func TestFilterDeployableComponentsFixture(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + // Get all stacks configuration. + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + "", // filterByStack - get all stacks + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err) + + tests := []struct { + stack string + expectedDeployable []string + }{ + { + stack: "dev", + expectedDeployable: []string{"vpc"}, // eks disabled, vpc-base abstract, no rds + }, + { + stack: "staging", + expectedDeployable: []string{"rds", "vpc"}, // vpc-base abstract, no eks + }, + { + stack: "prod", + expectedDeployable: []string{"eks", "rds", "vpc"}, // vpc-base abstract + }, + } + + for _, tt := range tests { + t.Run(tt.stack, func(t *testing.T) { + stackData := stacksMap[tt.stack].(map[string]any) + components := stackData["components"].(map[string]any) + terraform := components["terraform"].(map[string]any) + + deployable := shared.FilterDeployableComponents(terraform) + assert.Equal(t, tt.expectedDeployable, deployable) + }) + } +} + +// TestIsComponentDeployableFixture tests the IsComponentDeployable helper +// using the actual fixture data. +func TestIsComponentDeployableFixture(t *testing.T) { + t.Chdir("./fixtures/scenarios/component-menu-filtering") + + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, true) + require.NoError(t, err) + + // Get dev stack which has vpc-base (abstract) and eks (disabled). + stacksMap, err := exec.ExecuteDescribeStacks( + &atmosConfig, + "dev", // filterByStack + nil, // components + nil, // componentTypes + nil, // sections + true, // ignoreMissingFiles + true, // processTemplates + true, // processYamlFunctions + false, // includeEmptyStacks + nil, // skip + nil, // authManager + ) + require.NoError(t, err) + + devStack := stacksMap["dev"].(map[string]any) + components := devStack["components"].(map[string]any) + terraform := components["terraform"].(map[string]any) + + tests := []struct { + component string + expectedDeployable bool + reason string + }{ + { + component: "vpc-base", + expectedDeployable: false, + reason: "abstract component", + }, + { + component: "vpc", + expectedDeployable: true, + reason: "regular deployable component", + }, + { + component: "eks", + expectedDeployable: false, + reason: "disabled component", + }, + } + + for _, tt := range tests { + t.Run(tt.component, func(t *testing.T) { + componentConfig := terraform[tt.component] + result := shared.IsComponentDeployable(componentConfig) + assert.Equal(t, tt.expectedDeployable, result, + "%s should %sbe deployable (%s)", + tt.component, + map[bool]string{true: "", false: "NOT "}[tt.expectedDeployable], + tt.reason) + }) + } +} diff --git a/tests/fixtures/scenarios/component-menu-filtering/README.md b/tests/fixtures/scenarios/component-menu-filtering/README.md new file mode 100644 index 0000000000..d5e2a8ca9e --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/README.md @@ -0,0 +1,157 @@ +# Component Menu Filtering Test Fixture + +This fixture tests the component menu filtering functionality in Atmos, specifically: + +1. **Abstract component filtering**: Components with `metadata.type: abstract` should not appear in interactive component selection menus +2. **Disabled component filtering**: Components with `metadata.enabled: false` should not appear in interactive component selection menus +3. **Stack-scoped filtering**: When `--stack` flag is provided, only components deployed in that specific stack should appear + +## Background + +When users run Atmos terraform commands interactively (without specifying a component), they are presented with a menu to select a component. This menu should: + +- Filter out abstract components (base configurations meant for inheritance only) +- Filter out disabled components (components explicitly marked as not deployable) +- When a stack is specified via `--stack`, show only components that exist in that stack + +## Fixture Structure + +``` +component-menu-filtering/ +├── README.md +├── atmos.yaml +├── components/ +│ └── terraform/ +│ ├── vpc/main.tf # VPC component +│ ├── eks/main.tf # EKS component +│ └── rds/main.tf # RDS component +└── stacks/ + ├── catalog/ + │ ├── vpc.yaml # Abstract base VPC (type: abstract) + │ ├── eks.yaml # EKS base configuration + │ └── rds.yaml # RDS base configuration + └── deploy/ + ├── dev.yaml # Has: vpc, eks (disabled) + ├── staging.yaml # Has: vpc, rds + └── prod.yaml # Has: vpc, eks, rds +``` + +## Component Configuration Summary + +| Component | dev stack | staging stack | prod stack | Notes | +|-----------|-----------|---------------|------------|-------| +| vpc-base | N/A | N/A | N/A | Abstract (catalog only) | +| vpc | Yes | Yes | Yes | Deployable | +| eks | Disabled | No | Yes | Disabled in dev | +| rds | No | Yes | Yes | Not in dev | + +## Manual Testing Commands + +### Test 1: Verify data setup with describe stacks + +```bash +# Change to fixture directory +cd tests/fixtures/scenarios/component-menu-filtering + +# List all stacks +atmos describe stacks --format json | jq 'keys' +# Expected: ["dev", "prod", "staging"] + +# Show components with their metadata per stack +atmos describe stacks --format json | jq 'to_entries[] | {stack: .key, components: [.value.components.terraform | to_entries[] | {name: .key, type: .value.metadata.type, enabled: .value.metadata.enabled}]}' +# Expected: +# dev: vpc-base (abstract), vpc (enabled), eks (enabled: false) +# staging: vpc-base (abstract), vpc, rds +# prod: vpc-base (abstract), vpc, eks, rds + +# Verify vpc-base is abstract +atmos describe component vpc-base --stack dev | grep -A3 "^metadata:" +# Expected: type: abstract + +# Verify eks is disabled in dev +atmos describe component eks --stack dev | grep -A3 "^metadata:" +# Expected: enabled: false +``` + +### Test 2: Verify shell completion filters correctly (Non-TTY testable) + +Shell completion uses the same filtering logic as the interactive selector. These tests verify the filtering works correctly: + +```bash +cd tests/fixtures/scenarios/component-menu-filtering + +# Test all components completion (no --stack) +atmos __complete terraform plan "" 2>/dev/null | grep -v "^:" +# Expected: eks, rds, vpc +# NOTE: vpc-base (abstract) should NOT appear + +# Test dev stack - should show only vpc (eks is disabled, rds doesn't exist) +atmos __complete terraform plan --stack dev "" 2>/dev/null | grep -v "^:" +# Expected: vpc + +# Test staging stack - should show vpc and rds (no eks in staging) +atmos __complete terraform plan --stack staging "" 2>/dev/null | grep -v "^:" +# Expected: rds, vpc + +# Test prod stack - should show all three deployable components +atmos __complete terraform plan --stack prod "" 2>/dev/null | grep -v "^:" +# Expected: eks, rds, vpc +``` + +### Test 3: Verify interactive selector (TTY only) + +The interactive selector requires a TTY environment. Test these manually in an interactive terminal: + +```bash +cd tests/fixtures/scenarios/component-menu-filtering + +# Without component - should show interactive selector +# In TTY: Shows selector with vpc, eks, rds (not vpc-base) +# In non-TTY: Returns error "stack is required" +atmos terraform plan + +# With --stack but no component - runs multi-component execution +# (Runs all deployable components in the stack) +atmos terraform plan --stack dev +# In TTY or non-TTY: Runs vpc component (only deployable component in dev) + +# Verify a specific component works +atmos terraform plan vpc --stack dev +# Should run terraform plan for vpc in dev stack +``` + +### Test 4: Verify tab completion in interactive shell + +In a bash/zsh shell with Atmos completions installed: + +```bash +cd tests/fixtures/scenarios/component-menu-filtering + +# Type and press TAB twice +atmos terraform plan +# Expected: eks, rds, vpc (no vpc-base) + +atmos terraform plan --stack dev +# Expected: vpc only + +atmos terraform plan --stack staging +# Expected: rds, vpc + +atmos terraform plan --stack prod +# Expected: eks, rds, vpc +``` + +## Expected Behavior Summary + +1. **Abstract components (`metadata.type: abstract`)**: Never appear in menus or completions +2. **Disabled components (`metadata.enabled: false`)**: Never appear in menus or completions +3. **Stack filtering**: When `--stack` is provided, only components in that stack appear +4. **All stacks mode**: Without `--stack`, shows union of all deployable components across all stacks + +## Related Code + +- `cmd/terraform/shared/prompt.go`: Contains the component filtering logic +- `isComponentDeployable()`: Checks if a component is abstract or disabled +- `filterDeployableComponents()`: Filters out non-deployable components +- `listTerraformComponentsForStack()`: Lists components for a specific stack +- `listTerraformComponents()`: Lists all deployable components across all stacks diff --git a/tests/fixtures/scenarios/component-menu-filtering/atmos.yaml b/tests/fixtures/scenarios/component-menu-filtering/atmos.yaml new file mode 100644 index 0000000000..3d12c42db5 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/atmos.yaml @@ -0,0 +1,25 @@ +base_path: "./" + +components: + terraform: + base_path: "components/terraform" + apply_auto_approve: false + deploy_run_init: true + init_run_reconfigure: true + auto_generate_backend_file: false + +stacks: + base_path: "stacks" + included_paths: + - "deploy/**/*" + excluded_paths: + - "**/_defaults.yaml" + name_pattern: "{stage}" + +logs: + file: "/dev/stderr" + level: Info + +settings: + telemetry: + enabled: false diff --git a/tests/fixtures/scenarios/component-menu-filtering/components/terraform/eks/main.tf b/tests/fixtures/scenarios/component-menu-filtering/components/terraform/eks/main.tf new file mode 100644 index 0000000000..f4ecd1de71 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/components/terraform/eks/main.tf @@ -0,0 +1,29 @@ +# EKS Component - Mock for testing + +variable "cluster_name" { + type = string + default = "eks-cluster" + description = "Name of the EKS cluster" +} + +variable "kubernetes_version" { + type = string + default = "1.28" + description = "Kubernetes version" +} + +variable "node_count" { + type = number + default = 3 + description = "Number of worker nodes" +} + +output "cluster_name" { + value = var.cluster_name + description = "The EKS cluster name" +} + +output "kubernetes_version" { + value = var.kubernetes_version + description = "The Kubernetes version" +} diff --git a/tests/fixtures/scenarios/component-menu-filtering/components/terraform/rds/main.tf b/tests/fixtures/scenarios/component-menu-filtering/components/terraform/rds/main.tf new file mode 100644 index 0000000000..d92abd6107 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/components/terraform/rds/main.tf @@ -0,0 +1,29 @@ +# RDS Component - Mock for testing + +variable "db_name" { + type = string + default = "mydb" + description = "Name of the database" +} + +variable "engine" { + type = string + default = "postgres" + description = "Database engine" +} + +variable "instance_class" { + type = string + default = "db.t3.medium" + description = "RDS instance class" +} + +output "db_name" { + value = var.db_name + description = "The database name" +} + +output "engine" { + value = var.engine + description = "The database engine" +} diff --git a/tests/fixtures/scenarios/component-menu-filtering/components/terraform/vpc/main.tf b/tests/fixtures/scenarios/component-menu-filtering/components/terraform/vpc/main.tf new file mode 100644 index 0000000000..3cfda4e83f --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/components/terraform/vpc/main.tf @@ -0,0 +1,29 @@ +# VPC Component - Mock for testing + +variable "vpc_cidr" { + type = string + default = "10.0.0.0/16" + description = "CIDR block for the VPC" +} + +variable "environment" { + type = string + default = "dev" + description = "Environment name" +} + +variable "enable_dns_support" { + type = bool + default = true + description = "Enable DNS support in VPC" +} + +output "vpc_cidr" { + value = var.vpc_cidr + description = "The VPC CIDR block" +} + +output "environment" { + value = var.environment + description = "The environment name" +} diff --git a/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/eks.yaml b/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/eks.yaml new file mode 100644 index 0000000000..d995369684 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/eks.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +# EKS catalog configuration + +components: + terraform: + eks: + metadata: + component: eks + vars: + cluster_name: "eks-cluster" + kubernetes_version: "1.28" + node_count: 3 diff --git a/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/rds.yaml b/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/rds.yaml new file mode 100644 index 0000000000..c7eb45944b --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/rds.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +# RDS catalog configuration + +components: + terraform: + rds: + metadata: + component: rds + vars: + db_name: "mydb" + engine: "postgres" + instance_class: "db.t3.medium" diff --git a/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/vpc.yaml b/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/vpc.yaml new file mode 100644 index 0000000000..4d12784d08 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/stacks/catalog/vpc.yaml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +# VPC catalog configuration +# Contains abstract base component and deployable component definition + +components: + terraform: + # Abstract VPC base - should NOT appear in menus + # This is a base configuration meant to be inherited + vpc-base: + metadata: + type: abstract + component: vpc + vars: + enable_dns_support: true + + # Deployable VPC component - inherits from vpc-base + vpc: + metadata: + component: vpc + inherits: + - vpc-base + vars: + vpc_cidr: "10.0.0.0/16" diff --git a/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/dev.yaml b/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/dev.yaml new file mode 100644 index 0000000000..6432586c51 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/dev.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +# Dev stack configuration +# - vpc: deployable +# - eks: disabled (should NOT appear in menu) +# - rds: not present in this stack + +vars: + stage: dev + +import: + - catalog/vpc + - catalog/eks + +components: + terraform: + vpc: + vars: + vpc_cidr: "10.1.0.0/16" + environment: "dev" + + # EKS is disabled in dev - should NOT appear in component menu + eks: + metadata: + enabled: false + vars: + cluster_name: "eks-dev" + node_count: 1 diff --git a/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/prod.yaml b/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/prod.yaml new file mode 100644 index 0000000000..18cf93d579 --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/prod.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +# Production stack configuration +# - vpc: deployable +# - eks: deployable +# - rds: deployable + +vars: + stage: prod + +import: + - catalog/vpc + - catalog/eks + - catalog/rds + +components: + terraform: + vpc: + vars: + vpc_cidr: "10.0.0.0/16" + environment: "prod" + + eks: + vars: + cluster_name: "eks-prod" + kubernetes_version: "1.28" + node_count: 5 + + rds: + vars: + db_name: "prod-db" + instance_class: "db.r5.large" diff --git a/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/staging.yaml b/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/staging.yaml new file mode 100644 index 0000000000..1d24bb557f --- /dev/null +++ b/tests/fixtures/scenarios/component-menu-filtering/stacks/deploy/staging.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +# Staging stack configuration +# - vpc: deployable +# - eks: not present in this stack +# - rds: deployable + +vars: + stage: staging + +import: + - catalog/vpc + - catalog/rds + +components: + terraform: + vpc: + vars: + vpc_cidr: "10.2.0.0/16" + environment: "staging" + + rds: + vars: + db_name: "staging-db" + instance_class: "db.t3.small" From 667e69c66c49005a7bce4b4b2c5c69304df61ff2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:14:34 +0000 Subject: [PATCH 6/6] [autofix.ci] apply automated fixes --- NOTICE | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/NOTICE b/NOTICE index 041d37a3fd..b3b75ba327 100644 --- a/NOTICE +++ b/NOTICE @@ -107,15 +107,15 @@ APACHE 2.0 LICENSED DEPENDENCIES - github.com/aws/aws-sdk-go-v2/config License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/config/v1.32.6/config/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/config/v1.32.7/config/LICENSE.txt - github.com/aws/aws-sdk-go-v2/credentials License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.19.6/credentials/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.19.7/credentials/LICENSE.txt - github.com/aws/aws-sdk-go-v2/feature/ec2/imds License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.18.16/feature/ec2/imds/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.18.17/feature/ec2/imds/LICENSE.txt - github.com/aws/aws-sdk-go-v2/feature/s3/manager License: Apache-2.0 @@ -123,11 +123,11 @@ APACHE 2.0 LICENSED DEPENDENCIES - github.com/aws/aws-sdk-go-v2/internal/configsources License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.4.16/internal/configsources/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.4.17/internal/configsources/LICENSE.txt - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.7.16/internal/endpoints/v2/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.7.17/internal/endpoints/v2/LICENSE.txt - github.com/aws/aws-sdk-go-v2/internal/ini License: Apache-2.0 @@ -135,11 +135,11 @@ APACHE 2.0 LICENSED DEPENDENCIES - github.com/aws/aws-sdk-go-v2/internal/v4a License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/internal/v4a/v1.4.16/internal/v4a/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/internal/v4a/v1.4.17/internal/v4a/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/ecr License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ecr/v1.55.0/service/ecr/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ecr/v1.55.1/service/ecr/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding License: Apache-2.0 @@ -147,19 +147,19 @@ APACHE 2.0 LICENSED DEPENDENCIES - github.com/aws/aws-sdk-go-v2/service/internal/checksum License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/checksum/v1.9.7/service/internal/checksum/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/checksum/v1.9.8/service/internal/checksum/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.13.16/service/internal/presigned-url/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.13.17/service/internal/presigned-url/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/internal/s3shared License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/s3shared/v1.19.16/service/internal/s3shared/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/s3shared/v1.19.17/service/internal/s3shared/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/s3 License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/s3/v1.95.0/service/s3/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/s3/v1.95.1/service/s3/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/secretsmanager License: Apache-2.0 @@ -167,23 +167,23 @@ APACHE 2.0 LICENSED DEPENDENCIES - github.com/aws/aws-sdk-go-v2/service/signin License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/signin/v1.0.4/service/signin/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/signin/v1.0.5/service/signin/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/ssm License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.67.7/service/ssm/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.67.8/service/ssm/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/sso License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.30.8/service/sso/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.30.9/service/sso/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/ssooidc License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.35.12/service/ssooidc/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.35.13/service/ssooidc/LICENSE.txt - github.com/aws/aws-sdk-go-v2/service/sts License: Apache-2.0 - URL: https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.41.5/service/sts/LICENSE.txt + URL: https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.41.6/service/sts/LICENSE.txt - github.com/aws/smithy-go License: Apache-2.0 @@ -744,7 +744,7 @@ BSD LICENSED DEPENDENCIES - golang.org/x/crypto License: BSD-3-Clause - URL: https://cs.opensource.google/go/x/crypto/+/v0.46.0:LICENSE + URL: https://cs.opensource.google/go/x/crypto/+/v0.47.0:LICENSE - golang.org/x/exp License: BSD-3-Clause @@ -756,7 +756,7 @@ BSD LICENSED DEPENDENCIES - golang.org/x/net License: BSD-3-Clause - URL: https://cs.opensource.google/go/x/net/+/v0.48.0:LICENSE + URL: https://cs.opensource.google/go/x/net/+/v0.49.0:LICENSE - golang.org/x/oauth2 License: BSD-3-Clause @@ -768,15 +768,15 @@ BSD LICENSED DEPENDENCIES - golang.org/x/sys License: BSD-3-Clause - URL: https://cs.opensource.google/go/x/sys/+/v0.39.0:LICENSE + URL: https://cs.opensource.google/go/x/sys/+/v0.40.0:LICENSE - golang.org/x/term License: BSD-3-Clause - URL: https://cs.opensource.google/go/x/term/+/v0.38.0:LICENSE + URL: https://cs.opensource.google/go/x/term/+/v0.39.0:LICENSE - golang.org/x/text License: BSD-3-Clause - URL: https://cs.opensource.google/go/x/text/+/v0.32.0:LICENSE + URL: https://cs.opensource.google/go/x/text/+/v0.33.0:LICENSE - golang.org/x/time/rate License: BSD-3-Clause @@ -962,7 +962,7 @@ MIT LICENSED DEPENDENCIES - github.com/Azure/azure-sdk-for-go/sdk/azcore License: MIT - URL: https://github.com/Azure/azure-sdk-for-go/blob/sdk/azcore/v1.20.0/sdk/azcore/LICENSE.txt + URL: https://github.com/Azure/azure-sdk-for-go/blob/sdk/azcore/v1.21.0/sdk/azcore/LICENSE.txt - github.com/Azure/azure-sdk-for-go/sdk/azidentity License: MIT @@ -986,7 +986,7 @@ MIT LICENSED DEPENDENCIES - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob License: MIT - URL: https://github.com/Azure/azure-sdk-for-go/blob/sdk/storage/azblob/v1.6.3/sdk/storage/azblob/LICENSE.txt + URL: https://github.com/Azure/azure-sdk-for-go/blob/sdk/storage/azblob/v1.6.4/sdk/storage/azblob/LICENSE.txt - github.com/Azure/go-ntlmssp License: MIT @@ -1230,7 +1230,7 @@ MIT LICENSED DEPENDENCIES - github.com/getsentry/sentry-go License: MIT - URL: https://github.com/getsentry/sentry-go/blob/v0.40.0/LICENSE + URL: https://github.com/getsentry/sentry-go/blob/v0.41.0/LICENSE - github.com/go-logfmt/logfmt License: MIT @@ -1502,7 +1502,7 @@ MIT LICENSED DEPENDENCIES - github.com/posthog/posthog-go License: MIT - URL: https://github.com/posthog/posthog-go/blob/v1.8.2/LICENSE.md + URL: https://github.com/posthog/posthog-go/blob/v1.9.0/LICENSE.md - github.com/rivo/uniseg License: MIT