package terraform import ( "errors" "strings" "testing" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcltest" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" ) func TestNodeValidatableResource_ValidateProvisioner_valid(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() mp := &MockProvisioner{} ps := &configschema.Block{} ctx.ProvisionerSchemaSchema = ps ctx.ProvisionerProvisioner = mp pc := &configs.Provisioner{ Type: "baz", Config: hcl.EmptyBody(), Connection: &configs.Connection{ Config: configs.SynthBody("", map[string]cty.Value{ "host": cty.StringVal("localhost"), "type": cty.StringVal("ssh"), "port": cty.NumberIntVal(10022), }), }, } rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_foo", Name: "bar", Config: configs.SynthBody("", map[string]cty.Value{}), } node := NodeValidatableResource{ NodeAbstractResource: &NodeAbstractResource{ Addr: mustConfigResourceAddr("test_foo.bar"), Config: rc, }, } diags := node.validateProvisioner(ctx, pc) if diags.HasErrors() { t.Fatalf("node.Eval failed: %s", diags.Err()) } if !mp.ValidateProvisionerConfigCalled { t.Fatalf("p.ValidateProvisionerConfig not called") } } func TestNodeValidatableResource_ValidateProvisioner__warning(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() mp := &MockProvisioner{} ps := &configschema.Block{} ctx.ProvisionerSchemaSchema = ps ctx.ProvisionerProvisioner = mp pc := &configs.Provisioner{ Type: "baz", Config: hcl.EmptyBody(), } rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_foo", Name: "bar", Config: configs.SynthBody("", map[string]cty.Value{}), Managed: &configs.ManagedResource{}, } node := NodeValidatableResource{ NodeAbstractResource: &NodeAbstractResource{ Addr: mustConfigResourceAddr("test_foo.bar"), Config: rc, }, } { var diags tfdiags.Diagnostics diags = diags.Append(tfdiags.SimpleWarning("foo is deprecated")) mp.ValidateProvisionerConfigResponse = provisioners.ValidateProvisionerConfigResponse{ Diagnostics: diags, } } diags := node.validateProvisioner(ctx, pc) if len(diags) != 1 { t.Fatalf("wrong number of diagnostics in %s; want one warning", diags.ErrWithWarnings()) } if got, want := diags[0].Description().Summary, mp.ValidateProvisionerConfigResponse.Diagnostics[0].Description().Summary; got != want { t.Fatalf("wrong warning %q; want %q", got, want) } } func TestNodeValidatableResource_ValidateProvisioner__connectionInvalid(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() mp := &MockProvisioner{} ps := &configschema.Block{} ctx.ProvisionerSchemaSchema = ps ctx.ProvisionerProvisioner = mp pc := &configs.Provisioner{ Type: "baz", Config: hcl.EmptyBody(), Connection: &configs.Connection{ Config: configs.SynthBody("", map[string]cty.Value{ "type": cty.StringVal("ssh"), "bananananananana": cty.StringVal("foo"), "bazaz": cty.StringVal("bar"), }), }, } rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_foo", Name: "bar", Config: configs.SynthBody("", map[string]cty.Value{}), Managed: &configs.ManagedResource{}, } node := NodeValidatableResource{ NodeAbstractResource: &NodeAbstractResource{ Addr: mustConfigResourceAddr("test_foo.bar"), Config: rc, }, } diags := node.validateProvisioner(ctx, pc) if !diags.HasErrors() { t.Fatalf("node.Eval succeeded; want error") } if len(diags) != 3 { t.Fatalf("wrong number of diagnostics; want two errors\n\n%s", diags.Err()) } errStr := diags.Err().Error() if !(strings.Contains(errStr, "bananananananana") && strings.Contains(errStr, "bazaz")) { t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr) } } func TestNodeValidatableResource_ValidateResource_managedResource(t *testing.T) { mp := simpleMockProvider() mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { if got, want := req.TypeName, "test_object"; got != want { t.Fatalf("wrong resource type\ngot: %#v\nwant: %#v", got, want) } if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) { t.Fatalf("wrong value for test_string\ngot: %#v\nwant: %#v", got, want) } if got, want := req.Config.GetAttr("test_number"), cty.NumberIntVal(2); !got.RawEquals(want) { t.Fatalf("wrong value for test_number\ngot: %#v\nwant: %#v", got, want) } return providers.ValidateResourceConfigResponse{} } p := providers.Interface(mp) rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_object", Name: "foo", Config: configs.SynthBody("", map[string]cty.Value{ "test_string": cty.StringVal("bar"), "test_number": cty.NumberIntVal(2).Mark(marks.Sensitive), }), } 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 err := node.validateResource(ctx) if err != nil { t.Fatalf("err: %s", err) } if !mp.ValidateResourceConfigCalled { t.Fatal("Expected ValidateResourceConfig to be called, but it was not!") } } func TestNodeValidatableResource_ValidateResource_managedResourceCount(t *testing.T) { // Setup mp := simpleMockProvider() mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { if got, want := req.TypeName, "test_object"; got != want { t.Fatalf("wrong resource type\ngot: %#v\nwant: %#v", got, want) } if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) { t.Fatalf("wrong value for test_string\ngot: %#v\nwant: %#v", got, want) } return providers.ValidateResourceConfigResponse{} } p := providers.Interface(mp) ctx := &MockEvalContext{} ctx.installSimpleEval() ctx.ProviderSchemaSchema = mp.ProviderSchema() ctx.ProviderProvider = p tests := []struct { name string count hcl.Expression }{ { "simple count", hcltest.MockExprLiteral(cty.NumberIntVal(2)), }, { "marked count value", hcltest.MockExprLiteral(cty.NumberIntVal(3).Mark("marked")), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_object", Name: "foo", Count: test.count, Config: configs.SynthBody("", map[string]cty.Value{ "test_string": cty.StringVal("bar"), }), } node := NodeValidatableResource{ NodeAbstractResource: &NodeAbstractResource{ Addr: mustConfigResourceAddr("test_foo.bar"), Config: rc, ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), }, } diags := node.validateResource(ctx) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } if !mp.ValidateResourceConfigCalled { t.Fatal("Expected ValidateResourceConfig to be called, but it was not!") } }) } } func TestNodeValidatableResource_ValidateResource_dataSource(t *testing.T) { mp := simpleMockProvider() mp.ValidateDataResourceConfigFn = func(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { if got, want := req.TypeName, "test_object"; got != want { t.Fatalf("wrong resource type\ngot: %#v\nwant: %#v", got, want) } if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) { t.Fatalf("wrong value for test_string\ngot: %#v\nwant: %#v", got, want) } if got, want := req.Config.GetAttr("test_number"), cty.NumberIntVal(2); !got.RawEquals(want) { t.Fatalf("wrong value for test_number\ngot: %#v\nwant: %#v", got, want) } return providers.ValidateDataResourceConfigResponse{} } p := providers.Interface(mp) rc := &configs.Resource{ Mode: addrs.DataResourceMode, Type: "test_object", Name: "foo", Config: configs.SynthBody("", map[string]cty.Value{ "test_string": cty.StringVal("bar"), "test_number": cty.NumberIntVal(2).Mark(marks.Sensitive), }), } 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("err: %s", diags.Err()) } if !mp.ValidateDataResourceConfigCalled { t.Fatal("Expected ValidateDataSourceConfig to be called, but it was not!") } } func TestNodeValidatableResource_ValidateResource_valid(t *testing.T) { mp := simpleMockProvider() mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { return providers.ValidateResourceConfigResponse{} } p := providers.Interface(mp) rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_object", Name: "foo", Config: configs.SynthBody("", map[string]cty.Value{}), } node := NodeValidatableResource{ NodeAbstractResource: &NodeAbstractResource{ Addr: mustConfigResourceAddr("test_object.foo"), 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("err: %s", diags.Err()) } } func TestNodeValidatableResource_ValidateResource_warningsAndErrorsPassedThrough(t *testing.T) { mp := simpleMockProvider() mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { var diags tfdiags.Diagnostics diags = diags.Append(tfdiags.SimpleWarning("warn")) diags = diags.Append(errors.New("err")) return providers.ValidateResourceConfigResponse{ Diagnostics: diags, } } p := providers.Interface(mp) rc := &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_object", Name: "foo", Config: configs.SynthBody("", map[string]cty.Value{}), } 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.Fatal("unexpected success; want error") } bySeverity := map[tfdiags.Severity]tfdiags.Diagnostics{} for _, diag := range diags { bySeverity[diag.Severity()] = append(bySeverity[diag.Severity()], diag) } if len(bySeverity[tfdiags.Warning]) != 1 || bySeverity[tfdiags.Warning][0].Description().Summary != "warn" { t.Errorf("Expected 1 warning 'warn', got: %s", bySeverity[tfdiags.Warning].ErrWithWarnings()) } if len(bySeverity[tfdiags.Error]) != 1 || bySeverity[tfdiags.Error][0].Description().Summary != "err" { t.Errorf("Expected 1 error 'err', got: %s", bySeverity[tfdiags.Error].Err()) } } func TestNodeValidatableResource_ValidateResource_invalidDependsOn(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{}), DependsOn: []hcl.Traversal{ // Depending on path.module is pointless, since it is immediately // available, but we allow all of the referencable addrs here // for consistency: referencing them is harmless, and avoids the // need for us to document a different subset of addresses that // are valid in depends_on. // For the sake of this test, it's a valid address we can use that // doesn't require something else to exist in the configuration. { hcl.TraverseRoot{ Name: "path", }, hcl.TraverseAttr{ Name: "module", }, }, }, } 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 adding additional traversal steps at // the end of what we're referencing. This is intended to catch the // situation where the user tries to depend on e.g. a specific resource // attribute, rather than the whole resource, like aws_instance.foo.id. rc.DependsOn = append(rc.DependsOn, hcl.Traversal{ hcl.TraverseRoot{ Name: "path", }, hcl.TraverseAttr{ Name: "module", }, hcl.TraverseAttr{ Name: "extra", }, }) diags = node.validateResource(ctx) if !diags.HasErrors() { t.Fatal("no error for invalid depends_on") } if got, want := diags.Err().Error(), "Invalid depends_on reference"; !strings.Contains(got, want) { t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) } // Test for handling an unknown root without attribute, like a // typo that omits the dot inbetween "path.module". rc.DependsOn = append(rc.DependsOn, hcl.Traversal{ hcl.TraverseRoot{ Name: "pathmodule", }, }) diags = node.validateResource(ctx) if !diags.HasErrors() { t.Fatal("no error for invalid depends_on") } if got, want := diags.Err().Error(), "Invalid depends_on reference"; !strings.Contains(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) } }