Skip to content

Commit 95a14e8

Browse files
committed
Merge branch 'feature/dev-3093-create-a-cli-command-core-library' of https://github.com/cloudposse/atmos into feature/dev-3093-create-a-cli-command-core-library
2 parents 0e5a1ed + 3266760 commit 95a14e8

File tree

70 files changed

+10340
-3469
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+10340
-3469
lines changed

.golangci.yml

+7-8
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ linters-settings:
4646
dupl:
4747
threshold: 150 # Tokens count to trigger issue.
4848

49+
forbidigo:
50+
# Forbid specific function calls
51+
forbid:
52+
- p: "os\\.Getenv"
53+
msg: "Use `viper.BindEnv` for new environment variables instead of `os.Getenv`"
54+
4955
funlen:
5056
lines: 60 # Maximum number of lines per function
5157
statements: 40 # Maximum number of statements per function
@@ -61,14 +67,6 @@ linters-settings:
6167
excludes:
6268
- G101 # Look for hard coded credentials
6369

64-
gofmt:
65-
# Simplify code: gofmt -s
66-
simplify: true
67-
# Define indentation
68-
rewrite-rules:
69-
- pattern: "interface{}"
70-
replacement: "any"
71-
7270
cyclop:
7371
# Maximum function complexity
7472
max-complexity: 15
@@ -200,6 +198,7 @@ issues:
200198
- errcheck
201199
- funlen
202200
- gci
201+
- gocognit
203202
- gosec
204203
- revive
205204

cmd/list_errors_test.go

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/cloudposse/atmos/pkg/list/errors"
10+
)
11+
12+
// setupTestCommand creates a test command with the necessary flags.
13+
func setupTestCommand(use string) *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: use,
16+
}
17+
cmd.PersistentFlags().String("format", "", "Output format")
18+
cmd.PersistentFlags().String("delimiter", "", "Delimiter for CSV/TSV output")
19+
cmd.PersistentFlags().String("stack", "", "Stack pattern")
20+
cmd.PersistentFlags().String("query", "", "JQ query")
21+
cmd.PersistentFlags().Int("max-columns", 0, "Maximum columns")
22+
cmd.PersistentFlags().Bool("process-templates", true, "Enable/disable Go template processing")
23+
cmd.PersistentFlags().Bool("process-functions", true, "Enable/disable YAML functions processing")
24+
return cmd
25+
}
26+
27+
// TestComponentDefinitionNotFoundError tests that the ComponentDefinitionNotFoundError.
28+
func TestComponentDefinitionNotFoundError(t *testing.T) {
29+
testCases := []struct {
30+
name string
31+
componentName string
32+
expectedOutput string
33+
runFunc func(cmd *cobra.Command, args []string) (string, error)
34+
}{
35+
{
36+
name: "list values - component not found",
37+
componentName: "nonexistent-component",
38+
expectedOutput: "component 'nonexistent-component' does not exist",
39+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
40+
return "", &errors.ComponentDefinitionNotFoundError{Component: args[0]}
41+
},
42+
},
43+
{
44+
name: "list settings - component not found",
45+
componentName: "nonexistent-component",
46+
expectedOutput: "component 'nonexistent-component' does not exist",
47+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
48+
return "", &errors.ComponentDefinitionNotFoundError{Component: args[0]}
49+
},
50+
},
51+
{
52+
name: "list metadata - component not found",
53+
componentName: "nonexistent-component",
54+
expectedOutput: "component 'nonexistent-component' does not exist",
55+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
56+
return "", &errors.ComponentDefinitionNotFoundError{Component: args[0]}
57+
},
58+
},
59+
}
60+
61+
for _, tc := range testCases {
62+
t.Run(tc.name, func(t *testing.T) {
63+
// Create test command
64+
cmd := setupTestCommand(tc.name)
65+
args := []string{tc.componentName}
66+
67+
// Mock the listValues/listSettings/listMetadata function
68+
mockRunFunc := tc.runFunc
69+
70+
// Run the command with the mocked function
71+
output, err := mockRunFunc(cmd, args)
72+
assert.Equal(t, "", output)
73+
assert.Error(t, err)
74+
75+
// Check that the error is of the expected type
76+
var componentNotFoundErr *errors.ComponentDefinitionNotFoundError
77+
assert.ErrorAs(t, err, &componentNotFoundErr)
78+
assert.Equal(t, tc.componentName, componentNotFoundErr.Component)
79+
assert.Contains(t, componentNotFoundErr.Error(), tc.expectedOutput)
80+
81+
// Verify that the error would be properly returned by the RunE function
82+
// This simulates what would happen in the actual command execution
83+
// where errors are returned to main() instead of being logged
84+
assert.NotNil(t, err, "Error should be returned to be handled by main()")
85+
})
86+
}
87+
}
88+
89+
// TestNoValuesFoundError tests that the NoValuesFoundError is properly handled.
90+
func TestNoValuesFoundError(t *testing.T) {
91+
testCases := []struct {
92+
name string
93+
componentName string
94+
query string
95+
expectedOutput string
96+
runFunc func(cmd *cobra.Command, args []string) (string, error)
97+
shouldReturnNil bool
98+
}{
99+
{
100+
name: "list values - no values found",
101+
componentName: "test-component",
102+
expectedOutput: "No values found for component 'test-component'",
103+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
104+
return "", &errors.NoValuesFoundError{Component: args[0]}
105+
},
106+
shouldReturnNil: false,
107+
},
108+
{
109+
name: "list vars - no vars found",
110+
componentName: "test-component",
111+
query: ".vars",
112+
expectedOutput: "No vars found for component 'test-component'",
113+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
114+
cmd.Flags().Set("query", ".vars")
115+
return "", &errors.NoValuesFoundError{Component: args[0]}
116+
},
117+
shouldReturnNil: true,
118+
},
119+
{
120+
name: "list settings - no settings found with component",
121+
componentName: "test-component",
122+
expectedOutput: "No settings found for component 'test-component'",
123+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
124+
return "", &errors.NoValuesFoundError{Component: args[0]}
125+
},
126+
shouldReturnNil: false,
127+
},
128+
{
129+
name: "list settings - no settings found without component",
130+
componentName: "",
131+
expectedOutput: "No settings found",
132+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
133+
return "", &errors.NoValuesFoundError{}
134+
},
135+
shouldReturnNil: false,
136+
},
137+
{
138+
name: "list metadata - no metadata found with component",
139+
componentName: "test-component",
140+
expectedOutput: "No metadata found for component 'test-component'",
141+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
142+
return "", &errors.NoValuesFoundError{Component: args[0]}
143+
},
144+
shouldReturnNil: false,
145+
},
146+
{
147+
name: "list metadata - no metadata found without component",
148+
componentName: "",
149+
expectedOutput: "No metadata found",
150+
runFunc: func(cmd *cobra.Command, args []string) (string, error) {
151+
return "", &errors.NoValuesFoundError{}
152+
},
153+
shouldReturnNil: false,
154+
},
155+
}
156+
157+
for _, tc := range testCases {
158+
t.Run(tc.name, func(t *testing.T) {
159+
cmd := setupTestCommand(tc.name)
160+
args := []string{}
161+
if tc.componentName != "" {
162+
args = append(args, tc.componentName)
163+
}
164+
165+
if tc.query != "" {
166+
cmd.Flags().Set("query", tc.query)
167+
}
168+
169+
mockRunFunc := tc.runFunc
170+
171+
output, err := mockRunFunc(cmd, args)
172+
assert.Equal(t, "", output)
173+
assert.Error(t, err)
174+
175+
var noValuesErr *errors.NoValuesFoundError
176+
assert.ErrorAs(t, err, &noValuesErr)
177+
})
178+
}
179+
}

cmd/list_metadata.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
listutils "github.com/cloudposse/atmos/pkg/list/utils"
8+
9+
log "github.com/charmbracelet/log"
10+
"github.com/spf13/cobra"
11+
12+
e "github.com/cloudposse/atmos/internal/exec"
13+
"github.com/cloudposse/atmos/pkg/config"
14+
l "github.com/cloudposse/atmos/pkg/list"
15+
listerrors "github.com/cloudposse/atmos/pkg/list/errors"
16+
fl "github.com/cloudposse/atmos/pkg/list/flags"
17+
f "github.com/cloudposse/atmos/pkg/list/format"
18+
u "github.com/cloudposse/atmos/pkg/list/utils"
19+
"github.com/cloudposse/atmos/pkg/schema"
20+
utils "github.com/cloudposse/atmos/pkg/utils"
21+
)
22+
23+
// listMetadataCmd lists metadata across stacks.
24+
var listMetadataCmd = &cobra.Command{
25+
Use: "metadata [component]",
26+
Short: "List metadata across stacks",
27+
Long: "List metadata information across all stacks or for a specific component",
28+
Example: "atmos list metadata\n" +
29+
"atmos list metadata c1\n" +
30+
"atmos list metadata --query .component\n" +
31+
"atmos list metadata --format json\n" +
32+
"atmos list metadata --stack '*-{dev,staging}-*'\n" +
33+
"atmos list metadata --stack 'prod-*'",
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
checkAtmosConfig()
36+
output, err := listMetadata(cmd, args)
37+
if err != nil {
38+
return err
39+
}
40+
41+
utils.PrintMessage(output)
42+
return nil
43+
},
44+
}
45+
46+
func init() {
47+
fl.AddCommonListFlags(listMetadataCmd)
48+
49+
// Add template and function processing flags
50+
listMetadataCmd.PersistentFlags().Bool("process-templates", true, "Enable/disable Go template processing in Atmos stack manifests when executing the command")
51+
listMetadataCmd.PersistentFlags().Bool("process-functions", true, "Enable/disable YAML functions processing in Atmos stack manifests when executing the command")
52+
53+
AddStackCompletion(listMetadataCmd)
54+
55+
listCmd.AddCommand(listMetadataCmd)
56+
}
57+
58+
// setupMetadataOptions sets up the filter options for metadata listing.
59+
func setupMetadataOptions(commonFlags fl.CommonFlags, componentFilter string) *l.FilterOptions {
60+
query := commonFlags.Query
61+
if query == "" {
62+
query = ".metadata"
63+
}
64+
65+
return &l.FilterOptions{
66+
Component: l.KeyMetadata,
67+
ComponentFilter: componentFilter,
68+
Query: query,
69+
IncludeAbstract: false,
70+
MaxColumns: commonFlags.MaxColumns,
71+
FormatStr: commonFlags.Format,
72+
Delimiter: commonFlags.Delimiter,
73+
StackPattern: commonFlags.Stack,
74+
}
75+
}
76+
77+
// logNoMetadataFoundMessage logs an appropriate message when no metadata is found.
78+
func logNoMetadataFoundMessage(componentFilter string) {
79+
if componentFilter != "" {
80+
log.Info(fmt.Sprintf("No metadata found for component '%s'", componentFilter))
81+
} else {
82+
log.Info("No metadata found")
83+
}
84+
}
85+
86+
// MetadataParams contains the parameters needed for listing metadata.
87+
type MetadataParams struct {
88+
CommonFlags *fl.CommonFlags
89+
ProcessingFlags *fl.ProcessingFlags
90+
ComponentFilter string
91+
}
92+
93+
// initMetadataParams initializes and returns the parameters needed for listing metadata.
94+
func initMetadataParams(cmd *cobra.Command, args []string) (*MetadataParams, error) {
95+
commonFlags, err := fl.GetCommonListFlags(cmd)
96+
if err != nil {
97+
return nil, &listerrors.QueryError{
98+
Query: "common flags",
99+
Cause: err,
100+
}
101+
}
102+
103+
processingFlags := fl.GetProcessingFlags(cmd)
104+
105+
if f.Format(commonFlags.Format) == f.FormatCSV && commonFlags.Delimiter == f.DefaultTSVDelimiter {
106+
commonFlags.Delimiter = f.DefaultCSVDelimiter
107+
}
108+
109+
componentFilter := ""
110+
if len(args) > 0 {
111+
componentFilter = args[0]
112+
}
113+
114+
return &MetadataParams{
115+
CommonFlags: commonFlags,
116+
ProcessingFlags: processingFlags,
117+
ComponentFilter: componentFilter,
118+
}, nil
119+
}
120+
121+
func listMetadata(cmd *cobra.Command, args []string) (string, error) {
122+
params, err := initMetadataParams(cmd, args)
123+
if err != nil {
124+
return "", err
125+
}
126+
127+
// Initialize CLI config
128+
configAndStacksInfo := schema.ConfigAndStacksInfo{}
129+
atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true)
130+
if err != nil {
131+
return "", &listerrors.InitConfigError{Cause: err}
132+
}
133+
134+
if params.ComponentFilter != "" {
135+
if !listutils.CheckComponentExists(&atmosConfig, params.ComponentFilter) {
136+
return "", &listerrors.ComponentDefinitionNotFoundError{Component: params.ComponentFilter}
137+
}
138+
}
139+
140+
// Get all stacks
141+
stacksMap, err := e.ExecuteDescribeStacks(atmosConfig, "", nil, nil, nil, false,
142+
params.ProcessingFlags.Templates, params.ProcessingFlags.Functions, false, nil)
143+
if err != nil {
144+
return "", &listerrors.DescribeStacksError{Cause: err}
145+
}
146+
147+
log.Debug("Filtering metadata",
148+
"component", params.ComponentFilter, "query", params.CommonFlags.Query,
149+
"maxColumns", params.CommonFlags.MaxColumns, "format", params.CommonFlags.Format,
150+
"stackPattern", params.CommonFlags.Stack, "templates", params.ProcessingFlags.Templates)
151+
152+
filterOptions := setupMetadataOptions(*params.CommonFlags, params.ComponentFilter)
153+
output, err := l.FilterAndListValues(stacksMap, filterOptions)
154+
if err != nil {
155+
var noValuesErr *listerrors.NoValuesFoundError
156+
if errors.As(err, &noValuesErr) {
157+
logNoMetadataFoundMessage(params.ComponentFilter)
158+
return "", nil
159+
}
160+
return "", err
161+
}
162+
163+
if output == "" || u.IsEmptyTable(output) {
164+
logNoMetadataFoundMessage(params.ComponentFilter)
165+
return "", nil
166+
}
167+
168+
return output, nil
169+
}

0 commit comments

Comments
 (0)