Skip to content

Commit 180f12e

Browse files
committed
feat: change logs and update format command
1 parent 6f8132b commit 180f12e

File tree

8 files changed

+152
-168
lines changed

8 files changed

+152
-168
lines changed

cmd/stf/plan.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package stf
22

33
import (
4-
"os"
5-
64
"github.com/clouddrove/smurf/internal/terraform"
75
"github.com/spf13/cobra"
86
)
@@ -18,10 +16,7 @@ var planCmd = &cobra.Command{
1816
Short: "Generate and show an execution plan for Terraform",
1917
SilenceUsage: true,
2018
RunE: func(cmd *cobra.Command, args []string) error {
21-
err := terraform.Plan(planVarNameValue, planVarFile, planDir, planDestroy)
22-
if err != nil {
23-
os.Exit(1)
24-
}
19+
terraform.Plan(planVarNameValue, planVarFile, planDir, planDestroy)
2520
return nil
2621
},
2722
Example: `

internal/terraform/drift.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,51 @@ package terraform
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67

78
"github.com/hashicorp/terraform-exec/tfexec"
8-
"github.com/pterm/pterm"
99
)
1010

1111
// DetectDrift checks for drift between the Terraform state and the actual infrastructure.
1212
// It performs a `plan` with refresh enabled to detect any changes that differ
1313
// from the current state. If drift is detected, it lists the affected resources.
14-
// Provides user feedback through spinners and colored messages for better UX.
14+
// Provides user feedback through spinners and consistent Smurf log style.
1515
func DetectDrift(dir string) error {
1616
tf, err := GetTerraform(dir)
1717
if err != nil {
18+
Error("Failed to initialize Terraform: %v", err)
1819
return err
1920
}
2021

2122
planFile := "drift.plan"
22-
pterm.Info.Println("Checking for drift...")
23-
spinner, _ := pterm.DefaultSpinner.Start("Running terraform plan for drift detection")
24-
_, err = tf.Plan(context.Background(), tfexec.Out(planFile), tfexec.Refresh(true))
2523

24+
Info("Starting Terraform drift detection...")
25+
26+
// Generate drift plan
27+
_, err = tf.Plan(context.Background(), tfexec.Out(planFile), tfexec.Refresh(true))
2628
if err != nil {
27-
spinner.Fail("Terraform plan for drift detection failed")
28-
pterm.Error.Printf("Terraform plan for drift detection failed: %v\n", err)
29+
Error("Failed to execute Terraform plan for drift detection: %v", err)
2930
return err
3031
}
31-
spinner.Success("Terraform drift detection plan completed")
3232

33+
tf.SetStderr(os.Stderr)
34+
35+
// Parse plan file
3336
plan, err := tf.ShowPlanFile(context.Background(), planFile)
3437
if err != nil {
35-
pterm.Error.Printf("Error showing plan file: %v\n", err)
38+
Error("Failed to read drift plan file: %v", err)
3639
return err
3740
}
3841

39-
tf.SetStderr(os.Stderr)
40-
4142
if len(plan.ResourceChanges) > 0 {
42-
pterm.Warning.Println("Drift detected:")
43+
Warn("Drift detected in your infrastructure:")
4344
for _, change := range plan.ResourceChanges {
44-
pterm.Println(pterm.Yellow("- %s: %s", change.Address, change.Change.Actions))
45+
fmt.Printf(" - %s: %v\n", change.Address, change.Change.Actions)
4546
}
47+
Warn("Run 'terraform apply' to reconcile drifted resources.")
4648
} else {
47-
pterm.Success.Println("No drift detected.")
49+
Success("No drift detected. Your infrastructure is in sync.")
4850
}
4951

5052
return nil

internal/terraform/format.go

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import (
1010
"strings"
1111

1212
"github.com/hashicorp/terraform-exec/tfexec"
13-
"github.com/pterm/pterm"
1413
)
1514

16-
// FormatError represents a single formatting error with its details
15+
// FormatError represents a single formatting error with details
1716
type FormatError struct {
1817
ErrorType string
1918
Description string
@@ -32,13 +31,10 @@ type CustomFormatter struct {
3231

3332
// NewCustomFormatter creates a new formatter instance
3433
func NewCustomFormatter(tf *tfexec.Terraform, workDir string) *CustomFormatter {
35-
return &CustomFormatter{
36-
tf: tf,
37-
workDir: workDir,
38-
}
34+
return &CustomFormatter{tf: tf, workDir: workDir}
3935
}
4036

41-
// findTerraformFiles finds all .tf files in the given directory
37+
// findTerraformFiles finds all `.tf` files in the given directory
4238
func (cf *CustomFormatter) findTerraformFiles(root string, recursive bool) ([]string, error) {
4339
var files []string
4440

@@ -62,51 +58,48 @@ func (cf *CustomFormatter) findTerraformFiles(root string, recursive bool) ([]st
6258
return nil
6359
}
6460

65-
err := filepath.Walk(root, walkFn)
66-
if err != nil {
61+
if err := filepath.Walk(root, walkFn); err != nil {
6762
return nil, err
6863
}
69-
7064
return files, nil
7165
}
7266

7367
// formatError formats a single formatting error in Terraform style
7468
func (cf *CustomFormatter) formatError(err FormatError) string {
7569
var sb strings.Builder
7670

77-
errorSymbol := pterm.Red("│")
78-
errorPrefix := pterm.Red("Error: ")
79-
locationColor := pterm.White(err.Location)
80-
lineNumColor := pterm.White(fmt.Sprintf("line %d", err.LineNumber))
81-
82-
sb.WriteString("╷\n")
83-
sb.WriteString(fmt.Sprintf("%s %s%s\n", errorSymbol, errorPrefix, err.Description))
84-
sb.WriteString(fmt.Sprintf("%s\n", errorSymbol))
85-
sb.WriteString(fmt.Sprintf("%s on %s %s:\n", errorSymbol, locationColor, lineNumColor))
86-
sb.WriteString(fmt.Sprintf("%s %d: %s\n", errorSymbol, err.LineNumber, err.LineContent))
71+
sb.WriteString("\n\n")
72+
sb.WriteString(fmt.Sprintf("│ %s%s\n", RedText("Error: "), err.Description))
73+
sb.WriteString("│\n")
74+
sb.WriteString(fmt.Sprintf("│ on %s line %d:\n", GreyText(err.Location), err.LineNumber))
75+
sb.WriteString(fmt.Sprintf("│ %d: %s\n", err.LineNumber, err.LineContent))
8776
if err.NextLine != "" {
88-
sb.WriteString(fmt.Sprintf("%s %d: %s\n", errorSymbol, err.LineNumber+1, err.NextLine))
77+
sb.WriteString(fmt.Sprintf(" %d: %s\n", err.LineNumber+1, err.NextLine))
8978
}
9079
if err.HelpText != "" {
91-
sb.WriteString(fmt.Sprintf("%s\n", errorSymbol))
92-
sb.WriteString(fmt.Sprintf("%s %s\n", errorSymbol, err.HelpText))
80+
sb.WriteString("│\n")
81+
sb.WriteString(fmt.Sprintf("%s\n", GreyText(err.HelpText)))
9382
}
9483
sb.WriteString("╵\n")
95-
9684
return sb.String()
9785
}
9886

99-
// FormatWithDetails performs formatting and returns detailed output
87+
// FormatWithDetails performs Terraform formatting with rich logging
10088
func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, recursive bool) error {
101-
spinner, _ := pterm.DefaultSpinner.Start("Formatting Terraform configuration files...")
89+
Info("Starting Terraform formatting process...")
10290

10391
files, err := cf.findTerraformFiles(dir, recursive)
10492
if err != nil {
105-
spinner.Fail("Failed to find Terraform files")
93+
Error("Failed to find Terraform files: %v", err)
10694
return fmt.Errorf("error finding Terraform files: %w", err)
10795
}
10896

109-
formatted := make([]string, 0)
97+
if len(files) == 0 {
98+
Warn("No Terraform (.tf) files found in the directory.")
99+
return nil
100+
}
101+
102+
formatted := []string{}
110103
var formatErrors []FormatError
111104

112105
for _, file := range files {
@@ -116,7 +109,6 @@ func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, re
116109
ErrorType: "File read failed",
117110
Description: err.Error(),
118111
Location: file,
119-
LineNumber: 0,
120112
HelpText: "Failed to read file for formatting check",
121113
})
122114
continue
@@ -129,7 +121,6 @@ func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, re
129121
ErrorType: "Terraform init failed",
130122
Description: err.Error(),
131123
Location: file,
132-
LineNumber: 0,
133124
HelpText: "Failed to initialize Terraform for formatting",
134125
})
135126
continue
@@ -143,7 +134,7 @@ func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, re
143134
}
144135

145136
if bytes.Equal(content, outputBuffer.Bytes()) {
146-
continue
137+
continue // Already properly formatted
147138
}
148139

149140
err = os.WriteFile(file, outputBuffer.Bytes(), 0644)
@@ -152,7 +143,6 @@ func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, re
152143
ErrorType: "File write failed",
153144
Description: err.Error(),
154145
Location: file,
155-
LineNumber: 0,
156146
HelpText: "Failed to write formatted content to file",
157147
})
158148
continue
@@ -162,21 +152,21 @@ func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, re
162152
}
163153

164154
if len(formatErrors) > 0 {
165-
spinner.Fail(fmt.Sprintf("Formatting failed with %d errors", len(formatErrors)))
166-
for _, err := range formatErrors {
167-
fmt.Print(cf.formatError(err))
155+
for _, e := range formatErrors {
156+
fmt.Print(cf.formatError(e))
168157
}
158+
Error("Formatting failed with %d errors", len(formatErrors))
169159
return fmt.Errorf("formatting failed with %d errors", len(formatErrors))
170160
}
171161

172162
if len(formatted) > 0 {
173-
spinner.Success("Terraform files formatted successfully")
174-
pterm.Info.Println("\nFormatted files:")
163+
Success("Terraform files formatted successfully")
164+
Info("Formatted files:")
175165
for _, file := range formatted {
176-
pterm.Info.Printf("- %s\n", file)
166+
fmt.Printf(" %s\n", CyanText(file))
177167
}
178168
} else {
179-
spinner.Success("No files needed formatting")
169+
Success("No Terraform file changes detected")
180170
}
181171

182172
return nil
@@ -189,42 +179,44 @@ func (cf *CustomFormatter) parseFormatError(err error, file string) FormatError
189179
Description: err.Error(),
190180
Location: file,
191181
LineNumber: 1,
192-
LineContent: "",
193-
HelpText: "Please check the file syntax and try again",
182+
HelpText: "Please check file syntax and try again",
194183
}
195184
}
196185

186+
// GetFmtTerraform initializes and returns a Terraform executor
197187
func GetFmtTerraform() (*tfexec.Terraform, error) {
198188
workDir, err := os.Getwd()
199189
if err != nil {
190+
Error("Failed to get working directory: %v", err)
200191
return nil, fmt.Errorf("failed to get working directory: %w", err)
201192
}
202193

203194
terraformPath, err := exec.LookPath("terraform")
204195
if err != nil {
196+
Error("Terraform executable not found: %v", err)
205197
return nil, fmt.Errorf("terraform executable not found: %w", err)
206198
}
207199

208200
tf, err := tfexec.NewTerraform(workDir, terraformPath)
209201
if err != nil {
202+
Error("Failed to create Terraform executor: %v", err)
210203
return nil, fmt.Errorf("failed to create Terraform executor: %w", err)
211204
}
212205

213206
return tf, nil
214207
}
215208

216-
// Format applies a canonical format to Terraform configuration files.
217-
// It runs `terraform fmt` in the current directory to ensure that all
218-
// Terraform files adhere to the standard formatting conventions.
209+
// Format applies canonical formatting to all Terraform files.
219210
func Format(recursive bool) error {
220-
workDir, err := os.Getwd()
211+
tf, err := GetFmtTerraform()
221212
if err != nil {
222-
return fmt.Errorf("failed to get working directory: %w", err)
213+
return err
223214
}
224215

225-
tf, err := GetFmtTerraform()
216+
workDir, err := os.Getwd()
226217
if err != nil {
227-
return err
218+
Error("Failed to get working directory: %v", err)
219+
return fmt.Errorf("failed to get working directory: %w", err)
228220
}
229221

230222
formatter := NewCustomFormatter(tf, workDir)

internal/terraform/graph.go

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,38 @@ import (
77

88
"github.com/hashicorp/terraform-exec/tfexec"
99
tfjson "github.com/hashicorp/terraform-json"
10-
"github.com/pterm/pterm"
1110
)
1211

13-
// Graph generates a visual representation of Terraform resources
12+
// Graph generates a visual representation of Terraform resources.
13+
// It produces the Terraform DOT graph output, which can be visualized with Graphviz.
14+
// Uses Smurf unified logs for consistent and readable output.
1415
func Graph(dir string) error {
1516
tf, err := GetTerraform(dir)
1617
if err != nil {
18+
Error("Failed to initialize Terraform: %v", err)
1719
return err
1820
}
1921

20-
spinner, err := pterm.DefaultSpinner.Start("Reading state...")
21-
if err != nil {
22-
spinner.Fail("Failed to read state")
23-
}
22+
Info("Starting Terraform graph generation...")
23+
2424
graphDOT, err := tf.Graph(context.Background(), tfexec.DrawCycles(true))
2525
if err != nil {
26-
spinner.Fail("Failed to generate graph")
27-
return fmt.Errorf("error generating graph: %v", err)
26+
Error("Error generating Terraform graph: %v", err)
27+
return err
2828
}
2929

30-
fmt.Print(graphDOT)
31-
32-
spinner.Success("Graph generated successfully")
30+
fmt.Println()
31+
Success("Terraform Dependency Graph (DOT Format):")
32+
fmt.Println("-------------------------------------------------")
33+
fmt.Println(graphDOT)
34+
fmt.Println("-------------------------------------------------")
35+
Info("You can visualize this graph using Graphviz, e.g.:\n dot -Tpng graph.dot -o graph.png")
3336

3437
return nil
3538
}
3639

37-
// generateDOTGraph generates a DOT graph from the Terraform state
38-
// that can be rendered using Graphviz
40+
// generateDOTGraph creates a DOT graph from the Terraform state.
41+
// This is an alternate approach for rendering using tfjson.State.
3942
func generateDOTGraph(state *tfjson.State) string {
4043
var sb strings.Builder
4144

@@ -48,12 +51,11 @@ func generateDOTGraph(state *tfjson.State) string {
4851
}
4952

5053
sb.WriteString("}\n")
51-
5254
return sb.String()
5355
}
5456

55-
// processModule processes a module and its resources to generate a DOT graph
56-
// representation of the Terraform state
57+
// processModule processes a Terraform module and its resources
58+
// to generate a DOT-compatible graph structure.
5759
func processModule(sb *strings.Builder, module *tfjson.StateModule, prefix string, processedNodes map[string]bool) {
5860
if module == nil {
5961
return
@@ -62,8 +64,8 @@ func processModule(sb *strings.Builder, module *tfjson.StateModule, prefix strin
6264
moduleName := strings.TrimPrefix(prefix, ".")
6365
if moduleName != "" {
6466
sb.WriteString(fmt.Sprintf(" subgraph \"cluster_%s\" {\n", moduleName))
65-
sb.WriteString(fmt.Sprintf(" label = \"%s\"\n", moduleName))
66-
sb.WriteString(" fontname = \"sans-serif\"\n")
67+
sb.WriteString(fmt.Sprintf(" label = \"%s\";\n", moduleName))
68+
sb.WriteString(" fontname = \"sans-serif\";\n")
6769
}
6870

6971
for _, resource := range module.Resources {
@@ -101,7 +103,7 @@ func processModule(sb *strings.Builder, module *tfjson.StateModule, prefix strin
101103
}
102104
}
103105

104-
// getResourceAddress returns the full address of a resource
106+
// getResourceAddress returns the full resource address including prefixes.
105107
func getResourceAddress(resource *tfjson.StateResource, prefix string) string {
106108
if prefix == "" {
107109
return resource.Address

0 commit comments

Comments
 (0)