diff --git a/atmos.yaml b/atmos.yaml index daa5f91cc..2dbfc2225 100644 --- a/atmos.yaml +++ b/atmos.yaml @@ -191,59 +191,6 @@ commands: - 'echo Dependencies: "{{ .ComponentConfig.deps }}"' - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s plat-ue2-dev - atmos list components --stack plat-uw2-prod - atmos list components -s plat-ue2-dev --type abstract - atmos list components -s plat-ue2-dev -t enabled - atmos list components -s plat-ue2-dev -t disabled - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - - name: type - shorthand: t - description: Component types - abstract, enabled, or disabled - required: false - steps: - - > - {{ if .Flags.stack }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r .key - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r .key - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r .key - {{ else }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ else }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ end }} # Integrations integrations: diff --git a/cmd/describe_stacks.go b/cmd/describe_stacks.go index 3fd38ef4e..8b6d87fb1 100644 --- a/cmd/describe_stacks.go +++ b/cmd/describe_stacks.go @@ -45,5 +45,7 @@ func init() { 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") + describeStacksCmd.PersistentFlags().Bool("include-empty-stacks", false, "Include stacks with no components in the output: atmos describe stacks --include-empty-stacks") + describeCmd.AddCommand(describeStacksCmd) } diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 000000000..62862c8b2 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// listCmd commands list stacks and components +var listCmd = &cobra.Command{ + Use: "list", + Short: "Execute 'list' commands", + Long: `This command lists stacks and components`, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, +} + +func init() { + RootCmd.AddCommand(listCmd) +} diff --git a/cmd/list_components.go b/cmd/list_components.go new file mode 100644 index 000000000..03cf6feff --- /dev/null +++ b/cmd/list_components.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + + 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" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +// listComponentsCmd lists atmos components +var listComponentsCmd = &cobra.Command{ + Use: "components", + Short: "Execute 'list components' command", + Long: `This command lists all Atmos components or filters components by stacks.`, + Example: "atmos list components\n" + + "atmos list components -s ", + Run: func(cmd *cobra.Command, args []string) { + // Check Atmos configuration + checkAtmosConfig() + + stackFlag, _ := cmd.Flags().GetString("stack") + + configAndStacksInfo := schema.ConfigAndStacksInfo{} + cliConfig, err := config.InitCliConfig(configAndStacksInfo, true) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error initializing CLI config: %v", err), color.New(color.FgRed)) + return + } + + stacksMap, err := e.ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false, false) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error describing stacks: %v", err), color.New(color.FgRed)) + return + } + + output, err := l.FilterAndListComponents(stackFlag, stacksMap) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error: %v"+"\n", err), color.New(color.FgYellow)) + return + } + + u.PrintMessageInColor(output, color.New(color.FgGreen)) + }, +} + +func init() { + listComponentsCmd.PersistentFlags().StringP("stack", "s", "", "Filter components by stack (e.g., atmos list components -s stack1)") + listCmd.AddCommand(listComponentsCmd) +} diff --git a/cmd/list_stacks.go b/cmd/list_stacks.go new file mode 100644 index 000000000..38d5e547e --- /dev/null +++ b/cmd/list_stacks.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "fmt" + + 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" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +// listStacksCmd lists atmos stacks +var listStacksCmd = &cobra.Command{ + Use: "stacks", + Short: "Execute 'list stacks' command", + Long: `This command lists all Atmos stacks or all stacks for the specified component: atmos list stacks -c `, + Example: "atmos list stacks\n" + + "atmos list stacks -c ", + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Run: func(cmd *cobra.Command, args []string) { + // Check Atmos configuration + checkAtmosConfig() + + componentFlag, _ := cmd.Flags().GetString("component") + + configAndStacksInfo := schema.ConfigAndStacksInfo{} + cliConfig, err := config.InitCliConfig(configAndStacksInfo, true) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error initializing CLI config: %v", err), color.New(color.FgRed)) + return + } + + stacksMap, err := e.ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false, false) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error describing stacks: %v", err), color.New(color.FgRed)) + return + } + + output, err := l.FilterAndListStacks(stacksMap, componentFlag) + if err != nil { + u.PrintMessageInColor(fmt.Sprintf("Error filtering stacks: %v", err), color.New(color.FgRed)) + return + } + u.PrintMessageInColor(output, color.New(color.FgGreen)) + }, +} + +func init() { + listStacksCmd.DisableFlagParsing = false + listStacksCmd.PersistentFlags().StringP("component", "c", "", "atmos list stacks -c ") + listCmd.AddCommand(listStacksCmd) +} diff --git a/examples/demo-localstack/stacks/deploy/footbar.yaml b/examples/demo-localstack/stacks/deploy/footbar.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/examples/quick-start-advanced/Dockerfile b/examples/quick-start-advanced/Dockerfile index e617a6cfd..b1e74ff21 100644 --- a/examples/quick-start-advanced/Dockerfile +++ b/examples/quick-start-advanced/Dockerfile @@ -6,7 +6,7 @@ ARG GEODESIC_OS=debian # https://atmos.tools/ # https://github.com/cloudposse/atmos # https://github.com/cloudposse/atmos/releases -ARG ATMOS_VERSION=1.105.0 +ARG ATMOS_VERSION=1.109.0 # Terraform: https://github.com/hashicorp/terraform/releases ARG TF_VERSION=1.9.8 diff --git a/examples/quick-start-advanced/atmos.yaml b/examples/quick-start-advanced/atmos.yaml index 76af448d3..d0383d510 100644 --- a/examples/quick-start-advanced/atmos.yaml +++ b/examples/quick-start-advanced/atmos.yaml @@ -176,59 +176,6 @@ commands: - 'echo Environment: "{{ .ComponentConfig.vars.environment }}"' - 'echo Stage: "{{ .ComponentConfig.vars.stage }}"' - 'echo Dependencies: "{{ .ComponentConfig.deps }}"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s plat-ue2-dev - atmos list components --stack plat-uw2-prod - atmos list components -s plat-ue2-dev --type abstract - atmos list components -s plat-ue2-dev -t enabled - atmos list components -s plat-ue2-dev -t disabled - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - - name: type - shorthand: t - description: Component types - abstract, enabled, or disabled - required: false - steps: - - > - {{ if .Flags.stack }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r .key - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r .key - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r .key - {{ else }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ else }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ end }} # Validation schemas (for validating atmos stacks and components) schemas: diff --git a/examples/quick-start-advanced/rootfs/usr/local/etc/atmos/atmos.yaml b/examples/quick-start-advanced/rootfs/usr/local/etc/atmos/atmos.yaml index 89b9dd84e..a17ab1468 100644 --- a/examples/quick-start-advanced/rootfs/usr/local/etc/atmos/atmos.yaml +++ b/examples/quick-start-advanced/rootfs/usr/local/etc/atmos/atmos.yaml @@ -176,60 +176,6 @@ commands: - 'echo Environment: "{{ .ComponentConfig.vars.environment }}"' - 'echo Stage: "{{ .ComponentConfig.vars.stage }}"' - 'echo Dependencies: "{{ .ComponentConfig.deps }}"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s plat-ue2-dev - atmos list components --stack plat-uw2-prod - atmos list components -s plat-ue2-dev --type abstract - atmos list components -s plat-ue2-dev -t enabled - atmos list components -s plat-ue2-dev -t disabled - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - - name: type - shorthand: t - description: Component types - abstract, enabled, or disabled - required: false - steps: - - > - {{ if .Flags.stack }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r .key - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r .key - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r .key - {{ else }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ else }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ end }} - # Validation schemas (for validating atmos stacks and components) schemas: diff --git a/examples/tests/atmos.yaml b/examples/tests/atmos.yaml index fbbc8bc91..b3feeb7b5 100644 --- a/examples/tests/atmos.yaml +++ b/examples/tests/atmos.yaml @@ -189,37 +189,6 @@ commands: - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s tenant1-ue1-dev - atmos list components --stack tenant2-uw2-prod - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - steps: - - > - {{ if .Flags.stack }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - - name: set-eks-cluster description: | Download 'kubeconfig' and set EKS cluster. diff --git a/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml b/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml index e12a087f5..591230820 100644 --- a/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml +++ b/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml @@ -184,37 +184,6 @@ commands: - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s tenant1-ue1-dev - atmos list components --stack tenant2-uw2-prod - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - steps: - - > - {{ if .Flags.stack }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - - name: set-eks-cluster description: | Download 'kubeconfig' and set EKS cluster. diff --git a/go.mod b/go.mod index 5e55e5861..e826998af 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/arsham/figurine v1.3.0 github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.2.1 + github.com/charmbracelet/bubbletea v1.2.3 github.com/charmbracelet/glamour v0.8.0 github.com/charmbracelet/lipgloss v1.0.0 github.com/elewis787/boa v0.1.2 @@ -21,7 +21,7 @@ require ( github.com/hairyhenderson/gomplate/v3 v3.11.8 github.com/hashicorp/go-getter v1.7.6 github.com/hashicorp/hcl v1.0.0 - github.com/hashicorp/hcl/v2 v2.22.0 + github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20241107133921-3adb156ecfe2 github.com/hashicorp/terraform-exec v0.21.0 github.com/helmfile/vals v0.37.8 @@ -135,9 +135,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chainguard-dev/git-urls v1.0.2 // indirect github.com/charmbracelet/x/ansi v0.4.5 // indirect - github.com/charmbracelet/x/term v0.2.0 // indirect - github.com/cloudflare/circl v1.4.0 // indirect - github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect github.com/containerd/containerd v1.7.23 // indirect github.com/containerd/errdefs v0.3.0 // indirect @@ -335,8 +334,8 @@ require ( golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect diff --git a/go.sum b/go.sum index fa1656e6f..9a691e451 100644 --- a/go.sum +++ b/go.sum @@ -423,8 +423,8 @@ github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiw github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.2.1 h1:J041h57zculJKEKf/O2pS4edXGIz+V0YvojvfGXePIk= -github.com/charmbracelet/bubbletea v1.2.1/go.mod h1:viLoDL7hG4njLJSKU2gw7kB3LSEmWsrM80rO1dBJWBI= +github.com/charmbracelet/bubbletea v1.2.3 h1:d9MdMsANIYZB5pE1KkRqaUV6GfsiWm+/9z4fTuGVm9I= +github.com/charmbracelet/bubbletea v1.2.3/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= @@ -433,8 +433,8 @@ github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSe github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= -github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -853,12 +853,8 @@ github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9 github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= -github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/hashicorp/hcp-sdk-go v0.119.0 h1:31sjlO+VcDahdVE3K/+LKMoQe3v6tC0ESPayUcWtrf4= -github.com/hashicorp/hcp-sdk-go v0.119.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= -github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo= -github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= +github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= @@ -1444,11 +1440,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/internal/exec/atmos.go b/internal/exec/atmos.go index fe3d29b22..446c42ca1 100644 --- a/internal/exec/atmos.go +++ b/internal/exec/atmos.go @@ -41,7 +41,7 @@ func ExecuteAtmosCmd() error { // Get a map of stacks and components in the stacks // Don't process `Go` templates in Atmos stack manifests since we just need to display the stack and component names in the TUI - stacksMap, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false) + stacksMap, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false, false) if err != nil { return err } diff --git a/internal/exec/describe_affected_utils.go b/internal/exec/describe_affected_utils.go index ec7ca3f14..1f8d30511 100644 --- a/internal/exec/describe_affected_utils.go +++ b/internal/exec/describe_affected_utils.go @@ -408,7 +408,7 @@ func executeDescribeAffected( u.LogTrace(cliConfig, fmt.Sprintf("Current HEAD: %s", localRepoHead)) u.LogTrace(cliConfig, fmt.Sprintf("BASE: %s", remoteRepoHead)) - currentStacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, nil, false, true) + currentStacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, nil, false, true, false) if err != nil { return nil, nil, nil, err } @@ -444,7 +444,7 @@ func executeDescribeAffected( return nil, nil, nil, err } - remoteStacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, nil, true, true) + remoteStacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, nil, true, true, false) if err != nil { return nil, nil, nil, err } diff --git a/internal/exec/describe_dependents.go b/internal/exec/describe_dependents.go index c3ba57083..647f11185 100644 --- a/internal/exec/describe_dependents.go +++ b/internal/exec/describe_dependents.go @@ -78,7 +78,7 @@ func ExecuteDescribeDependents( var ok bool // Get all stacks with all components - stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, true) + stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, true, false) if err != nil { return nil, err } diff --git a/internal/exec/describe_stacks.go b/internal/exec/describe_stacks.go index c45d18541..8da25fa9c 100644 --- a/internal/exec/describe_stacks.go +++ b/internal/exec/describe_stacks.go @@ -55,6 +55,11 @@ func ExecuteDescribeStacksCmd(cmd *cobra.Command, args []string) error { return err } + includeEmptyStacks, err := cmd.Flags().GetBool("include-empty-stacks") + if err != nil { + return err + } + componentsCsv, err := flags.GetString("components") if err != nil { return err @@ -97,6 +102,7 @@ func ExecuteDescribeStacksCmd(cmd *cobra.Command, args []string) error { sections, false, processTemplates, + includeEmptyStacks, ) if err != nil { return err @@ -119,6 +125,7 @@ func ExecuteDescribeStacks( sections []string, ignoreMissingFiles bool, processTemplates bool, + includeEmptyStacks bool, ) (map[string]any, error) { stacksMap, _, err := FindStacksMap(cliConfig, ignoreMissingFiles) @@ -127,6 +134,7 @@ func ExecuteDescribeStacks( } finalStacksMap := make(map[string]any) + processedStacks := make(map[string]bool) var varsSection map[string]any var metadataSection map[string]any var settingsSection map[string]any @@ -136,12 +144,48 @@ func ExecuteDescribeStacks( var backendSection map[string]any var backendTypeSection string var stackName string - context := schema.Context{} for stackFileName, stackSection := range stacksMap { + var context schema.Context + // Delete the stack-wide imports delete(stackSection.(map[string]any), "imports") + // Check if components section exists and has explicit components + hasExplicitComponents := false + if componentsSection, ok := stackSection.(map[string]any)["components"]; ok { + if componentsSection != nil { + if terraformSection, ok := componentsSection.(map[string]any)["terraform"].(map[string]any); ok { + hasExplicitComponents = len(terraformSection) > 0 + } + if helmfileSection, ok := componentsSection.(map[string]any)["helmfile"].(map[string]any); ok { + hasExplicitComponents = hasExplicitComponents || len(helmfileSection) > 0 + } + } + } + + // Also check for imports + hasImports := false + if importsSection, ok := stackSection.(map[string]any)["import"].([]any); ok { + hasImports = len(importsSection) > 0 + } + + // Skip stacks without components or imports when includeEmptyStacks is false + if !includeEmptyStacks && !hasExplicitComponents && !hasImports { + continue + } + + stackName = stackFileName + if processedStacks[stackName] { + continue + } + processedStacks[stackName] = true + + if !u.MapKeyExists(finalStacksMap, stackName) { + finalStacksMap[stackName] = make(map[string]any) + finalStacksMap[stackName].(map[string]any)["components"] = make(map[string]any) + } + if componentsSection, ok := stackSection.(map[string]any)["components"].(map[string]any); ok { if len(componentTypes) == 0 || u.SliceContainsString(componentTypes, "terraform") { @@ -244,11 +288,11 @@ func ExecuteDescribeStacks( stackName = stackFileName } + // Only create the stack entry if it doesn't exist if !u.MapKeyExists(finalStacksMap, stackName) { finalStacksMap[stackName] = make(map[string]any) } - configAndStacksInfo.Stack = stackName configAndStacksInfo.ComponentSection["atmos_component"] = componentName configAndStacksInfo.ComponentSection["atmos_stack"] = stackName configAndStacksInfo.ComponentSection["stack"] = stackName @@ -432,6 +476,7 @@ func ExecuteDescribeStacks( stackName = stackFileName } + // Only create the stack entry if it doesn't exist if !u.MapKeyExists(finalStacksMap, stackName) { finalStacksMap[stackName] = make(map[string]any) } @@ -511,13 +556,59 @@ func ExecuteDescribeStacks( } } } + } + + // Filter out empty stacks after processing all stack files + if !includeEmptyStacks { + for stackName := range finalStacksMap { + if stackName == "" { + delete(finalStacksMap, stackName) + continue + } + + stackEntry, ok := finalStacksMap[stackName].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid stack entry type for stack %s", stackName) + } + componentsSection, hasComponents := stackEntry["components"].(map[string]any) + + if !hasComponents { + delete(finalStacksMap, stackName) + continue + } - // Filter out empty stacks (stacks without any components) - if st, ok := finalStacksMap[stackName].(map[string]any); ok { - if len(st) == 0 { + // Check if any component type (terraform/helmfile) has components + hasNonEmptyComponents := false + for _, components := range componentsSection { + if compTypeMap, ok := components.(map[string]any); ok { + for _, comp := range compTypeMap { + if compContent, ok := comp.(map[string]any); ok { + // Check for any meaningful content + relevantSections := []string{"vars", "metadata", "settings", "env", "workspace"} + for _, section := range relevantSections { + if _, hasSection := compContent[section]; hasSection { + hasNonEmptyComponents = true + break + } + } + } + } + } + if hasNonEmptyComponents { + break + } + } + + if !hasNonEmptyComponents { delete(finalStacksMap, stackName) + continue } } + } else { + // Process stacks normally without special handling for any prefixes + for stackName, stackConfig := range finalStacksMap { + finalStacksMap[stackName] = stackConfig + } } return finalStacksMap, nil diff --git a/internal/exec/stack_processor_utils.go b/internal/exec/stack_processor_utils.go index b6c6f4d0e..b616e2348 100644 --- a/internal/exec/stack_processor_utils.go +++ b/internal/exec/stack_processor_utils.go @@ -199,6 +199,9 @@ func ProcessYAMLConfigFile( return nil, nil, nil, err } } + if stackYamlConfig == "" { + return map[string]any{}, map[string]map[string]any{}, map[string]any{}, nil + } stackManifestTemplatesProcessed := stackYamlConfig stackManifestTemplatesErrorMessage := "" diff --git a/internal/exec/validate_stacks.go b/internal/exec/validate_stacks.go index 29c50bd15..13830b1a0 100644 --- a/internal/exec/validate_stacks.go +++ b/internal/exec/validate_stacks.go @@ -19,6 +19,8 @@ import ( u "github.com/cloudposse/atmos/pkg/utils" ) +const atmosManifestDefault = "https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json" + // ExecuteValidateStacksCmd executes `validate stacks` command func ExecuteValidateStacksCmd(cmd *cobra.Command, args []string) error { info, err := processCommandLineArgs("", cmd, args, nil) @@ -84,27 +86,30 @@ func ValidateStacks(cliConfig schema.CliConfiguration) error { // The path to the Atmos manifest JSON Schema can be absolute path or a path relative to the `base_path` setting in `atmos.yaml` var atmosManifestJsonSchemaFilePath string - if cliConfig.Schemas.Atmos.Manifest != "" { - atmosManifestJsonSchemaFileAbsPath := path.Join(cliConfig.BasePath, cliConfig.Schemas.Atmos.Manifest) - - if u.FileExists(cliConfig.Schemas.Atmos.Manifest) { - atmosManifestJsonSchemaFilePath = cliConfig.Schemas.Atmos.Manifest - } else if u.FileExists(atmosManifestJsonSchemaFileAbsPath) { - atmosManifestJsonSchemaFilePath = atmosManifestJsonSchemaFileAbsPath - } else if u.IsURL(cliConfig.Schemas.Atmos.Manifest) { - atmosManifestJsonSchemaFilePath, err = downloadSchemaFromURL(cliConfig.Schemas.Atmos.Manifest) - if err != nil { - return err - } - } else { - return fmt.Errorf("the Atmos JSON Schema file '%s' does not exist.\n"+ - "It can be configured in the 'schemas.atmos.manifest' section in 'atmos.yaml', or provided using the 'ATMOS_SCHEMAS_ATMOS_MANIFEST' "+ - "ENV variable or '--schemas-atmos-manifest' command line argument.\n"+ - "The path to the schema file should be an absolute path or a path relative to the 'base_path' setting in 'atmos.yaml'. \n"+ - "Alternatively, you can specify a schema file using a URL that will be downloaded automatically.", - cliConfig.Schemas.Atmos.Manifest) + if cliConfig.Schemas.Atmos.Manifest == "" { + cliConfig.Schemas.Atmos.Manifest = atmosManifestDefault + u.LogTrace(cliConfig, fmt.Sprintf("The Atmos JSON Schema file is not configured. Using the default schema '%s'", atmosManifestDefault)) + } + atmosManifestJsonSchemaFileAbsPath := path.Join(cliConfig.BasePath, cliConfig.Schemas.Atmos.Manifest) + + if u.FileExists(cliConfig.Schemas.Atmos.Manifest) { + atmosManifestJsonSchemaFilePath = cliConfig.Schemas.Atmos.Manifest + } else if u.FileExists(atmosManifestJsonSchemaFileAbsPath) { + atmosManifestJsonSchemaFilePath = atmosManifestJsonSchemaFileAbsPath + } else if u.IsURL(cliConfig.Schemas.Atmos.Manifest) { + atmosManifestJsonSchemaFilePath, err = downloadSchemaFromURL(cliConfig.Schemas.Atmos.Manifest) + if err != nil { + return err } + } else { + return fmt.Errorf("Schema file '%s' not found. Configure via:\n"+ + "1. 'schemas.atmos.manifest' in atmos.yaml\n"+ + "2. ATMOS_SCHEMAS_ATMOS_MANIFEST env var\n"+ + "3. --schemas-atmos-manifest flag\n\n"+ + "Accepts: absolute path, paths relative to base_path, or URL", + cliConfig.Schemas.Atmos.Manifest) } + // Include (process and validate) all YAML files in the `stacks` folder in all subfolders includedPaths := []string{"**/*"} // Don't exclude any YAML files for validation diff --git a/pkg/aws/atmos.yaml b/pkg/aws/atmos.yaml index 686a935a0..720b7f90b 100644 --- a/pkg/aws/atmos.yaml +++ b/pkg/aws/atmos.yaml @@ -172,37 +172,6 @@ commands: - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s tenant1-ue1-dev - atmos list components --stack tenant2-uw2-prod - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - steps: - - > - {{ if .Flags.stack }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - - name: set-eks-cluster description: | Download 'kubeconfig' and set EKS cluster. diff --git a/pkg/describe/describe_component.go b/pkg/describe/describe_component.go new file mode 100644 index 000000000..0feaf09b0 --- /dev/null +++ b/pkg/describe/describe_component.go @@ -0,0 +1,14 @@ +package describe + +import ( + e "github.com/cloudposse/atmos/internal/exec" +) + +// ExecuteDescribeComponent describes component config and returns the final map of component configuration in the stack +func ExecuteDescribeComponent( + component string, + stack string, + processTemplates bool, +) (map[string]any, error) { + return e.ExecuteDescribeComponent(component, stack, processTemplates) +} diff --git a/pkg/describe/describe_component_test.go b/pkg/describe/describe_component_test.go index 31b141169..03904a92c 100644 --- a/pkg/describe/describe_component_test.go +++ b/pkg/describe/describe_component_test.go @@ -78,3 +78,15 @@ func TestDescribeComponent6(t *testing.T) { assert.Nil(t, err) t.Log(componentSectionYaml) } + +func TestDescribeComponent7(t *testing.T) { + component := "infra/vpc" + stack := "tenant2-ue2-dev" + + componentSection, err := ExecuteDescribeComponent(component, stack, true) + assert.Nil(t, err) + + componentSectionYaml, err := u.ConvertToYAML(componentSection) + assert.Nil(t, err) + t.Log(componentSectionYaml) +} diff --git a/pkg/describe/describe_stacks.go b/pkg/describe/describe_stacks.go index e3aabc7b9..7cea75cee 100644 --- a/pkg/describe/describe_stacks.go +++ b/pkg/describe/describe_stacks.go @@ -13,7 +13,8 @@ func ExecuteDescribeStacks( componentTypes []string, sections []string, ignoreMissingFiles bool, + includeEmptyStacks bool, ) (map[string]any, error) { - return e.ExecuteDescribeStacks(cliConfig, filterByStack, components, componentTypes, sections, ignoreMissingFiles, true) + return e.ExecuteDescribeStacks(cliConfig, filterByStack, components, componentTypes, sections, ignoreMissingFiles, true, includeEmptyStacks) } diff --git a/pkg/describe/describe_stacks_test.go b/pkg/describe/describe_stacks_test.go index 977d8ab7d..eecf17fa9 100644 --- a/pkg/describe/describe_stacks_test.go +++ b/pkg/describe/describe_stacks_test.go @@ -16,7 +16,7 @@ func TestDescribeStacks(t *testing.T) { cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) assert.Nil(t, err) - stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false) + stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(stacks) @@ -32,7 +32,7 @@ func TestDescribeStacksWithFilter1(t *testing.T) { stack := "tenant1-ue2-dev" - stacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, nil, false) + stacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, nil, false, false) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(stacks) @@ -49,7 +49,7 @@ func TestDescribeStacksWithFilter2(t *testing.T) { stack := "tenant1-ue2-dev" components := []string{"infra/vpc"} - stacks, err := ExecuteDescribeStacks(cliConfig, stack, components, nil, nil, false) + stacks, err := ExecuteDescribeStacks(cliConfig, stack, components, nil, nil, false, false) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(stacks) @@ -66,7 +66,7 @@ func TestDescribeStacksWithFilter3(t *testing.T) { stack := "tenant1-ue2-dev" sections := []string{"vars"} - stacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, sections, false) + stacks, err := ExecuteDescribeStacks(cliConfig, stack, nil, nil, sections, false, false) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(stacks) @@ -83,7 +83,7 @@ func TestDescribeStacksWithFilter4(t *testing.T) { componentTypes := []string{"terraform"} sections := []string{"none"} - stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, componentTypes, sections, false) + stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, componentTypes, sections, false, false) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(stacks) @@ -101,7 +101,7 @@ func TestDescribeStacksWithFilter5(t *testing.T) { components := []string{"top-level-component1"} sections := []string{"vars"} - stacks, err := ExecuteDescribeStacks(cliConfig, "", components, componentTypes, sections, false) + stacks, err := ExecuteDescribeStacks(cliConfig, "", components, componentTypes, sections, false, false) assert.Nil(t, err) assert.Equal(t, 8, len(stacks)) @@ -133,7 +133,7 @@ func TestDescribeStacksWithFilter6(t *testing.T) { components := []string{"top-level-component1"} sections := []string{"workspace"} - stacks, err := ExecuteDescribeStacks(cliConfig, "tenant1-ue2-dev", components, componentTypes, sections, false) + stacks, err := ExecuteDescribeStacks(cliConfig, "tenant1-ue2-dev", components, componentTypes, sections, false, false) assert.Nil(t, err) assert.Equal(t, 1, len(stacks)) @@ -160,7 +160,7 @@ func TestDescribeStacksWithFilter7(t *testing.T) { components := []string{"test/test-component-override-3"} sections := []string{"workspace"} - stacks, err := ExecuteDescribeStacks(cliConfig, stack, components, componentTypes, sections, false) + stacks, err := ExecuteDescribeStacks(cliConfig, stack, components, componentTypes, sections, false, false) assert.Nil(t, err) assert.Equal(t, 1, len(stacks)) @@ -175,3 +175,94 @@ func TestDescribeStacksWithFilter7(t *testing.T) { assert.Nil(t, err) t.Log(stacksYaml) } + +func TestDescribeStacksWithEmptyStacks(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + assert.Nil(t, err) + + stacks, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false) + assert.Nil(t, err) + + initialStackCount := len(stacks) + + stacksWithEmpty, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, true) + assert.Nil(t, err) + + assert.Greater(t, len(stacksWithEmpty), initialStackCount, "Should include more stacks when empty stacks are included") + + foundEmptyStack := false + for _, stackContent := range stacksWithEmpty { + if components, ok := stackContent.(map[string]any)["components"].(map[string]any); ok { + if len(components) == 0 { + foundEmptyStack = true + break + } + if len(components) == 1 { + if terraformComps, hasTerraform := components["terraform"].(map[string]any); hasTerraform { + if len(terraformComps) == 0 { + foundEmptyStack = true + break + } + } + } + } + } + assert.True(t, foundEmptyStack, "Should find at least one empty stack") +} + +func TestDescribeStacksWithVariousEmptyStacks(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + assert.Nil(t, err) + + stacksWithoutEmpty, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, false) + assert.Nil(t, err) + initialCount := len(stacksWithoutEmpty) + + stacksWithEmpty, err := ExecuteDescribeStacks(cliConfig, "", nil, nil, nil, false, true) + assert.Nil(t, err) + + assert.Greater(t, len(stacksWithEmpty), initialCount, "Should have more stacks when including empty ones") + + var ( + emptyStacks []string + nonEmptyStacks []string + ) + + for stackName, stackContent := range stacksWithEmpty { + if stack, ok := stackContent.(map[string]any); ok { + if components, hasComponents := stack["components"].(map[string]any); hasComponents { + // Check for completely empty components + if len(components) == 0 { + emptyStacks = append(emptyStacks, stackName) + continue + } + + // Check if only terraform exists and is empty + if len(components) == 1 { + if terraformComps, hasTerraform := components["terraform"].(map[string]any); hasTerraform { + if len(terraformComps) == 0 { + emptyStacks = append(emptyStacks, stackName) + continue + } + } + } + + // If we have any components at all, consider it non-empty + for _, compType := range components { + if compMap, ok := compType.(map[string]any); ok && len(compMap) > 0 { + nonEmptyStacks = append(nonEmptyStacks, stackName) + break + } + } + } + } + } + + // Verify we found both types of stacks + assert.NotEmpty(t, emptyStacks, "Should find at least one empty stack") + assert.NotEmpty(t, nonEmptyStacks, "Should find at least one non-empty stack") +} diff --git a/pkg/generate/atmos.yaml b/pkg/generate/atmos.yaml index 95ec5e3fe..c2c2cacb5 100644 --- a/pkg/generate/atmos.yaml +++ b/pkg/generate/atmos.yaml @@ -172,37 +172,6 @@ commands: - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s tenant1-ue1-dev - atmos list components --stack tenant2-uw2-prod - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - steps: - - > - {{ if .Flags.stack }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - - name: set-eks-cluster description: | Download 'kubeconfig' and set EKS cluster. diff --git a/pkg/list/atmos.yaml b/pkg/list/atmos.yaml new file mode 100644 index 000000000..c2c2cacb5 --- /dev/null +++ b/pkg/list/atmos.yaml @@ -0,0 +1,319 @@ +# CLI config is loaded from the following locations (from lowest to highest priority): +# system dir ('/usr/local/etc/atmos' on Linux, '%LOCALAPPDATA%/atmos' on Windows) +# home dir (~/.atmos) +# current directory +# ENV vars +# Command-line arguments +# +# It supports POSIX-style Globs for file names/paths (double-star '**' is supported) +# https://en.wikipedia.org/wiki/Glob_(programming) + +# Base path for components, stacks and workflows configurations. +# Can also be set using 'ATMOS_BASE_PATH' ENV var, or '--base-path' command-line argument. +# Supports both absolute and relative paths. +# If not provided or is an empty string, 'components.terraform.base_path', 'components.helmfile.base_path', 'stacks.base_path' and 'workflows.base_path' +# are independent settings (supporting both absolute and relative paths). +# If 'base_path' is provided, 'components.terraform.base_path', 'components.helmfile.base_path', 'stacks.base_path' and 'workflows.base_path' +# are considered paths relative to 'base_path'. +base_path: "../../examples/tests" + +components: + terraform: + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument + # Supports both absolute and relative paths + base_path: "components/terraform" + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_APPLY_AUTO_APPROVE' ENV var + apply_auto_approve: false + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_DEPLOY_RUN_INIT' ENV var, or '--deploy-run-init' command-line argument + deploy_run_init: true + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_INIT_RUN_RECONFIGURE' ENV var, or '--init-run-reconfigure' command-line argument + init_run_reconfigure: true + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_AUTO_GENERATE_BACKEND_FILE' ENV var, or '--auto-generate-backend-file' command-line argument + auto_generate_backend_file: false + helmfile: + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_BASE_PATH' ENV var, or '--helmfile-dir' command-line argument + # Supports both absolute and relative paths + base_path: "components/helmfile" + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_USE_EKS' ENV var + # If not specified, defaults to 'true' + use_eks: true + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_KUBECONFIG_PATH' ENV var + kubeconfig_path: "/dev/shm" + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_HELM_AWS_PROFILE_PATTERN' ENV var + helm_aws_profile_pattern: "{namespace}-{tenant}-gbl-{stage}-helm" + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_CLUSTER_NAME_PATTERN' ENV var + cluster_name_pattern: "{namespace}-{tenant}-{environment}-{stage}-eks-cluster" + +stacks: + # Can also be set using 'ATMOS_STACKS_BASE_PATH' ENV var, or '--config-dir' and '--stacks-dir' command-line arguments + # Supports both absolute and relative paths + base_path: "stacks" + # Can also be set using 'ATMOS_STACKS_INCLUDED_PATHS' ENV var (comma-separated values string) + included_paths: + - "orgs/**/*" + # Can also be set using 'ATMOS_STACKS_EXCLUDED_PATHS' ENV var (comma-separated values string) + excluded_paths: + - "**/_defaults.yaml" + # Can also be set using 'ATMOS_STACKS_NAME_PATTERN' ENV var + name_pattern: "{tenant}-{environment}-{stage}" + +workflows: + # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments + # Supports both absolute and relative paths + base_path: "stacks/workflows" + +logs: + # Can also be set using 'ATMOS_LOGS_FILE' ENV var, or '--logs-file' command-line argument + # File or standard file descriptor to write logs to + # Logs can be written to any file or any standard file descriptor, including `/dev/stdout`, `/dev/stderr` and `/dev/null` + file: "/dev/stderr" + # Supported log levels: Trace, Debug, Info, Warning, Off + # Can also be set using 'ATMOS_LOGS_LEVEL' ENV var, or '--logs-level' command-line argument + level: Info + +# Custom CLI commands +commands: + - name: tf + description: Execute 'terraform' commands + # subcommands + commands: + - name: plan + description: This command plans terraform components + arguments: + - name: component + description: Name of the component + flags: + - name: stack + shorthand: s + description: Name of the stack + required: true + env: + - key: ENV_VAR_1 + value: ENV_VAR_1_value + - key: ENV_VAR_2 + # 'valueCommand' is an external command to execute to get the value for the ENV var + # Either 'value' or 'valueCommand' can be specified for the ENV var, but not both + valueCommand: echo ENV_VAR_2_value + # steps support Go templates + steps: + - atmos terraform plan {{ .Arguments.component }} -s {{ .Flags.stack }} + - name: terraform + description: Execute 'terraform' commands + # subcommands + commands: + - name: provision + description: This command provisions terraform components + arguments: + - name: component + description: Name of the component + flags: + - name: stack + shorthand: s + description: Name of the stack + required: true + # ENV var values support Go templates + env: + - key: ATMOS_COMPONENT + value: "{{ .Arguments.component }}" + - key: ATMOS_STACK + value: "{{ .Flags.stack }}" + steps: + - atmos terraform plan $ATMOS_COMPONENT -s $ATMOS_STACK + - atmos terraform apply $ATMOS_COMPONENT -s $ATMOS_STACK + - name: show + description: Execute 'show' commands + # subcommands + commands: + - name: component + description: Execute 'show component' command + arguments: + - name: component + description: Name of the component + flags: + - name: stack + shorthand: s + description: Name of the stack + required: true + # ENV var values support Go templates and have access to {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables + env: + - key: ATMOS_COMPONENT + value: "{{ .Arguments.component }}" + - key: ATMOS_STACK + value: "{{ .Flags.stack }}" + - key: ATMOS_TENANT + value: "{{ .ComponentConfig.vars.tenant }}" + - key: ATMOS_STAGE + value: "{{ .ComponentConfig.vars.stage }}" + - key: ATMOS_ENVIRONMENT + value: "{{ .ComponentConfig.vars.environment }}" + - key: ATMOS_IS_PROD + value: "{{ .ComponentConfig.settings.config.is_prod }}" + # If a custom command defines 'component_config' section with 'component' and 'stack', 'atmos' generates the config for the component in the stack + # and makes it available in {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables, + # exposing all the component sections (which are also shown by 'atmos describe component' command) + component_config: + component: "{{ .Arguments.component }}" + stack: "{{ .Flags.stack }}" + # Steps support using Go templates and can access all configuration settings (e.g. {{ .ComponentConfig.xxx.yyy.zzz }}) + # Steps also have access to the ENV vars defined in the 'env' section of the 'command' + steps: + - 'echo Atmos component from argument: "{{ .Arguments.component }}"' + - 'echo ATMOS_COMPONENT: "$ATMOS_COMPONENT"' + - 'echo Atmos stack: "{{ .Flags.stack }}"' + - 'echo Terraform component: "{{ .ComponentConfig.component }}"' + - 'echo Backend S3 bucket: "{{ .ComponentConfig.backend.bucket }}"' + - 'echo Terraform workspace: "{{ .ComponentConfig.workspace }}"' + - 'echo Namespace: "{{ .ComponentConfig.vars.namespace }}"' + - 'echo Tenant: "{{ .ComponentConfig.vars.tenant }}"' + - 'echo Environment: "{{ .ComponentConfig.vars.environment }}"' + - 'echo Stage: "{{ .ComponentConfig.vars.stage }}"' + - 'echo settings.spacelift.workspace_enabled: "{{ .ComponentConfig.settings.spacelift.workspace_enabled }}"' + - 'echo Dependencies: "{{ .ComponentConfig.deps }}"' + - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' + - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' + + - name: set-eks-cluster + description: | + Download 'kubeconfig' and set EKS cluster. + + Example usage: + atmos set-eks-cluster eks/cluster -s tenant1-ue1-dev -r admin + atmos set-eks-cluster eks/cluster -s tenant2-uw2-prod --role reader + verbose: false # Set to `true` to see verbose outputs + arguments: + - name: component + description: Name of the component + flags: + - name: stack + shorthand: s + description: Name of the stack + required: true + - name: role + shorthand: r + description: IAM role to use + required: true + # If a custom command defines 'component_config' section with 'component' and 'stack', + # Atmos generates the config for the component in the stack + # and makes it available in {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables, + # exposing all the component sections (which are also shown by 'atmos describe component' command) + component_config: + component: "{{ .Arguments.component }}" + stack: "{{ .Flags.stack }}" + env: + - key: KUBECONFIG + value: /dev/shm/kubecfg.{{ .Flags.stack }}-{{ .Flags.role }} + steps: + - > + aws + --profile {{ .ComponentConfig.vars.namespace }}-{{ .ComponentConfig.vars.tenant }}-gbl-{{ .ComponentConfig.vars.stage }}-{{ .Flags.role }} + --region {{ .ComponentConfig.vars.region }} + eks update-kubeconfig + --name={{ .ComponentConfig.vars.namespace }}-{{ .Flags.stack }}-eks-cluster + --kubeconfig="${KUBECONFIG}" + > /dev/null + - chmod 600 ${KUBECONFIG} + - echo ${KUBECONFIG} + +# Integrations +integrations: + + # Atlantis integration + # https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html + atlantis: + # Path and name of the Atlantis config file 'atlantis.yaml' + # Supports absolute and relative paths + # All the intermediate folders will be created automatically (e.g. 'path: /config/atlantis/atlantis.yaml') + # Can be overridden on the command line by using '--output-path' command-line argument in 'atmos atlantis generate repo-config' command + # If not specified (set to an empty string/omitted here, and set to an empty string on the command line), the content of the file will be dumped to 'stdout' + # On Linux/macOS, you can also use '--output-path=/dev/stdout' to dump the content to 'stdout' without setting it to an empty string in 'atlantis.path' + path: "atlantis.yaml" + + # Config templates + # Select a template by using the '--config-template ' command-line argument in 'atmos atlantis generate repo-config' command + config_templates: + config-1: + version: 3 + automerge: true + delete_source_branch_on_merge: true + parallel_plan: true + parallel_apply: true + allowed_regexp_prefixes: + - dev/ + - staging/ + - prod/ + + # Project templates + # Select a template by using the '--project-template ' command-line argument in 'atmos atlantis generate repo-config' command + project_templates: + project-1: + # generate a project entry for each component in every stack + name: "{tenant}-{environment}-{stage}-{component}" + workspace: "{workspace}" + dir: "{component-path}" + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - "**/*.tf" + - "varfiles/$PROJECT_NAME.tfvars.json" + apply_requirements: + - "approved" + + # Workflow templates + # https://www.runatlantis.io/docs/custom-workflows.html#custom-init-plan-apply-commands + # https://www.runatlantis.io/docs/custom-workflows.html#custom-run-command + workflow_templates: + workflow-1: + plan: + steps: + - run: terraform init -input=false + # When using workspaces, you need to select the workspace using the $WORKSPACE environment variable + - run: terraform workspace select $WORKSPACE || terraform workspace new $WORKSPACE + # You must output the plan using '-out $PLANFILE' because Atlantis expects plans to be in a specific location + - run: terraform plan -input=false -refresh -out $PLANFILE -var-file varfiles/$PROJECT_NAME.tfvars.json + apply: + steps: + - run: terraform apply $PLANFILE + +# Validation schemas (for validating atmos stacks and components) +schemas: + # https://json-schema.org + jsonschema: + # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments + # Supports both absolute and relative paths + base_path: "stacks/schemas/jsonschema" + # https://www.openpolicyagent.org + opa: + # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments + # Supports both absolute and relative paths + base_path: "stacks/schemas/opa" + # JSON Schema to validate Atmos manifests + # https://atmos.tools/cli/schemas/ + # https://atmos.tools/cli/commands/validate/stacks/ + # https://atmos.tools/quick-start/advanced/configure-validation/ + # https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + # https://json-schema.org/draft/2020-12/release-notes + # https://www.schemastore.org/json + # https://github.com/SchemaStore/schemastore + atmos: + # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments + # Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`) + manifest: "../quick-start-advanced/stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json" + +# `Go` templates in Atmos manifests +# https://atmos.tools/core-concepts/stacks/templates +# https://pkg.go.dev/text/template +templates: + settings: + enabled: true + evaluations: 1 + # https://masterminds.github.io/sprig + sprig: + enabled: true + # https://docs.gomplate.ca + gomplate: + enabled: true + timeout: 5 + # https://docs.gomplate.ca/datasources + datasources: {} diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go new file mode 100644 index 000000000..9b71ee4d3 --- /dev/null +++ b/pkg/list/list_components.go @@ -0,0 +1,65 @@ +package list + +import ( + "fmt" + "sort" + "strings" + + "github.com/samber/lo" +) + +// getStackComponents extracts Terraform components from the final map of stacks +func getStackComponents(stackData any) ([]string, error) { + stackMap, ok := stackData.(map[string]any) + if !ok { + return nil, fmt.Errorf("could not parse stacks") + } + + componentsMap, ok := stackMap["components"].(map[string]any) + if !ok { + return nil, fmt.Errorf("could not parse components") + } + + terraformComponents, ok := componentsMap["terraform"].(map[string]any) + if !ok { + return nil, fmt.Errorf("could not parse Terraform components") + } + + return lo.Keys(terraformComponents), nil +} + +// FilterAndListComponents filters and lists components based on the given stack +func FilterAndListComponents(stackFlag string, stacksMap map[string]any) (string, error) { + components := []string{} + + if stackFlag != "" { + // Filter components for the specified stack + if stackData, ok := stacksMap[stackFlag]; ok { + stackComponents, err := getStackComponents(stackData) + if err != nil { + return "", fmt.Errorf("error processing stack '%s': %w", stackFlag, err) + } + components = append(components, stackComponents...) + } else { + return "", fmt.Errorf("stack '%s' not found", stackFlag) + } + } else { + // Get all components from all stacks + for _, stackData := range stacksMap { + stackComponents, err := getStackComponents(stackData) + if err != nil { + continue // Skip invalid stacks + } + components = append(components, stackComponents...) + } + } + + // Remove duplicates and sort components + components = lo.Uniq(components) + sort.Strings(components) + + if len(components) == 0 { + return "No components found", nil + } + return strings.Join(components, "\n") + "\n", nil +} diff --git a/pkg/list/list_components_test.go b/pkg/list/list_components_test.go new file mode 100644 index 000000000..1d30b020f --- /dev/null +++ b/pkg/list/list_components_test.go @@ -0,0 +1,56 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + e "github.com/cloudposse/atmos/internal/exec" + cfg "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/schema" + u "github.com/cloudposse/atmos/pkg/utils" +) + +const ( + testStack = "tenant1-ue2-dev" +) + +func TestListComponents(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + assert.Nil(t, err) + + stacksMap, err := e.ExecuteDescribeStacks(cliConfig, "", nil, nil, + nil, false, false, false) + assert.Nil(t, err) + + output, err := FilterAndListComponents("", stacksMap) + assert.Nil(t, err) + dependentsYaml, err := u.ConvertToYAML(output) + assert.Nil(t, err) + // Add assertions to validate the output structure + assert.NotNil(t, dependentsYaml) + assert.Greater(t, len(dependentsYaml), 0) + t.Log(dependentsYaml) +} + +func TestListComponentsWithStack(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + assert.Nil(t, err) + + stacksMap, err := e.ExecuteDescribeStacks(cliConfig, testStack, nil, nil, + nil, false, false, false) + assert.Nil(t, err) + + output, err := FilterAndListStacks(stacksMap, testStack) + assert.Nil(t, err) + dependentsYaml, err := u.ConvertToYAML(output) + assert.Nil(t, err) + assert.NotNil(t, dependentsYaml) + assert.Greater(t, len(dependentsYaml), 0) + assert.Contains(t, dependentsYaml, testStack) + t.Log(dependentsYaml) +} diff --git a/pkg/list/list_stacks.go b/pkg/list/list_stacks.go new file mode 100644 index 000000000..6370512b6 --- /dev/null +++ b/pkg/list/list_stacks.go @@ -0,0 +1,45 @@ +package list + +import ( + "fmt" + "sort" + "strings" + + "github.com/samber/lo" +) + +// FilterAndListStacks filters stacks by the given component +func FilterAndListStacks(stacksMap map[string]any, component string) (string, error) { + if component != "" { + // Filter stacks by component + filteredStacks := []string{} + for stackName, stackData := range stacksMap { + v2, ok := stackData.(map[string]any) + if !ok { + continue + } + components, ok := v2["components"].(map[string]any) + if !ok { + continue + } + terraform, ok := components["terraform"].(map[string]any) + if !ok { + continue + } + if _, exists := terraform[component]; exists { + filteredStacks = append(filteredStacks, stackName) + } + } + + if len(filteredStacks) == 0 { + return fmt.Sprintf("No stacks found for component '%s'"+"\n", component), nil + } + sort.Strings(filteredStacks) + return strings.Join(filteredStacks, "\n") + "\n", nil + } + + // List all stacks + stacks := lo.Keys(stacksMap) + sort.Strings(stacks) + return strings.Join(stacks, "\n") + "\n", nil +} diff --git a/pkg/list/list_stacks_test.go b/pkg/list/list_stacks_test.go new file mode 100644 index 000000000..190b69016 --- /dev/null +++ b/pkg/list/list_stacks_test.go @@ -0,0 +1,54 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + e "github.com/cloudposse/atmos/internal/exec" + cfg "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/schema" + u "github.com/cloudposse/atmos/pkg/utils" +) + +const ( + testComponent = "infra/vpc" +) + +func TestListStacks(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + assert.Nil(t, err) + + stacksMap, err := e.ExecuteDescribeStacks(cliConfig, "", nil, nil, + nil, false, false, false) + assert.Nil(t, err) + + output, err := FilterAndListStacks(stacksMap, "") + assert.Nil(t, err) + dependentsYaml, err := u.ConvertToYAML(output) + assert.NotEmpty(t, dependentsYaml) +} + +func TestListStacksWithComponent(t *testing.T) { + configAndStacksInfo := schema.ConfigAndStacksInfo{} + + cliConfig, err := cfg.InitCliConfig(configAndStacksInfo, true) + assert.Nil(t, err) + component := testComponent + + stacksMap, err := e.ExecuteDescribeStacks(cliConfig, component, nil, nil, + nil, false, false, false) + assert.Nil(t, err) + + output, err := FilterAndListStacks(stacksMap, component) + assert.Nil(t, err) + dependentsYaml, err := u.ConvertToYAML(output) + assert.Nil(t, err) + + // Verify the output structure + assert.NotEmpty(t, dependentsYaml) + // Verify that only stacks with the specified component are included + assert.Contains(t, dependentsYaml, testComponent) +} diff --git a/pkg/validate/atmos.yaml b/pkg/validate/atmos.yaml index 95ec5e3fe..c2c2cacb5 100644 --- a/pkg/validate/atmos.yaml +++ b/pkg/validate/atmos.yaml @@ -172,37 +172,6 @@ commands: - 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"' - 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"' - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s tenant1-ue1-dev - atmos list components --stack tenant2-uw2-prod - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - steps: - - > - {{ if .Flags.stack }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - - name: set-eks-cluster description: | Download 'kubeconfig' and set EKS cluster. diff --git a/website/docs/cli/commands/list/_category_.json b/website/docs/cli/commands/list/_category_.json new file mode 100644 index 000000000..db8f13d4c --- /dev/null +++ b/website/docs/cli/commands/list/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "list", + "position": 6, + "className": "command", + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "usage" + } +} diff --git a/website/docs/cli/commands/list/list-components.mdx b/website/docs/cli/commands/list/list-components.mdx new file mode 100644 index 000000000..016b4b4ca --- /dev/null +++ b/website/docs/cli/commands/list/list-components.mdx @@ -0,0 +1,45 @@ +--- +title: atmos list components +sidebar_label: components +sidebar_class_name: command +id: component +description: Use this command to list Atmos components +--- +import Screengrab from '@site/src/components/Screengrab' + +:::note purpose +Use this command to list all Atmos components or Atmos components in a specified stack. +::: + + + +## Usage + +Execute the `list components` command like this: + +```shell +atmos list components +``` + +This command lists Atmos components in a specified stack. + +```shell +atmos list components -s +``` + +:::tip +Run `atmos list components --help` to see all the available options +::: + +## Examples + +```shell +atmos list components +atmos list components -s tenant1-ue2-dev +``` + +## Flags + +| Flag | Description | Alias | Required | +|:-----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------|:---------| +| `--stack` | Atmos stack | `-s` | no | diff --git a/website/docs/cli/commands/list/list-stacks.mdx b/website/docs/cli/commands/list/list-stacks.mdx new file mode 100644 index 000000000..ae261d3f0 --- /dev/null +++ b/website/docs/cli/commands/list/list-stacks.mdx @@ -0,0 +1,46 @@ +--- +title: atmos list stacks +sidebar_label: stacks +sidebar_class_name: command +id: stacks +description: Use this command to list all Stack configurations or a stack of a specified component. +--- +import Screengrab from '@site/src/components/Screengrab' +import Terminal from '@site/src/components/Terminal' + +:::note Purpose +Use this command to list Atmos stacks. +::: + + + +## Usage + +Execute the `list stacks` command like this: + +```shell +atmos list stacks +``` + +To view all stacks for a provided component, execute the `list stacks` command like this: + +```shell +atmos list stacks -c +``` + +:::tip +Run `atmos list stacks --help` to see all the available options +::: + +## Examples + +```shell +atmos list stacks +atmos list stacks -c vpc +``` + +## Flags + +| Flag | Description | Alias | Required | +|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|----------| +| `--component` | Atmos component | `-c` | no | diff --git a/website/docs/cli/commands/list/usage.mdx b/website/docs/cli/commands/list/usage.mdx new file mode 100644 index 000000000..95776da44 --- /dev/null +++ b/website/docs/cli/commands/list/usage.mdx @@ -0,0 +1,18 @@ +--- +title: atmos list +sidebar_label: list +sidebar_class_name: command +description: "List Atmos Stacks and Components" +--- +import Screengrab from '@site/src/components/Screengrab' +import DocCardList from '@theme/DocCardList'; + +:::note Purpose +Use these subcommands to validate Atmos configurations. +::: + + + +## Subcommands + + diff --git a/website/docs/cli/commands/pro/_category_.json b/website/docs/cli/commands/pro/_category_.json index 23a7e15c1..03b4abd65 100644 --- a/website/docs/cli/commands/pro/_category_.json +++ b/website/docs/cli/commands/pro/_category_.json @@ -1,6 +1,6 @@ { "label": "pro", - "position": 5, + "position": 7, "className": "command", "collapsible": true, "collapsed": true, diff --git a/website/docs/cli/commands/terraform/_category_.json b/website/docs/cli/commands/terraform/_category_.json index 6a2bc28ed..0991cb0b6 100644 --- a/website/docs/cli/commands/terraform/_category_.json +++ b/website/docs/cli/commands/terraform/_category_.json @@ -1,6 +1,6 @@ { "label": "terraform", - "position": 6, + "position": 8, "className": "command", "collapsible": true, "collapsed": true, diff --git a/website/docs/cli/commands/validate/_category_.json b/website/docs/cli/commands/validate/_category_.json index c375f5cfb..7a049b4a4 100644 --- a/website/docs/cli/commands/validate/_category_.json +++ b/website/docs/cli/commands/validate/_category_.json @@ -1,6 +1,6 @@ { "label": "validate", - "position": 7, + "position": 9, "className": "command", "collapsible": true, "collapsed": true, diff --git a/website/docs/cli/commands/vendor/_category_.json b/website/docs/cli/commands/vendor/_category_.json index 099960a5a..4ccaa9352 100644 --- a/website/docs/cli/commands/vendor/_category_.json +++ b/website/docs/cli/commands/vendor/_category_.json @@ -1,6 +1,6 @@ { "label": "vendor", - "position": 8, + "position": 10, "className": "command", "collapsible": true, "collapsed": true, diff --git a/website/docs/core-concepts/custom-commands/custom-commands.mdx b/website/docs/core-concepts/custom-commands/custom-commands.mdx index 63425f5ed..471376ab5 100644 --- a/website/docs/core-concepts/custom-commands/custom-commands.mdx +++ b/website/docs/core-concepts/custom-commands/custom-commands.mdx @@ -147,66 +147,6 @@ commands: - atmos terraform apply {{ .Arguments.component }} -s {{ .Flags.stack }} -auto-approve ``` -### List Atmos Stacks and Components - -```yaml -# Custom CLI commands -commands: - - name: list - description: Execute 'atmos list' commands - # subcommands - commands: - - name: stacks - description: | - List all Atmos stacks. - steps: - - > - atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g - - name: components - description: | - List all Atmos components in all stacks or in a single stack. - - Example usage: - atmos list components - atmos list components -s plat-ue2-dev - atmos list components --stack plat-uw2-prod - atmos list components -s plat-ue2-dev --type abstract - atmos list components -s plat-ue2-dev -t enabled - atmos list components -s plat-ue2-dev -t disabled - flags: - - name: stack - shorthand: s - description: Name of the stack - required: false - - name: type - shorthand: t - description: Component types - abstract, enabled, or disabled - required: false - steps: - - > - {{ if .Flags.stack }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r .key - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r .key - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --stack {{ .Flags.stack }} --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r .key - {{ else }} - atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ else }} - {{ if eq .Flags.type "enabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == true)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "disabled" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.vars.enabled == false)' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else if eq .Flags.type "abstract" }} - atmos describe stacks --format json | jq '.[].components.terraform | to_entries[] | select(.value.metadata.type == "abstract")' | jq -r '[.key]' | jq -s 'add' | jq 'unique | sort' | jq -r "values[]" - {{ else }} - atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]" - {{ end }} - {{ end }} -``` - ### Show Component Info ```yaml diff --git a/website/docs/integrations/atlantis.mdx b/website/docs/integrations/atlantis.mdx index 0a3222ae8..f2d426506 100644 --- a/website/docs/integrations/atlantis.mdx +++ b/website/docs/integrations/atlantis.mdx @@ -673,7 +673,7 @@ on: branches: [ main ] env: - ATMOS_VERSION: 1.105.0 + ATMOS_VERSION: 1.109.0 ATMOS_CLI_CONFIG_PATH: ./ jobs: