configs: support ignore_changes wildcards

The initial pass of implementation here missed the special case where
ignore_changes can, in the old parser, be set to ["*"] to ignore changes
to all attributes.

Since that syntax is awkward and non-obvious, our new decoder will instead
expect ignore_changes = all, using HCL2's capability to interpret an
expression as a literal keyword. For compatibility with old configurations
we will still accept the ["*"] form but emit a deprecation warning to
encourage moving to the new form.
This commit is contained in:
Martin Atkins 2018-02-15 11:25:06 -08:00
parent c5f5340b15
commit 4fa8c16ead
8 changed files with 122 additions and 9 deletions

View File

@ -3,6 +3,7 @@ package configs
import ( import (
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
) )
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -79,3 +80,20 @@ func shimTraversalInString(expr hcl.Expression, wantKeyword bool) (hcl.Expressio
SrcRange: srcRange, SrcRange: srcRange,
}, diags }, diags
} }
// shimIsIgnoreChangesStar returns true if the given expression seems to be
// a string literal whose value is "*". This is used to support a legacy
// form of ignore_changes = all .
//
// This function does not itself emit any diagnostics, so it's the caller's
// responsibility to emit a warning diagnostic when this function returns true.
func shimIsIgnoreChangesStar(expr hcl.Expression) bool {
val, valDiags := expr.Value(nil)
if valDiags.HasErrors() {
return false
}
if val.Type() != cty.String || val.IsNull() || !val.IsKnown() {
return false
}
return val.AsString() == "*"
}

View File

@ -129,6 +129,16 @@ func TestParserLoadConfigFileFailureMessages(t *testing.T) {
hcl.DiagWarning, hcl.DiagWarning,
"Quoted references are deprecated", "Quoted references are deprecated",
}, },
{
"valid-files/resources-ignorechanges-all-legacy.tf",
hcl.DiagWarning,
"Deprecated ignore_changes wildcard",
},
{
"valid-files/resources-ignorechanges-all-legacy.tf.json",
hcl.DiagWarning,
"Deprecated ignore_changes wildcard",
},
{ {
"valid-files/resources-provisioner-when-quoted.tf", "valid-files/resources-provisioner-when-quoted.tf",
hcl.DiagWarning, hcl.DiagWarning,

View File

@ -26,6 +26,7 @@ type ManagedResource struct {
CreateBeforeDestroy bool CreateBeforeDestroy bool
PreventDestroy bool PreventDestroy bool
IgnoreChanges []hcl.Traversal IgnoreChanges []hcl.Traversal
IgnoreAllChanges bool
CreateBeforeDestroySet bool CreateBeforeDestroySet bool
PreventDestroySet bool PreventDestroySet bool
@ -118,19 +119,66 @@ func decodeResourceBlock(block *hcl.Block) (*ManagedResource, hcl.Diagnostics) {
} }
if attr, exists := lcContent.Attributes["ignore_changes"]; exists { if attr, exists := lcContent.Attributes["ignore_changes"]; exists {
exprs, listDiags := hcl.ExprList(attr.Expr)
diags = append(diags, listDiags...)
for _, expr := range exprs { // ignore_changes can either be a list of relative traversals
expr, shimDiags := shimTraversalInString(expr, false) // or it can be just the keyword "all" to ignore changes to this
diags = append(diags, shimDiags...) // resource entirely.
// ignore_changes = [ami, instance_type]
// ignore_changes = all
// We also allow two legacy forms for compatibility with earlier
// versions:
// ignore_changes = ["ami", "instance_type"]
// ignore_changes = ["*"]
traversal, travDiags := hcl.RelTraversalForExpr(expr) kw := hcl.ExprAsKeyword(attr.Expr)
diags = append(diags, travDiags...)
if len(traversal) != 0 { switch {
r.IgnoreChanges = append(r.IgnoreChanges, traversal) case kw == "all":
r.IgnoreAllChanges = true
default:
exprs, listDiags := hcl.ExprList(attr.Expr)
diags = append(diags, listDiags...)
var ignoreAllRange hcl.Range
for _, expr := range exprs {
// our expr might be the literal string "*", which
// we accept as a deprecated way of saying "all".
if shimIsIgnoreChangesStar(expr) {
r.IgnoreAllChanges = true
ignoreAllRange = expr.Range()
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecated ignore_changes wildcard",
Detail: "The [\"*\"] form of ignore_changes wildcard is reprecated. Use \"ignore_changes = all\" to ignore changes to all attributes.",
Subject: attr.Expr.Range().Ptr(),
})
continue
}
expr, shimDiags := shimTraversalInString(expr, false)
diags = append(diags, shimDiags...)
traversal, travDiags := hcl.RelTraversalForExpr(expr)
diags = append(diags, travDiags...)
if len(traversal) != 0 {
r.IgnoreChanges = append(r.IgnoreChanges, traversal)
}
} }
if r.IgnoreAllChanges && len(r.IgnoreChanges) != 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid ignore_changes ruleset",
Detail: "Cannot mix wildcard string \"*\" with non-wildcard references.",
Subject: &ignoreAllRange,
Context: attr.Expr.Range().Ptr(),
})
}
} }
} }
case "connection": case "connection":

View File

@ -0,0 +1,5 @@
resource "aws_instance" "web" {
lifecycle {
ignore_changes = ["*", "foo"]
}
}

View File

@ -0,0 +1,5 @@
resource "aws_instance" "web" {
lifecycle {
ignore_changes = ["*"]
}
}

View File

@ -0,0 +1,11 @@
{
"resource": {
"aws_instance": {
"web": {
"lifecycle": {
"ignore_changes": ["*"]
}
}
}
}
}

View File

@ -0,0 +1,5 @@
resource "aws_instance" "web" {
lifecycle {
ignore_changes = all
}
}

View File

@ -0,0 +1,11 @@
{
"resource": {
"aws_instance": {
"web": {
"lifecycle": {
"ignore_changes": "all"
}
}
}
}
}