Skip to content

Commit 2af41f8

Browse files
fix: lb and lb pool config migration (#6170)
1 parent b3c4779 commit 2af41f8

File tree

4 files changed

+1115
-417
lines changed

4 files changed

+1115
-417
lines changed

cmd/migrate/load_balancer.go

Lines changed: 287 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
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
262301
func 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

Comments
 (0)