configupgrade: Upgrade indexing of splat syntax

This commit is contained in:
Radek Simko 2019-04-25 00:37:46 +01:00
parent 1f5cadeec0
commit 42ba7a3e00
No known key found for this signature in database
GPG Key ID: 1F1C84FE689A88D7
5 changed files with 186 additions and 77 deletions

View File

@ -0,0 +1,16 @@
resource "test_instance" "first_many" {
count = 2
}
resource "test_instance" "one" {
image = "${test_instance.first_many.*.id[0]}"
}
resource "test_instance" "splat_of_one" {
image = "${test_instance.one.*.id[0]}"
}
resource "test_instance" "second_many" {
count = "${length(test_instance.first_many)}"
security_groups = "${test_instance.first_many.*.id[count.index]}"
}

View File

@ -0,0 +1,16 @@
resource "test_instance" "first_many" {
count = 2
}
resource "test_instance" "one" {
image = test_instance.first_many[0].id
}
resource "test_instance" "splat_of_one" {
image = test_instance.one.*.id[0]
}
resource "test_instance" "second_many" {
count = length(test_instance.first_many)
security_groups = test_instance.first_many[count.index].id
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -248,41 +248,9 @@ Value:
// safe to do so. // safe to do so.
parts := strings.Split(tv.Name, ".") parts := strings.Split(tv.Name, ".")
// First we need to deal with the .count pseudo-attributes that 0.11 and transformed := transformCountPseudoAttribute(&buf, parts, an)
// prior allowed for resources. These no longer exist, because they if transformed {
// don't do anything we can't do with the length(...) function. break Value
if len(parts) > 0 {
var rAddr addrs.Resource
switch parts[0] {
case "data":
if len(parts) == 4 && parts[3] == "count" {
rAddr.Mode = addrs.DataResourceMode
rAddr.Type = parts[1]
rAddr.Name = parts[2]
}
default:
if len(parts) == 3 && parts[2] == "count" {
rAddr.Mode = addrs.ManagedResourceMode
rAddr.Type = parts[0]
rAddr.Name = parts[1]
}
}
// We need to check if the thing being referenced is actually an
// existing resource, because other three-part traversals might
// coincidentally end with "count".
if hasCount, exists := an.ResourceHasCount[rAddr]; exists {
if hasCount {
buf.WriteString("length(")
buf.WriteString(rAddr.String())
buf.WriteString(")")
} else {
// If the resource does not have count, the .count
// attr would've always returned 1 before.
buf.WriteString("1")
}
break Value
}
} }
parts = upgradeTraversalParts(parts, an) // might add/remove/change parts parts = upgradeTraversalParts(parts, an) // might add/remove/change parts
@ -293,42 +261,7 @@ Value:
break break
} }
first, remain := parts[0], parts[1:] printHilTraversalPartsAsHcl2(&buf, parts)
buf.WriteString(first)
seenSplat := false
for _, part := range remain {
if part == "*" {
seenSplat = true
buf.WriteString(".*")
continue
}
// Other special cases apply only if we've not previously
// seen a splat expression marker, since attribute vs. index
// syntax have different interpretations after a simple splat.
if !seenSplat {
if v, err := strconv.Atoi(part); err == nil {
// Looks like it's old-style index traversal syntax foo.0.bar
// so we'll replace with canonical index syntax foo[0].bar.
fmt.Fprintf(&buf, "[%d]", v)
continue
}
if !hcl2syntax.ValidIdentifier(part) {
// This should be rare since HIL's identifier syntax is _close_
// to HCL2's, but we'll get here if one of the intervening
// parts is not a valid identifier in isolation, since HIL
// did not consider these to be separate identifiers.
// e.g. foo.1bar would be invalid in HCL2; must instead be foo["1bar"].
buf.WriteByte('[')
printQuotedString(&buf, part)
buf.WriteByte(']')
continue
}
}
buf.WriteByte('.')
buf.WriteString(part)
}
case *hilast.Arithmetic: case *hilast.Arithmetic:
op, exists := hilArithmeticOpSyms[tv.Op] op, exists := hilArithmeticOpSyms[tv.Op]
@ -560,14 +493,74 @@ Value:
buf.Write(falseSrc) buf.Write(falseSrc)
case *hilast.Index: case *hilast.Index:
targetSrc, exprDiags := upgradeExpr(tv.Target, filename, true, an) target, ok := tv.Target.(*hilast.VariableAccess)
diags = diags.Append(exprDiags) if !ok {
panic(fmt.Sprintf("Index node with unsupported target type (%T)", tv.Target))
}
parts := strings.Split(target.Name, ".")
keySrc, exprDiags := upgradeExpr(tv.Key, filename, true, an) keySrc, exprDiags := upgradeExpr(tv.Key, filename, true, an)
diags = diags.Append(exprDiags) diags = diags.Append(exprDiags)
buf.Write(targetSrc)
buf.WriteString("[") transformed := transformCountPseudoAttribute(&buf, parts, an)
buf.Write(keySrc) if transformed {
buf.WriteString("]") break Value
}
parts = upgradeTraversalParts(parts, an) // might add/remove/change parts
vDiags := validateHilAddress(target.Name, filename)
if len(vDiags) > 0 {
diags = diags.Append(vDiags)
break
}
first, remain := parts[0], parts[1:]
var rAddr addrs.Resource
switch parts[0] {
case "data":
if len(parts) == 5 && parts[3] == "*" {
rAddr.Mode = addrs.DataResourceMode
rAddr.Type = parts[1]
rAddr.Name = parts[2]
}
default:
if len(parts) == 4 && parts[2] == "*" {
rAddr.Mode = addrs.ManagedResourceMode
rAddr.Type = parts[0]
rAddr.Name = parts[1]
}
}
// We need to check if the thing being referenced has count
// to retain backward compatibility
hasCount := false
if v, exists := an.ResourceHasCount[rAddr]; exists {
hasCount = v
}
hasSplat := false
buf.WriteString(first)
for _, part := range remain {
// Attempt to convert old-style splat indexing to new one
// e.g. res.label.*.attr[idx] to res.label[idx].attr
if part == "*" && hasCount {
hasSplat = true
buf.WriteString(fmt.Sprintf("[%s]", keySrc))
continue
}
buf.WriteByte('.')
buf.WriteString(part)
}
if !hasSplat {
buf.WriteString("[")
buf.Write(keySrc)
buf.WriteString("]")
}
case *hilast.Output: case *hilast.Output:
if len(tv.Exprs) == 1 { if len(tv.Exprs) == 1 {
@ -658,6 +651,85 @@ func getResourceLabel(parts []string) (string, bool) {
return parts[1], true return parts[1], true
} }
// transformCountPseudoAttribute deals with the .count pseudo-attributes
// that 0.11 and prior allowed for resources. These no longer exist,
// because they don't do anything we can't do with the length(...) function.
func transformCountPseudoAttribute(buf *bytes.Buffer, parts []string, an *analysis) (transformed bool) {
if len(parts) > 0 {
var rAddr addrs.Resource
switch parts[0] {
case "data":
if len(parts) == 4 && parts[3] == "count" {
rAddr.Mode = addrs.DataResourceMode
rAddr.Type = parts[1]
rAddr.Name = parts[2]
}
default:
if len(parts) == 3 && parts[2] == "count" {
rAddr.Mode = addrs.ManagedResourceMode
rAddr.Type = parts[0]
rAddr.Name = parts[1]
}
}
// We need to check if the thing being referenced is actually an
// existing resource, because other three-part traversals might
// coincidentally end with "count".
if hasCount, exists := an.ResourceHasCount[rAddr]; exists {
if hasCount {
buf.WriteString("length(")
buf.WriteString(rAddr.String())
buf.WriteString(")")
} else {
// If the resource does not have count, the .count
// attr would've always returned 1 before.
buf.WriteString("1")
}
transformed = true
return
}
}
return
}
func printHilTraversalPartsAsHcl2(buf *bytes.Buffer, parts []string) {
first, remain := parts[0], parts[1:]
buf.WriteString(first)
seenSplat := false
for _, part := range remain {
if part == "*" {
seenSplat = true
buf.WriteString(".*")
continue
}
// Other special cases apply only if we've not previously
// seen a splat expression marker, since attribute vs. index
// syntax have different interpretations after a simple splat.
if !seenSplat {
if v, err := strconv.Atoi(part); err == nil {
// Looks like it's old-style index traversal syntax foo.0.bar
// so we'll replace with canonical index syntax foo[0].bar.
fmt.Fprintf(buf, "[%d]", v)
continue
}
if !hcl2syntax.ValidIdentifier(part) {
// This should be rare since HIL's identifier syntax is _close_
// to HCL2's, but we'll get here if one of the intervening
// parts is not a valid identifier in isolation, since HIL
// did not consider these to be separate identifiers.
// e.g. foo.1bar would be invalid in HCL2; must instead be foo["1bar"].
buf.WriteByte('[')
printQuotedString(buf, part)
buf.WriteByte(']')
continue
}
}
buf.WriteByte('.')
buf.WriteString(part)
}
}
func upgradeHeredocBody(buf *bytes.Buffer, val *hilast.Output, filename string, an *analysis) tfdiags.Diagnostics { func upgradeHeredocBody(buf *bytes.Buffer, val *hilast.Output, filename string, an *analysis) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics

View File

@ -2,6 +2,7 @@ package configupgrade
import ( import (
"bytes" "bytes"
"flag"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -286,6 +287,7 @@ func init() {
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
flag.Parse()
if testing.Verbose() { if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG // if we're verbose, use the logging requested by TF_LOG
logging.SetOutput() logging.SetOutput()