From 2e1e51dedabffb2748553adfa68568af70beb840 Mon Sep 17 00:00:00 2001 From: Cerebrovinny Date: Fri, 7 Feb 2025 08:24:53 +0000 Subject: [PATCH 1/4] feat: add `list values` command to compare component configurations across stacks --- cmd/list_values.go | 131 +++++++++++ pkg/list/list_values.go | 206 ++++++++++++++++++ .../docs/cli/commands/list/list-values.mdx | 91 ++++++++ 3 files changed, 428 insertions(+) create mode 100644 cmd/list_values.go create mode 100644 pkg/list/list_values.go create mode 100644 website/docs/cli/commands/list/list-values.mdx diff --git a/cmd/list_values.go b/cmd/list_values.go new file mode 100644 index 000000000..73e096581 --- /dev/null +++ b/cmd/list_values.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + e "github.com/cloudposse/atmos/internal/exec" + "github.com/cloudposse/atmos/pkg/config" + l "github.com/cloudposse/atmos/pkg/list" + "github.com/cloudposse/atmos/pkg/schema" + "github.com/cloudposse/atmos/pkg/ui/theme" + u "github.com/cloudposse/atmos/pkg/utils" +) + +// listValuesCmd lists component values across stacks +var listValuesCmd = &cobra.Command{ + Use: "values [component]", + Short: "List component values across stacks", + Long: "List values for a component across all stacks where it is used", + Example: "atmos list values vpc\n" + + "atmos list values vpc --query .vars\n" + + "atmos list values vpc --abstract\n" + + "atmos list values vpc --max-columns 5\n" + + "atmos list values vpc --format json\n" + + "atmos list values vpc --format csv", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // Check Atmos configuration + checkAtmosConfig() + + flags := cmd.Flags() + + queryFlag, err := flags.GetString("query") + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error getting the 'query' flag: %v", err), theme.Colors.Error) + return + } + + abstractFlag, err := flags.GetBool("abstract") + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error getting the 'abstract' flag: %v", err), theme.Colors.Error) + return + } + + maxColumnsFlag, err := flags.GetInt("max-columns") + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error getting the 'max-columns' flag: %v", err), theme.Colors.Error) + return + } + + formatFlag, err := flags.GetString("format") + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error getting the 'format' flag: %v", err), theme.Colors.Error) + return + } + + delimiterFlag, err := flags.GetString("delimiter") + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error getting the 'delimiter' flag: %v", err), theme.Colors.Error) + return + } + + // Set appropriate default delimiter based on format + if formatFlag == l.FormatCSV && delimiterFlag == l.DefaultTSVDelimiter { + delimiterFlag = l.DefaultCSVDelimiter + } + + component := args[0] + configAndStacksInfo := schema.ConfigAndStacksInfo{} + atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error initializing CLI config: %v", err), theme.Colors.Error) + return + } + + // Get all stacks + stacksMap, err := e.ExecuteDescribeStacks(atmosConfig, "", nil, nil, nil, false, false, false, false, nil) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error describing stacks: %v", err), theme.Colors.Error) + return + } + + output, err := l.FilterAndListValues(stacksMap, component, queryFlag, abstractFlag, maxColumnsFlag, formatFlag, delimiterFlag) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error: %v"+"\n", err), theme.Colors.Warning) + return + } + + u.PrintMessageInColor(output, theme.Colors.Success) + }, +} + +// listVarsCmd is an alias for 'list values --query .vars' +var listVarsCmd = &cobra.Command{ + Use: "vars [component]", + Short: "List component vars across stacks (alias for 'list values --query .vars')", + Long: "List vars for a component across all stacks where it is used", + Example: "atmos list vars vpc\n" + + "atmos list vars vpc --abstract\n" + + "atmos list vars vpc --max-columns 5\n" + + "atmos list vars vpc --format json\n" + + "atmos list vars vpc --format csv", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // Set the query flag to .vars + if err := cmd.Flags().Set("query", ".vars"); err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error setting query flag: %v", err), theme.Colors.Error) + return + } + // Run the values command + listValuesCmd.Run(cmd, args) + }, +} + +func init() { + // Flags for both commands + commonFlags := func(cmd *cobra.Command) { + cmd.PersistentFlags().String("query", "", "JMESPath query to filter values") + cmd.PersistentFlags().Bool("abstract", false, "Include abstract components") + cmd.PersistentFlags().Int("max-columns", 10, "Maximum number of columns to display") + cmd.PersistentFlags().String("format", "", "Output format (table, json, csv, tsv)") + cmd.PersistentFlags().String("delimiter", "\t", "Delimiter for csv/tsv output (default: tab for tsv, comma for csv)") + } + + commonFlags(listValuesCmd) + commonFlags(listVarsCmd) + + listCmd.AddCommand(listValuesCmd) + listCmd.AddCommand(listVarsCmd) +} diff --git a/pkg/list/list_values.go b/pkg/list/list_values.go new file mode 100644 index 000000000..48bd8359d --- /dev/null +++ b/pkg/list/list_values.go @@ -0,0 +1,206 @@ +package list + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" + "github.com/cloudposse/atmos/internal/exec" + "github.com/cloudposse/atmos/pkg/ui/theme" + "github.com/cloudposse/atmos/pkg/utils" + "github.com/jmespath/go-jmespath" +) + +const ( + DefaultCSVDelimiter = "," + DefaultTSVDelimiter = "\t" +) + +// FilterAndListValues filters and lists component values across stacks +func FilterAndListValues(stacksMap map[string]interface{}, component, query string, includeAbstract bool, maxColumns int, format, delimiter string) (string, error) { + if err := ValidateFormat(format); err != nil { + return "", err + } + + // Set default delimiters based on format + if format == FormatCSV && delimiter == DefaultTSVDelimiter { + delimiter = DefaultCSVDelimiter + } + + // Filter out stacks that don't have the component + filteredStacks := make(map[string]interface{}) + for stackName, stackData := range stacksMap { + stack, ok := stackData.(map[string]interface{}) + if !ok { + continue + } + + components, ok := stack["components"].(map[string]interface{}) + if !ok { + continue + } + + terraform, ok := components["terraform"].(map[string]interface{}) + if !ok { + continue + } + + if componentConfig, exists := terraform[component]; exists { + // Skip abstract components if not included + if !includeAbstract { + if config, ok := componentConfig.(map[string]interface{}); ok { + if isAbstract, ok := config["abstract"].(bool); ok && isAbstract { + continue + } + } + } + filteredStacks[stackName] = componentConfig + } + } + + if len(filteredStacks) == 0 { + return fmt.Sprintf("No values found for component '%s'", component), nil + } + + // Apply JMESPath query if provided + if query != "" { + for stackName, stackData := range filteredStacks { + result, err := jmespath.Search(query, stackData) + if err != nil { + return "", fmt.Errorf("error applying query to stack '%s': %w", stackName, err) + } + filteredStacks[stackName] = result + } + } + + // Get all unique keys from all stacks + keys := make(map[string]bool) + for _, stackData := range filteredStacks { + if data, ok := stackData.(map[string]interface{}); ok { + for k := range data { + keys[k] = true + } + } + } + + // Convert keys to sorted slice + var sortedKeys []string + for k := range keys { + sortedKeys = append(sortedKeys, k) + } + sort.Strings(sortedKeys) + + // Get sorted stack names + var stackNames []string + for stackName := range filteredStacks { + stackNames = append(stackNames, stackName) + } + sort.Strings(stackNames) + + // Apply max columns limit + if maxColumns > 0 && len(stackNames) > maxColumns { + stackNames = stackNames[:maxColumns] + } + + // Create rows with values + var rows [][]string + for _, key := range sortedKeys { + row := make([]string, len(stackNames)+1) + row[0] = key + for i, stackName := range stackNames { + stackData := filteredStacks[stackName] + if data, ok := stackData.(map[string]interface{}); ok { + if val, exists := data[key]; exists { + // Convert value to string representation + switch v := val.(type) { + case string: + row[i+1] = v + case nil: + row[i+1] = "null" + default: + jsonBytes, err := json.Marshal(v) + if err != nil { + row[i+1] = fmt.Sprintf("%v", v) + } else { + row[i+1] = string(jsonBytes) + } + } + } + } + } + rows = append(rows, row) + } + + // Create header row + header := make([]string, len(stackNames)+1) + header[0] = "Key" + copy(header[1:], stackNames) + + // Handle different output formats + switch format { + case FormatJSON: + // Create a map of stacks and their values + result := make(map[string]interface{}) + for i, stackName := range stackNames { + stackValues := make(map[string]interface{}) + for _, row := range rows { + if row[i+1] != "" { + var value interface{} + if err := json.Unmarshal([]byte(row[i+1]), &value); err == nil { + stackValues[row[0]] = value + } else { + stackValues[row[0]] = row[i+1] + } + } + } + result[stackName] = stackValues + } + jsonBytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return "", fmt.Errorf("error formatting JSON output: %w", err) + } + return string(jsonBytes), nil + + case FormatCSV, FormatTSV: + var output strings.Builder + output.WriteString(strings.Join(header, delimiter) + utils.GetLineEnding()) + for _, row := range rows { + output.WriteString(strings.Join(row, delimiter) + utils.GetLineEnding()) + } + return output.String(), nil + + default: + // If format is empty or "table", use table format + if format == "" && exec.CheckTTYSupport() { + // Create a styled table for TTY + t := table.New(). + Border(lipgloss.ThickBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorBorder))). + StyleFunc(func(row, col int) lipgloss.Style { + style := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1) + if row == -1 { + return style.Inherit(theme.Styles.CommandName).Align(lipgloss.Center) + } + if col == 0 { + return style.Inherit(theme.Styles.CommandName) + } + return style.Inherit(theme.Styles.Description) + }). + Headers(header...). + Rows(rows...) + + return t.String() + utils.GetLineEnding(), nil + } + + // Default to simple tabular format for non-TTY or when format is explicitly "table" + var output strings.Builder + output.WriteString(strings.Join(header, delimiter) + utils.GetLineEnding()) + for _, row := range rows { + output.WriteString(strings.Join(row, delimiter) + utils.GetLineEnding()) + } + return output.String(), nil + } +} diff --git a/website/docs/cli/commands/list/list-values.mdx b/website/docs/cli/commands/list/list-values.mdx new file mode 100644 index 000000000..816ea8096 --- /dev/null +++ b/website/docs/cli/commands/list/list-values.mdx @@ -0,0 +1,91 @@ +--- +title: "atmos list values" +id: "list-values" +--- + +# atmos list values + +The `atmos list values` command displays component values across all stacks where the component is used. + +## Usage + +```shell +atmos list values [component] [flags] +``` + +## Description + +The `atmos list values` command helps you inspect component values across different stacks. It provides a tabular view where: + +- Each column represents a stack (e.g., dev-ue1, staging-ue1, prod-ue1) +- Each row represents a key in the component's configuration +- Cells contain the values for each key in each stack + +The command is particularly useful for: +- Comparing component configurations across different environments +- Verifying values are set correctly in each stack +- Understanding how a component is configured across your infrastructure + +## Flags + +
+
--query string
+
JMESPath query to filter values (e.g., ".vars" to show only variables)
+
--abstract
+
Include abstract components in the output
+
--max-columns int
+
Maximum number of columns to display (default 10)
+
--format string
+
Output format: `table`, `json`, `csv`, `tsv` (default "`table`")
+
--delimiter string
+
Delimiter for csv/tsv output (default: comma for csv, tab for tsv)
+
+ +## Examples + +List all values for a component: +```shell +atmos list values vpc +``` + +List only variables for a component (using the alias): +```shell +atmos list vars vpc +``` + +List values with a custom JMESPath query: +```shell +atmos list values vpc --query .config +``` + +Include abstract components: +```shell +atmos list values vpc --abstract +``` + +Limit the number of columns: +```shell +atmos list values vpc --max-columns 5 +``` + +Output in different formats: +```shell +atmos list values vpc --format json +atmos list values vpc --format csv +atmos list values vpc --format tsv +``` + +## Example Output + +```shell +> atmos list vars vpc + dev-ue1 staging-ue1 staging-ue2 prod-ue1 prod-ue2 +----- ------------ ------------ ------------ ------------ ------------ +cidr 10.0.0.0/16 10.0.0.0/16 10.0.0.0/16 10.0.0.0/16 10.0.0.0/16 +region us-east-1 us-east-1 us-east-2 us-east-1 us-east-2 +``` + +## Related Commands + +- [atmos list components](/cli/commands/list/component) - List available components +- [atmos describe component](/cli/commands/describe/component) - Show detailed information about a component From 547f984792e476f9781e40f987a9f59639186497 Mon Sep 17 00:00:00 2001 From: Cerebrovinny Date: Fri, 7 Feb 2025 08:54:11 +0000 Subject: [PATCH 2/4] feat: add TSV format support for workflow listing --- pkg/list/list_workflows.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/list/list_workflows.go b/pkg/list/list_workflows.go index 54de87cdd..12920d848 100644 --- a/pkg/list/list_workflows.go +++ b/pkg/list/list_workflows.go @@ -23,6 +23,7 @@ const ( FormatTable = "table" FormatJSON = "json" FormatCSV = "csv" + FormatTSV = "tsv" ) // ValidateFormat checks if the given format is supported @@ -30,7 +31,7 @@ func ValidateFormat(format string) error { if format == "" { return nil } - validFormats := []string{FormatTable, FormatJSON, FormatCSV} + validFormats := []string{FormatTable, FormatJSON, FormatCSV, FormatTSV} for _, f := range validFormats { if format == f { return nil From 917cdd0e71ff20b2e6c94727d1b0fb142d2b1bd6 Mon Sep 17 00:00:00 2001 From: Cerebrovinny Date: Fri, 7 Feb 2025 08:57:21 +0000 Subject: [PATCH 3/4] wip --- website/docs/cli/commands/list/list-values.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/docs/cli/commands/list/list-values.mdx b/website/docs/cli/commands/list/list-values.mdx index 816ea8096..e37d35274 100644 --- a/website/docs/cli/commands/list/list-values.mdx +++ b/website/docs/cli/commands/list/list-values.mdx @@ -54,6 +54,10 @@ atmos list vars vpc ``` List values with a custom JMESPath query: +```shell +TODO: define more outputs +``` + ```shell atmos list values vpc --query .config ``` @@ -79,10 +83,7 @@ atmos list values vpc --format tsv ```shell > atmos list vars vpc - dev-ue1 staging-ue1 staging-ue2 prod-ue1 prod-ue2 ------ ------------ ------------ ------------ ------------ ------------ -cidr 10.0.0.0/16 10.0.0.0/16 10.0.0.0/16 10.0.0.0/16 10.0.0.0/16 -region us-east-1 us-east-1 us-east-2 us-east-1 us-east-2 +TODO: define example output ``` ## Related Commands From 72a8e7685c887c4e3c28a7a9306427164391f6cb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:58:23 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8b9fc451a..47e01a952 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/hashicorp/terraform-exec v0.22.0 github.com/hexops/gotextdiff v1.0.3 github.com/jfrog/jfrog-client-go v1.50.0 + github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 github.com/jwalton/go-supportscolor v1.2.0 github.com/kubescape/go-git-url v0.0.30 @@ -205,7 +206,6 @@ require ( github.com/jfrog/build-info-go v1.10.9 // indirect github.com/jfrog/gofrog v1.7.6 // indirect github.com/jinzhu/copier v0.4.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/joho/godotenv v1.4.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect