Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn when ignore_changes includes a Computed attribute #30517

Merged
merged 5 commits into from Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 24 additions & 19 deletions internal/terraform/node_resource_abstract_instance.go
Expand Up @@ -1146,30 +1146,35 @@ func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value) (ct
func traversalsToPaths(traversals []hcl.Traversal) []cty.Path {
paths := make([]cty.Path, len(traversals))
for i, traversal := range traversals {
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))
}
}
path := traversalToPath(traversal)
paths[i] = path
}
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) {
type ignoreChange struct {
// Path is the full path, minus any trailing map index
Expand Down
29 changes: 25 additions & 4 deletions internal/terraform/node_resource_validate.go
Expand Up @@ -2,6 +2,7 @@ package terraform

import (
"fmt"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
Expand Down Expand Up @@ -383,10 +384,30 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
moreDiags := schema.StaticValidateTraversal(traversal)
diags = diags.Append(moreDiags)

// TODO: we want to notify users that they can't use
// ignore_changes for computed attributes, but we don't have an
// easy way to correlate the config value, schema and
// traversal together.
// ignore_changes cannot be used for Computed attributes,
// unless they are also Optional.
// If the traversal was valid, convert it to a cty.Path and
// 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,
})
}
}
}
}

Expand Down
145 changes: 145 additions & 0 deletions internal/terraform/node_resource_validate_test.go
Expand Up @@ -488,3 +488,148 @@ func TestNodeValidatableResource_ValidateResource_invalidDependsOn(t *testing.T)
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)
}
}