Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
40 changes: 40 additions & 0 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,46 @@ func listStacksForComponent(component string) ([]string, error) {
return output, err
}

// listStacks is a wrapper that calls the list package's listAllStacks function.
func listStacks(cmd *cobra.Command) ([]string, error) {
configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true)
if err != nil {
return nil, fmt.Errorf("error initializing CLI config: %v", err)
}

stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil)
if err != nil {
return nil, fmt.Errorf("error describing stacks: %v", err)
}

output, err := l.FilterAndListStacks(stacksMap, "")
return output, err
}

// listComponents is a wrapper that lists all components.
func listComponents(cmd *cobra.Command) ([]string, error) {
flags := cmd.Flags()
stackFlag, err := flags.GetString("stack")
if err != nil {
stackFlag = ""
}

configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := cfg.InitCliConfig(configAndStacksInfo, true)
if err != nil {
return nil, fmt.Errorf("error initializing CLI config: %v", err)
}

stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil)
if err != nil {
return nil, fmt.Errorf("error describing stacks: %v", err)
}

output, err := l.FilterAndListComponents(stackFlag, stacksMap)
return output, err
}

func AddStackCompletion(cmd *cobra.Command) {
if cmd.Flag("stack") == nil {
cmd.PersistentFlags().StringP("stack", "s", "", stackHint)
Expand Down
10 changes: 2 additions & 8 deletions cmd/docs_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,8 @@ Supports native terraform-docs injection.`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{"readme"},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return ErrInvalidArguments
}
err := e.ExecuteDocsGenerateCmd(cmd, args)
if err != nil {
return err
}
return nil
// cobra.ExactArgs(1) already validates argument count
return e.ExecuteDocsGenerateCmd(cmd, args)
},
}

Expand Down
18 changes: 0 additions & 18 deletions cmd/list.go

This file was deleted.

97 changes: 97 additions & 0 deletions cmd/list/components.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package list

import (
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/viper"

e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/flags"
"github.com/cloudposse/atmos/pkg/flags/global"
l "github.com/cloudposse/atmos/pkg/list"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui"
"github.com/cloudposse/atmos/pkg/ui/theme"
u "github.com/cloudposse/atmos/pkg/utils"
)

var componentsParser *flags.StandardParser

// ComponentsOptions contains parsed flags for the components command.
type ComponentsOptions struct {
global.Flags
Stack string
}

// componentsCmd lists atmos components.
var componentsCmd = &cobra.Command{
Use: "components",
Short: "List all Atmos components or filter by stack",
Long: "List Atmos components, with options to filter results by specific stacks.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// Check Atmos configuration
if err := checkAtmosConfig(); err != nil {
return err
}

// Parse flags using StandardParser with Viper precedence
v := viper.GetViper()
if err := componentsParser.BindFlagsToViper(cmd, v); err != nil {
return err
}

opts := &ComponentsOptions{
Flags: flags.ParseGlobalFlags(cmd, v),
Stack: v.GetString("stack"),
}

output, err := listComponentsWithOptions(opts)
if err != nil {
return err
}

if len(output) == 0 {
ui.Info("No components found")
return nil
}

u.PrintMessageInColor(strings.Join(output, "\n")+"\n", theme.Colors.Success)
return nil
},
}

func init() {
// Create parser with components-specific flags using functional options
componentsParser = flags.NewStandardParser(
flags.WithStringFlag("stack", "s", "", "Filter by stack name or pattern"),
flags.WithEnvVars("stack", "ATMOS_STACK"),
)

// Register flags
componentsParser.RegisterFlags(componentsCmd)

// Bind flags to Viper for environment variable support
if err := componentsParser.BindToViper(viper.GetViper()); err != nil {
panic(err)
}
}

func listComponentsWithOptions(opts *ComponentsOptions) ([]string, error) {
configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true)
if err != nil {
return nil, fmt.Errorf("error initializing CLI config: %v", err)
}

stacksMap, err := e.ExecuteDescribeStacks(&atmosConfig, "", nil, nil, nil, false, false, false, false, nil, nil)
if err != nil {
return nil, fmt.Errorf("error describing stacks: %v", err)
}

output, err := l.FilterAndListComponents(opts.Stack, stacksMap)
return output, err
}
120 changes: 120 additions & 0 deletions cmd/list/components_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//nolint:dupl // Test structure similarity is intentional for consistency
package list

import (
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

// TestListComponentsFlags tests that the list components command has the correct flags.
func TestListComponentsFlags(t *testing.T) {
cmd := &cobra.Command{
Use: "components",
Short: "List all Atmos components or filter by stack",
Long: "List Atmos components, with options to filter results by specific stacks.",
Args: cobra.NoArgs,
}

cmd.PersistentFlags().StringP("stack", "s", "", "Filter by stack name or pattern")

stackFlag := cmd.PersistentFlags().Lookup("stack")
assert.NotNil(t, stackFlag, "Expected stack flag to exist")
assert.Equal(t, "", stackFlag.DefValue)
assert.Equal(t, "s", stackFlag.Shorthand)
}

// TestListComponentsValidatesArgs tests that the command validates arguments.
func TestListComponentsValidatesArgs(t *testing.T) {
cmd := &cobra.Command{
Use: "components",
Args: cobra.NoArgs,
}

err := cmd.ValidateArgs([]string{})
assert.NoError(t, err, "Validation should pass with no arguments")

err = cmd.ValidateArgs([]string{"extra"})
assert.Error(t, err, "Validation should fail with arguments")
}

// TestListComponentsCommand tests the components command structure.
func TestListComponentsCommand(t *testing.T) {
assert.Equal(t, "components", componentsCmd.Use)
assert.Contains(t, componentsCmd.Short, "List all Atmos components")
assert.NotNil(t, componentsCmd.RunE)

// Check that NoArgs validator is set
err := componentsCmd.Args(componentsCmd, []string{"unexpected"})
assert.Error(t, err, "Should reject extra arguments")

err = componentsCmd.Args(componentsCmd, []string{})
assert.NoError(t, err, "Should accept no arguments")
}

// TestListComponentsWithOptions_EmptyStack tests filtering with empty stack pattern.
func TestListComponentsWithOptions_EmptyStack(t *testing.T) {
opts := &ComponentsOptions{
Stack: "",
}

// Test that the options are properly structured
assert.Equal(t, "", opts.Stack)
}

// TestListComponentsWithOptions_StackPattern tests filtering with stack pattern.
func TestListComponentsWithOptions_StackPattern(t *testing.T) {
opts := &ComponentsOptions{
Stack: "prod-*",
}

// Test that the options are properly structured
assert.Equal(t, "prod-*", opts.Stack)
}

// TestComponentsOptions_AllPatterns tests various stack pattern combinations.
func TestComponentsOptions_AllPatterns(t *testing.T) {
testCases := []struct {
name string
opts *ComponentsOptions
expectedStack string
}{
{
name: "wildcard pattern at end",
opts: &ComponentsOptions{Stack: "prod-*"},
expectedStack: "prod-*",
},
{
name: "wildcard pattern at start",
opts: &ComponentsOptions{Stack: "*-prod"},
expectedStack: "*-prod",
},
{
name: "wildcard pattern in middle",
opts: &ComponentsOptions{Stack: "prod-*-vpc"},
expectedStack: "prod-*-vpc",
},
{
name: "multiple wildcard patterns",
opts: &ComponentsOptions{Stack: "*-dev-*"},
expectedStack: "*-dev-*",
},
{
name: "exact stack name",
opts: &ComponentsOptions{Stack: "prod-us-east-1"},
expectedStack: "prod-us-east-1",
},
{
name: "empty stack",
opts: &ComponentsOptions{},
expectedStack: "",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expectedStack, tc.opts.Stack)
})
}
}
85 changes: 85 additions & 0 deletions cmd/list/instances.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package list

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

e "github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/flags"
"github.com/cloudposse/atmos/pkg/flags/global"
"github.com/cloudposse/atmos/pkg/list"
)

var instancesParser *flags.StandardParser

// InstancesOptions contains parsed flags for the instances command.
type InstancesOptions struct {
global.Flags
Format string
MaxColumns int
Delimiter string
Stack string
Query string
Upload bool
}

// instancesCmd lists atmos instances.
var instancesCmd = &cobra.Command{
Use: "instances",
Short: "List all Atmos instances",
Long: "This command lists all Atmos instances or is used to upload instances to the pro API.",
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// Check Atmos configuration
if err := checkAtmosConfig(); err != nil {
return err
}

// Parse flags using StandardParser with Viper precedence
v := viper.GetViper()
if err := instancesParser.BindFlagsToViper(cmd, v); err != nil {
return err
}

opts := &InstancesOptions{
Flags: flags.ParseGlobalFlags(cmd, v),
Format: v.GetString("format"),
MaxColumns: v.GetInt("max-columns"),
Delimiter: v.GetString("delimiter"),
Stack: v.GetString("stack"),
Query: v.GetString("query"),
Upload: v.GetBool("upload"),
}

return executeListInstancesCmd(cmd, args, opts)
},
}

func init() {
// Create parser with common list flags plus upload flag
instancesParser = newCommonListParser(
flags.WithBoolFlag("upload", "", false, "Upload instances to pro API"),
flags.WithEnvVars("upload", "ATMOS_LIST_UPLOAD"),
)

// Register flags
instancesParser.RegisterFlags(instancesCmd)

// Bind flags to Viper for environment variable support
if err := instancesParser.BindToViper(viper.GetViper()); err != nil {
panic(err)
}
}

func executeListInstancesCmd(cmd *cobra.Command, args []string, opts *InstancesOptions) error {
// Process and validate command line arguments.
configAndStacksInfo, err := e.ProcessCommandLineArgs("list", cmd, args, nil)
if err != nil {
return err
}
configAndStacksInfo.Command = "list"
configAndStacksInfo.SubCommand = "instances"

return list.ExecuteListInstancesCmd(&configAndStacksInfo, cmd, args)
}
Loading
Loading