github.com/opentofu/opentofu@v1.7.1/internal/legacy/tofu/resource_address.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  	"reflect"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  	"github.com/opentofu/opentofu/internal/configs"
    17  )
    18  
    19  // ResourceAddress is a way of identifying an individual resource (or,
    20  // eventually, a subset of resources) within the state. It is used for Targets.
    21  type ResourceAddress struct {
    22  	// Addresses a resource falling somewhere in the module path
    23  	// When specified alone, addresses all resources within a module path
    24  	Path []string
    25  
    26  	// Addresses a specific resource that occurs in a list
    27  	Index int
    28  
    29  	InstanceType    InstanceType
    30  	InstanceTypeSet bool
    31  	Name            string
    32  	Type            string
    33  	Mode            ResourceMode // significant only if InstanceTypeSet
    34  }
    35  
    36  // Copy returns a copy of this ResourceAddress
    37  func (r *ResourceAddress) Copy() *ResourceAddress {
    38  	if r == nil {
    39  		return nil
    40  	}
    41  
    42  	n := &ResourceAddress{
    43  		Path:         make([]string, 0, len(r.Path)),
    44  		Index:        r.Index,
    45  		InstanceType: r.InstanceType,
    46  		Name:         r.Name,
    47  		Type:         r.Type,
    48  		Mode:         r.Mode,
    49  	}
    50  
    51  	n.Path = append(n.Path, r.Path...)
    52  
    53  	return n
    54  }
    55  
    56  // String outputs the address that parses into this address.
    57  func (r *ResourceAddress) String() string {
    58  	var result []string
    59  	for _, p := range r.Path {
    60  		result = append(result, "module", p)
    61  	}
    62  
    63  	switch r.Mode {
    64  	case ManagedResourceMode:
    65  		// nothing to do
    66  	case DataResourceMode:
    67  		result = append(result, "data")
    68  	default:
    69  		panic(fmt.Errorf("unsupported resource mode %s", r.Mode))
    70  	}
    71  
    72  	if r.Type != "" {
    73  		result = append(result, r.Type)
    74  	}
    75  
    76  	if r.Name != "" {
    77  		name := r.Name
    78  		if r.InstanceTypeSet {
    79  			switch r.InstanceType {
    80  			case TypePrimary:
    81  				name += ".primary"
    82  			case TypeDeposed:
    83  				name += ".deposed"
    84  			case TypeTainted:
    85  				name += ".tainted"
    86  			}
    87  		}
    88  
    89  		if r.Index >= 0 {
    90  			name += fmt.Sprintf("[%d]", r.Index)
    91  		}
    92  		result = append(result, name)
    93  	}
    94  
    95  	return strings.Join(result, ".")
    96  }
    97  
    98  // HasResourceSpec returns true if the address has a resource spec, as
    99  // defined in the documentation:
   100  //
   101  //	https://opentofu.org/docs/cli/state/resource-addressing/
   102  //
   103  // In particular, this returns false if the address contains only
   104  // a module path, thus addressing the entire module.
   105  func (r *ResourceAddress) HasResourceSpec() bool {
   106  	return r.Type != "" && r.Name != ""
   107  }
   108  
   109  // WholeModuleAddress returns the resource address that refers to all
   110  // resources in the same module as the receiver address.
   111  func (r *ResourceAddress) WholeModuleAddress() *ResourceAddress {
   112  	return &ResourceAddress{
   113  		Path:            r.Path,
   114  		Index:           -1,
   115  		InstanceTypeSet: false,
   116  	}
   117  }
   118  
   119  // MatchesResourceConfig returns true if the receiver matches the given
   120  // configuration resource within the given _static_ module path. Note that
   121  // the module path in a resource address is a _dynamic_ module path, and
   122  // multiple dynamic resource paths may map to a single static path if
   123  // count and for_each are in use on module calls.
   124  //
   125  // Since resource configuration blocks represent all of the instances of
   126  // a multi-instance resource, the index of the address (if any) is not
   127  // considered.
   128  func (r *ResourceAddress) MatchesResourceConfig(path addrs.Module, rc *configs.Resource) bool {
   129  	if r.HasResourceSpec() {
   130  		// FIXME: Some ugliness while we are between worlds. Functionality
   131  		// in "addrs" should eventually replace this ResourceAddress idea
   132  		// completely, but for now we'll need to translate to the old
   133  		// way of representing resource modes.
   134  		switch r.Mode {
   135  		case ManagedResourceMode:
   136  			if rc.Mode != addrs.ManagedResourceMode {
   137  				return false
   138  			}
   139  		case DataResourceMode:
   140  			if rc.Mode != addrs.DataResourceMode {
   141  				return false
   142  			}
   143  		}
   144  		if r.Type != rc.Type || r.Name != rc.Name {
   145  			return false
   146  		}
   147  	}
   148  
   149  	addrPath := r.Path
   150  
   151  	// normalize
   152  	if len(addrPath) == 0 {
   153  		addrPath = nil
   154  	}
   155  	if len(path) == 0 {
   156  		path = nil
   157  	}
   158  	rawPath := []string(path)
   159  	return reflect.DeepEqual(addrPath, rawPath)
   160  }
   161  
   162  // stateId returns the ID that this resource should be entered with
   163  // in the state. This is also used for diffs. In the future, we'd like to
   164  // move away from this string field so I don't export this.
   165  func (r *ResourceAddress) stateId() string {
   166  	result := fmt.Sprintf("%s.%s", r.Type, r.Name)
   167  	switch r.Mode {
   168  	case ManagedResourceMode:
   169  		// Done
   170  	case DataResourceMode:
   171  		result = fmt.Sprintf("data.%s", result)
   172  	default:
   173  		panic(fmt.Errorf("unknown resource mode: %s", r.Mode))
   174  	}
   175  	if r.Index >= 0 {
   176  		result += fmt.Sprintf(".%d", r.Index)
   177  	}
   178  
   179  	return result
   180  }
   181  
   182  // parseResourceAddressInternal parses the somewhat bespoke resource
   183  // identifier used in states and diffs, such as "instance.name.0".
   184  func parseResourceAddressInternal(s string) (*ResourceAddress, error) {
   185  	// Split based on ".". Every resource address should have at least two
   186  	// elements (type and name).
   187  	parts := strings.Split(s, ".")
   188  	if len(parts) < 2 || len(parts) > 4 {
   189  		return nil, fmt.Errorf("Invalid internal resource address format: %s", s)
   190  	}
   191  
   192  	// Data resource if we have at least 3 parts and the first one is data
   193  	mode := ManagedResourceMode
   194  	if len(parts) > 2 && parts[0] == "data" {
   195  		mode = DataResourceMode
   196  		parts = parts[1:]
   197  	}
   198  
   199  	// If we're not a data resource and we have more than 3, then it is an error
   200  	if len(parts) > 3 && mode != DataResourceMode {
   201  		return nil, fmt.Errorf("Invalid internal resource address format: %s", s)
   202  	}
   203  
   204  	// Build the parts of the resource address that are guaranteed to exist
   205  	addr := &ResourceAddress{
   206  		Type:         parts[0],
   207  		Name:         parts[1],
   208  		Index:        -1,
   209  		InstanceType: TypePrimary,
   210  		Mode:         mode,
   211  	}
   212  
   213  	// If we have more parts, then we have an index. Parse that.
   214  	if len(parts) > 2 {
   215  		idx, err := strconv.ParseInt(parts[2], 0, 0)
   216  		if err != nil {
   217  			return nil, fmt.Errorf("Error parsing resource address %q: %w", s, err)
   218  		}
   219  
   220  		addr.Index = int(idx)
   221  	}
   222  
   223  	return addr, nil
   224  }
   225  
   226  func ParseResourceAddress(s string) (*ResourceAddress, error) {
   227  	matches, err := tokenizeResourceAddress(s)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	mode := ManagedResourceMode
   232  	if matches["data_prefix"] != "" {
   233  		mode = DataResourceMode
   234  	}
   235  	resourceIndex, err := ParseResourceIndex(matches["index"])
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	instanceType, err := ParseInstanceType(matches["instance_type"])
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	path := ParseResourcePath(matches["path"])
   244  
   245  	// not allowed to say "data." without a type following
   246  	if mode == DataResourceMode && matches["type"] == "" {
   247  		return nil, fmt.Errorf(
   248  			"invalid resource address %q: must target specific data instance",
   249  			s,
   250  		)
   251  	}
   252  
   253  	return &ResourceAddress{
   254  		Path:            path,
   255  		Index:           resourceIndex,
   256  		InstanceType:    instanceType,
   257  		InstanceTypeSet: matches["instance_type"] != "",
   258  		Name:            matches["name"],
   259  		Type:            matches["type"],
   260  		Mode:            mode,
   261  	}, nil
   262  }
   263  
   264  // ParseResourceAddressForInstanceDiff creates a ResourceAddress for a
   265  // resource name as described in a module diff.
   266  //
   267  // For historical reasons a different addressing format is used in this
   268  // context. The internal format should not be shown in the UI and instead
   269  // this function should be used to translate to a ResourceAddress and
   270  // then, where appropriate, use the String method to produce a canonical
   271  // resource address string for display in the UI.
   272  //
   273  // The given path slice must be empty (or nil) for the root module, and
   274  // otherwise consist of a sequence of module names traversing down into
   275  // the module tree. If a non-nil path is provided, the caller must not
   276  // modify its underlying array after passing it to this function.
   277  func ParseResourceAddressForInstanceDiff(path []string, key string) (*ResourceAddress, error) {
   278  	addr, err := parseResourceAddressInternal(key)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	addr.Path = path
   283  	return addr, nil
   284  }
   285  
   286  // NewLegacyResourceAddress creates a ResourceAddress from a new-style
   287  // addrs.AbsResource value.
   288  //
   289  // This is provided for shimming purposes so that we can still easily call into
   290  // older functions that expect the ResourceAddress type.
   291  func NewLegacyResourceAddress(addr addrs.AbsResource) *ResourceAddress {
   292  	ret := &ResourceAddress{
   293  		Type: addr.Resource.Type,
   294  		Name: addr.Resource.Name,
   295  	}
   296  
   297  	switch addr.Resource.Mode {
   298  	case addrs.ManagedResourceMode:
   299  		ret.Mode = ManagedResourceMode
   300  	case addrs.DataResourceMode:
   301  		ret.Mode = DataResourceMode
   302  	default:
   303  		panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Mode))
   304  	}
   305  
   306  	path := make([]string, len(addr.Module))
   307  	for i, step := range addr.Module {
   308  		if step.InstanceKey != addrs.NoKey {
   309  			// At the time of writing this can't happen because we don't
   310  			// ket generate keyed module instances. This legacy codepath must
   311  			// be removed before we can support "count" and "for_each" for
   312  			// modules.
   313  			panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey))
   314  		}
   315  
   316  		path[i] = step.Name
   317  	}
   318  	ret.Path = path
   319  	ret.Index = -1
   320  
   321  	return ret
   322  }
   323  
   324  // NewLegacyResourceInstanceAddress creates a ResourceAddress from a new-style
   325  // addrs.AbsResource value.
   326  //
   327  // This is provided for shimming purposes so that we can still easily call into
   328  // older functions that expect the ResourceAddress type.
   329  func NewLegacyResourceInstanceAddress(addr addrs.AbsResourceInstance) *ResourceAddress {
   330  	ret := &ResourceAddress{
   331  		Type: addr.Resource.Resource.Type,
   332  		Name: addr.Resource.Resource.Name,
   333  	}
   334  
   335  	switch addr.Resource.Resource.Mode {
   336  	case addrs.ManagedResourceMode:
   337  		ret.Mode = ManagedResourceMode
   338  	case addrs.DataResourceMode:
   339  		ret.Mode = DataResourceMode
   340  	default:
   341  		panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Resource.Mode))
   342  	}
   343  
   344  	path := make([]string, len(addr.Module))
   345  	for i, step := range addr.Module {
   346  		if step.InstanceKey != addrs.NoKey {
   347  			// At the time of writing this can't happen because we don't
   348  			// ket generate keyed module instances. This legacy codepath must
   349  			// be removed before we can support "count" and "for_each" for
   350  			// modules.
   351  			panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey))
   352  		}
   353  
   354  		path[i] = step.Name
   355  	}
   356  	ret.Path = path
   357  
   358  	if addr.Resource.Key == addrs.NoKey {
   359  		ret.Index = -1
   360  	} else if ik, ok := addr.Resource.Key.(addrs.IntKey); ok {
   361  		ret.Index = int(ik)
   362  	} else if _, ok := addr.Resource.Key.(addrs.StringKey); ok {
   363  		ret.Index = -1
   364  	} else {
   365  		panic(fmt.Errorf("cannot shim resource instance with key %#v to legacy ResourceAddress.Index", addr.Resource.Key))
   366  	}
   367  
   368  	return ret
   369  }
   370  
   371  // AbsResourceInstanceAddr converts the receiver, a legacy resource address, to
   372  // the new resource address type addrs.AbsResourceInstance.
   373  //
   374  // This method can be used only on an address that has a resource specification.
   375  // It will panic if called on a module-path-only ResourceAddress. Use
   376  // method HasResourceSpec to check before calling, in contexts where it is
   377  // unclear.
   378  //
   379  // addrs.AbsResourceInstance does not represent the "tainted" and "deposed"
   380  // states, and so if these are present on the receiver then they are discarded.
   381  //
   382  // This is provided for shimming purposes so that we can easily adapt functions
   383  // that are returning the legacy ResourceAddress type, for situations where
   384  // the new type is required.
   385  func (addr *ResourceAddress) AbsResourceInstanceAddr() addrs.AbsResourceInstance {
   386  	if !addr.HasResourceSpec() {
   387  		panic("AbsResourceInstanceAddr called on ResourceAddress with no resource spec")
   388  	}
   389  
   390  	ret := addrs.AbsResourceInstance{
   391  		Module: addr.ModuleInstanceAddr(),
   392  		Resource: addrs.ResourceInstance{
   393  			Resource: addrs.Resource{
   394  				Type: addr.Type,
   395  				Name: addr.Name,
   396  			},
   397  		},
   398  	}
   399  
   400  	switch addr.Mode {
   401  	case ManagedResourceMode:
   402  		ret.Resource.Resource.Mode = addrs.ManagedResourceMode
   403  	case DataResourceMode:
   404  		ret.Resource.Resource.Mode = addrs.DataResourceMode
   405  	default:
   406  		panic(fmt.Errorf("cannot shim %s to addrs.ResourceMode value", addr.Mode))
   407  	}
   408  
   409  	if addr.Index != -1 {
   410  		ret.Resource.Key = addrs.IntKey(addr.Index)
   411  	}
   412  
   413  	return ret
   414  }
   415  
   416  // ModuleInstanceAddr returns the module path portion of the receiver as a
   417  // addrs.ModuleInstance value.
   418  func (addr *ResourceAddress) ModuleInstanceAddr() addrs.ModuleInstance {
   419  	path := make(addrs.ModuleInstance, len(addr.Path))
   420  	for i, name := range addr.Path {
   421  		path[i] = addrs.ModuleInstanceStep{Name: name}
   422  	}
   423  	return path
   424  }
   425  
   426  // Contains returns true if and only if the given node is contained within
   427  // the receiver.
   428  //
   429  // Containment is defined in terms of the module and resource hierarchy:
   430  // a resource is contained within its module and any ancestor modules,
   431  // an indexed resource instance is contained with the unindexed resource, etc.
   432  func (addr *ResourceAddress) Contains(other *ResourceAddress) bool {
   433  	ourPath := addr.Path
   434  	givenPath := other.Path
   435  	if len(givenPath) < len(ourPath) {
   436  		return false
   437  	}
   438  	for i := range ourPath {
   439  		if ourPath[i] != givenPath[i] {
   440  			return false
   441  		}
   442  	}
   443  
   444  	// If the receiver is a whole-module address then the path prefix
   445  	// matching is all we need.
   446  	if !addr.HasResourceSpec() {
   447  		return true
   448  	}
   449  
   450  	if addr.Type != other.Type || addr.Name != other.Name || addr.Mode != other.Mode {
   451  		return false
   452  	}
   453  
   454  	if addr.Index != -1 && addr.Index != other.Index {
   455  		return false
   456  	}
   457  
   458  	if addr.InstanceTypeSet && (addr.InstanceTypeSet != other.InstanceTypeSet || addr.InstanceType != other.InstanceType) {
   459  		return false
   460  	}
   461  
   462  	return true
   463  }
   464  
   465  // Equals returns true if the receiver matches the given address.
   466  //
   467  // The name of this method is a misnomer, since it doesn't test for exact
   468  // equality. Instead, it tests that the _specified_ parts of each
   469  // address match, treating any unspecified parts as wildcards.
   470  //
   471  // See also Contains, which takes a more hierarchical approach to comparing
   472  // addresses.
   473  func (addr *ResourceAddress) Equals(raw interface{}) bool {
   474  	other, ok := raw.(*ResourceAddress)
   475  	if !ok {
   476  		return false
   477  	}
   478  
   479  	pathMatch := len(addr.Path) == 0 && len(other.Path) == 0 ||
   480  		reflect.DeepEqual(addr.Path, other.Path)
   481  
   482  	indexMatch := addr.Index == -1 ||
   483  		other.Index == -1 ||
   484  		addr.Index == other.Index
   485  
   486  	nameMatch := addr.Name == "" ||
   487  		other.Name == "" ||
   488  		addr.Name == other.Name
   489  
   490  	typeMatch := addr.Type == "" ||
   491  		other.Type == "" ||
   492  		addr.Type == other.Type
   493  
   494  	// mode is significant only when type is set
   495  	modeMatch := addr.Type == "" ||
   496  		other.Type == "" ||
   497  		addr.Mode == other.Mode
   498  
   499  	return pathMatch &&
   500  		indexMatch &&
   501  		addr.InstanceType == other.InstanceType &&
   502  		nameMatch &&
   503  		typeMatch &&
   504  		modeMatch
   505  }
   506  
   507  // Less returns true if and only if the receiver should be sorted before
   508  // the given address when presenting a list of resource addresses to
   509  // an end-user.
   510  //
   511  // This sort uses lexicographic sorting for most components, but uses
   512  // numeric sort for indices, thus causing index 10 to sort after
   513  // index 9, rather than after index 1.
   514  func (addr *ResourceAddress) Less(other *ResourceAddress) bool {
   515  
   516  	switch {
   517  
   518  	case len(addr.Path) != len(other.Path):
   519  		return len(addr.Path) < len(other.Path)
   520  
   521  	case !reflect.DeepEqual(addr.Path, other.Path):
   522  		// If the two paths are the same length but don't match, we'll just
   523  		// cheat and compare the string forms since it's easier than
   524  		// comparing all of the path segments in turn, and lexicographic
   525  		// comparison is correct for the module path portion.
   526  		addrStr := addr.String()
   527  		otherStr := other.String()
   528  		return addrStr < otherStr
   529  
   530  	case addr.Mode != other.Mode:
   531  		return addr.Mode == DataResourceMode
   532  
   533  	case addr.Type != other.Type:
   534  		return addr.Type < other.Type
   535  
   536  	case addr.Name != other.Name:
   537  		return addr.Name < other.Name
   538  
   539  	case addr.Index != other.Index:
   540  		// Since "Index" is -1 for an un-indexed address, this also conveniently
   541  		// sorts unindexed addresses before indexed ones, should they both
   542  		// appear for some reason.
   543  		return addr.Index < other.Index
   544  
   545  	case addr.InstanceTypeSet != other.InstanceTypeSet:
   546  		return !addr.InstanceTypeSet
   547  
   548  	case addr.InstanceType != other.InstanceType:
   549  		// InstanceType is actually an enum, so this is just an arbitrary
   550  		// sort based on the enum numeric values, and thus not particularly
   551  		// meaningful.
   552  		return addr.InstanceType < other.InstanceType
   553  
   554  	default:
   555  		return false
   556  
   557  	}
   558  }
   559  
   560  func ParseResourceIndex(s string) (int, error) {
   561  	if s == "" {
   562  		return -1, nil
   563  	}
   564  	return strconv.Atoi(s)
   565  }
   566  
   567  func ParseResourcePath(s string) []string {
   568  	if s == "" {
   569  		return nil
   570  	}
   571  	parts := strings.Split(s, ".")
   572  	path := make([]string, 0, len(parts))
   573  	for _, s := range parts {
   574  		// Due to the limitations of the regexp match below, the path match has
   575  		// some noise in it we have to filter out :|
   576  		if s == "" || s == "module" {
   577  			continue
   578  		}
   579  		path = append(path, s)
   580  	}
   581  	return path
   582  }
   583  
   584  func ParseInstanceType(s string) (InstanceType, error) {
   585  	switch s {
   586  	case "", "primary":
   587  		return TypePrimary, nil
   588  	case "deposed":
   589  		return TypeDeposed, nil
   590  	case "tainted":
   591  		return TypeTainted, nil
   592  	default:
   593  		return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s)
   594  	}
   595  }
   596  
   597  func tokenizeResourceAddress(s string) (map[string]string, error) {
   598  	// Example of portions of the regexp below using the
   599  	// string "aws_instance.web.tainted[1]"
   600  	re := regexp.MustCompile(`\A` +
   601  		// "module.foo.module.bar" (optional)
   602  		`(?P<path>(?:module\.(?P<module_name>[^.]+)\.?)*)` +
   603  		// possibly "data.", if targeting is a data resource
   604  		`(?P<data_prefix>(?:data\.)?)` +
   605  		// "aws_instance.web" (optional when module path specified)
   606  		`(?:(?P<type>[^.]+)\.(?P<name>[^.[]+))?` +
   607  		// "tainted" (optional, omission implies: "primary")
   608  		`(?:\.(?P<instance_type>\w+))?` +
   609  		// "1" (optional, omission implies: "0")
   610  		`(?:\[(?P<index>\d+)\])?` +
   611  		`\z`)
   612  
   613  	groupNames := re.SubexpNames()
   614  	rawMatches := re.FindAllStringSubmatch(s, -1)
   615  	if len(rawMatches) != 1 {
   616  		return nil, fmt.Errorf("invalid resource address %q", s)
   617  	}
   618  
   619  	matches := make(map[string]string)
   620  	for i, m := range rawMatches[0] {
   621  		matches[groupNames[i]] = m
   622  	}
   623  
   624  	return matches, nil
   625  }