@@ -2,6 +2,7 @@ package main
22
33import (
44 "fmt"
5+ "regexp"
56 "strings"
67 "github.com/cloudflare/terraform-provider-cloudflare/cmd/migrate/ast"
78 "github.com/hashicorp/hcl/v2"
@@ -32,7 +33,12 @@ func transformLoadBalancerBlock(block *hclwrite.Block, diags ast.Diagnostics) {
3233 transformPoolBlocksToMap (block , "country_pools" , "country" )
3334 transformPoolBlocksToMap (block , "pop_pools" , "pop" )
3435
35- // Transform rules attribute to ensure region_pools.region is a list
36+ // Transform dynamic rules blocks to for expressions
37+ // v4: dynamic "rules" { for_each = ...; content { ... } }
38+ // v5: rules = [for ... : { ... }]
39+ transformDynamicRulesBlocksToAttribute (block , diags )
40+
41+ // Transform rules attribute to ensure region_pools is a map
3642 transformLoadBalancerRules (block , diags )
3743}
3844
@@ -187,77 +193,110 @@ func transformPoolArrayToMap(block *hclwrite.Block, blockName string, keyField s
187193}
188194
189195
190- // transformLoadBalancerRules transforms the rules attribute to ensure region_pools.region is a list
191- func transformLoadBalancerRules (block * hclwrite.Block , diags ast. Diagnostics ) {
196+ // transformLoadBalancerRulesString applies string-level transformations to consolidate region_pools
197+ func transformLoadBalancerRulesString (block * hclwrite.Block ) {
192198 // Get the rules attribute
193199 rulesAttr := block .Body ().GetAttribute ("rules" )
194200 if rulesAttr == nil {
195201 return
196202 }
197203
198- // Parse the rules expression
199- rulesExpr := ast .WriteExpr2Expr (rulesAttr .Expr (), diags )
204+ // Get the tokens as a string
205+ tokens := rulesAttr .Expr ().BuildTokens (nil )
206+ content := string (tokens .Bytes ())
200207
201- // Check if it's a tuple (list of rules)
202- tup , ok := rulesExpr .(* hclsyntax.TupleConsExpr )
203- if ! ok {
204- // Can't parse - keep as is since we don't know the structure
208+ // Check if we have region_pools to process
209+ if ! strings .Contains (content , "region_pools" ) {
205210 return
206211 }
207212
208- // Process each rule in the list
209- modified := false
210- for _ , ruleExpr := range tup .Exprs {
211- // Check if the rule is an object
212- ruleObj , ok := ruleExpr .(* hclsyntax.ObjectConsExpr )
213- if ! ok {
213+ // Process each overrides block separately to avoid mixing rules
214+ // Find all overrides blocks
215+ overridesPattern := `overrides\s*=\s*\{[^{}]*(?:\{[^}]*\}[^{}]*)*\}`
216+ overridesMatches := regexp .MustCompile (overridesPattern ).FindAllString (content , - 1 )
217+
218+ for _ , overridesBlock := range overridesMatches {
219+ // Check if this overrides block has region_pools
220+ if ! strings .Contains (overridesBlock , "region_pools" ) {
214221 continue
215222 }
216223
217- // Find the overrides attribute
218- for _ , item := range ruleObj .Items {
219- keyExpr , ok := item .KeyExpr .(* hclsyntax.ObjectConsKeyExpr )
220- if ! ok {
221- continue
222- }
224+ // Find all region_pools within this specific overrides block
225+ regionPoolsPattern := `region_pools\s*=\s*\{[^}]*?\}`
226+ regionPoolsMatches := regexp .MustCompile (regionPoolsPattern ).FindAllString (overridesBlock , - 1 )
227+
228+ if len (regionPoolsMatches ) == 0 {
229+ continue
230+ }
231+
232+ // Parse each region_pools to extract region and pool_ids
233+ var regionPoolData [][]string
234+ for _ , rpMatch := range regionPoolsMatches {
235+ // Extract region
236+ regionPattern := `region\s*=\s*"([^"]+)"`
237+ regionMatch := regexp .MustCompile (regionPattern ).FindStringSubmatch (rpMatch )
223238
224- if ast . Expr2S ( keyExpr , diags ) == "overrides" {
225- // Process the overrides object
226- overridesObj , ok := item . ValueExpr .( * hclsyntax. ObjectConsExpr )
227- if ! ok {
228- continue
229- }
230-
231- // Find region_pools in overrides
232- for _ , overrideItem := range overridesObj . Items {
233- overrideKeyExpr , ok := overrideItem . KeyExpr .( * hclsyntax. ObjectConsKeyExpr )
234- if ! ok {
235- continue
236- }
237-
238- if ast . Expr2S ( overrideKeyExpr , diags ) == "region_pools" {
239- // Transform region_pools
240- if transformRegionPools ( & overrideItem . ValueExpr , diags ) {
241- modified = true
242- }
243- }
239+ // Extract pool_ids
240+ poolIDsPattern := `pool_ids\s*=\s*(\[(?:[^\[\]]*|\[[^\]]*\])*\])`
241+ poolIDsMatch := regexp . MustCompile ( poolIDsPattern ). FindStringSubmatch ( rpMatch )
242+
243+ if regionMatch != nil && poolIDsMatch != nil {
244+ regionPoolData = append ( regionPoolData , [] string { rpMatch , regionMatch [ 1 ], poolIDsMatch [ 1 ]})
245+ }
246+ }
247+
248+ // If we found region_pools to consolidate
249+ if len ( regionPoolData ) > 0 {
250+ // Build the consolidated map
251+ consolidatedMap := "region_pools = { \n "
252+ for i , data := range regionPoolData {
253+ region := data [ 1 ]
254+ poolIds := data [ 2 ]
255+ if i < len ( regionPoolData ) - 1 {
256+ consolidatedMap += fmt . Sprintf ( " %s = %s, \n " , region , poolIds )
257+ } else {
258+ consolidatedMap += fmt . Sprintf ( " %s = %s \n " , region , poolIds )
244259 }
245260 }
261+ consolidatedMap += " }"
262+
263+ // Create modified overrides block
264+ modifiedOverrides := overridesBlock
265+
266+ // Replace first region_pools with consolidated map
267+ modifiedOverrides = strings .Replace (modifiedOverrides , regionPoolData [0 ][0 ], consolidatedMap , 1 )
268+
269+ // Remove all other region_pools blocks
270+ for i := 1 ; i < len (regionPoolData ); i ++ {
271+ modifiedOverrides = strings .Replace (modifiedOverrides , regionPoolData [i ][0 ], "" , 1 )
272+ }
273+
274+ // Clean up extra whitespace and commas
275+ modifiedOverrides = regexp .MustCompile (`\n\s*\n\s*\n` ).ReplaceAllString (modifiedOverrides , "\n " )
276+
277+ // Replace the original overrides block with the modified one
278+ content = strings .Replace (content , overridesBlock , modifiedOverrides , 1 )
246279 }
247280 }
248281
249- // Only update if we made changes
250- if modified {
251- // Convert the modified expression back to a string and reparse as HCL
252- exprStr := ast .Expr2S (rulesExpr , diags )
253- // Use hclwrite to parse and format the expression properly
254- newExpr , err := hclwrite .ParseConfig ([]byte ("temp = " + exprStr ), "temp.hcl" , hcl .InitialPos )
255- if err == nil && newExpr .Body ().Attributes ()["temp" ] != nil {
256- block .Body ().SetAttributeRaw ("rules" , newExpr .Body ().Attributes ()["temp" ].Expr ().BuildTokens (nil ))
257- }
282+ // Parse the modified content and set it back
283+ tempConfig := fmt .Sprintf ("temp = %s" , content )
284+ tempFile , err := hclwrite .ParseConfig ([]byte (tempConfig ), "temp.hcl" , hcl .InitialPos )
285+ if err == nil && tempFile .Body ().GetAttribute ("temp" ) != nil {
286+ block .Body ().SetAttributeRaw ("rules" , tempFile .Body ().GetAttribute ("temp" ).Expr ().BuildTokens (nil ))
258287 }
259288}
260289
290+
291+ // transformLoadBalancerRules transforms the rules attribute to:
292+ // 1. Consolidate multiple region_pools objects into a single map
293+ // 2. Fix the structure from { region = "X", pool_ids = [...] } to { "X" = [...] }
294+ func transformLoadBalancerRules (block * hclwrite.Block , diags ast.Diagnostics ) {
295+ // Just use the string-level transformation which handles the region_pools consolidation
296+ // This preserves complex expressions that can't be parsed
297+ transformLoadBalancerRulesString (block )
298+ }
299+
261300// transformRegionPools ensures that each region_pools element has region as a list
262301func transformRegionPools (expr * hclsyntax.Expression , diags ast.Diagnostics ) bool {
263302 modified := false
@@ -314,3 +353,201 @@ func transformSingleRegionPool(poolExpr hclsyntax.Expression, diags ast.Diagnost
314353 return modified
315354}
316355
356+ // extractRegionPoolIntoMap extracts region and pool_ids from a region_pool object and adds to map
357+ func extractRegionPoolIntoMap (poolObj * hclsyntax.ObjectConsExpr , regionPoolsMap map [string ]hclsyntax.Expression , diags ast.Diagnostics ) {
358+ var regionValues []string
359+ var poolIDsExpr hclsyntax.Expression
360+
361+ for _ , item := range poolObj .Items {
362+ keyExpr , ok := item .KeyExpr .(* hclsyntax.ObjectConsKeyExpr )
363+ if ! ok {
364+ continue
365+ }
366+
367+ keyName := ast .Expr2S (keyExpr , diags )
368+ switch keyName {
369+ case "region" :
370+ // Extract region value(s)
371+
372+ // Convert the expression to string regardless of type
373+ regionStr := ast .Expr2S (item .ValueExpr , diags )
374+
375+ // Clean up the value - remove quotes and whitespace
376+ regionStr = strings .TrimSpace (regionStr )
377+ if len (regionStr ) >= 2 && regionStr [0 ] == '"' && regionStr [len (regionStr )- 1 ] == '"' {
378+ regionStr = regionStr [1 : len (regionStr )- 1 ]
379+ }
380+
381+ if regionStr != "" {
382+ regionValues = append (regionValues , regionStr )
383+ }
384+ case "pool_ids" :
385+ poolIDsExpr = item .ValueExpr
386+ }
387+ }
388+
389+ // Add to map for each region
390+ for _ , region := range regionValues {
391+ if poolIDsExpr != nil {
392+ regionPoolsMap [region ] = poolIDsExpr
393+ }
394+ }
395+ }
396+
397+ // extractPoolIntoMap extracts key field and pool_ids from a pool object and adds to map
398+ func extractPoolIntoMap (poolObj * hclsyntax.ObjectConsExpr , keyField string , poolMap map [string ]hclsyntax.Expression , diags ast.Diagnostics ) {
399+ var keyValue string
400+ var poolIDsExpr hclsyntax.Expression
401+
402+ for _ , item := range poolObj .Items {
403+ keyExpr , ok := item .KeyExpr .(* hclsyntax.ObjectConsKeyExpr )
404+ if ! ok {
405+ continue
406+ }
407+
408+ keyName := ast .Expr2S (keyExpr , diags )
409+ if keyName == keyField {
410+ // Extract key value
411+ if litExpr , ok := item .ValueExpr .(* hclsyntax.LiteralValueExpr ); ok {
412+ if litExpr .Val .Type ().FriendlyName () == "string" {
413+ keyValue = litExpr .Val .AsString ()
414+ }
415+ }
416+ } else if keyName == "pool_ids" {
417+ poolIDsExpr = item .ValueExpr
418+ }
419+ }
420+
421+ // Add to map
422+ if keyValue != "" && poolIDsExpr != nil {
423+ poolMap [keyValue ] = poolIDsExpr
424+ }
425+ }
426+
427+ // transformDynamicRulesBlocksToAttribute converts dynamic "rules" blocks to a rules attribute with for expression
428+ func transformDynamicRulesBlocksToAttribute (block * hclwrite.Block , diags ast.Diagnostics ) {
429+ body := block .Body ()
430+ dynamicRulesBlocks := []* hclwrite.Block {}
431+
432+ // Find all dynamic blocks with label "rules"
433+ for _ , childBlock := range body .Blocks () {
434+ if childBlock .Type () == "dynamic" && len (childBlock .Labels ()) > 0 && childBlock .Labels ()[0 ] == "rules" {
435+ dynamicRulesBlocks = append (dynamicRulesBlocks , childBlock )
436+ }
437+ }
438+
439+ if len (dynamicRulesBlocks ) == 0 {
440+ return
441+ }
442+
443+ // Process each dynamic rules block
444+ for _ , dynBlock := range dynamicRulesBlocks {
445+ dynBody := dynBlock .Body ()
446+
447+ // Get the for_each expression
448+ forEachAttr := dynBody .GetAttribute ("for_each" )
449+ if forEachAttr == nil {
450+ continue
451+ }
452+
453+ // Find the content block
454+ var contentBlock * hclwrite.Block
455+ for _ , cb := range dynBody .Blocks () {
456+ if cb .Type () == "content" {
457+ contentBlock = cb
458+ break
459+ }
460+ }
461+
462+ if contentBlock == nil {
463+ continue
464+ }
465+
466+ // Get the for_each expression tokens
467+ forEachTokens := forEachAttr .Expr ().BuildTokens (nil )
468+
469+ // Build the for expression
470+ // rules = [for rule in <for_each> : { <content> }]
471+ tokens := hclwrite.Tokens {
472+ & hclwrite.Token {Type : hclsyntax .TokenOBrack , Bytes : []byte ("[" )},
473+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte ("for" )},
474+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" rules" )},
475+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" in" )},
476+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" " )},
477+ }
478+
479+ // Add the for_each expression
480+ tokens = append (tokens , forEachTokens ... )
481+ tokens = append (tokens ,
482+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" " )},
483+ & hclwrite.Token {Type : hclsyntax .TokenColon , Bytes : []byte (":" )},
484+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" " )},
485+ & hclwrite.Token {Type : hclsyntax .TokenOBrace , Bytes : []byte ("{" )},
486+ & hclwrite.Token {Type : hclsyntax .TokenNewline , Bytes : []byte ("\n " )},
487+ )
488+
489+ // Add content block attributes as object properties
490+ contentBody := contentBlock .Body ()
491+
492+ // Process attributes in the content block
493+ attrs := contentBody .Attributes ()
494+ first := true
495+ for name , attr := range attrs {
496+ if ! first {
497+ tokens = append (tokens , & hclwrite.Token {Type : hclsyntax .TokenComma , Bytes : []byte ("," )})
498+ tokens = append (tokens , & hclwrite.Token {Type : hclsyntax .TokenNewline , Bytes : []byte ("\n " )})
499+ }
500+ first = false
501+
502+ // Add indentation
503+ tokens = append (tokens , & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" " )})
504+
505+ // Add attribute name
506+ tokens = append (tokens , & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (name )})
507+ tokens = append (tokens , & hclwrite.Token {Type : hclsyntax .TokenEqual , Bytes : []byte (" = " )})
508+
509+ // Get the attribute value expression tokens
510+ attrTokens := attr .Expr ().BuildTokens (nil )
511+
512+ // For rules, we use a simpler replacement since "rules" is used as the entire iterator
513+ // not "rules.key" and "rules.value" like in the origins case
514+ // We just replace "rules.value" -> "rules" and "rules.key" -> "rules"
515+ attrStr := string (attrTokens .Bytes ())
516+ // Only replace when preceded by a dot to avoid replacing other uses of "rules"
517+ attrStr = strings .ReplaceAll (attrStr , ".rules.value" , ".rules" )
518+ attrStr = strings .ReplaceAll (attrStr , ".rules.key" , ".rules" )
519+ // Also handle at the start of expression
520+ if strings .HasPrefix (attrStr , "rules.value" ) {
521+ attrStr = "rules" + attrStr [11 :] // len("rules.value") = 11
522+ }
523+ if strings .HasPrefix (attrStr , "rules.key" ) {
524+ attrStr = "rules" + attrStr [9 :] // len("rules.key") = 9
525+ }
526+
527+ // Parse the modified expression
528+ tempConfig := fmt .Sprintf ("temp = %s" , attrStr )
529+ tempFile , parseErr := hclwrite .ParseConfig ([]byte (tempConfig ), "temp.hcl" , hcl .InitialPos )
530+ if parseErr == nil && tempFile .Body ().GetAttribute ("temp" ) != nil {
531+ tokens = append (tokens , tempFile .Body ().GetAttribute ("temp" ).Expr ().BuildTokens (nil )... )
532+ } else {
533+ // Fall back to original tokens if parsing fails
534+ tokens = append (tokens , attrTokens ... )
535+ }
536+ }
537+
538+ // Close the object and list
539+ tokens = append (tokens ,
540+ & hclwrite.Token {Type : hclsyntax .TokenNewline , Bytes : []byte ("\n " )},
541+ & hclwrite.Token {Type : hclsyntax .TokenIdent , Bytes : []byte (" " )},
542+ & hclwrite.Token {Type : hclsyntax .TokenCBrace , Bytes : []byte ("}" )},
543+ & hclwrite.Token {Type : hclsyntax .TokenCBrack , Bytes : []byte ("]" )},
544+ )
545+
546+ // Remove the dynamic block
547+ body .RemoveBlock (dynBlock )
548+
549+ // Add the rules attribute
550+ body .SetAttributeRaw ("rules" , tokens )
551+ }
552+ }
553+
0 commit comments