Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for native --template in Atmos for describe output #989

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
109 changes: 84 additions & 25 deletions cmd/describe_stacks.go
Original file line number Diff line number Diff line change
@@ -1,51 +1,110 @@
package cmd

import (
"fmt"
"strings"

"github.com/spf13/cobra"

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

// describeStacksCmd describes configuration for stacks and components in the stacks
// describeStacksCmd describes atmos stacks with rich formatting options
var describeStacksCmd = &cobra.Command{
Use: "stacks",
Short: "Display configuration for Atmos stacks and their components",
Long: "This command shows the configuration details for Atmos stacks and the components within those stacks.",
Use: "stacks",
Short: "Show detailed information about Atmos stacks",
Long: "This command shows detailed information about Atmos stacks with rich formatting options for output customization. It supports filtering by stack, selecting specific fields, and transforming output using YQ expressions.",
Example: "# Show detailed information for all stacks (colored YAML output)\n" +
"atmos describe stacks\n\n" +
"# Filter by a specific stack\n" +
"atmos describe stacks -s dev\n\n" +
"# Show specific fields in JSON format\n" +
"atmos describe stacks --json name,components\n\n" +
"# Show vars for a specific stack\n" +
"atmos describe stacks -s dev --json name,components --jq '.dev.components.terraform.myapp.vars'\n\n" +
"# Transform JSON output using YQ expressions\n" +
"atmos describe stacks --json name,components --jq '.dev'\n\n" +
"# List available JSON fields\n" +
"atmos describe stacks --json",
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
// Check Atmos configuration
checkAtmosConfig()

err := e.ExecuteDescribeStacksCmd(cmd, args)
if err != nil {
u.LogErrorAndExit(err)
}
},
}
jsonFlag, _ := cmd.Flags().GetString("json")
jqFlag, _ := cmd.Flags().GetString("jq")
templateFlag, _ := cmd.Flags().GetString("template")
stackFlag, _ := cmd.Flags().GetString("stack")

func init() {
describeStacksCmd.DisableFlagParsing = false

describeStacksCmd.PersistentFlags().String("file", "", "Write the result to file: atmos describe stacks --file=stacks.yaml")

describeStacksCmd.PersistentFlags().String("format", "yaml", "Specify the output format: atmos describe stacks --format=yaml|json ('yaml' is default)")
// Validate that --json is provided when using --jq or --template
if (jqFlag != "" || templateFlag != "") && jsonFlag == "" {
u.PrintMessageInColor("Error: --json flag is required when using --jq or --template", theme.Colors.Error)
return
}

describeStacksCmd.PersistentFlags().StringP("stack", "s", "",
"Filter by a specific stack: atmos describe stacks -s <stack>\n"+
"The filter supports names of the top-level stack manifests (including subfolder paths), and 'atmos' stack names (derived from the context vars)",
)
// Validate that only one of --jq or --template is used
if jqFlag != "" && templateFlag != "" {
u.PrintMessageInColor("Error: cannot use both --jq and --template flags at the same time", theme.Colors.Error)
return
}

describeStacksCmd.PersistentFlags().String("components", "", "Filter by specific 'atmos' components: atmos describe stacks --components=<component1>,<component2>")
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
}

describeStacksCmd.PersistentFlags().String("component-types", "", "Filter by specific component types: atmos describe stacks --component-types=terraform|helmfile. Supported component types: terraform, helmfile")
stacksMap, err := e.ExecuteDescribeStacks(atmosConfig, stackFlag, nil, nil, nil, false, false, false)
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error describing stacks: %v", err), theme.Colors.Error)
return
}

describeStacksCmd.PersistentFlags().String("sections", "", "Output only the specified component sections: atmos describe stacks --sections=vars,settings. Available component sections: backend, backend_type, deps, env, inheritance, metadata, remote_state_backend, remote_state_backend_type, settings, vars")
// If --json is provided with no value, show available fields
if jsonFlag == "" && (cmd.Flags().Changed("json") || jqFlag != "" || templateFlag != "") {
availableFields := []string{
"name",
"components",
"terraform",
"helmfile",
"description",
"namespace",
"base_component",
"vars",
"env",
"backend",
}
u.PrintMessageInColor("Available JSON fields:\n"+strings.Join(availableFields, "\n"), theme.Colors.Info)
return
}

describeStacksCmd.PersistentFlags().Bool("process-templates", true, "Enable/disable Go template processing in Atmos stack manifests when executing the command: atmos describe stacks --process-templates=false")
output, err := e.FormatStacksOutput(stacksMap, jsonFlag, jqFlag, templateFlag)
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error formatting output: %v", err), theme.Colors.Error)
return
}

describeStacksCmd.PersistentFlags().Bool("include-empty-stacks", false, "Include stacks with no components in the output: atmos describe stacks --include-empty-stacks")
// Only use colored output for non-JSON/template formats
if jsonFlag == "" && jqFlag == "" && templateFlag == "" {
u.PrintMessageInColor(output, theme.Colors.Success)
} else {
fmt.Println(output)
u.LogErrorAndExit(err)
}
},
}

func init() {
describeStacksCmd.DisableFlagParsing = false
describeStacksCmd.Flags().StringP("stack", "s", "", "Filter by a specific stack")
describeStacksCmd.Flags().String("json", "", "Comma-separated list of fields to include in JSON output")
describeStacksCmd.Flags().String("jq", "", "JQ query to transform JSON output (requires --json)")
describeStacksCmd.Flags().String("template", "", "Go template to format JSON output (requires --json)")
describeCmd.AddCommand(describeStacksCmd)
}
71 changes: 71 additions & 0 deletions cmd/describe_stacks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cmd

import (
"bytes"
"encoding/json"
"testing"

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

func TestDescribeStacksCmd(t *testing.T) {
// Create a new command for testing
cmd := &cobra.Command{Use: "test"}
cmd.AddCommand(describeStacksCmd)

t.Run("Show available JSON fields", func(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"stacks", "--json"})
err := cmd.Execute()
assert.NoError(t, err)
output := b.String()
assert.Contains(t, output, "name")
assert.Contains(t, output, "components")
assert.Contains(t, output, "vars")
})

t.Run("Error when using --jq without --json", func(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"stacks", "--jq", "."})
err := cmd.Execute()
assert.NoError(t, err)
output := b.String()
assert.Contains(t, output, "Error: --json flag is required when using --jq")
})

t.Run("Error when using --template without --json", func(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"stacks", "--template", "{{.}}"})
err := cmd.Execute()
assert.NoError(t, err)
output := b.String()
assert.Contains(t, output, "Error: --json flag is required when using --template")
})

t.Run("Error when using both --jq and --template", func(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"stacks", "--json", "name", "--jq", ".", "--template", "{{.}}"})
err := cmd.Execute()
assert.NoError(t, err)
output := b.String()
assert.Contains(t, output, "Error: cannot use both --jq and --template flags at the same time")
})

t.Run("JSON output with field selection", func(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"stacks", "--json", "description"})
err := cmd.Execute()
assert.NoError(t, err)
output := b.String()
var result map[string]interface{}
err = json.Unmarshal([]byte(output), &result)
assert.NoError(t, err)
// Add more specific assertions based on your test data
})
}
30 changes: 19 additions & 11 deletions cmd/list_stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

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"
Expand All @@ -16,18 +15,18 @@ import (
// listStacksCmd lists atmos stacks
var listStacksCmd = &cobra.Command{
Use: "stacks",
Short: "List all Atmos stacks or stacks for a specific component",
Long: "This command lists all Atmos stacks, or filters the list to show only the stacks associated with a specified component.",
Example: "atmos list stacks\n" +
"atmos list stacks -c <component>",
Short: "List all Atmos stacks",
Long: "This command lists all Atmos stacks. For detailed filtering and output formatting, use 'atmos describe stacks'.",
Example: "# List all stacks\n" +
"atmos list stacks\n\n" +
"# For detailed stack information and filtering, use:\n" +
"atmos describe stacks",
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
// Check Atmos configuration
checkAtmosConfig()

componentFlag, _ := cmd.Flags().GetString("component")

configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true)
if err != nil {
Expand All @@ -41,17 +40,26 @@ var listStacksCmd = &cobra.Command{
return
}

output, err := l.FilterAndListStacks(stacksMap, componentFlag)
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error filtering stacks: %v", err), theme.Colors.Error)
// Simple list of stack names
var stackNames []string
for stackName := range stacksMap {
stackNames = append(stackNames, stackName)
}

if len(stackNames) == 0 {
u.PrintMessageInColor("No stacks found\n", theme.Colors.Warning)
return
}

output := "Available stacks:\n"
for _, name := range stackNames {
output += fmt.Sprintf(" %s\n", name)
}
u.PrintMessageInColor(output, theme.Colors.Success)
},
}

func init() {
listStacksCmd.DisableFlagParsing = false
listStacksCmd.PersistentFlags().StringP("component", "c", "", "atmos list stacks -c <component>")
listCmd.AddCommand(listStacksCmd)
}
2 changes: 2 additions & 0 deletions go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading