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 3 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
25 changes: 21 additions & 4 deletions internal/terraform/node_resource_validate.go
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/didyoumean"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
Expand Down Expand Up @@ -383,10 +384,26 @@ 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 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Invalid ignore_changes",
Detail: fmt.Sprintf("ignore_changes cannot be used with non-Optional Computed attributes. Please remove \"%s\" from ignore_changes.", hcl2shim.FlatmapKeyFromPath(path)),
kmoe marked this conversation as resolved.
Show resolved Hide resolved

Subject: &n.Config.TypeRange,
})
}
}
}
}

Expand Down
143 changes: 143 additions & 0 deletions internal/terraform/node_resource_validate_test.go
Expand Up @@ -488,3 +488,146 @@ 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(), "Invalid ignore_changes: ignore_changes cannot be used with non-Optional Computed attributes. Please remove \"computed_string\" from ignore_changes."; !strings.Contains(got, want) {
t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want)
}
}