Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/terraform/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 6 additions & 3 deletions cmd/terraform/generate/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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")
Expand Down
9 changes: 6 additions & 3 deletions cmd/terraform/generate/planfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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")
Expand Down
9 changes: 6 additions & 3 deletions cmd/terraform/generate/varfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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")
Expand Down
191 changes: 175 additions & 16 deletions cmd/terraform/shared/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package shared
import (
"errors"
"sort"
"strings"

"github.com/spf13/cobra"

Expand All @@ -15,12 +16,24 @@ 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.
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,
)
Expand Down Expand Up @@ -59,17 +72,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.
Expand All @@ -91,27 +133,77 @@ 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)
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
}

// 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{}{}
}
}
}
Expand All @@ -126,15 +218,59 @@ 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 := initCliConfig(configAndStacksInfo, true)
if err != nil {
return nil, err
}

stacksMap, err := 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{}
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
}
Expand Down Expand Up @@ -171,12 +307,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
}
Expand All @@ -188,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()
}
Loading
Loading