terraform: refactor EvalValidateResource
EvalValidateResource is now a method on NodeValidatableResource, and the functions called by (the new) validateResource are now standalone functions. This particular refactor gets the prize for "most complicated test refactoring".
This commit is contained in:
parent
fbe3219fbe
commit
d50dc9cf16
|
@ -1,301 +0,0 @@
|
||||||
package terraform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
|
||||||
"github.com/hashicorp/terraform/configs"
|
|
||||||
"github.com/hashicorp/terraform/providers"
|
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
"github.com/zclconf/go-cty/cty/gocty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EvalValidateProvisioner validates the configuration of a provisioner
|
|
||||||
// belonging to a resource. The provisioner config is expected to contain the
|
|
||||||
// merged connection configurations.
|
|
||||||
|
|
||||||
// EvalValidateResource validates the configuration of a resource.
|
|
||||||
type EvalValidateResource struct {
|
|
||||||
Addr addrs.Resource
|
|
||||||
Provider *providers.Interface
|
|
||||||
ProviderSchema **ProviderSchema
|
|
||||||
Config *configs.Resource
|
|
||||||
ProviderMetas map[addrs.Provider]*configs.ProviderMeta
|
|
||||||
|
|
||||||
// ConfigVal, if non-nil, will be updated with the value resulting from
|
|
||||||
// evaluating the given configuration body. Since validation is performed
|
|
||||||
// very early, this value is likely to contain lots of unknown values,
|
|
||||||
// but its type will conform to the schema of the resource type associated
|
|
||||||
// with the resource instance being validated.
|
|
||||||
ConfigVal *cty.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *EvalValidateResource) Validate(ctx EvalContext) tfdiags.Diagnostics {
|
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
|
|
||||||
if *n.ProviderSchema == nil {
|
|
||||||
diags = diags.Append(fmt.Errorf("EvalValidateResource has nil schema for %s", n.Addr))
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := *n.Provider
|
|
||||||
cfg := *n.Config
|
|
||||||
schema := *n.ProviderSchema
|
|
||||||
mode := cfg.Mode
|
|
||||||
|
|
||||||
keyData := EvalDataForNoInstanceKey
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case n.Config.Count != nil:
|
|
||||||
// If the config block has count, we'll evaluate with an unknown
|
|
||||||
// number as count.index so we can still type check even though
|
|
||||||
// we won't expand count until the plan phase.
|
|
||||||
keyData = InstanceKeyEvalData{
|
|
||||||
CountIndex: cty.UnknownVal(cty.Number),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic type-checking of the count argument. More complete validation
|
|
||||||
// of this will happen when we DynamicExpand during the plan walk.
|
|
||||||
countDiags := n.validateCount(ctx, n.Config.Count)
|
|
||||||
diags = diags.Append(countDiags)
|
|
||||||
|
|
||||||
case n.Config.ForEach != nil:
|
|
||||||
keyData = InstanceKeyEvalData{
|
|
||||||
EachKey: cty.UnknownVal(cty.String),
|
|
||||||
EachValue: cty.UnknownVal(cty.DynamicPseudoType),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate the for_each expression here so we can expose the diagnostics
|
|
||||||
forEachDiags := n.validateForEach(ctx, n.Config.ForEach)
|
|
||||||
diags = diags.Append(forEachDiags)
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
|
|
||||||
|
|
||||||
// Validate the provider_meta block for the provider this resource
|
|
||||||
// belongs to, if there is one.
|
|
||||||
//
|
|
||||||
// Note: this will return an error for every resource a provider
|
|
||||||
// uses in a module, if the provider_meta for that module is
|
|
||||||
// incorrect. The only way to solve this that we've foudn is to
|
|
||||||
// insert a new ProviderMeta graph node in the graph, and make all
|
|
||||||
// that provider's resources in the module depend on the node. That's
|
|
||||||
// an awful heavy hammer to swing for this feature, which should be
|
|
||||||
// used only in limited cases with heavy coordination with the
|
|
||||||
// Terraform team, so we're going to defer that solution for a future
|
|
||||||
// enhancement to this functionality.
|
|
||||||
/*
|
|
||||||
if n.ProviderMetas != nil {
|
|
||||||
if m, ok := n.ProviderMetas[n.ProviderAddr.ProviderConfig.Type]; ok && m != nil {
|
|
||||||
// if the provider doesn't support this feature, throw an error
|
|
||||||
if (*n.ProviderSchema).ProviderMeta == nil {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", cfg.ProviderConfigAddr()),
|
|
||||||
Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", n.Addr),
|
|
||||||
Subject: &m.ProviderRange,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_, _, metaDiags := ctx.EvaluateBlock(m.Config, (*n.ProviderSchema).ProviderMeta, nil, EvalDataForNoInstanceKey)
|
|
||||||
diags = diags.Append(metaDiags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// BUG(paddy): we're not validating provider_meta blocks on EvalValidate right now
|
|
||||||
// because the ProviderAddr for the resource isn't available on the EvalValidate
|
|
||||||
// struct.
|
|
||||||
|
|
||||||
// Provider entry point varies depending on resource mode, because
|
|
||||||
// managed resources and data resources are two distinct concepts
|
|
||||||
// in the provider abstraction.
|
|
||||||
switch mode {
|
|
||||||
case addrs.ManagedResourceMode:
|
|
||||||
schema, _ := schema.SchemaForResourceType(mode, cfg.Type)
|
|
||||||
if schema == nil {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid resource type",
|
|
||||||
Detail: fmt.Sprintf("The provider %s does not support resource type %q.", cfg.ProviderConfigAddr(), cfg.Type),
|
|
||||||
Subject: &cfg.TypeRange,
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData)
|
|
||||||
diags = diags.Append(valDiags)
|
|
||||||
if valDiags.HasErrors() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Managed != nil { // can be nil only in tests with poorly-configured mocks
|
|
||||||
for _, traversal := range cfg.Managed.IgnoreChanges {
|
|
||||||
// validate the ignore_changes traversals apply.
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use unmarked value for validate request
|
|
||||||
unmarkedConfigVal, _ := configVal.UnmarkDeep()
|
|
||||||
req := providers.ValidateResourceTypeConfigRequest{
|
|
||||||
TypeName: cfg.Type,
|
|
||||||
Config: unmarkedConfigVal,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := provider.ValidateResourceTypeConfig(req)
|
|
||||||
diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config))
|
|
||||||
|
|
||||||
if n.ConfigVal != nil {
|
|
||||||
*n.ConfigVal = configVal
|
|
||||||
}
|
|
||||||
|
|
||||||
case addrs.DataResourceMode:
|
|
||||||
schema, _ := schema.SchemaForResourceType(mode, cfg.Type)
|
|
||||||
if schema == nil {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid data source",
|
|
||||||
Detail: fmt.Sprintf("The provider %s does not support data source %q.", cfg.ProviderConfigAddr(), cfg.Type),
|
|
||||||
Subject: &cfg.TypeRange,
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
configVal, _, valDiags := ctx.EvaluateBlock(cfg.Config, schema, nil, keyData)
|
|
||||||
diags = diags.Append(valDiags)
|
|
||||||
if valDiags.HasErrors() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use unmarked value for validate request
|
|
||||||
unmarkedConfigVal, _ := configVal.UnmarkDeep()
|
|
||||||
req := providers.ValidateDataSourceConfigRequest{
|
|
||||||
TypeName: cfg.Type,
|
|
||||||
Config: unmarkedConfigVal,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := provider.ValidateDataSourceConfig(req)
|
|
||||||
diags = diags.Append(resp.Diagnostics.InConfigBody(cfg.Config))
|
|
||||||
}
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *EvalValidateResource) validateCount(ctx EvalContext, expr hcl.Expression) tfdiags.Diagnostics {
|
|
||||||
if expr == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
|
|
||||||
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
|
|
||||||
diags = diags.Append(countDiags)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
if countVal.IsNull() {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid count argument",
|
|
||||||
Detail: `The given "count" argument value is null. An integer is required.`,
|
|
||||||
Subject: expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
countVal, err = convert.Convert(countVal, cty.Number)
|
|
||||||
if err != nil {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid count argument",
|
|
||||||
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
|
|
||||||
Subject: expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the value isn't known then that's the best we can do for now, but
|
|
||||||
// we'll check more thoroughly during the plan walk.
|
|
||||||
if !countVal.IsKnown() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we _do_ know the value, then we can do a few more checks here.
|
|
||||||
var count int
|
|
||||||
err = gocty.FromCtyValue(countVal, &count)
|
|
||||||
if err != nil {
|
|
||||||
// Isn't a whole number, etc.
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid count argument",
|
|
||||||
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
|
|
||||||
Subject: expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
if count < 0 {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid count argument",
|
|
||||||
Detail: `The given "count" argument value is unsuitable: count cannot be negative.`,
|
|
||||||
Subject: expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *EvalValidateResource) validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
|
||||||
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true)
|
|
||||||
// If the value isn't known then that's the best we can do for now, but
|
|
||||||
// we'll check more thoroughly during the plan walk
|
|
||||||
if !val.IsKnown() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
if forEachDiags.HasErrors() {
|
|
||||||
diags = diags.Append(forEachDiags)
|
|
||||||
}
|
|
||||||
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiags.Diagnostics) {
|
|
||||||
for _, traversal := range dependsOn {
|
|
||||||
ref, refDiags := addrs.ParseRef(traversal)
|
|
||||||
diags = diags.Append(refDiags)
|
|
||||||
if !refDiags.HasErrors() && len(ref.Remaining) != 0 {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid depends_on reference",
|
|
||||||
Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.",
|
|
||||||
Subject: ref.Remaining.SourceRange().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// The ref must also refer to something that exists. To test that,
|
|
||||||
// we'll just eval it and count on the fact that our evaluator will
|
|
||||||
// detect references to non-existent objects.
|
|
||||||
if !diags.HasErrors() {
|
|
||||||
scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey)
|
|
||||||
if scope != nil { // sometimes nil in tests, due to incomplete mocks
|
|
||||||
_, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType)
|
|
||||||
diags = diags.Append(refDiags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diags
|
|
||||||
}
|
|
|
@ -1,337 +0,0 @@
|
||||||
package terraform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
|
||||||
"github.com/hashicorp/hcl/v2/hcltest"
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
|
||||||
"github.com/hashicorp/terraform/configs"
|
|
||||||
"github.com/hashicorp/terraform/providers"
|
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEvalValidateResource_managedResource(t *testing.T) {
|
|
||||||
mp := simpleMockProvider()
|
|
||||||
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
|
||||||
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.ValidateResourceTypeConfigResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("sensitive"),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
node := &EvalValidateResource{
|
|
||||||
Addr: addrs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "aws_instance",
|
|
||||||
Name: "foo",
|
|
||||||
},
|
|
||||||
Provider: &p,
|
|
||||||
Config: rc,
|
|
||||||
ProviderSchema: &mp.GetSchemaReturn,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &MockEvalContext{}
|
|
||||||
ctx.installSimpleEval()
|
|
||||||
|
|
||||||
err := node.Validate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mp.ValidateResourceTypeConfigCalled {
|
|
||||||
t.Fatal("Expected ValidateResourceTypeConfig to be called, but it was not!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalValidateResource_managedResourceCount(t *testing.T) {
|
|
||||||
mp := simpleMockProvider()
|
|
||||||
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
|
||||||
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.ValidateResourceTypeConfigResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := providers.Interface(mp)
|
|
||||||
rc := &configs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "test_object",
|
|
||||||
Name: "foo",
|
|
||||||
Count: hcltest.MockExprLiteral(cty.NumberIntVal(2)),
|
|
||||||
Config: configs.SynthBody("", map[string]cty.Value{
|
|
||||||
"test_string": cty.StringVal("bar"),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
node := &EvalValidateResource{
|
|
||||||
Addr: addrs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "aws_instance",
|
|
||||||
Name: "foo",
|
|
||||||
},
|
|
||||||
Provider: &p,
|
|
||||||
Config: rc,
|
|
||||||
ProviderSchema: &mp.GetSchemaReturn,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &MockEvalContext{}
|
|
||||||
ctx.installSimpleEval()
|
|
||||||
|
|
||||||
err := node.Validate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mp.ValidateResourceTypeConfigCalled {
|
|
||||||
t.Fatal("Expected ValidateResourceTypeConfig to be called, but it was not!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalValidateResource_dataSource(t *testing.T) {
|
|
||||||
mp := simpleMockProvider()
|
|
||||||
mp.ValidateDataSourceConfigFn = func(req providers.ValidateDataSourceConfigRequest) providers.ValidateDataSourceConfigResponse {
|
|
||||||
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.ValidateDataSourceConfigResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("sensitive"),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
node := &EvalValidateResource{
|
|
||||||
Addr: addrs.Resource{
|
|
||||||
Mode: addrs.DataResourceMode,
|
|
||||||
Type: "aws_ami",
|
|
||||||
Name: "foo",
|
|
||||||
},
|
|
||||||
Provider: &p,
|
|
||||||
Config: rc,
|
|
||||||
ProviderSchema: &mp.GetSchemaReturn,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &MockEvalContext{}
|
|
||||||
ctx.installSimpleEval()
|
|
||||||
|
|
||||||
err := node.Validate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mp.ValidateDataSourceConfigCalled {
|
|
||||||
t.Fatal("Expected ValidateDataSourceConfig to be called, but it was not!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalValidateResource_validReturnsNilError(t *testing.T) {
|
|
||||||
mp := simpleMockProvider()
|
|
||||||
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
|
||||||
return providers.ValidateResourceTypeConfigResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := providers.Interface(mp)
|
|
||||||
rc := &configs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "test_object",
|
|
||||||
Name: "foo",
|
|
||||||
Config: configs.SynthBody("", map[string]cty.Value{}),
|
|
||||||
}
|
|
||||||
node := &EvalValidateResource{
|
|
||||||
Addr: addrs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "test_object",
|
|
||||||
Name: "foo",
|
|
||||||
},
|
|
||||||
Provider: &p,
|
|
||||||
Config: rc,
|
|
||||||
ProviderSchema: &mp.GetSchemaReturn,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &MockEvalContext{}
|
|
||||||
ctx.installSimpleEval()
|
|
||||||
|
|
||||||
err := node.Validate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Expected nil error, got: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEvalValidateResource_warningsAndErrorsPassedThrough(t *testing.T) {
|
|
||||||
mp := simpleMockProvider()
|
|
||||||
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
diags = diags.Append(tfdiags.SimpleWarning("warn"))
|
|
||||||
diags = diags.Append(errors.New("err"))
|
|
||||||
return providers.ValidateResourceTypeConfigResponse{
|
|
||||||
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 := &EvalValidateResource{
|
|
||||||
Addr: addrs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "test_object",
|
|
||||||
Name: "foo",
|
|
||||||
},
|
|
||||||
Provider: &p,
|
|
||||||
Config: rc,
|
|
||||||
ProviderSchema: &mp.GetSchemaReturn,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &MockEvalContext{}
|
|
||||||
ctx.installSimpleEval()
|
|
||||||
|
|
||||||
err := node.Validate(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("unexpected success; want error")
|
|
||||||
}
|
|
||||||
|
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
diags = diags.Append(err)
|
|
||||||
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 TestEvalValidateResource_invalidDependsOn(t *testing.T) {
|
|
||||||
mp := simpleMockProvider()
|
|
||||||
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
|
||||||
return providers.ValidateResourceTypeConfigResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 := &EvalValidateResource{
|
|
||||||
Addr: addrs.Resource{
|
|
||||||
Mode: addrs.ManagedResourceMode,
|
|
||||||
Type: "aws_instance",
|
|
||||||
Name: "foo",
|
|
||||||
},
|
|
||||||
Provider: &p,
|
|
||||||
Config: rc,
|
|
||||||
ProviderSchema: &mp.GetSchemaReturn,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &MockEvalContext{}
|
|
||||||
ctx.installSimpleEval()
|
|
||||||
|
|
||||||
diags := node.Validate(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.Validate(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.Validate(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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,9 +7,12 @@ import (
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/hashicorp/terraform/configs/configschema"
|
"github.com/hashicorp/terraform/configs/configschema"
|
||||||
|
"github.com/hashicorp/terraform/providers"
|
||||||
"github.com/hashicorp/terraform/provisioners"
|
"github.com/hashicorp/terraform/provisioners"
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
|
"github.com/zclconf/go-cty/cty/gocty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NodeValidatableResource represents a resource that is used for validation
|
// NodeValidatableResource represents a resource that is used for validation
|
||||||
|
@ -36,31 +39,7 @@ func (n *NodeValidatableResource) Path() addrs.ModuleInstance {
|
||||||
|
|
||||||
// GraphNodeEvalable
|
// GraphNodeEvalable
|
||||||
func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
||||||
addr := n.ResourceAddr()
|
diags = diags.Append(n.validateResource(ctx))
|
||||||
config := n.Config
|
|
||||||
|
|
||||||
// Declare the variables will be used are used to pass values along
|
|
||||||
// the evaluation sequence below. These are written to via pointers
|
|
||||||
// passed to the EvalNodes.
|
|
||||||
var configVal cty.Value
|
|
||||||
provider, providerSchema, err := GetProvider(ctx, n.ResolvedProvider)
|
|
||||||
diags = diags.Append(err)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
evalValidateResource := &EvalValidateResource{
|
|
||||||
Addr: addr.Resource,
|
|
||||||
Provider: &provider,
|
|
||||||
ProviderMetas: n.ProviderMetas,
|
|
||||||
ProviderSchema: &providerSchema,
|
|
||||||
Config: config,
|
|
||||||
ConfigVal: &configVal,
|
|
||||||
}
|
|
||||||
diags = diags.Append(evalValidateResource.Validate(ctx))
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
|
|
||||||
if managed := n.Config.Managed; managed != nil {
|
if managed := n.Config.Managed; managed != nil {
|
||||||
hasCount := n.Config.Count != nil
|
hasCount := n.Config.Count != nil
|
||||||
|
@ -69,9 +48,9 @@ func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (di
|
||||||
// Validate all the provisioners
|
// Validate all the provisioners
|
||||||
for _, p := range managed.Provisioners {
|
for _, p := range managed.Provisioners {
|
||||||
if p.Connection == nil {
|
if p.Connection == nil {
|
||||||
p.Connection = config.Managed.Connection
|
p.Connection = n.Config.Managed.Connection
|
||||||
} else if config.Managed.Connection != nil {
|
} else if n.Config.Managed.Connection != nil {
|
||||||
p.Connection.Config = configs.MergeBodies(config.Managed.Connection.Config, p.Connection.Config)
|
p.Connection.Config = configs.MergeBodies(n.Config.Managed.Connection.Config, p.Connection.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Provisioner Config
|
// Validate Provisioner Config
|
||||||
|
@ -277,3 +256,266 @@ var connectionBlockSupersetSchema = &configschema.Block{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diagnostics {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
provider, providerSchema, err := GetProvider(ctx, n.ResolvedProvider)
|
||||||
|
diags = diags.Append(err)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
if providerSchema == nil {
|
||||||
|
diags = diags.Append(fmt.Errorf("validateResource has nil schema for %s", n.Addr))
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
keyData := EvalDataForNoInstanceKey
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case n.Config.Count != nil:
|
||||||
|
// If the config block has count, we'll evaluate with an unknown
|
||||||
|
// number as count.index so we can still type check even though
|
||||||
|
// we won't expand count until the plan phase.
|
||||||
|
keyData = InstanceKeyEvalData{
|
||||||
|
CountIndex: cty.UnknownVal(cty.Number),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic type-checking of the count argument. More complete validation
|
||||||
|
// of this will happen when we DynamicExpand during the plan walk.
|
||||||
|
countDiags := validateCount(ctx, n.Config.Count)
|
||||||
|
diags = diags.Append(countDiags)
|
||||||
|
|
||||||
|
case n.Config.ForEach != nil:
|
||||||
|
keyData = InstanceKeyEvalData{
|
||||||
|
EachKey: cty.UnknownVal(cty.String),
|
||||||
|
EachValue: cty.UnknownVal(cty.DynamicPseudoType),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the for_each expression here so we can expose the diagnostics
|
||||||
|
forEachDiags := validateForEach(ctx, n.Config.ForEach)
|
||||||
|
diags = diags.Append(forEachDiags)
|
||||||
|
}
|
||||||
|
|
||||||
|
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
|
||||||
|
|
||||||
|
// Validate the provider_meta block for the provider this resource
|
||||||
|
// belongs to, if there is one.
|
||||||
|
//
|
||||||
|
// Note: this will return an error for every resource a provider
|
||||||
|
// uses in a module, if the provider_meta for that module is
|
||||||
|
// incorrect. The only way to solve this that we've found is to
|
||||||
|
// insert a new ProviderMeta graph node in the graph, and make all
|
||||||
|
// that provider's resources in the module depend on the node. That's
|
||||||
|
// an awful heavy hammer to swing for this feature, which should be
|
||||||
|
// used only in limited cases with heavy coordination with the
|
||||||
|
// Terraform team, so we're going to defer that solution for a future
|
||||||
|
// enhancement to this functionality.
|
||||||
|
/*
|
||||||
|
if n.ProviderMetas != nil {
|
||||||
|
if m, ok := n.ProviderMetas[n.ProviderAddr.ProviderConfig.Type]; ok && m != nil {
|
||||||
|
// if the provider doesn't support this feature, throw an error
|
||||||
|
if (*n.ProviderSchema).ProviderMeta == nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", cfg.ProviderConfigAddr()),
|
||||||
|
Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", n.Addr),
|
||||||
|
Subject: &m.ProviderRange,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_, _, metaDiags := ctx.EvaluateBlock(m.Config, (*n.ProviderSchema).ProviderMeta, nil, EvalDataForNoInstanceKey)
|
||||||
|
diags = diags.Append(metaDiags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// BUG(paddy): we're not validating provider_meta blocks on EvalValidate right now
|
||||||
|
// because the ProviderAddr for the resource isn't available on the EvalValidate
|
||||||
|
// struct.
|
||||||
|
|
||||||
|
// Provider entry point varies depending on resource mode, because
|
||||||
|
// managed resources and data resources are two distinct concepts
|
||||||
|
// in the provider abstraction.
|
||||||
|
switch n.Config.Mode {
|
||||||
|
case addrs.ManagedResourceMode:
|
||||||
|
schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type)
|
||||||
|
if schema == nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid resource type",
|
||||||
|
Detail: fmt.Sprintf("The provider %s does not support resource type %q.", n.Config.ProviderConfigAddr(), n.Config.Type),
|
||||||
|
Subject: &n.Config.TypeRange,
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData)
|
||||||
|
diags = diags.Append(valDiags)
|
||||||
|
if valDiags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks
|
||||||
|
for _, traversal := range n.Config.Managed.IgnoreChanges {
|
||||||
|
// validate the ignore_changes traversals apply.
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use unmarked value for validate request
|
||||||
|
unmarkedConfigVal, _ := configVal.UnmarkDeep()
|
||||||
|
req := providers.ValidateResourceTypeConfigRequest{
|
||||||
|
TypeName: n.Config.Type,
|
||||||
|
Config: unmarkedConfigVal,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := provider.ValidateResourceTypeConfig(req)
|
||||||
|
diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
|
||||||
|
|
||||||
|
case addrs.DataResourceMode:
|
||||||
|
schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type)
|
||||||
|
if schema == nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid data source",
|
||||||
|
Detail: fmt.Sprintf("The provider %s does not support data source %q.", n.Config.ProviderConfigAddr(), n.Config.Type),
|
||||||
|
Subject: &n.Config.TypeRange,
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData)
|
||||||
|
diags = diags.Append(valDiags)
|
||||||
|
if valDiags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use unmarked value for validate request
|
||||||
|
unmarkedConfigVal, _ := configVal.UnmarkDeep()
|
||||||
|
req := providers.ValidateDataSourceConfigRequest{
|
||||||
|
TypeName: n.Config.Type,
|
||||||
|
Config: unmarkedConfigVal,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := provider.ValidateDataSourceConfig(req)
|
||||||
|
diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCount(ctx EvalContext, expr hcl.Expression) tfdiags.Diagnostics {
|
||||||
|
if expr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
|
||||||
|
diags = diags.Append(countDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if countVal.IsNull() {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid count argument",
|
||||||
|
Detail: `The given "count" argument value is null. An integer is required.`,
|
||||||
|
Subject: expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
countVal, err = convert.Convert(countVal, cty.Number)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid count argument",
|
||||||
|
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
|
||||||
|
Subject: expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the value isn't known then that's the best we can do for now, but
|
||||||
|
// we'll check more thoroughly during the plan walk.
|
||||||
|
if !countVal.IsKnown() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we _do_ know the value, then we can do a few more checks here.
|
||||||
|
var count int
|
||||||
|
err = gocty.FromCtyValue(countVal, &count)
|
||||||
|
if err != nil {
|
||||||
|
// Isn't a whole number, etc.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid count argument",
|
||||||
|
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
|
||||||
|
Subject: expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if count < 0 {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid count argument",
|
||||||
|
Detail: `The given "count" argument value is unsuitable: count cannot be negative.`,
|
||||||
|
Subject: expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
||||||
|
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true)
|
||||||
|
// If the value isn't known then that's the best we can do for now, but
|
||||||
|
// we'll check more thoroughly during the plan walk
|
||||||
|
if !val.IsKnown() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
if forEachDiags.HasErrors() {
|
||||||
|
diags = diags.Append(forEachDiags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiags.Diagnostics) {
|
||||||
|
for _, traversal := range dependsOn {
|
||||||
|
ref, refDiags := addrs.ParseRef(traversal)
|
||||||
|
diags = diags.Append(refDiags)
|
||||||
|
if !refDiags.HasErrors() && len(ref.Remaining) != 0 {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid depends_on reference",
|
||||||
|
Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.",
|
||||||
|
Subject: ref.Remaining.SourceRange().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ref must also refer to something that exists. To test that,
|
||||||
|
// we'll just eval it and count on the fact that our evaluator will
|
||||||
|
// detect references to non-existent objects.
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey)
|
||||||
|
if scope != nil { // sometimes nil in tests, due to incomplete mocks
|
||||||
|
_, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType)
|
||||||
|
diags = diags.Append(refDiags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hcltest"
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/hashicorp/terraform/configs/configschema"
|
"github.com/hashicorp/terraform/configs/configschema"
|
||||||
|
"github.com/hashicorp/terraform/providers"
|
||||||
"github.com/hashicorp/terraform/provisioners"
|
"github.com/hashicorp/terraform/provisioners"
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
@ -149,3 +152,316 @@ func TestNodeValidatableResource_ValidateProvisioner__conntectionInvalid(t *test
|
||||||
t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr)
|
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.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
||||||
|
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.ValidateResourceTypeConfigResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("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.GetSchemaReturn
|
||||||
|
ctx.ProviderProvider = p
|
||||||
|
|
||||||
|
err := node.validateResource(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mp.ValidateResourceTypeConfigCalled {
|
||||||
|
t.Fatal("Expected ValidateResourceTypeConfig to be called, but it was not!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeValidatableResource_ValidateResource_managedResourceCount(t *testing.T) {
|
||||||
|
mp := simpleMockProvider()
|
||||||
|
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
||||||
|
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.ValidateResourceTypeConfigResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := providers.Interface(mp)
|
||||||
|
rc := &configs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_object",
|
||||||
|
Name: "foo",
|
||||||
|
Count: hcltest.MockExprLiteral(cty.NumberIntVal(2)),
|
||||||
|
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"]`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := &MockEvalContext{}
|
||||||
|
ctx.installSimpleEval()
|
||||||
|
ctx.ProviderSchemaSchema = mp.GetSchemaReturn
|
||||||
|
ctx.ProviderProvider = p
|
||||||
|
|
||||||
|
diags := node.validateResource(ctx)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("err: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mp.ValidateResourceTypeConfigCalled {
|
||||||
|
t.Fatal("Expected ValidateResourceTypeConfig to be called, but it was not!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeValidatableResource_ValidateResource_dataSource(t *testing.T) {
|
||||||
|
mp := simpleMockProvider()
|
||||||
|
mp.ValidateDataSourceConfigFn = func(req providers.ValidateDataSourceConfigRequest) providers.ValidateDataSourceConfigResponse {
|
||||||
|
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.ValidateDataSourceConfigResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("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.GetSchemaReturn
|
||||||
|
ctx.ProviderProvider = p
|
||||||
|
|
||||||
|
diags := node.validateResource(ctx)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("err: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mp.ValidateDataSourceConfigCalled {
|
||||||
|
t.Fatal("Expected ValidateDataSourceConfig to be called, but it was not!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeValidatableResource_ValidateResource_valid(t *testing.T) {
|
||||||
|
mp := simpleMockProvider()
|
||||||
|
mp.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
||||||
|
return providers.ValidateResourceTypeConfigResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.GetSchemaReturn
|
||||||
|
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.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
diags = diags.Append(tfdiags.SimpleWarning("warn"))
|
||||||
|
diags = diags.Append(errors.New("err"))
|
||||||
|
return providers.ValidateResourceTypeConfigResponse{
|
||||||
|
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.GetSchemaReturn
|
||||||
|
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.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
|
||||||
|
return providers.ValidateResourceTypeConfigResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.GetSchemaReturn
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue