github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/evaluate_valid.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  
     9  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
    11  	"github.com/hashicorp/terraform-plugin-sdk/internal/helper/didyoumean"
    12  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    13  )
    14  
    15  // StaticValidateReferences checks the given references against schemas and
    16  // other statically-checkable rules, producing error diagnostics if any
    17  // problems are found.
    18  //
    19  // If this method returns errors for a particular reference then evaluating
    20  // that reference is likely to generate a very similar error, so callers should
    21  // not run this method and then also evaluate the source expression(s) and
    22  // merge the two sets of diagnostics together, since this will result in
    23  // confusing redundant errors.
    24  //
    25  // This method can find more errors than can be found by evaluating an
    26  // expression with a partially-populated scope, since it checks the referenced
    27  // names directly against the schema rather than relying on evaluation errors.
    28  //
    29  // The result may include warning diagnostics if, for example, deprecated
    30  // features are referenced.
    31  func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
    32  	var diags tfdiags.Diagnostics
    33  	for _, ref := range refs {
    34  		moreDiags := d.staticValidateReference(ref, self)
    35  		diags = diags.Append(moreDiags)
    36  	}
    37  	return diags
    38  }
    39  
    40  func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
    41  	modCfg := d.Evaluator.Config.DescendentForInstance(d.ModulePath)
    42  	if modCfg == nil {
    43  		// This is a bug in the caller rather than a problem with the
    44  		// reference, but rather than crashing out here in an unhelpful way
    45  		// we'll just ignore it and trust a different layer to catch it.
    46  		return nil
    47  	}
    48  
    49  	if ref.Subject == addrs.Self {
    50  		// The "self" address is a special alias for the address given as
    51  		// our self parameter here, if present.
    52  		if self == nil {
    53  			var diags tfdiags.Diagnostics
    54  			diags = diags.Append(&hcl.Diagnostic{
    55  				Severity: hcl.DiagError,
    56  				Summary:  `Invalid "self" reference`,
    57  				// This detail message mentions some current practice that
    58  				// this codepath doesn't really "know about". If the "self"
    59  				// object starts being supported in more contexts later then
    60  				// we'll need to adjust this message.
    61  				Detail:  `The "self" object is not available in this context. This object can be used only in resource provisioner and connection blocks.`,
    62  				Subject: ref.SourceRange.ToHCL().Ptr(),
    63  			})
    64  			return diags
    65  		}
    66  
    67  		synthRef := *ref // shallow copy
    68  		synthRef.Subject = self
    69  		ref = &synthRef
    70  	}
    71  
    72  	switch addr := ref.Subject.(type) {
    73  
    74  	// For static validation we validate both resource and resource instance references the same way.
    75  	// We mostly disregard the index, though we do some simple validation of
    76  	// its _presence_ in staticValidateSingleResourceReference and
    77  	// staticValidateMultiResourceReference respectively.
    78  	case addrs.Resource:
    79  		var diags tfdiags.Diagnostics
    80  		diags = diags.Append(d.staticValidateSingleResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange))
    81  		diags = diags.Append(d.staticValidateResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange))
    82  		return diags
    83  	case addrs.ResourceInstance:
    84  		var diags tfdiags.Diagnostics
    85  		diags = diags.Append(d.staticValidateMultiResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange))
    86  		diags = diags.Append(d.staticValidateResourceReference(modCfg, addr.ContainingResource(), ref.Remaining, ref.SourceRange))
    87  		return diags
    88  
    89  	// We also handle all module call references the same way, disregarding index.
    90  	case addrs.ModuleCall:
    91  		return d.staticValidateModuleCallReference(modCfg, addr, ref.Remaining, ref.SourceRange)
    92  	case addrs.ModuleCallInstance:
    93  		return d.staticValidateModuleCallReference(modCfg, addr.Call, ref.Remaining, ref.SourceRange)
    94  	case addrs.ModuleCallOutput:
    95  		// This one is a funny one because we will take the output name referenced
    96  		// and use it to fake up a "remaining" that would make sense for the
    97  		// module call itself, rather than for the specific output, and then
    98  		// we can just re-use our static module call validation logic.
    99  		remain := make(hcl.Traversal, len(ref.Remaining)+1)
   100  		copy(remain[1:], ref.Remaining)
   101  		remain[0] = hcl.TraverseAttr{
   102  			Name: addr.Name,
   103  
   104  			// Using the whole reference as the source range here doesn't exactly
   105  			// match how HCL would normally generate an attribute traversal,
   106  			// but is close enough for our purposes.
   107  			SrcRange: ref.SourceRange.ToHCL(),
   108  		}
   109  		return d.staticValidateModuleCallReference(modCfg, addr.Call.Call, remain, ref.SourceRange)
   110  
   111  	default:
   112  		// Anything else we'll just permit through without any static validation
   113  		// and let it be caught during dynamic evaluation, in evaluate.go .
   114  		return nil
   115  	}
   116  }
   117  
   118  func (d *evaluationStateData) staticValidateSingleResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
   119  	// If we have at least one step in "remain" and this resource has
   120  	// "count" set then we know for sure this in invalid because we have
   121  	// something like:
   122  	//     aws_instance.foo.bar
   123  	// ...when we really need
   124  	//     aws_instance.foo[count.index].bar
   125  
   126  	// It is _not_ safe to do this check when remain is empty, because that
   127  	// would also match aws_instance.foo[count.index].bar due to `count.index`
   128  	// not being statically-resolvable as part of a reference, and match
   129  	// direct references to the whole aws_instance.foo tuple.
   130  	if len(remain) == 0 {
   131  		return nil
   132  	}
   133  
   134  	var diags tfdiags.Diagnostics
   135  
   136  	cfg := modCfg.Module.ResourceByAddr(addr)
   137  	if cfg == nil {
   138  		// We'll just bail out here and catch this in our subsequent call to
   139  		// staticValidateResourceReference, then.
   140  		return diags
   141  	}
   142  
   143  	if cfg.Count != nil {
   144  		diags = diags.Append(&hcl.Diagnostic{
   145  			Severity: hcl.DiagError,
   146  			Summary:  `Missing resource instance key`,
   147  			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),
   148  			Subject:  rng.ToHCL().Ptr(),
   149  		})
   150  	}
   151  	if cfg.ForEach != nil {
   152  		diags = diags.Append(&hcl.Diagnostic{
   153  			Severity: hcl.DiagError,
   154  			Summary:  `Missing resource instance key`,
   155  			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),
   156  			Subject:  rng.ToHCL().Ptr(),
   157  		})
   158  	}
   159  
   160  	return diags
   161  }
   162  
   163  func (d *evaluationStateData) staticValidateMultiResourceReference(modCfg *configs.Config, addr addrs.ResourceInstance, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
   164  	var diags tfdiags.Diagnostics
   165  
   166  	cfg := modCfg.Module.ResourceByAddr(addr.ContainingResource())
   167  	if cfg == nil {
   168  		// We'll just bail out here and catch this in our subsequent call to
   169  		// staticValidateResourceReference, then.
   170  		return diags
   171  	}
   172  
   173  	if addr.Key == addrs.NoKey {
   174  		// This is a different path into staticValidateSingleResourceReference
   175  		return d.staticValidateSingleResourceReference(modCfg, addr.ContainingResource(), remain, rng)
   176  	} else {
   177  		if cfg.Count == nil && cfg.ForEach == nil {
   178  			diags = diags.Append(&hcl.Diagnostic{
   179  				Severity: hcl.DiagError,
   180  				Summary:  `Unexpected resource instance key`,
   181  				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()),
   182  				Subject:  rng.ToHCL().Ptr(),
   183  			})
   184  		}
   185  	}
   186  
   187  	return diags
   188  }
   189  
   190  func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
   191  	var diags tfdiags.Diagnostics
   192  
   193  	var modeAdjective string
   194  	switch addr.Mode {
   195  	case addrs.ManagedResourceMode:
   196  		modeAdjective = "managed"
   197  	case addrs.DataResourceMode:
   198  		modeAdjective = "data"
   199  	default:
   200  		// should never happen
   201  		modeAdjective = "<invalid-mode>"
   202  	}
   203  
   204  	cfg := modCfg.Module.ResourceByAddr(addr)
   205  	if cfg == nil {
   206  		diags = diags.Append(&hcl.Diagnostic{
   207  			Severity: hcl.DiagError,
   208  			Summary:  `Reference to undeclared resource`,
   209  			Detail:   fmt.Sprintf(`A %s resource %q %q has not been declared in %s.`, modeAdjective, addr.Type, addr.Name, moduleConfigDisplayAddr(modCfg.Path)),
   210  			Subject:  rng.ToHCL().Ptr(),
   211  		})
   212  		return diags
   213  	}
   214  
   215  	// Normally accessing this directly is wrong because it doesn't take into
   216  	// account provider inheritance, etc but it's okay here because we're only
   217  	// paying attention to the type anyway.
   218  	providerType := cfg.ProviderConfigAddr().Type
   219  	schema, _ := d.Evaluator.Schemas.ResourceTypeConfig(providerType, addr.Mode, addr.Type)
   220  
   221  	if schema == nil {
   222  		// Prior validation should've taken care of a resource block with an
   223  		// unsupported type, so we should never get here but we'll handle it
   224  		// here anyway for robustness.
   225  		diags = diags.Append(&hcl.Diagnostic{
   226  			Severity: hcl.DiagError,
   227  			Summary:  `Invalid resource type`,
   228  			Detail:   fmt.Sprintf(`A %s resource type %q is not supported by provider %q.`, modeAdjective, addr.Type, providerType),
   229  			Subject:  rng.ToHCL().Ptr(),
   230  		})
   231  		return diags
   232  	}
   233  
   234  	// As a special case we'll detect attempts to access an attribute called
   235  	// "count" and produce a special error for it, since versions of Terraform
   236  	// prior to v0.12 offered this as a weird special case that we can no
   237  	// longer support.
   238  	if len(remain) > 0 {
   239  		if step, ok := remain[0].(hcl.TraverseAttr); ok && step.Name == "count" {
   240  			diags = diags.Append(&hcl.Diagnostic{
   241  				Severity: hcl.DiagError,
   242  				Summary:  `Invalid resource count attribute`,
   243  				Detail:   fmt.Sprintf(`The special "count" attribute is no longer supported after Terraform v0.12. Instead, use length(%s) to count resource instances.`, addr),
   244  				Subject:  rng.ToHCL().Ptr(),
   245  			})
   246  			return diags
   247  		}
   248  	}
   249  
   250  	// If we got this far then we'll try to validate the remaining traversal
   251  	// steps against our schema.
   252  	moreDiags := schema.StaticValidateTraversal(remain)
   253  	diags = diags.Append(moreDiags)
   254  
   255  	return diags
   256  }
   257  
   258  func (d *evaluationStateData) staticValidateModuleCallReference(modCfg *configs.Config, addr addrs.ModuleCall, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
   259  	var diags tfdiags.Diagnostics
   260  
   261  	// For now, our focus here is just in testing that the referenced module
   262  	// call exists. All other validation is deferred until evaluation time.
   263  	_, exists := modCfg.Module.ModuleCalls[addr.Name]
   264  	if !exists {
   265  		var suggestions []string
   266  		for name := range modCfg.Module.ModuleCalls {
   267  			suggestions = append(suggestions, name)
   268  		}
   269  		sort.Strings(suggestions)
   270  		suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
   271  		if suggestion != "" {
   272  			suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
   273  		}
   274  
   275  		diags = diags.Append(&hcl.Diagnostic{
   276  			Severity: hcl.DiagError,
   277  			Summary:  `Reference to undeclared module`,
   278  			Detail:   fmt.Sprintf(`No module call named %q is declared in %s.%s`, addr.Name, moduleConfigDisplayAddr(modCfg.Path), suggestion),
   279  			Subject:  rng.ToHCL().Ptr(),
   280  		})
   281  		return diags
   282  	}
   283  
   284  	return diags
   285  }
   286  
   287  // moduleConfigDisplayAddr returns a string describing the given module
   288  // address that is appropriate for returning to users in situations where the
   289  // root module is possible. Specifically, it returns "the root module" if the
   290  // root module instance is given, or a string representation of the module
   291  // address otherwise.
   292  func moduleConfigDisplayAddr(addr addrs.Module) string {
   293  	switch {
   294  	case addr.IsRoot():
   295  		return "the root module"
   296  	default:
   297  		return addr.String()
   298  	}
   299  }