Skip to content

Commit daca676

Browse files
vaishakdineshtamas-jozsa
authored andcommitted
reafactor: refactor state upgraders
1 parent 558207d commit daca676

File tree

24 files changed

+875
-3311
lines changed

24 files changed

+875
-3311
lines changed

cmd/tf-migrate/main.go

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ This tool provides automated transformations for:
7171

7272
var validVersionPath = map[string]struct{}{
7373
"v4-v5": {},
74+
"v5-v5": {}, // Allow same-version "migrations" (bypass mode - generates moved blocks only)
7475
}
7576

7677
func main() {
@@ -198,6 +199,14 @@ func runMigration(log hclog.Logger, cfg config) error {
198199
return err
199200
}
200201

202+
// Check for same-version migration (bypass mode)
203+
if cfg.sourceVersion == cfg.targetVersion {
204+
fmt.Printf("\n⚠ Same-version migration detected (%s → %s)\n", cfg.sourceVersion, cfg.targetVersion)
205+
fmt.Println("Running in bypass mode: Config will be processed but transformations will be minimal")
206+
fmt.Println("This triggers provider StateUpgraders via moved blocks for testing purposes")
207+
fmt.Println()
208+
}
209+
201210
// Load state file first if present (needed for cross-referencing in config transformations)
202211
var stateJSON string
203212
if cfg.stateFile != "" {
@@ -337,7 +346,6 @@ func processConfigFiles(log hclog.Logger, p *pipeline.Pipeline, cfg config, stat
337346
return parsedConfigs, nil
338347
}
339348

340-
// applyGlobalPostprocessing applies cross-file reference updates for resource and attribute renames
341349
func applyGlobalPostprocessing(log hclog.Logger, cfg config, outputPaths []string) error {
342350
// Collect resource renames and attribute renames from all migrators
343351
providers := getProviders(cfg.resourcesToMigrate...)
@@ -406,28 +414,25 @@ func applyGlobalPostprocessing(log hclog.Logger, cfg config, outputPaths []strin
406414
contentStr := string(content)
407415
modified := false
408416

409-
// Apply all resource type renames
417+
// Apply all resource type renames, but skip content within moved blocks
410418
for oldType, newType := range renames {
411-
newContent := strings.ReplaceAll(contentStr, oldType+".", newType+".")
419+
newContent := replaceSkippingMovedBlocks(contentStr, oldType+".", newType+".")
412420
if newContent != contentStr {
413421
modified = true
414422
contentStr = newContent
415423
log.Debug("Updated references", "file", filepath.Base(outputPath), "old", oldType, "new", newType)
416424
}
417425
}
418426

419-
// Apply all attribute renames
427+
// Apply all attribute renames, but skip content within moved blocks
420428
// Pattern: data.cloudflare_zones.<instance_name>.zones → data.cloudflare_zones.<instance_name>.result
421429
// We need to match: <ResourceType>.<instance_name>.<OldAttribute>
422430
for _, rename := range attributeRenames {
423431
// Build regex pattern: data\.cloudflare_zones\.([a-zA-Z0-9_-]+)\.zones
424432
// The instance name can contain letters, numbers, underscores, and hyphens
425-
pattern := regexp.QuoteMeta(rename.ResourceType) + `\.([a-zA-Z0-9_-]+)\.` + regexp.QuoteMeta(rename.OldAttribute)
426-
re := regexp.MustCompile(pattern)
427-
428-
// Replace with: data.cloudflare_zones.$1.result (preserving instance name)
433+
pattern := rename.ResourceType + `\.([a-zA-Z0-9_-]+)\.` + rename.OldAttribute
429434
replacement := rename.ResourceType + ".$1." + rename.NewAttribute
430-
newContent := re.ReplaceAllString(contentStr, replacement)
435+
newContent := regexReplaceSkippingMovedBlocks(contentStr, pattern, replacement)
431436

432437
if newContent != contentStr {
433438
modified = true
@@ -452,6 +457,79 @@ func applyGlobalPostprocessing(log hclog.Logger, cfg config, outputPaths []strin
452457
return nil
453458
}
454459

460+
// replaceSkippingMovedBlocks replaces old with new in content, but skips any content within moved blocks
461+
func replaceSkippingMovedBlocks(content, old, new string) string {
462+
// Regex to match moved blocks: moved { ... }
463+
// This matches multiline moved blocks with any content inside
464+
movedBlockPattern := regexp.MustCompile(`(?s)moved\s*\{[^}]*\}`)
465+
466+
// Find all moved block positions
467+
matches := movedBlockPattern.FindAllStringIndex(content, -1)
468+
if len(matches) == 0 {
469+
// No moved blocks, do simple replacement
470+
return strings.ReplaceAll(content, old, new)
471+
}
472+
473+
// Build result by processing content in segments
474+
var result strings.Builder
475+
lastEnd := 0
476+
477+
for _, match := range matches {
478+
start, end := match[0], match[1]
479+
480+
// Process content before this moved block
481+
before := content[lastEnd:start]
482+
result.WriteString(strings.ReplaceAll(before, old, new))
483+
484+
// Copy the moved block as-is
485+
result.WriteString(content[start:end])
486+
487+
lastEnd = end
488+
}
489+
490+
// Process remaining content after last moved block
491+
result.WriteString(strings.ReplaceAll(content[lastEnd:], old, new))
492+
493+
return result.String()
494+
}
495+
496+
// regexReplaceSkippingMovedBlocks replaces regex matches in content, but skips any content within moved blocks
497+
func regexReplaceSkippingMovedBlocks(content, pattern, replacement string) string {
498+
// Regex to match moved blocks
499+
movedBlockPattern := regexp.MustCompile(`(?s)moved\s*\{[^}]*\}`)
500+
501+
// Find all moved block positions
502+
matches := movedBlockPattern.FindAllStringIndex(content, -1)
503+
if len(matches) == 0 {
504+
// No moved blocks, do simple replacement
505+
re := regexp.MustCompile(pattern)
506+
return re.ReplaceAllString(content, replacement)
507+
}
508+
509+
// Build result by processing content in segments
510+
var result strings.Builder
511+
lastEnd := 0
512+
re := regexp.MustCompile(pattern)
513+
514+
for _, match := range matches {
515+
start, end := match[0], match[1]
516+
517+
// Process content before this moved block
518+
before := content[lastEnd:start]
519+
result.WriteString(re.ReplaceAllString(before, replacement))
520+
521+
// Copy the moved block as-is
522+
result.WriteString(content[start:end])
523+
524+
lastEnd = end
525+
}
526+
527+
// Process remaining content after last moved block
528+
result.WriteString(re.ReplaceAllString(content[lastEnd:], replacement))
529+
530+
return result.String()
531+
}
532+
455533
func processStateFile(log hclog.Logger, p *pipeline.Pipeline, cfg config, apiClient *cloudflare.Client, parsedConfigs map[string]*hclwrite.File) error {
456534
if p == nil {
457535
return fmt.Errorf("state pipeline is nil")

integration/test_runner.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,23 @@ func (r *TestRunner) runMigration(dir string) error {
113113
return fmt.Errorf("building tf-migrate: %w\nOutput: %s", err, output)
114114
}
115115

116-
// Run migration
117-
migrateCmd := exec.Command(
118-
filepath.Join(r.TfMigrateDir, "tf-migrate"),
116+
// Build command arguments
117+
args := []string{
119118
"migrate",
120119
"--config-dir", dir,
121-
"--state-file", filepath.Join(dir, "terraform.tfstate"),
122120
"--source-version", r.SourceVersion,
123121
"--target-version", r.TargetVersion,
124122
"--backup=false",
125-
)
123+
}
124+
125+
// Only add state-file flag if the file exists
126+
stateFile := filepath.Join(dir, "terraform.tfstate")
127+
if _, err := os.Stat(stateFile); err == nil {
128+
args = append(args, "--state-file", stateFile)
129+
}
130+
131+
// Run migration
132+
migrateCmd := exec.Command(filepath.Join(r.TfMigrateDir, "tf-migrate"), args...)
126133
// Set GODEBUG to make map iteration deterministic for consistent test output
127134
migrateCmd.Env = append(os.Environ(), "GODEBUG=randommapseed=0")
128135

integration/v4_to_v5/testdata/argo/expected/argo.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ variable "cloudflare_domain" {
1313
type = string
1414
}
1515

16+
1617
resource "cloudflare_argo_smart_routing" "both_with_lifecycle" {
1718
zone_id = var.cloudflare_zone_id
1819
value = "on"
1920
lifecycle {
2021
ignore_changes = [value]
2122
}
2223
}
24+
2325
moved {
2426
from = cloudflare_argo.both_with_lifecycle
2527
to = cloudflare_argo_smart_routing.both_with_lifecycle
2628
}
29+
2730
resource "cloudflare_argo_tiered_caching" "both_with_lifecycle_tiered" {
2831
zone_id = var.cloudflare_zone_id
2932
value = "on"

integration/v4_to_v5/testdata/argo/expected/terraform.tfstate

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
"serial": 1,
2525
"terraform_version": "1.5.0",
2626
"version": 4
27-
}
27+
}

0 commit comments

Comments
 (0)