Skip to content
18 changes: 12 additions & 6 deletions array_attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ var _ attributeChange = &ArrayAttributeChange{}

// IsArrayAttributeChangeLine returns true if the line is a valid attribute change
// This requires the line to start with "+", "-" or "~", not be followed with "resource" or "data", and ends with "[".
// Terraform may append ForcesReplacementComment after the opening bracket.
func IsArrayAttributeChangeLine(line string) bool {
line = strings.TrimSpace(line)
// validPrefix := strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "~")
validSuffix := strings.HasSuffix(line, "[") || IsOneLineEmptyArrayAttribute(line)
base := strings.TrimSpace(strings.TrimSuffix(line, ForcesReplacementComment))
validSuffix := strings.HasSuffix(base, "[") || IsOneLineEmptyArrayAttribute(line)
return validSuffix && !IsResourceChangeLine(line)
}

// IsArrayAttributeTerminator returns true if the line is "]" or "] -> null"
func IsArrayAttributeTerminator(line string) bool {
return strings.TrimSuffix(strings.TrimSpace(line), " -> null") == "]"
return strings.TrimSuffix(strings.TrimSpace(line), " -> null") == "]" || strings.TrimSuffix(strings.TrimSpace(line), " -> (known after apply)") == "]"
}

// IsOneLineEmptyArrayAttribute returns true if the line ends with a "[]"
func IsOneLineEmptyArrayAttribute(line string) bool {
return strings.HasSuffix(line, "[]")
line = strings.TrimSpace(line)
line = strings.TrimSuffix(line, ForcesReplacementComment)
return strings.HasSuffix(strings.TrimSpace(line), "[]")
}

// NewArrayAttributeChangeFromLine initializes an ArrayAttributeChange from a line containing an array attribute change
Expand All @@ -57,10 +60,13 @@ func NewArrayAttributeChangeFromLine(line string) (*ArrayAttributeChange, error)
UpdateType: DestroyResource,
}, nil
} else if strings.HasPrefix(line, "~") {
// replace
updateType := UpdateInPlaceResource
if strings.HasSuffix(strings.TrimSpace(line), ForcesReplacementComment) {
updateType = ForceReplaceResource
}
return &ArrayAttributeChange{
Name: attributeName,
UpdateType: UpdateInPlaceResource,
UpdateType: updateType,
}, nil
} else {
return &ArrayAttributeChange{
Expand Down
8 changes: 8 additions & 0 deletions array_attribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ func TestNewArrayAttributeChangeFromLine(t *testing.T) {
UpdateType: UpdateInPlaceResource,
},
},
"attribute changed forces replacement": {
line: `~ array_test = [` + ForcesReplacementComment,
shouldError: false,
expected: &ArrayAttributeChange{
Name: "array_test",
UpdateType: ForceReplaceResource,
},
},
"attribute is unchanged": {
line: `attribute [`,
shouldError: false,
Expand Down
31 changes: 23 additions & 8 deletions attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const (
ATTRIBUTE_DEFINITON_DELIMITER = " = "
SENSITIVE_VALUE = "(sensitive value)"
COMPUTED_VALUE = "(known after apply)"
// ForcesReplacementComment is the suffix Terraform appends to attributes that trigger replace.
ForcesReplacementComment = " # forces replacement"
)

type attributeChange interface {
Expand Down Expand Up @@ -92,9 +94,9 @@ func NewAttributeChangeFromLine(line string) (*AttributeChange, error) {
// replace
updateType := UpdateInPlaceResource

if strings.HasSuffix(attribute[1], " # forces replacement") {
if strings.HasSuffix(attribute[1], ForcesReplacementComment) {
updateType = ForceReplaceResource
attribute[1] = strings.TrimSuffix(attribute[1], " # forces replacement")
attribute[1] = strings.TrimSuffix(attribute[1], ForcesReplacementComment)
}

values := strings.Split(attribute[1], ATTRIBUTE_CHANGE_DELIMITER)
Expand All @@ -112,11 +114,18 @@ func NewAttributeChangeFromLine(line string) (*AttributeChange, error) {
UpdateType: updateType,
}, nil
} else {
valStr := strings.TrimSpace(attribute[1])
updateType := NoOpResource
if strings.HasSuffix(valStr, ForcesReplacementComment) {
valStr = strings.TrimSpace(strings.TrimSuffix(valStr, ForcesReplacementComment))
updateType = ForceReplaceResource
}
conv := doTypeConversion(valStr)
return &AttributeChange{
Name: dequote(strings.TrimSpace(attribute[0])),
OldValue: doTypeConversion(attribute[1]),
NewValue: doTypeConversion(attribute[1]),
UpdateType: NoOpResource,
OldValue: conv,
NewValue: conv,
UpdateType: updateType,
}, nil
}
}
Expand All @@ -143,9 +152,15 @@ func NewAttributeChangeFromArray(line string) (*AttributeChange, error) {
UpdateType: DestroyResource,
}, nil
} else if strings.HasPrefix(line, "~") {
// replace
// TODO: confirm this is possible? I think array entries are immutable
return nil, fmt.Errorf("unexpected replace single attribute in array %s", line)
parts := strings.Split(line, ATTRIBUTE_CHANGE_DELIMITER)
if len(parts) != 2 {
return nil, fmt.Errorf("unexpected replace single attribute in array %s", line)
}
return &AttributeChange{
OldValue: normalizeArrayAttribute(parts[0]),
NewValue: normalizeArrayAttribute(parts[1]),
UpdateType: UpdateInPlaceResource,
}, nil
} else {
return &AttributeChange{
OldValue: normalizeArrayAttribute(line),
Expand Down
19 changes: 19 additions & 0 deletions attribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,16 @@ func TestNewAttributeChangeFromLine(t *testing.T) {
UpdateType: NoOpResource,
},
},
"unchanged attribute forces replacement": {
line: `id = "namespace-id"` + ForcesReplacementComment,
shouldError: false,
expected: &AttributeChange{
Name: "id",
OldValue: "namespace-id",
NewValue: "namespace-id",
UpdateType: ForceReplaceResource,
},
},
"resource line": {
line: `+ resource "type" "name" {`,
shouldError: true,
Expand Down Expand Up @@ -294,6 +304,15 @@ func TestNewAttributeChangeFromArray(t *testing.T) {
UpdateType: NewResource,
},
},
"array item swapped": {
line: `~ "old" -> "new"`,
shouldError: false,
expected: &AttributeChange{
OldValue: "old",
NewValue: "new",
UpdateType: UpdateInPlaceResource,
},
},
"other line": {
line: `}`,
shouldError: true,
Expand Down
2 changes: 1 addition & 1 deletion heredoc_attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func NewHeredocAttributeChangeFromLine(line string) (*HeredocAttributeChange, er
} else if strings.HasPrefix(line, "~") {
// replace
updateType := UpdateInPlaceResource
if strings.HasSuffix(attribute[1], " # forces replacement") {
if strings.HasSuffix(attribute[1], ForcesReplacementComment) {
updateType = ForceReplaceResource
}

Expand Down
2 changes: 1 addition & 1 deletion jsonencode.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewJSONEncodeAttributeChangeFromLine(line string) (*JSONEncodeAttributeChan
} else if strings.HasPrefix(line, "~") {
// replace
updateType := UpdateInPlaceResource
if strings.HasSuffix(attribute[1], " # forces replacement") {
if strings.HasSuffix(attribute[1], ForcesReplacementComment) {
updateType = ForceReplaceResource
}

Expand Down
18 changes: 12 additions & 6 deletions map_attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,24 @@ var _ attributeChange = &MapAttributeChange{}

// IsMapAttributeChangeLine returns true if the line is a valid attribute change
// This requires the line to start with "+", "-" or "~", not be followed with "resource" or "data", and ends with "{".
// Terraform may append ForcesReplacementComment after the opening brace.
func IsMapAttributeChangeLine(line string) bool {
line = strings.TrimSpace(line)
// validPrefix := strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "~")
validSuffix := strings.HasSuffix(line, "{") || IsOneLineEmptyMapAttribute(line)
base := strings.TrimSpace(strings.TrimSuffix(line, ForcesReplacementComment))
validSuffix := strings.HasSuffix(base, "{") || IsOneLineEmptyMapAttribute(line)
return validSuffix && !IsResourceChangeLine(line)
}

// IsMapAttributeTerminator returns true if the line is a "}" or "},"
func IsMapAttributeTerminator(line string) bool {
return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSpace(line), ","), " -> null") == "}"
return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSpace(line), ","), " -> null") == "}" || strings.TrimSuffix(strings.TrimSuffix(strings.TrimSpace(line), ","), " -> (known after apply)") == "}"
}

// IsOneLineEmptyMapAttribute returns true if the line ends with a "{}"
func IsOneLineEmptyMapAttribute(line string) bool {
return strings.HasSuffix(line, "{}")
line = strings.TrimSpace(line)
line = strings.TrimSuffix(line, ForcesReplacementComment)
return strings.HasSuffix(strings.TrimSpace(line), "{}")
}

// NewMapAttributeChangeFromLine initializes an AttributeChange from a line containing an attribute change
Expand All @@ -54,10 +57,13 @@ func NewMapAttributeChangeFromLine(line string) (*MapAttributeChange, error) {
UpdateType: DestroyResource,
}, nil
} else if strings.HasPrefix(line, "~") {
// replace
updateType := UpdateInPlaceResource
if strings.HasSuffix(strings.TrimSpace(line), ForcesReplacementComment) {
updateType = ForceReplaceResource
}
return &MapAttributeChange{
Name: attributeName,
UpdateType: UpdateInPlaceResource,
UpdateType: updateType,
}, nil
} else {
return &MapAttributeChange{
Expand Down
8 changes: 8 additions & 0 deletions map_attribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ func TestNewMapAttributeChangeFromLine(t *testing.T) {
UpdateType: UpdateInPlaceResource,
},
},
"attribute changed forces replacement": {
line: `~ triggers = {` + ForcesReplacementComment,
shouldError: false,
expected: &MapAttributeChange{
Name: "triggers",
UpdateType: ForceReplaceResource,
},
},
"attribute is unchanged": {
line: `attribute {`,
shouldError: false,
Expand Down
Loading