github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/parse_target.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package addrs
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/hashicorp/hcl/v2/hclsyntax"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/terramate-io/tf/tfdiags"
    13  )
    14  
    15  // Target describes a targeted address with source location information.
    16  type Target struct {
    17  	Subject     Targetable
    18  	SourceRange tfdiags.SourceRange
    19  }
    20  
    21  // ParseTarget attempts to interpret the given traversal as a targetable
    22  // address. The given traversal must be absolute, or this function will
    23  // panic.
    24  //
    25  // If no error diagnostics are returned, the returned target includes the
    26  // address that was extracted and the source range it was extracted from.
    27  //
    28  // If error diagnostics are returned then the Target value is invalid and
    29  // must not be used.
    30  func ParseTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) {
    31  	path, remain, diags := parseModuleInstancePrefix(traversal)
    32  	if diags.HasErrors() {
    33  		return nil, diags
    34  	}
    35  
    36  	rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange())
    37  
    38  	if len(remain) == 0 {
    39  		return &Target{
    40  			Subject:     path,
    41  			SourceRange: rng,
    42  		}, diags
    43  	}
    44  
    45  	riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
    46  	diags = diags.Append(moreDiags)
    47  	if diags.HasErrors() {
    48  		return nil, diags
    49  	}
    50  
    51  	var subject Targetable
    52  	switch {
    53  	case riAddr.Resource.Key == NoKey:
    54  		// We always assume that a no-key instance is meant to
    55  		// be referring to the whole resource, because the distinction
    56  		// doesn't really matter for targets anyway.
    57  		subject = riAddr.ContainingResource()
    58  	default:
    59  		subject = riAddr
    60  	}
    61  
    62  	return &Target{
    63  		Subject:     subject,
    64  		SourceRange: rng,
    65  	}, diags
    66  }
    67  
    68  func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) {
    69  	// Note that this helper is used as part of both ParseTarget and
    70  	// ParseMoveEndpoint, so its error messages should be generic
    71  	// enough to suit both situations.
    72  
    73  	var diags tfdiags.Diagnostics
    74  
    75  	mode := ManagedResourceMode
    76  	if remain.RootName() == "data" {
    77  		mode = DataResourceMode
    78  		remain = remain[1:]
    79  	}
    80  
    81  	if len(remain) < 2 {
    82  		diags = diags.Append(&hcl.Diagnostic{
    83  			Severity: hcl.DiagError,
    84  			Summary:  "Invalid address",
    85  			Detail:   "Resource specification must include a resource type and name.",
    86  			Subject:  remain.SourceRange().Ptr(),
    87  		})
    88  		return AbsResourceInstance{}, diags
    89  	}
    90  
    91  	var typeName, name string
    92  	switch tt := remain[0].(type) {
    93  	case hcl.TraverseRoot:
    94  		typeName = tt.Name
    95  	case hcl.TraverseAttr:
    96  		typeName = tt.Name
    97  	default:
    98  		switch mode {
    99  		case ManagedResourceMode:
   100  			diags = diags.Append(&hcl.Diagnostic{
   101  				Severity: hcl.DiagError,
   102  				Summary:  "Invalid address",
   103  				Detail:   "A resource type name is required.",
   104  				Subject:  remain[0].SourceRange().Ptr(),
   105  			})
   106  		case DataResourceMode:
   107  			diags = diags.Append(&hcl.Diagnostic{
   108  				Severity: hcl.DiagError,
   109  				Summary:  "Invalid address",
   110  				Detail:   "A data source name is required.",
   111  				Subject:  remain[0].SourceRange().Ptr(),
   112  			})
   113  		default:
   114  			panic("unknown mode")
   115  		}
   116  		return AbsResourceInstance{}, diags
   117  	}
   118  
   119  	switch tt := remain[1].(type) {
   120  	case hcl.TraverseAttr:
   121  		name = tt.Name
   122  	default:
   123  		diags = diags.Append(&hcl.Diagnostic{
   124  			Severity: hcl.DiagError,
   125  			Summary:  "Invalid address",
   126  			Detail:   "A resource name is required.",
   127  			Subject:  remain[1].SourceRange().Ptr(),
   128  		})
   129  		return AbsResourceInstance{}, diags
   130  	}
   131  
   132  	remain = remain[2:]
   133  	switch len(remain) {
   134  	case 0:
   135  		return moduleAddr.ResourceInstance(mode, typeName, name, NoKey), diags
   136  	case 1:
   137  		if tt, ok := remain[0].(hcl.TraverseIndex); ok {
   138  			key, err := ParseInstanceKey(tt.Key)
   139  			if err != nil {
   140  				diags = diags.Append(&hcl.Diagnostic{
   141  					Severity: hcl.DiagError,
   142  					Summary:  "Invalid address",
   143  					Detail:   fmt.Sprintf("Invalid resource instance key: %s.", err),
   144  					Subject:  remain[0].SourceRange().Ptr(),
   145  				})
   146  				return AbsResourceInstance{}, diags
   147  			}
   148  
   149  			return moduleAddr.ResourceInstance(mode, typeName, name, key), diags
   150  		} else {
   151  			diags = diags.Append(&hcl.Diagnostic{
   152  				Severity: hcl.DiagError,
   153  				Summary:  "Invalid address",
   154  				Detail:   "Resource instance key must be given in square brackets.",
   155  				Subject:  remain[0].SourceRange().Ptr(),
   156  			})
   157  			return AbsResourceInstance{}, diags
   158  		}
   159  	default:
   160  		diags = diags.Append(&hcl.Diagnostic{
   161  			Severity: hcl.DiagError,
   162  			Summary:  "Invalid address",
   163  			Detail:   "Unexpected extra operators after address.",
   164  			Subject:  remain[1].SourceRange().Ptr(),
   165  		})
   166  		return AbsResourceInstance{}, diags
   167  	}
   168  }
   169  
   170  // ParseTargetStr is a helper wrapper around ParseTarget that takes a string
   171  // and parses it with the HCL native syntax traversal parser before
   172  // interpreting it.
   173  //
   174  // This should be used only in specialized situations since it will cause the
   175  // created references to not have any meaningful source location information.
   176  // If a target string is coming from a source that should be identified in
   177  // error messages then the caller should instead parse it directly using a
   178  // suitable function from the HCL API and pass the traversal itself to
   179  // ParseTarget.
   180  //
   181  // Error diagnostics are returned if either the parsing fails or the analysis
   182  // of the traversal fails. There is no way for the caller to distinguish the
   183  // two kinds of diagnostics programmatically. If error diagnostics are returned
   184  // the returned target may be nil or incomplete.
   185  func ParseTargetStr(str string) (*Target, tfdiags.Diagnostics) {
   186  	var diags tfdiags.Diagnostics
   187  
   188  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
   189  	diags = diags.Append(parseDiags)
   190  	if parseDiags.HasErrors() {
   191  		return nil, diags
   192  	}
   193  
   194  	target, targetDiags := ParseTarget(traversal)
   195  	diags = diags.Append(targetDiags)
   196  	return target, diags
   197  }
   198  
   199  // ParseAbsResource attempts to interpret the given traversal as an absolute
   200  // resource address, using the same syntax as expected by ParseTarget.
   201  //
   202  // If no error diagnostics are returned, the returned target includes the
   203  // address that was extracted and the source range it was extracted from.
   204  //
   205  // If error diagnostics are returned then the AbsResource value is invalid and
   206  // must not be used.
   207  func ParseAbsResource(traversal hcl.Traversal) (AbsResource, tfdiags.Diagnostics) {
   208  	addr, diags := ParseTarget(traversal)
   209  	if diags.HasErrors() {
   210  		return AbsResource{}, diags
   211  	}
   212  
   213  	switch tt := addr.Subject.(type) {
   214  
   215  	case AbsResource:
   216  		return tt, diags
   217  
   218  	case AbsResourceInstance: // Catch likely user error with specialized message
   219  		// Assume that the last element of the traversal must be the index,
   220  		// since that's required for a valid resource instance address.
   221  		indexStep := traversal[len(traversal)-1]
   222  		diags = diags.Append(&hcl.Diagnostic{
   223  			Severity: hcl.DiagError,
   224  			Summary:  "Invalid address",
   225  			Detail:   "A resource address is required. This instance key identifies a specific resource instance, which is not expected here.",
   226  			Subject:  indexStep.SourceRange().Ptr(),
   227  		})
   228  		return AbsResource{}, diags
   229  
   230  	case ModuleInstance: // Catch likely user error with specialized message
   231  		diags = diags.Append(&hcl.Diagnostic{
   232  			Severity: hcl.DiagError,
   233  			Summary:  "Invalid address",
   234  			Detail:   "A resource address is required here. The module path must be followed by a resource specification.",
   235  			Subject:  traversal.SourceRange().Ptr(),
   236  		})
   237  		return AbsResource{}, diags
   238  
   239  	default: // Generic message for other address types
   240  		diags = diags.Append(&hcl.Diagnostic{
   241  			Severity: hcl.DiagError,
   242  			Summary:  "Invalid address",
   243  			Detail:   "A resource address is required here.",
   244  			Subject:  traversal.SourceRange().Ptr(),
   245  		})
   246  		return AbsResource{}, diags
   247  
   248  	}
   249  }
   250  
   251  // ParseAbsResourceStr is a helper wrapper around ParseAbsResource that takes a
   252  // string and parses it with the HCL native syntax traversal parser before
   253  // interpreting it.
   254  //
   255  // Error diagnostics are returned if either the parsing fails or the analysis
   256  // of the traversal fails. There is no way for the caller to distinguish the
   257  // two kinds of diagnostics programmatically. If error diagnostics are returned
   258  // the returned address may be incomplete.
   259  //
   260  // Since this function has no context about the source of the given string,
   261  // any returned diagnostics will not have meaningful source location
   262  // information.
   263  func ParseAbsResourceStr(str string) (AbsResource, tfdiags.Diagnostics) {
   264  	var diags tfdiags.Diagnostics
   265  
   266  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
   267  	diags = diags.Append(parseDiags)
   268  	if parseDiags.HasErrors() {
   269  		return AbsResource{}, diags
   270  	}
   271  
   272  	addr, addrDiags := ParseAbsResource(traversal)
   273  	diags = diags.Append(addrDiags)
   274  	return addr, diags
   275  }
   276  
   277  // ParseAbsResourceInstance attempts to interpret the given traversal as an
   278  // absolute resource instance address, using the same syntax as expected by
   279  // ParseTarget.
   280  //
   281  // If no error diagnostics are returned, the returned target includes the
   282  // address that was extracted and the source range it was extracted from.
   283  //
   284  // If error diagnostics are returned then the AbsResource value is invalid and
   285  // must not be used.
   286  func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) {
   287  	addr, diags := ParseTarget(traversal)
   288  	if diags.HasErrors() {
   289  		return AbsResourceInstance{}, diags
   290  	}
   291  
   292  	switch tt := addr.Subject.(type) {
   293  
   294  	case AbsResource:
   295  		return tt.Instance(NoKey), diags
   296  
   297  	case AbsResourceInstance:
   298  		return tt, diags
   299  
   300  	case ModuleInstance: // Catch likely user error with specialized message
   301  		diags = diags.Append(&hcl.Diagnostic{
   302  			Severity: hcl.DiagError,
   303  			Summary:  "Invalid address",
   304  			Detail:   "A resource instance address is required here. The module path must be followed by a resource instance specification.",
   305  			Subject:  traversal.SourceRange().Ptr(),
   306  		})
   307  		return AbsResourceInstance{}, diags
   308  
   309  	default: // Generic message for other address types
   310  		diags = diags.Append(&hcl.Diagnostic{
   311  			Severity: hcl.DiagError,
   312  			Summary:  "Invalid address",
   313  			Detail:   "A resource address is required here.",
   314  			Subject:  traversal.SourceRange().Ptr(),
   315  		})
   316  		return AbsResourceInstance{}, diags
   317  
   318  	}
   319  }
   320  
   321  // ParseAbsResourceInstanceStr is a helper wrapper around
   322  // ParseAbsResourceInstance that takes a string and parses it with the HCL
   323  // native syntax traversal parser before interpreting it.
   324  //
   325  // Error diagnostics are returned if either the parsing fails or the analysis
   326  // of the traversal fails. There is no way for the caller to distinguish the
   327  // two kinds of diagnostics programmatically. If error diagnostics are returned
   328  // the returned address may be incomplete.
   329  //
   330  // Since this function has no context about the source of the given string,
   331  // any returned diagnostics will not have meaningful source location
   332  // information.
   333  func ParseAbsResourceInstanceStr(str string) (AbsResourceInstance, tfdiags.Diagnostics) {
   334  	var diags tfdiags.Diagnostics
   335  
   336  	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
   337  	diags = diags.Append(parseDiags)
   338  	if parseDiags.HasErrors() {
   339  		return AbsResourceInstance{}, diags
   340  	}
   341  
   342  	addr, addrDiags := ParseAbsResourceInstance(traversal)
   343  	diags = diags.Append(addrDiags)
   344  	return addr, diags
   345  }
   346  
   347  // ModuleAddr returns the module address portion of the subject of
   348  // the recieving target.
   349  //
   350  // Regardless of specific address type, all targets always include
   351  // a module address. They might also include something in that
   352  // module, which this method always discards if so.
   353  func (t *Target) ModuleAddr() ModuleInstance {
   354  	switch addr := t.Subject.(type) {
   355  	case ModuleInstance:
   356  		return addr
   357  	case Module:
   358  		// We assume that a module address is really
   359  		// referring to a module path containing only
   360  		// single-instance modules.
   361  		return addr.UnkeyedInstanceShim()
   362  	case AbsResourceInstance:
   363  		return addr.Module
   364  	case AbsResource:
   365  		return addr.Module
   366  	default:
   367  		// The above cases should be exhaustive for all
   368  		// implementations of Targetable.
   369  		panic(fmt.Sprintf("unsupported target address type %T", addr))
   370  	}
   371  }