Warn when ignore_changes includes a Computed attribute (#30517)

* ignore_changes attributes must exist in schema

Add a test verifying that attempting to add a nonexistent attribute to
ignore_changes throws an error.

* ignore_changes cannot be used with Computed attrs

Return a warning if a Computed attribute is present in ignore_changes,
unless the attribute is also Optional.

ignore_changes on a non-Optional Computed attribute is a no-op, so the user
likely did not want to set this in config.
An Optional Computed attribute, however, is still subject to ignore_changes
behaviour, since it is possible to make changes in the configuration that
Terraform must ignore.
This commit is contained in:
kmoe 2022-02-18 10:38:29 +00:00 committed by GitHub
parent 68e70d71d4
commit 161374725c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 194 additions and 23 deletions

View File

@ -1146,30 +1146,35 @@ func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value) (ct
func traversalsToPaths(traversals []hcl.Traversal) []cty.Path { func traversalsToPaths(traversals []hcl.Traversal) []cty.Path {
paths := make([]cty.Path, len(traversals)) paths := make([]cty.Path, len(traversals))
for i, traversal := range traversals { for i, traversal := range traversals {
path := make(cty.Path, len(traversal)) path := traversalToPath(traversal)
for si, step := range traversal {
switch ts := step.(type) {
case hcl.TraverseRoot:
path[si] = cty.GetAttrStep{
Name: ts.Name,
}
case hcl.TraverseAttr:
path[si] = cty.GetAttrStep{
Name: ts.Name,
}
case hcl.TraverseIndex:
path[si] = cty.IndexStep{
Key: ts.Key,
}
default:
panic(fmt.Sprintf("unsupported traversal step %#v", step))
}
}
paths[i] = path paths[i] = path
} }
return paths return paths
} }
func traversalToPath(traversal hcl.Traversal) cty.Path {
path := make(cty.Path, len(traversal))
for si, step := range traversal {
switch ts := step.(type) {
case hcl.TraverseRoot:
path[si] = cty.GetAttrStep{
Name: ts.Name,
}
case hcl.TraverseAttr:
path[si] = cty.GetAttrStep{
Name: ts.Name,
}
case hcl.TraverseIndex:
path[si] = cty.IndexStep{
Key: ts.Key,
}
default:
panic(fmt.Sprintf("unsupported traversal step %#v", step))
}
}
return path
}
func processIgnoreChangesIndividual(prior, config cty.Value, ignoreChangesPath []cty.Path) (cty.Value, tfdiags.Diagnostics) { func processIgnoreChangesIndividual(prior, config cty.Value, ignoreChangesPath []cty.Path) (cty.Value, tfdiags.Diagnostics) {
type ignoreChange struct { type ignoreChange struct {
// Path is the full path, minus any trailing map index // Path is the full path, minus any trailing map index

View File

@ -2,6 +2,7 @@ package terraform
import ( import (
"fmt" "fmt"
"strings"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/addrs"
@ -383,10 +384,30 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
moreDiags := schema.StaticValidateTraversal(traversal) moreDiags := schema.StaticValidateTraversal(traversal)
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)
// TODO: we want to notify users that they can't use // ignore_changes cannot be used for Computed attributes,
// ignore_changes for computed attributes, but we don't have an // unless they are also Optional.
// easy way to correlate the config value, schema and // If the traversal was valid, convert it to a cty.Path and
// traversal together. // use that to check whether the Attribute is Computed and
// non-Optional.
if !diags.HasErrors() {
path := traversalToPath(traversal)
attrSchema := schema.AttributeByPath(path)
if attrSchema != nil && !attrSchema.Optional && attrSchema.Computed {
// ignore_changes uses absolute traversal syntax in config despite
// using relative traversals, so we strip the leading "." added by
// FormatCtyPath for a better error message.
attrDisplayPath := strings.TrimPrefix(tfdiags.FormatCtyPath(path), ".")
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Redundant ignore_changes element",
Detail: fmt.Sprintf("Adding an attribute name to ignore_changes tells Terraform to ignore future changes to the argument in configuration after the object has been created, retaining the value originally configured.\n\nThe attribute %s is decided by the provider alone and therefore there can be no configured value to compare with. Including this attribute in ignore_changes has no effect. Remove the attribute from ignore_changes to quiet this warning.", attrDisplayPath),
Subject: &n.Config.TypeRange,
})
}
}
} }
} }

View File

@ -488,3 +488,148 @@ func TestNodeValidatableResource_ValidateResource_invalidDependsOn(t *testing.T)
t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want)
} }
} }
func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesNonexistent(t *testing.T) {
mp := simpleMockProvider()
mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
return providers.ValidateResourceConfigResponse{}
}
// We'll check a _valid_ config first, to make sure we're not failing
// for some other reason, and then make it invalid.
p := providers.Interface(mp)
rc := &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_object",
Name: "foo",
Config: configs.SynthBody("", map[string]cty.Value{}),
Managed: &configs.ManagedResource{
IgnoreChanges: []hcl.Traversal{
{
hcl.TraverseAttr{
Name: "test_string",
},
},
},
},
}
node := NodeValidatableResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: mustConfigResourceAddr("test_foo.bar"),
Config: rc,
ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
},
}
ctx := &MockEvalContext{}
ctx.installSimpleEval()
ctx.ProviderSchemaSchema = mp.ProviderSchema()
ctx.ProviderProvider = p
diags := node.validateResource(ctx)
if diags.HasErrors() {
t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings())
}
// Now we'll make it invalid by attempting to ignore a nonexistent
// attribute.
rc.Managed.IgnoreChanges = append(rc.Managed.IgnoreChanges, hcl.Traversal{
hcl.TraverseAttr{
Name: "nonexistent",
},
})
diags = node.validateResource(ctx)
if !diags.HasErrors() {
t.Fatal("no error for invalid ignore_changes")
}
if got, want := diags.Err().Error(), "Unsupported attribute: This object has no argument, nested block, or exported attribute named \"nonexistent\""; !strings.Contains(got, want) {
t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want)
}
}
func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesComputed(t *testing.T) {
// construct a schema with a computed attribute
ms := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"test_string": {
Type: cty.String,
Optional: true,
},
"computed_string": {
Type: cty.String,
Computed: true,
Optional: false,
},
},
}
mp := &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Provider: providers.Schema{Block: ms},
ResourceTypes: map[string]providers.Schema{
"test_object": providers.Schema{Block: ms},
},
},
}
mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
return providers.ValidateResourceConfigResponse{}
}
// We'll check a _valid_ config first, to make sure we're not failing
// for some other reason, and then make it invalid.
p := providers.Interface(mp)
rc := &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_object",
Name: "foo",
Config: configs.SynthBody("", map[string]cty.Value{}),
Managed: &configs.ManagedResource{
IgnoreChanges: []hcl.Traversal{
{
hcl.TraverseAttr{
Name: "test_string",
},
},
},
},
}
node := NodeValidatableResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: mustConfigResourceAddr("test_foo.bar"),
Config: rc,
ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
},
}
ctx := &MockEvalContext{}
ctx.installSimpleEval()
ctx.ProviderSchemaSchema = mp.ProviderSchema()
ctx.ProviderProvider = p
diags := node.validateResource(ctx)
if diags.HasErrors() {
t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings())
}
// Now we'll make it invalid by attempting to ignore a computed
// attribute.
rc.Managed.IgnoreChanges = append(rc.Managed.IgnoreChanges, hcl.Traversal{
hcl.TraverseAttr{
Name: "computed_string",
},
})
diags = node.validateResource(ctx)
if diags.HasErrors() {
t.Fatalf("got unexpected error: %s", diags.ErrWithWarnings())
}
if got, want := diags.ErrWithWarnings().Error(), `Redundant ignore_changes element: Adding an attribute name to ignore_changes tells Terraform to ignore future changes to the argument in configuration after the object has been created, retaining the value originally configured.
The attribute computed_string is decided by the provider alone and therefore there can be no configured value to compare with. Including this attribute in ignore_changes has no effect. Remove the attribute from ignore_changes to quiet this warning.`; !strings.Contains(got, want) {
t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want)
}
}