Skip to content

Commit dc0177c

Browse files
fix(cli): preflight listing of resources (#293)
1 parent 13c7c60 commit dc0177c

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

cmd/tf-migrate/preflight.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/hashicorp/hcl/v2/hclwrite"
1212

1313
"github.com/cloudflare/tf-migrate/internal/transform"
14+
tfhcl "github.com/cloudflare/tf-migrate/internal/transform/hcl"
1415
)
1516

1617
// resourceClassification describes how a resource will be handled during migration.
@@ -157,6 +158,11 @@ func classifyResource(block *hclwrite.Block, file string, providers transform.Mi
157158

158159
// Check if this resource will be renamed
159160
if newType, ok := renames[resourceType]; ok {
161+
// Conditional renames: some v4 types map to one of two v5 types
162+
// depending on block attributes. Override the static rename target
163+
// with the per-instance target based on attribute inspection.
164+
newType = resolveConditionalRename(resourceType, newType, block)
165+
160166
return &scannedResource{
161167
File: file,
162168
ResourceType: resourceType,
@@ -176,6 +182,51 @@ func classifyResource(block *hclwrite.Block, file string, providers transform.Mi
176182
}
177183
}
178184

185+
// resolveConditionalRename overrides the static rename target for resource types
186+
// where the v5 target depends on per-instance block attributes.
187+
//
188+
// Currently handles:
189+
// - cloudflare_zero_trust_device_profiles / cloudflare_device_settings_policy:
190+
// Routes to cloudflare_zero_trust_device_custom_profile when the block has
191+
// match + precedence and is not explicitly default=true.
192+
// - cloudflare_zero_trust_local_fallback_domain / cloudflare_fallback_domain:
193+
// Routes to cloudflare_zero_trust_device_custom_profile_local_domain_fallback
194+
// when the block has a non-empty policy_id.
195+
func resolveConditionalRename(resourceType, staticNewType string, block *hclwrite.Block) string {
196+
body := block.Body()
197+
198+
switch resourceType {
199+
case "cloudflare_zero_trust_device_profiles", "cloudflare_device_settings_policy":
200+
// Mirror the routing logic from zero_trust_device_profiles TransformConfig:
201+
// custom if !default && match && precedence, else default.
202+
isExplicitDefault := false
203+
if attr := body.GetAttribute("default"); attr != nil {
204+
val, ok := tfhcl.ExtractBoolFromAttribute(attr)
205+
if ok {
206+
isExplicitDefault = val
207+
}
208+
}
209+
hasMatch := body.GetAttribute("match") != nil
210+
hasPrecedence := body.GetAttribute("precedence") != nil
211+
212+
if !isExplicitDefault && hasMatch && hasPrecedence {
213+
return "cloudflare_zero_trust_device_custom_profile"
214+
}
215+
216+
case "cloudflare_zero_trust_local_fallback_domain", "cloudflare_fallback_domain":
217+
// Mirror the routing logic from zero_trust_local_fallback_domain TransformConfig:
218+
// custom if policy_id is present and non-empty, else default.
219+
if attr := body.GetAttribute("policy_id"); attr != nil {
220+
val := tfhcl.ExtractStringFromAttribute(attr)
221+
if val != "" || tfhcl.IsExpressionAttribute(attr) {
222+
return "cloudflare_zero_trust_device_custom_profile_local_domain_fallback"
223+
}
224+
}
225+
}
226+
227+
return staticNewType
228+
}
229+
179230
// parseMovedBlock extracts from/to information from a moved block.
180231
func parseMovedBlock(block *hclwrite.Block, file string) *existingMovedBlock {
181232
body := block.Body()

cmd/tf-migrate/preflight_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,96 @@ func TestRunPreMigrationScan_EmptyDirectory(t *testing.T) {
381381
}
382382
}
383383

384+
// TestPreflightScan_ConditionalRenames verifies that the pre-migration scan
385+
// correctly distinguishes default vs custom device profiles and fallback domains.
386+
// Regression test for https://github.com/cloudflare/tf-migrate/issues/290.
387+
func TestPreflightScan_ConditionalRenames(t *testing.T) {
388+
tmpDir := t.TempDir()
389+
390+
content := `
391+
resource "cloudflare_zero_trust_device_profiles" "default_profile" {
392+
account_id = "abc123"
393+
default = true
394+
}
395+
396+
resource "cloudflare_zero_trust_device_profiles" "custom_profile" {
397+
account_id = "abc123"
398+
name = "Custom"
399+
match = "identity.email == \"user@example.com\""
400+
precedence = 100
401+
default = false
402+
}
403+
404+
resource "cloudflare_zero_trust_device_profiles" "custom_implicit" {
405+
account_id = "abc123"
406+
name = "Custom Implicit"
407+
match = "identity.email == \"other@example.com\""
408+
precedence = 200
409+
}
410+
411+
resource "cloudflare_fallback_domain" "default_fallback" {
412+
account_id = "abc123"
413+
domains {
414+
suffix = "example.com"
415+
}
416+
}
417+
418+
resource "cloudflare_fallback_domain" "custom_fallback" {
419+
account_id = "abc123"
420+
policy_id = cloudflare_zero_trust_device_profiles.custom_profile.id
421+
domains {
422+
suffix = "example.com"
423+
}
424+
}
425+
`
426+
if err := os.WriteFile(filepath.Join(tmpDir, "main.tf"), []byte(content), 0644); err != nil {
427+
t.Fatal(err)
428+
}
429+
430+
log := hclog.NewNullLogger()
431+
cfg := config{
432+
configDir: tmpDir,
433+
sourceVersion: "v4",
434+
targetVersion: "v5",
435+
}
436+
437+
report, err := runPreMigrationScan(log, cfg)
438+
if err != nil {
439+
t.Fatalf("runPreMigrationScan error: %v", err)
440+
}
441+
442+
// Build a map of resource name → NewType for renamed resources
443+
renames := make(map[string]string)
444+
for _, r := range report.Resources {
445+
if r.Class == classRenamed {
446+
renames[r.ResourceName] = r.NewType
447+
}
448+
}
449+
450+
tests := []struct {
451+
name string
452+
expected string
453+
}{
454+
{"default_profile", "cloudflare_zero_trust_device_default_profile"},
455+
{"custom_profile", "cloudflare_zero_trust_device_custom_profile"},
456+
{"custom_implicit", "cloudflare_zero_trust_device_custom_profile"},
457+
{"default_fallback", "cloudflare_zero_trust_device_default_profile_local_domain_fallback"},
458+
{"custom_fallback", "cloudflare_zero_trust_device_custom_profile_local_domain_fallback"},
459+
}
460+
461+
for _, tt := range tests {
462+
t.Run(tt.name, func(t *testing.T) {
463+
got, ok := renames[tt.name]
464+
if !ok {
465+
t.Fatalf("Resource %q not found in rename results", tt.name)
466+
}
467+
if got != tt.expected {
468+
t.Errorf("Resource %q: got NewType=%q, want %q", tt.name, got, tt.expected)
469+
}
470+
})
471+
}
472+
}
473+
384474
func contains(s, substr string) bool {
385475
return len(s) >= len(substr) && searchString(s, substr)
386476
}

0 commit comments

Comments
 (0)