github.com/opentofu/opentofu@v1.7.1/internal/tofu/evaluate_valid.go (about)

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