Skip to content

Commit cd0b079

Browse files
authored
Merge pull request #330 from clouddrove/fix/smurf-stf
Add Timeout Support to Terraform Format Command
2 parents cd39408 + f31134b commit cd0b079

File tree

4 files changed

+90
-54
lines changed

4 files changed

+90
-54
lines changed

cmd/stf/format.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,36 @@ package stf
22

33
import (
44
"os"
5+
"time"
56

67
"github.com/clouddrove/smurf/internal/terraform"
78
"github.com/spf13/cobra"
89
)
910

1011
var recursive bool
12+
var timeout time.Duration
1113

1214
// formatCmd defines a subcommand that formats the Terraform Infrastructure.
1315
var formatCmd = &cobra.Command{
14-
Use: "format",
16+
Use: "fmt",
1517
Short: "Format the Terraform Infrastructure",
1618
SilenceUsage: true,
1719
RunE: func(cmd *cobra.Command, args []string) error {
18-
err := terraform.Format(recursive)
20+
err := terraform.Format(recursive, timeout)
1921
if err != nil {
2022
os.Exit(1)
2123
}
2224
return nil
2325
},
2426
Example: `
25-
smurf stf format
27+
smurf stf fmt
28+
smurf stf fmt --timeout 30s
29+
smurf stf fmt --recursive --timeout 2m
2630
`,
2731
}
2832

2933
func init() {
30-
formatCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Run the command recursively on all subdirectories .By default, only the given directory (or current directory) is processed.")
34+
formatCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Run the command recursively on all subdirectories. By default, only the given directory (or current directory) is processed.")
35+
formatCmd.Flags().DurationVarP(&timeout, "timeout", "t", 0, "Timeout for the formatting process (e.g., 30s, 2m, 1h). Zero means no timeout.")
3136
stfCmd.AddCommand(formatCmd)
3237
}

cmd/stf/provision.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var varNameValue []string
1010
var varFile []string
1111
var lock bool
1212
var upgrade bool
13-
var provisionDir string // Define provisionDir variable
13+
var provisionDir string
1414

1515
var provisionCmd = &cobra.Command{
1616
Use: "provision",
@@ -37,14 +37,15 @@ var provisionCmd = &cobra.Command{
3737
},
3838
Example: `
3939
smurf stf provision
40-
smurf stf provision --dir=/path/to/terraform/files
40+
smurf stf provision --dir=/path/to/terraform/files --auto-approve
4141
`,
4242
}
4343

4444
func init() {
4545
provisionCmd.Flags().StringSliceVar(&varNameValue, "var", []string{}, "Specify a variable in 'NAME=VALUE' format")
4646
provisionCmd.Flags().StringArrayVar(&varFile, "var-file", []string{}, "Specify a file containing variables")
4747
provisionCmd.Flags().BoolVar(&provisionApprove, "approve", true, "Skip interactive approval of plan before applying")
48+
provisionCmd.Flags().BoolVar(&applyAutoApprove, "auto-approve", false, "Skip interactive approval of plan before applying")
4849
provisionCmd.Flags().BoolVar(&lock, "lock", false, "Don't hold a state lock during the operation. This is dangerous if others might concurrently run commands against the same workspace.")
4950
provisionCmd.Flags().BoolVar(&upgrade, "upgrade", false, "Upgrade the Terraform modules and plugins to the latest versions")
5051
provisionCmd.Flags().StringVar(&provisionDir, "dir", "", "Specify the directory for Terraform operations")

internal/terraform/format.go

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os/exec"
99
"path/filepath"
1010
"strings"
11+
"time"
1112

1213
"github.com/hashicorp/terraform-exec/tfexec"
1314
)
@@ -99,81 +100,102 @@ func (cf *CustomFormatter) FormatWithDetails(ctx context.Context, dir string, re
99100
return nil
100101
}
101102

102-
formatted := []string{}
103-
var formatErrors []FormatError
103+
// Find files that need formatting
104+
filesNeedFormatting := []string{}
105+
timeoutReached := false
106+
processedCount := 0
104107

105108
for _, file := range files {
109+
// Check if context has been cancelled (timeout reached)
110+
select {
111+
case <-ctx.Done():
112+
timeoutReached = true
113+
break
114+
default:
115+
// Continue processing
116+
}
117+
118+
if timeoutReached {
119+
break
120+
}
121+
122+
processedCount++
123+
106124
content, err := os.ReadFile(file)
107125
if err != nil {
108-
formatErrors = append(formatErrors, FormatError{
109-
ErrorType: "File read failed",
110-
Description: err.Error(),
111-
Location: file,
112-
HelpText: "Failed to read file for formatting check",
113-
})
114126
continue
115127
}
116128

117129
fileDir := filepath.Dir(file)
118130
tf, err := tfexec.NewTerraform(fileDir, cf.tf.ExecPath())
119131
if err != nil {
120-
formatErrors = append(formatErrors, FormatError{
121-
ErrorType: "Terraform init failed",
122-
Description: err.Error(),
123-
Location: file,
124-
HelpText: "Failed to initialize Terraform for formatting",
125-
})
126132
continue
127133
}
128134

129135
var outputBuffer bytes.Buffer
130136
err = tf.Format(ctx, bytes.NewReader(content), &outputBuffer)
131137
if err != nil {
132-
formatErrors = append(formatErrors, cf.parseFormatError(err, file))
138+
if ctx.Err() == context.DeadlineExceeded {
139+
timeoutReached = true
140+
break
141+
}
133142
continue
134143
}
135144

136-
if bytes.Equal(content, outputBuffer.Bytes()) {
137-
continue // Already properly formatted
138-
}
145+
// Check if file needs formatting
146+
if !bytes.Equal(content, outputBuffer.Bytes()) {
147+
filesNeedFormatting = append(filesNeedFormatting, file)
139148

140-
err = os.WriteFile(file, outputBuffer.Bytes(), 0644)
141-
if err != nil {
142-
formatErrors = append(formatErrors, FormatError{
143-
ErrorType: "File write failed",
144-
Description: err.Error(),
145-
Location: file,
146-
HelpText: "Failed to write formatted content to file",
147-
})
148-
continue
149+
// Apply formatting if we haven't timed out yet
150+
if !timeoutReached {
151+
os.WriteFile(file, outputBuffer.Bytes(), 0644)
152+
}
149153
}
150-
151-
formatted = append(formatted, file)
152154
}
153155

154-
if len(formatErrors) > 0 {
155-
for _, e := range formatErrors {
156-
fmt.Print(cf.formatError(e))
156+
// Show files that need formatting
157+
if len(filesNeedFormatting) > 0 {
158+
fmt.Println("\nYou need to format following files:")
159+
for i, file := range filesNeedFormatting {
160+
relPath, err := filepath.Rel(cf.workDir, file)
161+
if err != nil {
162+
relPath = file
163+
}
164+
// Show numbering starting from 1
165+
fmt.Printf(" %d. %s\n", i+1, CyanText(relPath))
157166
}
158-
Error("Formatting failed with %d errors", len(formatErrors))
159-
return fmt.Errorf("formatting failed with %d errors", len(formatErrors))
160-
}
161167

162-
if len(formatted) > 0 {
163-
Success("Terraform files formatted successfully ✅")
164-
Info("Formatted files:")
165-
for _, file := range formatted {
166-
fmt.Printf(" %s\n", CyanText(file))
168+
// Only show "formatted" message if we actually formatted them
169+
if !timeoutReached {
170+
Success("\nFormatted %d file(s).", len(filesNeedFormatting))
167171
}
168172
} else {
169-
Success("No Terraform file changes detected")
173+
Success("No Terraform files need formatting.")
174+
}
175+
176+
// Show timeout message if reached
177+
if timeoutReached {
178+
fmt.Println() // Empty line before timeout message
179+
Warn("Timeout reached after processing %d/%d files. Some files may have been skipped.",
180+
processedCount, len(files))
170181
}
171182

172-
return nil
183+
return nil // Always return success
173184
}
174185

175186
// parseFormatError converts terraform-exec errors into our FormatError type
176187
func (cf *CustomFormatter) parseFormatError(err error, file string) FormatError {
188+
// Check if it's a timeout error from terraform-exec
189+
if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "deadline") {
190+
return FormatError{
191+
ErrorType: "Processing Timeout",
192+
Description: "File processing took too long",
193+
Location: file,
194+
LineNumber: 1,
195+
HelpText: "This file may be very large or complex. Consider formatting it separately.",
196+
}
197+
}
198+
177199
return FormatError{
178200
ErrorType: "Format Error",
179201
Description: err.Error(),
@@ -206,8 +228,8 @@ func GetFmtTerraform() (*tfexec.Terraform, error) {
206228
return tf, nil
207229
}
208230

209-
// Format applies canonical formatting to all Terraform files.
210-
func Format(recursive bool) error {
231+
// Format applies canonical formatting to all Terraform files with optional timeout.
232+
func Format(recursive bool, timeout time.Duration) error {
211233
tf, err := GetFmtTerraform()
212234
if err != nil {
213235
return err
@@ -220,5 +242,16 @@ func Format(recursive bool) error {
220242
}
221243

222244
formatter := NewCustomFormatter(tf, workDir)
223-
return formatter.FormatWithDetails(context.Background(), ".", recursive)
245+
246+
// Create context with timeout if specified
247+
ctx := context.Background()
248+
if timeout > 0 {
249+
var cancel context.CancelFunc
250+
ctx, cancel = context.WithTimeout(context.Background(), timeout)
251+
defer cancel()
252+
253+
Info("Formatting with timeout: %v", timeout)
254+
}
255+
256+
return formatter.FormatWithDetails(ctx, ".", recursive)
224257
}

internal/terraform/output.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,12 @@ func Output(dir string, useAI bool) error {
2323
tf.SetStderr(os.Stderr)
2424

2525
pterm.Info.Println("Refreshing Infrastructure state...")
26-
spinner, _ := pterm.DefaultSpinner.Start("Refreshing Infrastructure state...")
2726
err = tf.Refresh(context.Background())
2827
if err != nil {
29-
spinner.Fail("Error refreshing state")
3028
pterm.Error.Printf("Error refreshing state: %v\n", err)
3129
ai.AIExplainError(useAI, err.Error())
3230
return err
3331
}
34-
spinner.Success("State refreshed successfully.")
3532

3633
outputs, err := tf.Output(context.Background())
3734
if err != nil {

0 commit comments

Comments
 (0)