Skip to content

Commit bf1dc05

Browse files
author
Samyak Rout
committed
feat: Enhance Terraform code generation and command execution workflow
This commit includes significant improvements to the Terraform code generation and command execution logic, ensuring better handling of modules, variables, and backend configurations. Changes: - Updated `main.go`: - Added support for Terraform commands (`init`, `plan`, `apply`, `build`, and `print`). - Integrated dynamic backend configuration and variable file support for `vars.tfvars`. - Enhanced command execution with detailed outputs for debugging. - Updated `handlers/generate.go`: - Improved module dependency resolution and variable mapping. - Streamlined template data preparation for Terraform files. - Added functionality to generate module-specific files (main.tf, variables.tf, outputs.tf). - Updated `models/config.go`: - Defined models for modules, variables, and provider configurations. - Added support for backend-specific fields like `access_key`. - Updated `utils/filesystem.go`: - Enhanced template generation logic with custom helper functions (e.g., `add`). - Improved file and directory handling for consistent output structure. Highlights: - `vars.tfvars.tmpl` now correctly maps lists, maps, and other variable types. - Backend configurations dynamically adjust based on `infratype` (prod/nonprod). - `terraform-generator.json` configuration fully supports module and provider settings. These updates ensure better modularity, robustness, and usability of the Terraform generation and execution workflow.
1 parent adbe187 commit bf1dc05

File tree

5 files changed

+173
-127
lines changed

5 files changed

+173
-127
lines changed

infra_as_code/terraform_generator/backend/handlers/generate.go

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,48 +39,41 @@ func GenerateTerraform(req *models.GenerateRequest) error {
3939
// Load configuration from terraform-generator.json
4040
config, err := utils.LoadConfig("configs/terraform-generator.json")
4141
if err != nil {
42-
return err
42+
return fmt.Errorf("error loading configuration: %w", err)
4343
}
4444

4545
// Filter provider data based on the input provider
4646
providerData := filterProviderData(config.Providers, req.Provider)
4747
if providerData == nil {
48-
return errors.New("specified provider not found in configuration")
48+
return fmt.Errorf("specified provider '%s' not found in configuration", req.Provider)
4949
}
5050

5151
// Resolve module dependencies
52-
moduleNames := make([]string, len(req.Modules))
53-
for i, moduleName := range req.Modules {
54-
moduleNames[i] = strings.TrimSpace(moduleName)
55-
}
56-
modules, err := resolveModuleDependencies(moduleNames, config.Modules)
52+
modules, err := resolveModuleDependencies(req.Modules, config.Modules)
5753
if err != nil {
58-
return err
54+
return fmt.Errorf("error resolving module dependencies: %w", err)
5955
}
6056

6157
// Update basePath to include 'output' directory
6258
basePath := filepath.Join("output", "terraform", req.OrganisationName)
6359

6460
// Generate module files
6561
if err := generateModuleFiles(basePath, modules, req.Provider); err != nil {
66-
return err
62+
return fmt.Errorf("error generating module files: %w", err)
6763
}
6864

65+
// Generate files for a single product or customers
6966
if len(req.Customers) > 0 {
7067
return processCustomers(req, config, basePath, providerData, modules)
7168
}
7269

73-
// Process for a single product
70+
// Generate product-specific files
7471
productPath := filepath.Join(basePath, req.ProductName)
7572
if err := utils.CreateDirectories([]string{filepath.Join(productPath, "backend")}); err != nil {
76-
return err
73+
return fmt.Errorf("error creating directories for product: %w", err)
7774
}
7875

79-
if err := generateProductFiles(req, config, productPath, providerData, modules); err != nil {
80-
return err
81-
}
82-
83-
return nil
76+
return generateProductFiles(req, config, productPath, providerData, modules)
8477
}
8578

8679
// filterProviderData filters provider details based on the specified provider name.
@@ -159,7 +152,6 @@ func generateModuleFiles(basePath string, modules []models.Module, provider stri
159152
"ResourceName": module.ModuleName,
160153
}
161154

162-
// Prepare list of files to generate
163155
files := []struct {
164156
Template string
165157
Dest string
@@ -188,7 +180,7 @@ func generateModuleFiles(basePath string, modules []models.Module, provider stri
188180
// Generate files
189181
for _, file := range files {
190182
if err := utils.GenerateFileFromTemplate(file.Template, file.Dest, data); err != nil {
191-
return err
183+
return fmt.Errorf("error generating file %s: %w", file.Dest, err)
192184
}
193185
}
194186
}
@@ -254,7 +246,7 @@ func prepareTemplateData(req *models.GenerateRequest, config *models.Config, pro
254246
for _, module := range modules {
255247
vars := make(map[string]string)
256248
for varName, varDef := range module.Variables {
257-
vars[varName] = varDef.Value // Use the 'value' field for module calls
249+
vars[varName] = fmt.Sprintf("%v", varDef.Value) // Convert value to string
258250
}
259251
moduleVariables[module.ModuleName] = vars
260252
}
@@ -263,14 +255,14 @@ func prepareTemplateData(req *models.GenerateRequest, config *models.Config, pro
263255
"Provider": provider,
264256
"TerraformVersion": config.TerraformVersion,
265257
"Modules": modules,
266-
"ModuleVariables": moduleVariables,
258+
"ModuleVariables": moduleVariables, // Include populated module variables
267259
"OrganisationName": req.OrganisationName,
268260
"ProductName": req.ProductName,
269261
"CustomerName": customerName,
270262
"Region": config.Region,
271263
"Environment": config.Environment,
272264
"Backend": config.Backend,
273-
"Variables": genericVariables, // Only generic variables
265+
"Variables": genericVariables,
274266
}
275267
}
276268

infra_as_code/terraform_generator/backend/main.go

Lines changed: 107 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ func main() {
2323
customers := generateCmd.String("customers", "", "Comma-separated list of customers")
2424

2525
terraformCmd := flag.NewFlagSet("terraform", flag.ExitOnError)
26-
tfCommand := terraformCmd.String("command", "", "Terraform command to execute (init, plan, apply, build)")
26+
tfCommand := terraformCmd.String("command", "", "Terraform command to execute (init, plan, apply, build, print)")
2727
tfCompany := terraformCmd.String("company", "", "Company name (required)")
2828
tfProduct := terraformCmd.String("product", "", "Product name (required)")
2929
tfProvider := terraformCmd.String("provider", "", "Provider name (required)")
30+
tfInfratype := terraformCmd.String("infratype", "", "Infrastructure type (prod or nonprod)")
3031

3132
if len(os.Args) < 2 {
3233
fmt.Println("Expected 'generate' or 'terraform' subcommands")
@@ -37,61 +38,13 @@ func main() {
3738
case "generate":
3839
generateCmd.Parse(os.Args[2:])
3940
if generateCmd.Parsed() {
40-
if *company == "" || *product == "" || *provider == "" {
41-
fmt.Println("Error: --company, --product, and --provider are required")
42-
generateCmd.Usage()
43-
os.Exit(1)
44-
}
45-
46-
// Prepare the request data
47-
req := models.GenerateRequest{
48-
OrganisationName: *company,
49-
ProductName: *product,
50-
Provider: *provider,
51-
Modules: []string{},
52-
}
53-
54-
// Handle modules
55-
if *modules != "" {
56-
moduleNames := strings.Split(*modules, ",")
57-
for _, moduleName := range moduleNames {
58-
moduleName = strings.TrimSpace(moduleName)
59-
req.Modules = append(req.Modules, moduleName)
60-
}
61-
}
62-
63-
// Handle customers
64-
if *customers != "" {
65-
req.Customers = strings.Split(*customers, ",")
66-
for i, customer := range req.Customers {
67-
req.Customers[i] = strings.TrimSpace(customer)
68-
}
69-
}
70-
71-
// Call the generation logic
72-
err := handlers.GenerateTerraform(&req)
73-
if err != nil {
74-
fmt.Printf("Error generating Terraform code: %v\n", err)
75-
os.Exit(1)
76-
}
77-
78-
fmt.Println("Terraform code generated successfully")
41+
handleGenerateCommand(*company, *product, *provider, *modules, *customers)
7942
}
8043

8144
case "terraform":
8245
terraformCmd.Parse(os.Args[2:])
8346
if terraformCmd.Parsed() {
84-
if *tfCommand == "" || *tfCompany == "" || *tfProduct == "" || *tfProvider == "" {
85-
fmt.Println("Error: --command, --company, --product, and --provider are required")
86-
terraformCmd.Usage()
87-
os.Exit(1)
88-
}
89-
90-
// Run the Terraform command
91-
err := runTerraformCommand(*tfCommand, *tfCompany, *tfProduct, *tfProvider)
92-
if err != nil {
93-
log.Fatalf("Error executing Terraform command: %v\n", err)
94-
}
47+
handleTerraformCommand(*tfCommand, *tfCompany, *tfProduct, *tfProvider, *tfInfratype)
9548
}
9649

9750
default:
@@ -100,45 +53,123 @@ func main() {
10053
}
10154
}
10255

103-
// runTerraformCommand executes Terraform commands (init, plan, apply, build)
104-
func runTerraformCommand(command, company, product, provider string) error {
105-
// Determine the working directory
56+
func handleGenerateCommand(company, product, provider, modules, customers string) {
57+
if company == "" || product == "" || provider == "" {
58+
fmt.Println("Error: --company, --product, and --provider are required")
59+
os.Exit(1)
60+
}
61+
62+
req := models.GenerateRequest{
63+
OrganisationName: company,
64+
ProductName: product,
65+
Provider: provider,
66+
Modules: []string{},
67+
}
68+
69+
// Handle modules
70+
if modules != "" {
71+
moduleNames := strings.Split(modules, ",")
72+
for _, moduleName := range moduleNames {
73+
req.Modules = append(req.Modules, strings.TrimSpace(moduleName))
74+
}
75+
}
76+
77+
// Handle customers
78+
if customers != "" {
79+
req.Customers = strings.Split(customers, ",")
80+
for i := range req.Customers {
81+
req.Customers[i] = strings.TrimSpace(req.Customers[i])
82+
}
83+
}
84+
85+
if err := handlers.GenerateTerraform(&req); err != nil {
86+
fmt.Printf("Error generating Terraform code: %v\n", err)
87+
os.Exit(1)
88+
}
89+
90+
fmt.Println("Terraform code generated successfully")
91+
}
92+
93+
func handleTerraformCommand(command, company, product, provider, infratype string) {
94+
if command == "" || company == "" || product == "" || provider == "" {
95+
fmt.Println("Error: --command, --company, --product, and --provider are required")
96+
os.Exit(1)
97+
}
98+
99+
// 'init', 'build', and 'print' commands require '--infratype'
100+
if (command == "init" || command == "build" || command == "print") && infratype == "" {
101+
fmt.Println("Error: --infratype is required for 'init', 'build', and 'print' commands")
102+
os.Exit(1)
103+
}
104+
105+
if command == "print" {
106+
printTerraformCommands(command, company, product, provider, infratype)
107+
} else {
108+
if err := runTerraformCommand(command, company, product, provider, infratype); err != nil {
109+
log.Fatalf("Error executing Terraform command: %v\n", err)
110+
}
111+
}
112+
}
113+
114+
func runTerraformCommand(command, company, product, provider, infratype string) error {
106115
terraformDir := filepath.Join("output", "terraform", company, product)
107116
if _, err := os.Stat(terraformDir); os.IsNotExist(err) {
108117
return fmt.Errorf("Terraform directory %s does not exist", terraformDir)
109118
}
110119

111-
// Change to the Terraform directory
112120
if err := os.Chdir(terraformDir); err != nil {
113121
return fmt.Errorf("error changing directory to %s: %v", terraformDir, err)
114122
}
115123

116-
// Map of commands to Terraform actions
117-
tfCommands := map[string][]string{
118-
"init": {"init"},
119-
"plan": {"plan"},
120-
"apply": {"apply", "-auto-approve"},
121-
"build": {"init", "plan", "apply", "-auto-approve"},
122-
}
124+
switch command {
125+
case "init":
126+
backendConfig := fmt.Sprintf("backend/%s_%s.tfvars", product, strings.ToLower(infratype))
127+
args := []string{"init", "-no-color", "-get=true", "-force-copy", fmt.Sprintf("-backend-config=%s", backendConfig)}
128+
return executeCommand("terraform", args)
129+
130+
case "plan":
131+
args := []string{"plan", "-no-color", "-input=false", "-lock=true", "-refresh=true", "-var-file=./vars.tfvars"}
132+
return executeCommand("terraform", args)
133+
134+
case "apply":
135+
args := []string{"apply", "-no-color", "-input=false", "-auto-approve=true", "-lock=true", "-lock-timeout=7200s", "-refresh=true", "-var-file=./vars.tfvars"}
136+
return executeCommand("terraform", args)
137+
138+
case "build":
139+
backendConfig := fmt.Sprintf("backend/%s_%s.tfvars", product, strings.ToLower(infratype))
140+
buildCommands := [][]string{
141+
{"init", "-no-color", "-get=true", "-force-copy", fmt.Sprintf("-backend-config=%s", backendConfig)},
142+
{"plan", "-no-color", "-input=false", "-lock=true", "-refresh=true", "-var-file=./vars.tfvars"},
143+
{"apply", "-no-color", "-input=false", "-auto-approve=true", "-lock=true", "-lock-timeout=7200s", "-refresh=true", "-var-file=./vars.tfvars"},
144+
}
123145

124-
// Get the commands for the specified action
125-
actions, ok := tfCommands[command]
126-
if !ok {
146+
for _, args := range buildCommands {
147+
if err := executeCommand("terraform", args); err != nil {
148+
return err
149+
}
150+
}
151+
152+
default:
127153
return fmt.Errorf("unsupported Terraform command: %s", command)
128154
}
129155

130-
// Execute the commands in sequence
131-
for i := 0; i < len(actions); i++ {
132-
cmd := exec.Command("terraform", actions[i])
133-
cmd.Stdout = os.Stdout
134-
cmd.Stderr = os.Stderr
156+
return nil
157+
}
135158

136-
fmt.Printf("Running Terraform command: terraform %s\n", actions[i])
159+
func printTerraformCommands(command, company, product, provider, infratype string) {
160+
terraformDir := filepath.Join("output", "terraform", company, product)
161+
fmt.Printf("Working directory: %s\n", terraformDir)
137162

138-
if err := cmd.Run(); err != nil {
139-
return fmt.Errorf("error running Terraform command '%s': %v", actions[i], err)
140-
}
141-
}
163+
backendConfig := fmt.Sprintf("backend/%s_%s.tfvars", product, strings.ToLower(infratype))
164+
fmt.Printf("terraform init -no-color -get=true -force-copy -backend-config=%s\n", backendConfig)
165+
fmt.Println("terraform plan -no-color -input=false -lock=true -refresh=true -var-file=./vars.tfvars")
166+
fmt.Println("terraform apply -no-color -input=false -auto-approve=true -lock=true -lock-timeout=7200s -refresh=true -var-file=./vars.tfvars")
167+
}
142168

143-
return nil
169+
func executeCommand(command string, args []string) error {
170+
cmd := exec.Command(command, args...)
171+
cmd.Stdout = os.Stdout
172+
cmd.Stderr = os.Stderr
173+
fmt.Printf("Running command: %s %s\n", command, strings.Join(args, " "))
174+
return cmd.Run()
144175
}

infra_as_code/terraform_generator/backend/templates/azure/main.tf.tmpl

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ module "{{ $module.ModuleName }}" {
77
{{- end }}
88
{{- if $module.DependsOn }}
99
depends_on = [
10-
{{- $deps := $module.DependsOn }}
11-
{{- $depsLen := len $deps }}
12-
{{- range $index, $dependency := $deps }}
13-
module.{{ $dependency }}{{ if lt (add $index 1) $depsLen }},{{ end }}
10+
{{- range $index, $dependency := $module.DependsOn }}
11+
module.{{ $dependency }}{{ if lt (add $index 1) (len $module.DependsOn) }},{{ end }}
1412
{{- end }}
1513
]
1614
{{- end }}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
{{- range $key, $metadata := .Variables }}
2-
{{ $key }}: "{{ $metadata.Value }}"
2+
{{- if eq $metadata.Type "list(string)" }}
3+
{{ $key }} = {{ toJSON $metadata.Value }}
4+
{{- else if eq $metadata.Type "map(string)" }}
5+
{{ $key }} = {
6+
{{- range $mapKey, $mapValue := $metadata.Value }}
7+
{{ $mapKey }} = "{{ $mapValue }}"
8+
{{- end }}
9+
}
10+
{{- else }}
11+
{{ $key }} = "{{ $metadata.Value }}"
12+
{{- end }}
313
{{- end }}

0 commit comments

Comments
 (0)