github.com/opentofu/opentofu@v1.7.1/internal/tofu/evaluate_triggers.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  	"strings"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	"github.com/opentofu/opentofu/internal/addrs"
    14  	"github.com/opentofu/opentofu/internal/instances"
    15  	"github.com/opentofu/opentofu/internal/tfdiags"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  func evalReplaceTriggeredByExpr(expr hcl.Expression, keyData instances.RepetitionData) (*addrs.Reference, tfdiags.Diagnostics) {
    20  	var ref *addrs.Reference
    21  	var diags tfdiags.Diagnostics
    22  
    23  	traversal, diags := triggersExprToTraversal(expr, keyData)
    24  	if diags.HasErrors() {
    25  		return nil, diags
    26  	}
    27  
    28  	// We now have a static traversal, so we can just turn it into an addrs.Reference.
    29  	ref, ds := addrs.ParseRef(traversal)
    30  	diags = diags.Append(ds)
    31  
    32  	return ref, diags
    33  }
    34  
    35  // trggersExprToTraversal takes an hcl expression limited to the syntax allowed
    36  // in replace_triggered_by, and converts it to a static traversal. The
    37  // RepetitionData contains the data necessary to evaluate the only allowed
    38  // variables in the expression, count.index and each.key.
    39  func triggersExprToTraversal(expr hcl.Expression, keyData instances.RepetitionData) (hcl.Traversal, tfdiags.Diagnostics) {
    40  	var trav hcl.Traversal
    41  	var diags tfdiags.Diagnostics
    42  
    43  	switch e := expr.(type) {
    44  	case *hclsyntax.RelativeTraversalExpr:
    45  		t, d := triggersExprToTraversal(e.Source, keyData)
    46  		diags = diags.Append(d)
    47  		trav = append(trav, t...)
    48  		trav = append(trav, e.Traversal...)
    49  
    50  	case *hclsyntax.ScopeTraversalExpr:
    51  		// a static reference, we can just append the traversal
    52  		trav = append(trav, e.Traversal...)
    53  
    54  	case *hclsyntax.IndexExpr:
    55  		// Get the collection from the index expression
    56  		t, d := triggersExprToTraversal(e.Collection, keyData)
    57  		diags = diags.Append(d)
    58  		if diags.HasErrors() {
    59  			return nil, diags
    60  		}
    61  		trav = append(trav, t...)
    62  
    63  		// The index key is the only place where we could have variables that
    64  		// reference count and each, so we need to parse those independently.
    65  		idx, hclDiags := parseIndexKeyExpr(e.Key, keyData)
    66  		diags = diags.Append(hclDiags)
    67  
    68  		trav = append(trav, idx)
    69  
    70  	default:
    71  		// Something unexpected got through config validation. We're not sure
    72  		// what it is, but we'll point it out in the diagnostics for the user
    73  		// to fix.
    74  		diags = diags.Append(&hcl.Diagnostic{
    75  			Severity: hcl.DiagError,
    76  			Summary:  "Invalid replace_triggered_by expression",
    77  			Detail:   "Unexpected expression found in replace_triggered_by.",
    78  			Subject:  e.Range().Ptr(),
    79  		})
    80  	}
    81  
    82  	return trav, diags
    83  }
    84  
    85  // parseIndexKeyExpr takes an hcl.Expression and parses it as an index key, while
    86  // evaluating any references to count.index or each.key.
    87  func parseIndexKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, hcl.Diagnostics) {
    88  	idx := hcl.TraverseIndex{
    89  		SrcRange: expr.Range(),
    90  	}
    91  
    92  	trav, diags := hcl.RelTraversalForExpr(expr)
    93  	if diags.HasErrors() {
    94  		return idx, diags
    95  	}
    96  
    97  	keyParts := []string{}
    98  
    99  	for _, t := range trav {
   100  		attr, ok := t.(hcl.TraverseAttr)
   101  		if !ok {
   102  			diags = append(diags, &hcl.Diagnostic{
   103  				Severity: hcl.DiagError,
   104  				Summary:  "Invalid index expression",
   105  				Detail:   "Only constant values, count.index or each.key are allowed in index expressions.",
   106  				Subject:  expr.Range().Ptr(),
   107  			})
   108  			return idx, diags
   109  		}
   110  		keyParts = append(keyParts, attr.Name)
   111  	}
   112  
   113  	switch strings.Join(keyParts, ".") {
   114  	case "count.index":
   115  		if keyData.CountIndex == cty.NilVal {
   116  			diags = diags.Append(&hcl.Diagnostic{
   117  				Severity: hcl.DiagError,
   118  				Summary:  `Reference to "count" in non-counted context`,
   119  				Detail:   `The "count" object can only be used in "resource" blocks when the "count" argument is set.`,
   120  				Subject:  expr.Range().Ptr(),
   121  			})
   122  		}
   123  		idx.Key = keyData.CountIndex
   124  
   125  	case "each.key":
   126  		if keyData.EachKey == cty.NilVal {
   127  			diags = diags.Append(&hcl.Diagnostic{
   128  				Severity: hcl.DiagError,
   129  				Summary:  `Reference to "each" in context without for_each`,
   130  				Detail:   `The "each" object can be used only in "resource" blocks when the "for_each" argument is set.`,
   131  				Subject:  expr.Range().Ptr(),
   132  			})
   133  		}
   134  		idx.Key = keyData.EachKey
   135  	default:
   136  		// Something may have slipped through validation, probably from a json
   137  		// configuration.
   138  		diags = append(diags, &hcl.Diagnostic{
   139  			Severity: hcl.DiagError,
   140  			Summary:  "Invalid index expression",
   141  			Detail:   "Only constant values, count.index or each.key are allowed in index expressions.",
   142  			Subject:  expr.Range().Ptr(),
   143  		})
   144  	}
   145  
   146  	return idx, diags
   147  
   148  }