Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/cloudflare/tf-migrate
go 1.25.1

require (
github.com/cloudflare/cloudflare-go/v6 v6.2.0
github.com/cloudflare/cloudflare-go/v6 v6.6.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/hcl/v2 v2.24.0
github.com/sergi/go-diff v1.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/cloudflare/cloudflare-go/v6 v6.2.0 h1:VuJAXeVlnftU/XIcAi/xXwEkU/TOaHhmM68HKVpyLD8=
github.com/cloudflare/cloudflare-go/v6 v6.2.0/go.mod h1:Lj3MUqjvKctXRpdRhLQxZYRrNZHuRs0XYuH8JtQGyoI=
github.com/cloudflare/cloudflare-go/v6 v6.6.0 h1:EboC3hfMoxnDnU9f8Feth3/EYTiIwF5jBkSrMNV2vno=
github.com/cloudflare/cloudflare-go/v6 v6.6.0/go.mod h1:Lj3MUqjvKctXRpdRhLQxZYRrNZHuRs0XYuH8JtQGyoI=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
98 changes: 82 additions & 16 deletions internal/e2e-runner/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"time"
)

// TerraformRunner handles terraform command execution
Expand All @@ -41,26 +42,91 @@ func NewTerraformRunner(workDir string) *TerraformRunner {

// Run executes a terraform command
func (tr *TerraformRunner) Run(args ...string) (string, error) {
cmd := exec.Command("terraform", args...)
cmd.Dir = tr.WorkDir

// Set environment variables
cmd.Env = os.Environ()
for k, v := range tr.EnvVars {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
// Add parallelism limit to reduce API rate limit issues
// Check if this is a plan or apply command and parallelism not already set
if len(args) > 0 && (args[0] == "plan" || args[0] == "apply") {
hasParallelism := false
for _, arg := range args {
if strings.HasPrefix(arg, "-parallelism=") {
hasParallelism = true
break
}
}
if !hasParallelism {
// Insert parallelism flag after the command but before any positional args (like plan file)
// Find the last flag (starts with -) or insert after command
insertIndex := 1
for i := 1; i < len(args); i++ {
if strings.HasPrefix(args[i], "-") {
insertIndex = i + 1
} else {
// Found first positional argument, insert before it
break
}
}
// Insert parallelism flag at the correct position
// Using parallelism=3 to reduce API rate limit issues (default is 10)
newArgs := make([]string, 0, len(args)+1)
newArgs = append(newArgs, args[:insertIndex]...)
newArgs = append(newArgs, "-parallelism=5")
newArgs = append(newArgs, args[insertIndex:]...)
args = newArgs
}
}

// Add TF_CLI_CONFIG_FILE if set
if tr.TFConfigFile != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", tr.TFConfigFile))
}
// Retry logic for rate limiting (429 errors)
maxRetries := 3
retryDelay := 5 * time.Second

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
var err error
var output string

err := cmd.Run()
output := stdout.String() + stderr.String()
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
// Wait before retry with exponential backoff
waitTime := retryDelay * time.Duration(attempt)
printYellow("Rate limit detected, waiting %v before retry %d/%d...", waitTime, attempt, maxRetries)
time.Sleep(waitTime)
}

// Create command for each attempt
cmd := exec.Command("terraform", args...)
cmd.Dir = tr.WorkDir

// Set environment variables
cmd.Env = os.Environ()
for k, v := range tr.EnvVars {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}

// Add TF_CLI_CONFIG_FILE if set
if tr.TFConfigFile != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", tr.TFConfigFile))
}

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err = cmd.Run()
output = stdout.String() + stderr.String()

// Check if this is a rate limit error
if err != nil && strings.Contains(output, "429 Too Many Requests") {
if attempt < maxRetries {
continue // Retry
}
// Max retries reached, will return error below
} else {
// Success or non-rate-limit error
break
}
}

// Add a small delay after plan/apply to avoid rate limiting
if len(args) > 0 && (args[0] == "plan" || args[0] == "apply") {
time.Sleep(1 * time.Second)
}

// Sanitize output if enabled
if tr.SanitizeOutput {
Expand Down