github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/addrs/parse_ref.go (about)

     1  package addrs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/hashicorp/hcl/v2/hclsyntax"
     8  	"github.com/hashicorp/terraform/tfdiags"
     9  )
    10  
    11  // Reference describes a reference to an address with source location
    12  // information.
    13  type Reference struct {
    14  	Subject     Referenceable
    15  	SourceRange tfdiags.SourceRange
    16  	Remaining   hcl.Traversal
    17  }
    18  
    19  // ParseRef attempts to extract a referencable address from the prefix of the
    20  // given traversal, which must be an absolute traversal or this function
    21  // will panic.
    22  //
    23  // If no error diagnostics are returned, the returned reference includes the
    24  // address that was extracted, the source range it was extracted from, and any
    25  // remaining relative traversal that was not consumed as part of the
    26  // reference.
    27  //
    28  // If error diagnostics are returned then the Reference value is invalid and
    29  // must not be used.
    30  func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
    31  	ref, diags := parseRef(traversal)
    32  
    33  	// Normalize a little to make life easier for callers.
    34  	if ref != nil {
    35  		if len(ref.Remaining) == 0 {
    36  			ref.Remaining = nil
    37  		}
    38  	}
    39  
    40  	return ref, diags
    41  }
    42  
    43  // ParseRefStr is a helper wrapper around ParseRef that takes a string
    44  // and parses it with the HCL native syntax traversal parser before
    45  // interpreting it.
    46  //
    47  // This should be used only in specialized situations since it will cause the
    48  // created references to not have any meaningful source location information.
    49  // If a reference string is coming from a source that should be identified in
    50  // error messages then the caller should instead parse it directly using a
    51  // suitable function from the HCL API and pass the traversal itself to
    52  // ParseRef.
    53  //
    54  // Error diagnostics are returned if either the parsing fails or the analysis
    55  // of the traversal fails. There is no way for the caller to distinguish the
    56  // two kinds of diagnostics programmatically. If error diagnostics are returned
    57  // the returned reference may be nil or incomplete.
    58  func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) {
    59  	var diags tfdiags.Diagnostics
    60  
    61  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
    62  	diags = diags.Append(parseDiags)
    63  	if parseDiags.HasErrors() {
    64  		return nil, diags
    65  	}
    66  
    67  	ref, targetDiags := ParseRef(traversal)
    68  	diags = diags.Append(targetDiags)
    69  	return ref, diags
    70  }
    71  
    72  func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
    73  	var diags tfdiags.Diagnostics
    74  
    75  	root := traversal.RootName()
    76  	rootRange := traversal[0].SourceRange()
    77  
    78  	switch root {
    79  
    80  	case "count":
    81  		name, rng, remain, diags := parseSingleAttrRef(traversal)
    82  		return &Reference{
    83  			Subject:     CountAttr{Name: name},
    84  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
    85  			Remaining:   remain,
    86  		}, diags
    87  
    88  	case "each":
    89  		name, rng, remain, diags := parseSingleAttrRef(traversal)
    90  		return &Reference{
    91  			Subject:     ForEachAttr{Name: name},
    92  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
    93  			Remaining:   remain,
    94  		}, diags
    95  
    96  	case "data":
    97  		if len(traversal) < 3 {
    98  			diags = diags.Append(&hcl.Diagnostic{
    99  				Severity: hcl.DiagError,
   100  				Summary:  "Invalid reference",
   101  				Detail:   `The "data" object must be followed by two attribute names: the data source type and the resource name.`,
   102  				Subject:  traversal.SourceRange().Ptr(),
   103  			})
   104  			return nil, diags
   105  		}
   106  		remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser
   107  		return parseResourceRef(DataResourceMode, rootRange, remain)
   108  
   109  	case "local":
   110  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   111  		return &Reference{
   112  			Subject:     LocalValue{Name: name},
   113  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   114  			Remaining:   remain,
   115  		}, diags
   116  
   117  	case "module":
   118  		callName, callRange, remain, diags := parseSingleAttrRef(traversal)
   119  		if diags.HasErrors() {
   120  			return nil, diags
   121  		}
   122  
   123  		// A traversal starting with "module" can either be a reference to
   124  		// an entire module instance or to a single output from a module
   125  		// instance, depending on what we find after this introducer.
   126  
   127  		callInstance := ModuleCallInstance{
   128  			Call: ModuleCall{
   129  				Name: callName,
   130  			},
   131  			Key: NoKey,
   132  		}
   133  
   134  		if len(remain) == 0 {
   135  			// Reference to an entire module instance. Might alternatively
   136  			// be a reference to a collection of instances of a particular
   137  			// module, but the caller will need to deal with that ambiguity
   138  			// since we don't have enough context here.
   139  			return &Reference{
   140  				Subject:     callInstance,
   141  				SourceRange: tfdiags.SourceRangeFromHCL(callRange),
   142  				Remaining:   remain,
   143  			}, diags
   144  		}
   145  
   146  		if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok {
   147  			var err error
   148  			callInstance.Key, err = ParseInstanceKey(idxTrav.Key)
   149  			if err != nil {
   150  				diags = diags.Append(&hcl.Diagnostic{
   151  					Severity: hcl.DiagError,
   152  					Summary:  "Invalid index key",
   153  					Detail:   fmt.Sprintf("Invalid index for module instance: %s.", err),
   154  					Subject:  &idxTrav.SrcRange,
   155  				})
   156  				return nil, diags
   157  			}
   158  			remain = remain[1:]
   159  
   160  			if len(remain) == 0 {
   161  				// Also a reference to an entire module instance, but we have a key
   162  				// now.
   163  				return &Reference{
   164  					Subject:     callInstance,
   165  					SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)),
   166  					Remaining:   remain,
   167  				}, diags
   168  			}
   169  		}
   170  
   171  		if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok {
   172  			remain = remain[1:]
   173  			return &Reference{
   174  				Subject: ModuleCallOutput{
   175  					Name: attrTrav.Name,
   176  					Call: callInstance,
   177  				},
   178  				SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)),
   179  				Remaining:   remain,
   180  			}, diags
   181  		}
   182  
   183  		diags = diags.Append(&hcl.Diagnostic{
   184  			Severity: hcl.DiagError,
   185  			Summary:  "Invalid reference",
   186  			Detail:   "Module instance objects do not support this operation.",
   187  			Subject:  remain[0].SourceRange().Ptr(),
   188  		})
   189  		return nil, diags
   190  
   191  	case "path":
   192  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   193  		return &Reference{
   194  			Subject:     PathAttr{Name: name},
   195  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   196  			Remaining:   remain,
   197  		}, diags
   198  
   199  	case "self":
   200  		return &Reference{
   201  			Subject:     Self,
   202  			SourceRange: tfdiags.SourceRangeFromHCL(rootRange),
   203  			Remaining:   traversal[1:],
   204  		}, diags
   205  
   206  	case "terraform":
   207  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   208  		return &Reference{
   209  			Subject:     TerraformAttr{Name: name},
   210  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   211  			Remaining:   remain,
   212  		}, diags
   213  
   214  	case "var":
   215  		name, rng, remain, diags := parseSingleAttrRef(traversal)
   216  		return &Reference{
   217  			Subject:     InputVariable{Name: name},
   218  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   219  			Remaining:   remain,
   220  		}, diags
   221  
   222  	default:
   223  		return parseResourceRef(ManagedResourceMode, rootRange, traversal)
   224  	}
   225  }
   226  
   227  func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
   228  	var diags tfdiags.Diagnostics
   229  
   230  	if len(traversal) < 2 {
   231  		diags = diags.Append(&hcl.Diagnostic{
   232  			Severity: hcl.DiagError,
   233  			Summary:  "Invalid reference",
   234  			Detail:   `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`,
   235  			Subject:  hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(),
   236  		})
   237  		return nil, diags
   238  	}
   239  
   240  	var typeName, name string
   241  	switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode
   242  	case hcl.TraverseRoot:
   243  		typeName = tt.Name
   244  	case hcl.TraverseAttr:
   245  		typeName = tt.Name
   246  	default:
   247  		// If it isn't a TraverseRoot then it must be a "data" reference.
   248  		diags = diags.Append(&hcl.Diagnostic{
   249  			Severity: hcl.DiagError,
   250  			Summary:  "Invalid reference",
   251  			Detail:   `The "data" object does not support this operation.`,
   252  			Subject:  traversal[0].SourceRange().Ptr(),
   253  		})
   254  		return nil, diags
   255  	}
   256  
   257  	attrTrav, ok := traversal[1].(hcl.TraverseAttr)
   258  	if !ok {
   259  		var what string
   260  		switch mode {
   261  		case DataResourceMode:
   262  			what = "data source"
   263  		default:
   264  			what = "resource type"
   265  		}
   266  		diags = diags.Append(&hcl.Diagnostic{
   267  			Severity: hcl.DiagError,
   268  			Summary:  "Invalid reference",
   269  			Detail:   fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what),
   270  			Subject:  traversal[1].SourceRange().Ptr(),
   271  		})
   272  		return nil, diags
   273  	}
   274  	name = attrTrav.Name
   275  	rng := hcl.RangeBetween(startRange, attrTrav.SrcRange)
   276  	remain := traversal[2:]
   277  
   278  	resourceAddr := Resource{
   279  		Mode: mode,
   280  		Type: typeName,
   281  		Name: name,
   282  	}
   283  	resourceInstAddr := ResourceInstance{
   284  		Resource: resourceAddr,
   285  		Key:      NoKey,
   286  	}
   287  
   288  	if len(remain) == 0 {
   289  		// This might actually be a reference to the collection of all instances
   290  		// of the resource, but we don't have enough context here to decide
   291  		// so we'll let the caller resolve that ambiguity.
   292  		return &Reference{
   293  			Subject:     resourceAddr,
   294  			SourceRange: tfdiags.SourceRangeFromHCL(rng),
   295  		}, diags
   296  	}
   297  
   298  	if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok {
   299  		var err error
   300  		resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key)
   301  		if err != nil {
   302  			diags = diags.Append(&hcl.Diagnostic{
   303  				Severity: hcl.DiagError,
   304  				Summary:  "Invalid index key",
   305  				Detail:   fmt.Sprintf("Invalid index for resource instance: %s.", err),
   306  				Subject:  &idxTrav.SrcRange,
   307  			})
   308  			return nil, diags
   309  		}
   310  		remain = remain[1:]
   311  		rng = hcl.RangeBetween(rng, idxTrav.SrcRange)
   312  	}
   313  
   314  	return &Reference{
   315  		Subject:     resourceInstAddr,
   316  		SourceRange: tfdiags.SourceRangeFromHCL(rng),
   317  		Remaining:   remain,
   318  	}, diags
   319  }
   320  
   321  func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
   322  	var diags tfdiags.Diagnostics
   323  
   324  	root := traversal.RootName()
   325  	rootRange := traversal[0].SourceRange()
   326  
   327  	if len(traversal) < 2 {
   328  		diags = diags.Append(&hcl.Diagnostic{
   329  			Severity: hcl.DiagError,
   330  			Summary:  "Invalid reference",
   331  			Detail:   fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root),
   332  			Subject:  &rootRange,
   333  		})
   334  		return "", hcl.Range{}, nil, diags
   335  	}
   336  	if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
   337  		return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags
   338  	}
   339  	diags = diags.Append(&hcl.Diagnostic{
   340  		Severity: hcl.DiagError,
   341  		Summary:  "Invalid reference",
   342  		Detail:   fmt.Sprintf("The %q object does not support this operation.", root),
   343  		Subject:  traversal[1].SourceRange().Ptr(),
   344  	})
   345  	return "", hcl.Range{}, nil, diags
   346  }