2018-11-20 19:25:05 -06:00
package terraform
import (
"fmt"
2018-11-28 13:25:44 -06:00
"sort"
2018-11-20 19:25:05 -06:00
2019-09-09 17:58:44 -05:00
"github.com/hashicorp/hcl/v2"
2018-11-20 19:25:05 -06:00
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
2018-11-28 13:25:44 -06:00
"github.com/hashicorp/terraform/helper/didyoumean"
2018-11-20 19:25:05 -06:00
"github.com/hashicorp/terraform/tfdiags"
)
// StaticValidateReferences checks the given references against schemas and
// other statically-checkable rules, producing error diagnostics if any
// problems are found.
//
// If this method returns errors for a particular reference then evaluating
// that reference is likely to generate a very similar error, so callers should
// not run this method and then also evaluate the source expression(s) and
// merge the two sets of diagnostics together, since this will result in
// confusing redundant errors.
//
// This method can find more errors than can be found by evaluating an
// expression with a partially-populated scope, since it checks the referenced
// names directly against the schema rather than relying on evaluation errors.
//
// The result may include warning diagnostics if, for example, deprecated
// features are referenced.
func ( d * evaluationStateData ) StaticValidateReferences ( refs [ ] * addrs . Reference , self addrs . Referenceable ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
for _ , ref := range refs {
moreDiags := d . staticValidateReference ( ref , self )
diags = diags . Append ( moreDiags )
}
return diags
}
func ( d * evaluationStateData ) staticValidateReference ( ref * addrs . Reference , self addrs . Referenceable ) tfdiags . Diagnostics {
modCfg := d . Evaluator . Config . DescendentForInstance ( d . ModulePath )
if modCfg == nil {
// This is a bug in the caller rather than a problem with the
// reference, but rather than crashing out here in an unhelpful way
// we'll just ignore it and trust a different layer to catch it.
return nil
}
if ref . Subject == addrs . Self {
// The "self" address is a special alias for the address given as
// our self parameter here, if present.
if self == nil {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Invalid "self" reference ` ,
// This detail message mentions some current practice that
// this codepath doesn't really "know about". If the "self"
// object starts being supported in more contexts later then
// we'll need to adjust this message.
Detail : ` The "self" object is not available in this context. This object can be used only in resource provisioner and connection blocks. ` ,
Subject : ref . SourceRange . ToHCL ( ) . Ptr ( ) ,
} )
return diags
}
synthRef := * ref // shallow copy
synthRef . Subject = self
ref = & synthRef
}
switch addr := ref . Subject . ( type ) {
2018-12-20 14:28:23 -06:00
// For static validation we validate both resource and resource instance references the same way.
// We mostly disregard the index, though we do some simple validation of
// its _presence_ in staticValidateSingleResourceReference and
// staticValidateMultiResourceReference respectively.
2018-11-20 19:25:05 -06:00
case addrs . Resource :
2018-12-20 14:28:23 -06:00
var diags tfdiags . Diagnostics
diags = diags . Append ( d . staticValidateSingleResourceReference ( modCfg , addr , ref . Remaining , ref . SourceRange ) )
diags = diags . Append ( d . staticValidateResourceReference ( modCfg , addr , ref . Remaining , ref . SourceRange ) )
return diags
2018-11-20 19:25:05 -06:00
case addrs . ResourceInstance :
2018-12-20 14:28:23 -06:00
var diags tfdiags . Diagnostics
diags = diags . Append ( d . staticValidateMultiResourceReference ( modCfg , addr , ref . Remaining , ref . SourceRange ) )
diags = diags . Append ( d . staticValidateResourceReference ( modCfg , addr . ContainingResource ( ) , ref . Remaining , ref . SourceRange ) )
return diags
2018-11-20 19:25:05 -06:00
2018-11-28 13:25:44 -06:00
// We also handle all module call references the same way, disregarding index.
case addrs . ModuleCall :
return d . staticValidateModuleCallReference ( modCfg , addr , ref . Remaining , ref . SourceRange )
case addrs . ModuleCallInstance :
return d . staticValidateModuleCallReference ( modCfg , addr . Call , ref . Remaining , ref . SourceRange )
2020-03-20 11:18:48 -05:00
case addrs . AbsModuleCallOutput :
2018-11-28 13:25:44 -06:00
// This one is a funny one because we will take the output name referenced
// and use it to fake up a "remaining" that would make sense for the
// module call itself, rather than for the specific output, and then
// we can just re-use our static module call validation logic.
remain := make ( hcl . Traversal , len ( ref . Remaining ) + 1 )
copy ( remain [ 1 : ] , ref . Remaining )
remain [ 0 ] = hcl . TraverseAttr {
Name : addr . Name ,
// Using the whole reference as the source range here doesn't exactly
// match how HCL would normally generate an attribute traversal,
// but is close enough for our purposes.
SrcRange : ref . SourceRange . ToHCL ( ) ,
}
return d . staticValidateModuleCallReference ( modCfg , addr . Call . Call , remain , ref . SourceRange )
2018-11-20 19:25:05 -06:00
default :
// Anything else we'll just permit through without any static validation
// and let it be caught during dynamic evaluation, in evaluate.go .
return nil
}
}
2018-12-20 14:28:23 -06:00
func ( d * evaluationStateData ) staticValidateSingleResourceReference ( modCfg * configs . Config , addr addrs . Resource , remain hcl . Traversal , rng tfdiags . SourceRange ) tfdiags . Diagnostics {
// If we have at least one step in "remain" and this resource has
// "count" set then we know for sure this in invalid because we have
// something like:
// aws_instance.foo.bar
// ...when we really need
// aws_instance.foo[count.index].bar
// It is _not_ safe to do this check when remain is empty, because that
// would also match aws_instance.foo[count.index].bar due to `count.index`
// not being statically-resolvable as part of a reference, and match
// direct references to the whole aws_instance.foo tuple.
if len ( remain ) == 0 {
return nil
}
var diags tfdiags . Diagnostics
cfg := modCfg . Module . ResourceByAddr ( addr )
if cfg == nil {
// We'll just bail out here and catch this in our subsequent call to
// staticValidateResourceReference, then.
return diags
}
if cfg . Count != nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Missing resource instance key ` ,
Detail : fmt . Sprintf ( "Because %s has \"count\" set, its attributes must be accessed on specific instances.\n\nFor example, to correlate with indices of a referring resource, use:\n %s[count.index]" , addr , addr ) ,
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
}
if cfg . ForEach != nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Missing resource instance key ` ,
Detail : fmt . Sprintf ( "Because %s has \"for_each\" set, its attributes must be accessed on specific instances.\n\nFor example, to correlate with indices of a referring resource, use:\n %s[each.key]" , addr , addr ) ,
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
}
return diags
}
func ( d * evaluationStateData ) staticValidateMultiResourceReference ( modCfg * configs . Config , addr addrs . ResourceInstance , remain hcl . Traversal , rng tfdiags . SourceRange ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
cfg := modCfg . Module . ResourceByAddr ( addr . ContainingResource ( ) )
if cfg == nil {
// We'll just bail out here and catch this in our subsequent call to
// staticValidateResourceReference, then.
return diags
}
if addr . Key == addrs . NoKey {
// This is a different path into staticValidateSingleResourceReference
return d . staticValidateSingleResourceReference ( modCfg , addr . ContainingResource ( ) , remain , rng )
} else {
if cfg . Count == nil && cfg . ForEach == nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Unexpected resource instance key ` ,
Detail : fmt . Sprintf ( ` Because %s does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource. ` , addr . ContainingResource ( ) ) ,
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
}
}
return diags
}
2018-11-20 19:25:05 -06:00
func ( d * evaluationStateData ) staticValidateResourceReference ( modCfg * configs . Config , addr addrs . Resource , remain hcl . Traversal , rng tfdiags . SourceRange ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
var modeAdjective string
switch addr . Mode {
case addrs . ManagedResourceMode :
modeAdjective = "managed"
case addrs . DataResourceMode :
modeAdjective = "data"
default :
// should never happen
modeAdjective = "<invalid-mode>"
}
cfg := modCfg . Module . ResourceByAddr ( addr )
if cfg == nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Reference to undeclared resource ` ,
2018-12-20 14:28:23 -06:00
Detail : fmt . Sprintf ( ` A %s resource %q %q has not been declared in %s. ` , modeAdjective , addr . Type , addr . Name , moduleConfigDisplayAddr ( modCfg . Path ) ) ,
2018-11-20 19:25:05 -06:00
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
return diags
}
2020-02-14 09:19:24 -06:00
providerFqn := modCfg . Module . ProviderForLocalConfig ( cfg . ProviderConfigAddr ( ) )
2020-02-03 07:18:04 -06:00
schema , _ := d . Evaluator . Schemas . ResourceTypeConfig ( providerFqn , addr . Mode , addr . Type )
2018-11-20 19:25:05 -06:00
if schema == nil {
// Prior validation should've taken care of a resource block with an
// unsupported type, so we should never get here but we'll handle it
// here anyway for robustness.
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Invalid resource type ` ,
2020-03-31 13:03:33 -05:00
Detail : fmt . Sprintf ( ` A %s resource type %q is not supported by provider %q. ` , modeAdjective , addr . Type , providerFqn . String ( ) ) ,
2018-11-20 19:25:05 -06:00
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
return diags
}
// As a special case we'll detect attempts to access an attribute called
// "count" and produce a special error for it, since versions of Terraform
// prior to v0.12 offered this as a weird special case that we can no
// longer support.
if len ( remain ) > 0 {
if step , ok := remain [ 0 ] . ( hcl . TraverseAttr ) ; ok && step . Name == "count" {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Invalid resource count attribute ` ,
Detail : fmt . Sprintf ( ` The special "count" attribute is no longer supported after Terraform v0.12. Instead, use length(%s) to count resource instances. ` , addr ) ,
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
return diags
}
}
// If we got this far then we'll try to validate the remaining traversal
// steps against our schema.
moreDiags := schema . StaticValidateTraversal ( remain )
diags = diags . Append ( moreDiags )
return diags
}
2018-11-28 13:25:44 -06:00
func ( d * evaluationStateData ) staticValidateModuleCallReference ( modCfg * configs . Config , addr addrs . ModuleCall , remain hcl . Traversal , rng tfdiags . SourceRange ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
// For now, our focus here is just in testing that the referenced module
// call exists. All other validation is deferred until evaluation time.
_ , exists := modCfg . Module . ModuleCalls [ addr . Name ]
if ! exists {
var suggestions [ ] string
for name := range modCfg . Module . ModuleCalls {
suggestions = append ( suggestions , name )
}
sort . Strings ( suggestions )
suggestion := didyoumean . NameSuggestion ( addr . Name , suggestions )
if suggestion != "" {
suggestion = fmt . Sprintf ( " Did you mean %q?" , suggestion )
}
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : ` Reference to undeclared module ` ,
Detail : fmt . Sprintf ( ` No module call named %q is declared in %s.%s ` , addr . Name , moduleConfigDisplayAddr ( modCfg . Path ) , suggestion ) ,
Subject : rng . ToHCL ( ) . Ptr ( ) ,
} )
return diags
}
return diags
}
2018-11-20 19:25:05 -06:00
// moduleConfigDisplayAddr returns a string describing the given module
// address that is appropriate for returning to users in situations where the
// root module is possible. Specifically, it returns "the root module" if the
// root module instance is given, or a string representation of the module
// address otherwise.
func moduleConfigDisplayAddr ( addr addrs . Module ) string {
switch {
case addr . IsRoot ( ) :
return "the root module"
default :
return addr . String ( )
}
}