diff --git a/cmd/describe_component.go b/cmd/describe_component.go index cecbf8aa6..939502e8c 100644 --- a/cmd/describe_component.go +++ b/cmd/describe_component.go @@ -10,7 +10,7 @@ import ( var describeComponentCmd = &cobra.Command{ Use: "component", Short: "Execute 'describe component' command", - Long: `This command shows configuration for a component in a stack: atmos describe component -s `, + Long: `This command shows configuration for an atmos component in a stack: atmos describe component -s `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteDescribeComponent(cmd, args) diff --git a/cmd/describe_stacks.go b/cmd/describe_stacks.go index ae1e5aa4d..aaf91ad65 100644 --- a/cmd/describe_stacks.go +++ b/cmd/describe_stacks.go @@ -10,7 +10,7 @@ import ( 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 `, + Long: `This command shows configuration for atmos stacks and components in the stacks: atmos describe stacks `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteDescribeStacks(cmd, args) diff --git a/cmd/helmfile_generate_varfile.go b/cmd/helmfile_generate_varfile.go index 1c60e8132..30f8195d6 100644 --- a/cmd/helmfile_generate_varfile.go +++ b/cmd/helmfile_generate_varfile.go @@ -10,7 +10,7 @@ import ( var helmfileGenerateVarfileCmd = &cobra.Command{ Use: "varfile", Short: "Execute 'helmfile generate varfile' command", - Long: `This command generates a varfile for a helmfile component: atmos helmfile generate varfile -s -f `, + Long: `This command generates a varfile for an atmos helmfile component: atmos helmfile generate varfile -s -f `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteHelmfileGenerateVarfile(cmd, args) diff --git a/cmd/terraform_generate_backends.go b/cmd/terraform_generate_backends.go new file mode 100644 index 000000000..c6211613c --- /dev/null +++ b/cmd/terraform_generate_backends.go @@ -0,0 +1,31 @@ +package cmd + +import ( + e "github.com/cloudposse/atmos/internal/exec" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/spf13/cobra" +) + +// terraformGenerateBackendsCmd generates backend configs for all terraform components +var terraformGenerateBackendsCmd = &cobra.Command{ + Use: "backends", + Short: "Execute 'terraform generate backends' command", + Long: `This command generates backend configs for all terraform components`, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Run: func(cmd *cobra.Command, args []string) { + err := e.ExecuteTerraformGenerateBackendsCmd(cmd, args) + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + }, +} + +func init() { + terraformGenerateBackendsCmd.DisableFlagParsing = false + + terraformGenerateBackendsCmd.PersistentFlags().String("format", "hcl", "Output format.\n"+ + "Supported formats: hcl, json ('hcl' is default).\n"+ + "atmos terraform generate backends --format=hcl/json") + + terraformGenerateCmd.AddCommand(terraformGenerateBackendsCmd) +} diff --git a/cmd/terraform_generate_varfile.go b/cmd/terraform_generate_varfile.go index bbf573cba..fd4242bfb 100644 --- a/cmd/terraform_generate_varfile.go +++ b/cmd/terraform_generate_varfile.go @@ -10,7 +10,7 @@ import ( var terraformGenerateVarfileCmd = &cobra.Command{ Use: "varfile", Short: "Execute 'terraform generate varfile' command", - Long: `This command generates a varfile for a terraform component: atmos terraform generate varfile -s -f `, + Long: `This command generates a varfile for an atmos terraform component: atmos terraform generate varfile -s -f `, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteTerraformGenerateVarfile(cmd, args) diff --git a/cmd/terraform_generate_varfiles.go b/cmd/terraform_generate_varfiles.go index 3a2fbdf70..81de5bd5d 100644 --- a/cmd/terraform_generate_varfiles.go +++ b/cmd/terraform_generate_varfiles.go @@ -10,7 +10,7 @@ import ( var terraformGenerateVarfilesCmd = &cobra.Command{ Use: "varfiles", Short: "Execute 'terraform generate varfiles' command", - Long: `This command generates varfiles for all terraform components in all stacks`, + Long: `This command generates varfiles for all atmos terraform components in all stacks`, FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteTerraformGenerateVarfilesCmd(cmd, args) @@ -48,8 +48,8 @@ func init() { ) terraformGenerateVarfilesCmd.PersistentFlags().String("format", "json", "Output format.\n"+ - "Supported formats: json, yaml, hcl.\n"+ - "atmos terraform generate varfiles --file-template --format=json/yaml/hcl ('json' is default)") + "Supported formats: json, yaml, hcl ('json' is default).\n"+ + "atmos terraform generate varfiles --file-template --format=json/yaml/hcl") err := terraformGenerateVarfilesCmd.MarkPersistentFlagRequired("file-template") if err != nil { diff --git a/examples/complete/stacks/mixins/region/us-east-1.yaml b/examples/complete/stacks/mixins/region/us-east-1.yaml index 5370a543c..0676d2e24 100644 --- a/examples/complete/stacks/mixins/region/us-east-1.yaml +++ b/examples/complete/stacks/mixins/region/us-east-1.yaml @@ -5,6 +5,8 @@ vars: components: terraform: vpc: + metadata: + component: infra/vpc vars: availability_zones: - us-east-1a diff --git a/examples/complete/stacks/mixins/region/us-east-2.yaml b/examples/complete/stacks/mixins/region/us-east-2.yaml index f4c131d92..b073d02b5 100644 --- a/examples/complete/stacks/mixins/region/us-east-2.yaml +++ b/examples/complete/stacks/mixins/region/us-east-2.yaml @@ -5,6 +5,8 @@ vars: components: terraform: vpc: + metadata: + component: infra/vpc vars: availability_zones: - us-east-2a diff --git a/go.mod b/go.mod index 9f901ef25..c41673216 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/fatih/color v1.13.0 github.com/hashicorp/go-getter v1.6.2 github.com/hashicorp/hcl v1.0.0 + github.com/hashicorp/hcl/v2 v2.14.0 github.com/imdario/mergo v0.3.13 github.com/json-iterator/go v1.1.12 github.com/mitchellh/go-homedir v1.1.0 @@ -15,6 +16,7 @@ require ( github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.8.0 + github.com/zclconf/go-cty v1.11.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -23,6 +25,8 @@ require ( cloud.google.com/go/compute v1.7.0 // indirect cloud.google.com/go/iam v0.4.0 // indirect cloud.google.com/go/storage v1.22.1 // indirect + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/aws/aws-sdk-go v1.15.78 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -44,6 +48,7 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index b9a28c015..5ad4ac409 100644 --- a/go.sum +++ b/go.sum @@ -66,7 +66,11 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= @@ -114,6 +118,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -218,6 +223,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc= +github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= @@ -239,6 +246,7 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -255,6 +263,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -283,6 +293,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= @@ -316,6 +327,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0= +github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/exec/terraform_generate_backend.go b/internal/exec/terraform_generate_backend.go index d7b1f809a..f8e554106 100644 --- a/internal/exec/terraform_generate_backend.go +++ b/internal/exec/terraform_generate_backend.go @@ -43,7 +43,7 @@ func ExecuteTerraformGenerateBackend(cmd *cobra.Command, args []string) error { return fmt.Errorf("\nCould not find 'backend' config for the '%s' component.\n", component) } - var componentBackendConfig = generateComponentBackendConfig(info.ComponentBackendType, info.ComponentBackendSection) + componentBackendConfig := generateComponentBackendConfig(info.ComponentBackendType, info.ComponentBackendSection) u.PrintInfoVerbose("Component backend config:\n\n") err = u.PrintAsJSON(componentBackendConfig) @@ -67,7 +67,7 @@ func ExecuteTerraformGenerateBackend(cmd *cobra.Command, args []string) error { fmt.Println() u.PrintInfo("Writing the backend config to file:") - fmt.Println(backendFilePath) + u.PrintMessage(backendFilePath) if !info.DryRun { err = u.WriteToFileAsJSON(backendFilePath, componentBackendConfig, 0644) diff --git a/internal/exec/terraform_generate_backends.go b/internal/exec/terraform_generate_backends.go new file mode 100644 index 000000000..65d1cc94d --- /dev/null +++ b/internal/exec/terraform_generate_backends.go @@ -0,0 +1,138 @@ +package exec + +import ( + "fmt" + c "github.com/cloudposse/atmos/pkg/config" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/spf13/cobra" + "path" + "path/filepath" +) + +// ExecuteTerraformGenerateBackendsCmd executes `terraform generate backends` command +func ExecuteTerraformGenerateBackendsCmd(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + format, err := flags.GetString("format") + if err != nil { + return err + } + if format != "" && format != "json" && format != "hcl" { + return fmt.Errorf("invalid '--format' argument '%s'. Valid values are 'json' and 'hcl", format) + } + if format == "" { + format = "hcl" + } + + return ExecuteTerraformGenerateBackends(format) +} + +// ExecuteTerraformGenerateBackends generates backend configs for all terraform components +func ExecuteTerraformGenerateBackends(format string) error { + var configAndStacksInfo c.ConfigAndStacksInfo + stacksMap, err := FindStacksMap(configAndStacksInfo, false) + if err != nil { + return err + } + + fmt.Println() + + var ok bool + var componentsSection map[string]any + var terraformSection map[string]any + var componentSection map[string]any + var backendSection map[any]any + var backendType string + processedTerraformComponents := map[string]any{} + + for _, stackSection := range stacksMap { + if componentsSection, ok = stackSection.(map[any]any)["components"].(map[string]any); !ok { + continue + } + + if terraformSection, ok = componentsSection["terraform"].(map[string]any); !ok { + continue + } + + for componentName, compSection := range terraformSection { + if componentSection, ok = compSection.(map[string]any); !ok { + continue + } + + // Find terraform component. + // If `component` attribute is present, it's the terraform component. + // Otherwise, the YAML component name is the terraform component. + terraformComponent := componentName + if componentAttribute, ok := componentSection["component"].(string); ok { + terraformComponent = componentAttribute + } + + // If the terraform component has been already processed, continue + if u.MapKeyExists(processedTerraformComponents, terraformComponent) { + continue + } + + processedTerraformComponents[terraformComponent] = terraformComponent + + // Component backend + if backendSection, ok = componentSection["backend"].(map[any]any); !ok { + continue + } + + // Backend type + if backendType, ok = componentSection["backend_type"].(string); !ok { + continue + } + + // Component metadata + metadataSection := map[any]any{} + if metadataSection, ok = componentSection["metadata"].(map[any]any); ok { + if componentType, ok := metadataSection["type"].(string); ok { + // Don't process abstract components + if componentType == "abstract" { + continue + } + } + } + + // Absolute path to the terraform component + backendFilePath := path.Join( + c.Config.BasePath, + c.Config.Components.Terraform.BasePath, + terraformComponent, + "backend.tf", + ) + + if format == "json" { + backendFilePath = backendFilePath + ".json" + } + + backendFileAbsolutePath, err := filepath.Abs(backendFilePath) + if err != nil { + return err + } + + // Write the backend config to the file + u.PrintMessage(fmt.Sprintf("Writing backend config for the terraform component '%s' to file '%s'", terraformComponent, backendFilePath)) + + if format == "json" { + componentBackendConfig := generateComponentBackendConfig(backendType, backendSection) + err = u.WriteToFileAsJSON(backendFileAbsolutePath, componentBackendConfig, 0644) + if err != nil { + return err + } + } else if format == "hcl" { + err = u.WriteTerraformBackendConfigToFileAsHcl(backendFileAbsolutePath, backendType, backendSection) + if err != nil { + return err + } + } else { + return fmt.Errorf("invalid '--format' argument '%s'. Valid values are 'hcl' (default) and 'json", format) + } + } + } + + fmt.Println() + + return nil +} diff --git a/pkg/utils/hcl_utils.go b/pkg/utils/hcl_utils.go index 026410d39..9af40eed9 100644 --- a/pkg/utils/hcl_utils.go +++ b/pkg/utils/hcl_utils.go @@ -1,9 +1,12 @@ package utils import ( + "github.com/cloudposse/atmos/pkg/convert" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/printer" jsonParser "github.com/hashicorp/hcl/json/parser" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" "os" "strings" @@ -24,7 +27,7 @@ func PrintAsHcl(data any) error { return nil } -// WriteToFileAsHcl converts the provided value to HCL (HashiCorp Language) and writes it to the provided file +// WriteToFileAsHcl converts the provided value to HCL (HashiCorp Language) and writes it to the specified file func WriteToFileAsHcl(filePath string, data any, fileMode os.FileMode) error { astree, err := ConvertToHclAst(data) if err != nil { @@ -75,3 +78,54 @@ func ConvertToHclAst(data any) (ast.Node, error) { return astree.Node, nil } + +// WriteTerraformBackendConfigToFileAsHcl writes the provided Terraform backend config to the specified file +// https://dev.to/pdcommunity/write-terraform-files-in-go-with-hclwrite-2e1j +// https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclwrite +func WriteTerraformBackendConfigToFileAsHcl(filePath string, backendType string, backendConfig map[any]any) error { + hclFile := hclwrite.NewEmptyFile() + rootBody := hclFile.Body() + tfBlock := rootBody.AppendNewBlock("terraform", nil) + tfBlockBody := tfBlock.Body() + backendBlock := tfBlockBody.AppendNewBlock("backend", []string{backendType}) + backendBlockBody := backendBlock.Body() + + backendConfigSortedKeys := StringKeysFromMap(convert.MapsOfInterfacesToMapsOfStrings(backendConfig)) + + for _, name := range backendConfigSortedKeys { + v := backendConfig[name] + + if v == nil { + backendBlockBody.SetAttributeValue(name, cty.NilVal) + } else if i, ok := v.(string); ok { + backendBlockBody.SetAttributeValue(name, cty.StringVal(i)) + } else if i, ok := v.(bool); ok { + backendBlockBody.SetAttributeValue(name, cty.BoolVal(i)) + } else if i, ok := v.(int64); ok { + backendBlockBody.SetAttributeValue(name, cty.NumberIntVal(i)) + } else if i, ok := v.(uint64); ok { + backendBlockBody.SetAttributeValue(name, cty.NumberUIntVal(i)) + } else if i, ok := v.(float64); ok { + backendBlockBody.SetAttributeValue(name, cty.NumberFloatVal(i)) + } + } + + f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + + defer func(f *os.File) { + err := f.Close() + if err != nil { + PrintError(err) + } + }(f) + + _, err = f.Write(hclFile.Bytes()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/utils/json_utils.go b/pkg/utils/json_utils.go index 3c2adf6d1..224bdf6be 100644 --- a/pkg/utils/json_utils.go +++ b/pkg/utils/json_utils.go @@ -32,9 +32,14 @@ func WriteToFileAsJSON(filePath string, data any, fileMode os.FileMode) error { // ConvertToJSON converts the provided value to a JSON-encoded string func ConvertToJSON(data any) (string, error) { - // ConfigCompatibleWithStandardLibrary will sort the map keys in the alphabetical order - var json = jsoniter.ConfigCompatibleWithStandardLibrary - j, err := json.MarshalIndent(data, "", strings.Repeat(" ", 2)) + var json = jsoniter.Config{ + EscapeHTML: true, + ObjectFieldMustBeSimpleString: false, + SortMapKeys: true, + ValidateJsonRawMessage: true, + } + + j, err := json.Froze().MarshalIndent(data, "", strings.Repeat(" ", 3)) if err != nil { return "", err }