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 atmos describe stacks command #133

Merged
merged 19 commits into from Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions cmd/describe_stacks.go
@@ -0,0 +1,32 @@
package cmd

import (
e "github.com/cloudposse/atmos/internal/exec"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/spf13/cobra"
)

// describeComponentCmd describes configuration for components
var describeStacksCmd = &cobra.Command{
Use: "stacks",
Short: "Execute 'describe stacks' command",
Long: `This command shows configuration for stacks and components in the stacks: atmos describe stacks <options>`,
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true},
Run: func(cmd *cobra.Command, args []string) {
err := e.ExecuteDescribeStacks(cmd, args)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
},
}

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 output format: atmos describe stacks --format=yaml/json ('yaml' is default)")
describeStacksCmd.PersistentFlags().StringP("stack", "s", "", "Filter by a specific stack: atmos describe stacks -s <stack>")
describeStacksCmd.PersistentFlags().String("components", "", "Filter by specific components: atmos describe stacks --components=<component1>,<component2>")
describeStacksCmd.PersistentFlags().String("sections", "", "Output only these sections: atmos describe stacks --sections=vars,settings. Available sections: backend, backend_type, deps, env, inheritance, metadata, remote_state_backend, remote_state_backend_type, settings, vars")

describeCmd.AddCommand(describeStacksCmd)
}
4 changes: 2 additions & 2 deletions internal/exec/describe_component.go
Expand Up @@ -30,10 +30,10 @@ func ExecuteDescribeComponent(cmd *cobra.Command, args []string) error {
configAndStacksInfo.Stack = stack

configAndStacksInfo.ComponentType = "terraform"
configAndStacksInfo, err = ProcessStacks(configAndStacksInfo)
configAndStacksInfo, err = ProcessStacks(configAndStacksInfo, true)
if err != nil {
configAndStacksInfo.ComponentType = "helmfile"
configAndStacksInfo, err = ProcessStacks(configAndStacksInfo)
configAndStacksInfo, err = ProcessStacks(configAndStacksInfo, true)
if err != nil {
return err
}
Expand Down
147 changes: 147 additions & 0 deletions internal/exec/describe_stacks.go
@@ -0,0 +1,147 @@
package exec

import (
"errors"
"fmt"
c "github.com/cloudposse/atmos/pkg/config"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/spf13/cobra"
"strings"
)

// ExecuteDescribeStacks executes `describe stacks` command
func ExecuteDescribeStacks(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()

filterByStack, err := flags.GetString("stack")
if err != nil {
return err
}

format, err := flags.GetString("format")
if err != nil {
return err
}
if format != "" && format != "yaml" && format != "json" {
return errors.New(fmt.Sprintf("Invalid '--format' flag '%s'. Valid values are 'yaml' (default) and 'json'", format))
}
if format == "" {
format = "yaml"
}

file, err := flags.GetString("file")
if err != nil {
return err
}

componentsCsv, err := flags.GetString("components")
if err != nil {
return err
}
var components []string
if componentsCsv != "" {
components = strings.Split(componentsCsv, ",")
}

sectionsCsv, err := flags.GetString("sections")
if err != nil {
return err
}
var sections []string
if sectionsCsv != "" {
sections = strings.Split(sectionsCsv, ",")
}

var configAndStacksInfo c.ConfigAndStacksInfo
configAndStacksInfo.Stack = filterByStack
stacksMap, err := FindStacksMap(configAndStacksInfo, filterByStack != "")
if err != nil {
return err
}

finalStacksMap := make(map[string]interface{})

for stackName, stackSection := range stacksMap {
if filterByStack == "" || filterByStack == stackName {
// Delete the stack-wide imports
delete(stackSection.(map[interface{}]interface{}), "imports")

if !u.MapKeyExists(finalStacksMap, stackName) {
finalStacksMap[stackName] = make(map[string]interface{})
}

if componentsSection, ok := stackSection.(map[interface{}]interface{})["components"].(map[string]interface{}); ok {
if terraformSection, ok2 := componentsSection["terraform"].(map[string]interface{}); ok2 {
for compName, comp := range terraformSection {
if len(components) == 0 || u.SliceContainsString(components, compName) {
if !u.MapKeyExists(finalStacksMap[stackName].(map[string]interface{}), "components") {
finalStacksMap[stackName].(map[string]interface{})["components"] = make(map[string]interface{})
}
if !u.MapKeyExists(finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{}), "terraform") {
finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["terraform"] = make(map[string]interface{})
}
if !u.MapKeyExists(finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["terraform"].(map[string]interface{}), compName) {
finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["terraform"].(map[string]interface{})[compName] = make(map[string]interface{})
}

for sectionName, section := range comp.(map[string]interface{}) {
if len(sections) == 0 || u.SliceContainsString(sections, sectionName) {
finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["terraform"].(map[string]interface{})[compName].(map[string]interface{})[sectionName] = section
}
}
}
}
}
if helmfileSection, ok3 := componentsSection["helmfile"].(map[string]interface{}); ok3 {
for compName, comp := range helmfileSection {
if len(components) == 0 || u.SliceContainsString(components, compName) {
if !u.MapKeyExists(finalStacksMap[stackName].(map[string]interface{}), "components") {
finalStacksMap[stackName].(map[string]interface{})["components"] = make(map[string]interface{})
}
if !u.MapKeyExists(finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{}), "helmfile") {
finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["helmfile"] = make(map[string]interface{})
}
if !u.MapKeyExists(finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["helmfile"].(map[string]interface{}), compName) {
finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["helmfile"].(map[string]interface{})[compName] = make(map[string]interface{})
}

for sectionName, section := range comp.(map[string]interface{}) {
if len(sections) == 0 || u.SliceContainsString(sections, sectionName) {
finalStacksMap[stackName].(map[string]interface{})["components"].(map[string]interface{})["helmfile"].(map[string]interface{})[compName].(map[string]interface{})[sectionName] = section
}
}
}
}
}
}
}
}

if format == "yaml" {
if file == "" {
err = u.PrintAsYAML(finalStacksMap)
if err != nil {
return err
}
} else {
err = u.WriteToFileAsYAML(file, finalStacksMap, 0644)
if err != nil {
return err
}
}
} else if format == "json" {
if file == "" {
err = u.PrintAsJSON(finalStacksMap)
if err != nil {
return err
}
} else {
err = u.WriteToFileAsJSON(file, finalStacksMap, 0644)
if err != nil {
return err
}
}
}

return nil
}
2 changes: 1 addition & 1 deletion internal/exec/helmfile_generate_varfile.go
Expand Up @@ -29,7 +29,7 @@ func ExecuteHelmfileGenerateVarfile(cmd *cobra.Command, args []string) error {
info.Stack = stack
info.ComponentType = "helmfile"

info, err = ProcessStacks(info)
info, err = ProcessStacks(info, true)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/terraform_generate_backend.go
Expand Up @@ -30,7 +30,7 @@ func ExecuteTerraformGenerateBackend(cmd *cobra.Command, args []string) error {
info.Stack = stack
info.ComponentType = "terraform"

info, err = ProcessStacks(info)
info, err = ProcessStacks(info, true)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/terraform_generate_varfile.go
Expand Up @@ -29,7 +29,7 @@ func ExecuteTerraformGenerateVarfile(cmd *cobra.Command, args []string) error {
info.Stack = stack
info.ComponentType = "terraform"

info, err = ProcessStacks(info)
info, err = ProcessStacks(info, true)
if err != nil {
return err
}
Expand Down
50 changes: 30 additions & 20 deletions internal/exec/utils.go
Expand Up @@ -192,34 +192,20 @@ func processArgsConfigAndStacks(componentType string, cmd *cobra.Command, args [
return configAndStacksInfo, err
}

return ProcessStacks(configAndStacksInfo)
return ProcessStacks(configAndStacksInfo, true)
}

// ProcessStacks processes stack config
func ProcessStacks(configAndStacksInfo c.ConfigAndStacksInfo) (c.ConfigAndStacksInfo, error) {
// Check if stack was provided
if len(configAndStacksInfo.Stack) < 1 {
message := fmt.Sprintf("'stack' is required. Usage: atmos %s <command> <component> -s <stack>", configAndStacksInfo.ComponentType)
return configAndStacksInfo, errors.New(message)
}

// Check if component was provided
if len(configAndStacksInfo.ComponentFromArg) < 1 {
message := fmt.Sprintf("'component' is required. Usage: atmos %s <command> <component> <arguments_and_flags>", configAndStacksInfo.ComponentType)
return configAndStacksInfo, errors.New(message)
}

configAndStacksInfo.StackFromArg = configAndStacksInfo.Stack

// FindStacksMap processes stack config and returns a map of all stacks
func FindStacksMap(configAndStacksInfo c.ConfigAndStacksInfo, checkStack bool) (map[string]interface{}, error) {
// Process and merge CLI configurations
err := c.InitConfig()
if err != nil {
return configAndStacksInfo, err
return nil, err
}

err = c.ProcessConfig(configAndStacksInfo)
err = c.ProcessConfig(configAndStacksInfo, checkStack)
if err != nil {
return configAndStacksInfo, err
return nil, err
}

// Process stack config file(s)
Expand All @@ -229,6 +215,30 @@ func ProcessStacks(configAndStacksInfo c.ConfigAndStacksInfo) (c.ConfigAndStacks
false,
true)

if err != nil {
return nil, err
}

return stacksMap, nil
}

// ProcessStacks processes stack config
func ProcessStacks(configAndStacksInfo c.ConfigAndStacksInfo, checkStack bool) (c.ConfigAndStacksInfo, error) {
// Check if stack was provided
if len(configAndStacksInfo.Stack) < 1 {
message := fmt.Sprintf("'stack' is required. Usage: atmos %s <command> <component> -s <stack>", configAndStacksInfo.ComponentType)
return configAndStacksInfo, errors.New(message)
}
aknysh marked this conversation as resolved.
Show resolved Hide resolved

// Check if component was provided
if len(configAndStacksInfo.ComponentFromArg) < 1 {
message := fmt.Sprintf("'component' is required. Usage: atmos %s <command> <component> <arguments_and_flags>", configAndStacksInfo.ComponentType)
return configAndStacksInfo, errors.New(message)
}

configAndStacksInfo.StackFromArg = configAndStacksInfo.Stack

stacksMap, err := FindStacksMap(configAndStacksInfo, checkStack)
if err != nil {
return configAndStacksInfo, err
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/component/component_processor.go
Expand Up @@ -15,10 +15,10 @@ func ProcessComponentInStack(component string, stack string) (map[string]interfa
configAndStacksInfo.Stack = stack

configAndStacksInfo.ComponentType = "terraform"
configAndStacksInfo, err := e.ProcessStacks(configAndStacksInfo)
configAndStacksInfo, err := e.ProcessStacks(configAndStacksInfo, true)
if err != nil {
configAndStacksInfo.ComponentType = "helmfile"
configAndStacksInfo, err = e.ProcessStacks(configAndStacksInfo)
configAndStacksInfo, err = e.ProcessStacks(configAndStacksInfo, true)
if err != nil {
return nil, err
}
Expand Down
62 changes: 32 additions & 30 deletions pkg/config/config.go
Expand Up @@ -158,7 +158,7 @@ func InitConfig() error {
}

// ProcessConfig processes and checks CLI configuration
func ProcessConfig(configAndStacksInfo ConfigAndStacksInfo) error {
func ProcessConfig(configAndStacksInfo ConfigAndStacksInfo, checkStack bool) error {
// Process ENV vars
err := processEnvVars()
if err != nil {
Expand Down Expand Up @@ -240,40 +240,42 @@ func ProcessConfig(configAndStacksInfo ConfigAndStacksInfo) error {
ProcessedConfig.StackConfigFilesAbsolutePaths = stackConfigFilesAbsolutePaths
ProcessedConfig.StackConfigFilesRelativePaths = stackConfigFilesRelativePaths

if stackIsPhysicalPath == true {
if g.LogVerbose {
color.Cyan(fmt.Sprintf("\nThe stack '%s' matches the stack config file %s\n",
configAndStacksInfo.Stack,
stackConfigFilesRelativePaths[0]),
)
}
ProcessedConfig.StackType = "Directory"
} else {
// The stack is a logical name
// Check if it matches the pattern specified in 'StackNamePattern'
if len(Config.Stacks.NamePattern) == 0 {
errorMessage := "\nStack name pattern must be provided and must not be empty. Check the CLI config in 'atmos.yaml'"
return errors.New(errorMessage)
}

stackParts := strings.Split(configAndStacksInfo.Stack, "-")
stackNamePatternParts := strings.Split(Config.Stacks.NamePattern, "-")

if len(stackParts) == len(stackNamePatternParts) {
if checkStack {
if stackIsPhysicalPath == true {
if g.LogVerbose {
color.Cyan(fmt.Sprintf("\nThe stack '%s' matches the stack name pattern '%s'",
color.Cyan(fmt.Sprintf("\nThe stack '%s' matches the stack config file %s\n",
configAndStacksInfo.Stack,
Config.Stacks.NamePattern),
stackConfigFilesRelativePaths[0]),
)
}
ProcessedConfig.StackType = "Logical"
ProcessedConfig.StackType = "Directory"
} else {
errorMessage := fmt.Sprintf("\nThe stack '%s' does not exist in the config directories, "+
"and it does not match the stack name pattern '%s'",
configAndStacksInfo.Stack,
Config.Stacks.NamePattern,
)
return errors.New(errorMessage)
// The stack is a logical name
// Check if it matches the pattern specified in 'StackNamePattern'
if len(Config.Stacks.NamePattern) == 0 {
errorMessage := "\nStack name pattern must be provided and must not be empty. Check the CLI config in 'atmos.yaml'"
return errors.New(errorMessage)
}

stackParts := strings.Split(configAndStacksInfo.Stack, "-")
stackNamePatternParts := strings.Split(Config.Stacks.NamePattern, "-")

if len(stackParts) == len(stackNamePatternParts) {
if g.LogVerbose {
color.Cyan(fmt.Sprintf("\nThe stack '%s' matches the stack name pattern '%s'",
configAndStacksInfo.Stack,
Config.Stacks.NamePattern),
)
}
ProcessedConfig.StackType = "Logical"
} else {
errorMessage := fmt.Sprintf("\nThe stack '%s' does not exist in the config directories, "+
"and it does not match the stack name pattern '%s'",
configAndStacksInfo.Stack,
Config.Stacks.NamePattern,
)
return errors.New(errorMessage)
}
}
}

Expand Down