| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package terraform |
| |
| import ( |
| "fmt" |
| "reflect" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs" |
| ) |
| |
| // ResourceAddress is a way of identifying an individual resource (or, |
| // eventually, a subset of resources) within the state. It is used for Targets. |
| type ResourceAddress struct { |
| // Addresses a resource falling somewhere in the module path |
| // When specified alone, addresses all resources within a module path |
| Path []string |
| |
| // Addresses a specific resource that occurs in a list |
| Index int |
| |
| InstanceType InstanceType |
| InstanceTypeSet bool |
| Name string |
| Type string |
| Mode ResourceMode // significant only if InstanceTypeSet |
| } |
| |
| // Copy returns a copy of this ResourceAddress |
| func (r *ResourceAddress) Copy() *ResourceAddress { |
| if r == nil { |
| return nil |
| } |
| |
| n := &ResourceAddress{ |
| Path: make([]string, 0, len(r.Path)), |
| Index: r.Index, |
| InstanceType: r.InstanceType, |
| Name: r.Name, |
| Type: r.Type, |
| Mode: r.Mode, |
| } |
| |
| n.Path = append(n.Path, r.Path...) |
| |
| return n |
| } |
| |
| // String outputs the address that parses into this address. |
| func (r *ResourceAddress) String() string { |
| var result []string |
| for _, p := range r.Path { |
| result = append(result, "module", p) |
| } |
| |
| switch r.Mode { |
| case ManagedResourceMode: |
| // nothing to do |
| case DataResourceMode: |
| result = append(result, "data") |
| default: |
| panic(fmt.Errorf("unsupported resource mode %s", r.Mode)) |
| } |
| |
| if r.Type != "" { |
| result = append(result, r.Type) |
| } |
| |
| if r.Name != "" { |
| name := r.Name |
| if r.InstanceTypeSet { |
| switch r.InstanceType { |
| case TypePrimary: |
| name += ".primary" |
| case TypeDeposed: |
| name += ".deposed" |
| case TypeTainted: |
| name += ".tainted" |
| } |
| } |
| |
| if r.Index >= 0 { |
| name += fmt.Sprintf("[%d]", r.Index) |
| } |
| result = append(result, name) |
| } |
| |
| return strings.Join(result, ".") |
| } |
| |
| // HasResourceSpec returns true if the address has a resource spec, as |
| // defined in the documentation: |
| // |
| // https://www.terraform.io/docs/cli/state/resource-addressing.html |
| // |
| // In particular, this returns false if the address contains only |
| // a module path, thus addressing the entire module. |
| func (r *ResourceAddress) HasResourceSpec() bool { |
| return r.Type != "" && r.Name != "" |
| } |
| |
| // WholeModuleAddress returns the resource address that refers to all |
| // resources in the same module as the receiver address. |
| func (r *ResourceAddress) WholeModuleAddress() *ResourceAddress { |
| return &ResourceAddress{ |
| Path: r.Path, |
| Index: -1, |
| InstanceTypeSet: false, |
| } |
| } |
| |
| // MatchesResourceConfig returns true if the receiver matches the given |
| // configuration resource within the given _static_ module path. Note that |
| // the module path in a resource address is a _dynamic_ module path, and |
| // multiple dynamic resource paths may map to a single static path if |
| // count and for_each are in use on module calls. |
| // |
| // Since resource configuration blocks represent all of the instances of |
| // a multi-instance resource, the index of the address (if any) is not |
| // considered. |
| func (r *ResourceAddress) MatchesResourceConfig(path addrs.Module, rc *configs.Resource) bool { |
| if r.HasResourceSpec() { |
| // FIXME: Some ugliness while we are between worlds. Functionality |
| // in "addrs" should eventually replace this ResourceAddress idea |
| // completely, but for now we'll need to translate to the old |
| // way of representing resource modes. |
| switch r.Mode { |
| case ManagedResourceMode: |
| if rc.Mode != addrs.ManagedResourceMode { |
| return false |
| } |
| case DataResourceMode: |
| if rc.Mode != addrs.DataResourceMode { |
| return false |
| } |
| } |
| if r.Type != rc.Type || r.Name != rc.Name { |
| return false |
| } |
| } |
| |
| addrPath := r.Path |
| |
| // normalize |
| if len(addrPath) == 0 { |
| addrPath = nil |
| } |
| if len(path) == 0 { |
| path = nil |
| } |
| rawPath := []string(path) |
| return reflect.DeepEqual(addrPath, rawPath) |
| } |
| |
| // stateId returns the ID that this resource should be entered with |
| // in the state. This is also used for diffs. In the future, we'd like to |
| // move away from this string field so I don't export this. |
| func (r *ResourceAddress) stateId() string { |
| result := fmt.Sprintf("%s.%s", r.Type, r.Name) |
| switch r.Mode { |
| case ManagedResourceMode: |
| // Done |
| case DataResourceMode: |
| result = fmt.Sprintf("data.%s", result) |
| default: |
| panic(fmt.Errorf("unknown resource mode: %s", r.Mode)) |
| } |
| if r.Index >= 0 { |
| result += fmt.Sprintf(".%d", r.Index) |
| } |
| |
| return result |
| } |
| |
| // parseResourceAddressInternal parses the somewhat bespoke resource |
| // identifier used in states and diffs, such as "instance.name.0". |
| func parseResourceAddressInternal(s string) (*ResourceAddress, error) { |
| // Split based on ".". Every resource address should have at least two |
| // elements (type and name). |
| parts := strings.Split(s, ".") |
| if len(parts) < 2 || len(parts) > 4 { |
| return nil, fmt.Errorf("Invalid internal resource address format: %s", s) |
| } |
| |
| // Data resource if we have at least 3 parts and the first one is data |
| mode := ManagedResourceMode |
| if len(parts) > 2 && parts[0] == "data" { |
| mode = DataResourceMode |
| parts = parts[1:] |
| } |
| |
| // If we're not a data resource and we have more than 3, then it is an error |
| if len(parts) > 3 && mode != DataResourceMode { |
| return nil, fmt.Errorf("Invalid internal resource address format: %s", s) |
| } |
| |
| // Build the parts of the resource address that are guaranteed to exist |
| addr := &ResourceAddress{ |
| Type: parts[0], |
| Name: parts[1], |
| Index: -1, |
| InstanceType: TypePrimary, |
| Mode: mode, |
| } |
| |
| // If we have more parts, then we have an index. Parse that. |
| if len(parts) > 2 { |
| idx, err := strconv.ParseInt(parts[2], 0, 0) |
| if err != nil { |
| return nil, fmt.Errorf("Error parsing resource address %q: %s", s, err) |
| } |
| |
| addr.Index = int(idx) |
| } |
| |
| return addr, nil |
| } |
| |
| func ParseResourceAddress(s string) (*ResourceAddress, error) { |
| matches, err := tokenizeResourceAddress(s) |
| if err != nil { |
| return nil, err |
| } |
| mode := ManagedResourceMode |
| if matches["data_prefix"] != "" { |
| mode = DataResourceMode |
| } |
| resourceIndex, err := ParseResourceIndex(matches["index"]) |
| if err != nil { |
| return nil, err |
| } |
| instanceType, err := ParseInstanceType(matches["instance_type"]) |
| if err != nil { |
| return nil, err |
| } |
| path := ParseResourcePath(matches["path"]) |
| |
| // not allowed to say "data." without a type following |
| if mode == DataResourceMode && matches["type"] == "" { |
| return nil, fmt.Errorf( |
| "invalid resource address %q: must target specific data instance", |
| s, |
| ) |
| } |
| |
| return &ResourceAddress{ |
| Path: path, |
| Index: resourceIndex, |
| InstanceType: instanceType, |
| InstanceTypeSet: matches["instance_type"] != "", |
| Name: matches["name"], |
| Type: matches["type"], |
| Mode: mode, |
| }, nil |
| } |
| |
| // ParseResourceAddressForInstanceDiff creates a ResourceAddress for a |
| // resource name as described in a module diff. |
| // |
| // For historical reasons a different addressing format is used in this |
| // context. The internal format should not be shown in the UI and instead |
| // this function should be used to translate to a ResourceAddress and |
| // then, where appropriate, use the String method to produce a canonical |
| // resource address string for display in the UI. |
| // |
| // The given path slice must be empty (or nil) for the root module, and |
| // otherwise consist of a sequence of module names traversing down into |
| // the module tree. If a non-nil path is provided, the caller must not |
| // modify its underlying array after passing it to this function. |
| func ParseResourceAddressForInstanceDiff(path []string, key string) (*ResourceAddress, error) { |
| addr, err := parseResourceAddressInternal(key) |
| if err != nil { |
| return nil, err |
| } |
| addr.Path = path |
| return addr, nil |
| } |
| |
| // NewLegacyResourceAddress creates a ResourceAddress from a new-style |
| // addrs.AbsResource value. |
| // |
| // This is provided for shimming purposes so that we can still easily call into |
| // older functions that expect the ResourceAddress type. |
| func NewLegacyResourceAddress(addr addrs.AbsResource) *ResourceAddress { |
| ret := &ResourceAddress{ |
| Type: addr.Resource.Type, |
| Name: addr.Resource.Name, |
| } |
| |
| switch addr.Resource.Mode { |
| case addrs.ManagedResourceMode: |
| ret.Mode = ManagedResourceMode |
| case addrs.DataResourceMode: |
| ret.Mode = DataResourceMode |
| default: |
| panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Mode)) |
| } |
| |
| path := make([]string, len(addr.Module)) |
| for i, step := range addr.Module { |
| if step.InstanceKey != addrs.NoKey { |
| // At the time of writing this can't happen because we don't |
| // ket generate keyed module instances. This legacy codepath must |
| // be removed before we can support "count" and "for_each" for |
| // modules. |
| panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey)) |
| } |
| |
| path[i] = step.Name |
| } |
| ret.Path = path |
| ret.Index = -1 |
| |
| return ret |
| } |
| |
| // NewLegacyResourceInstanceAddress creates a ResourceAddress from a new-style |
| // addrs.AbsResource value. |
| // |
| // This is provided for shimming purposes so that we can still easily call into |
| // older functions that expect the ResourceAddress type. |
| func NewLegacyResourceInstanceAddress(addr addrs.AbsResourceInstance) *ResourceAddress { |
| ret := &ResourceAddress{ |
| Type: addr.Resource.Resource.Type, |
| Name: addr.Resource.Resource.Name, |
| } |
| |
| switch addr.Resource.Resource.Mode { |
| case addrs.ManagedResourceMode: |
| ret.Mode = ManagedResourceMode |
| case addrs.DataResourceMode: |
| ret.Mode = DataResourceMode |
| default: |
| panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Resource.Mode)) |
| } |
| |
| path := make([]string, len(addr.Module)) |
| for i, step := range addr.Module { |
| if step.InstanceKey != addrs.NoKey { |
| // At the time of writing this can't happen because we don't |
| // ket generate keyed module instances. This legacy codepath must |
| // be removed before we can support "count" and "for_each" for |
| // modules. |
| panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey)) |
| } |
| |
| path[i] = step.Name |
| } |
| ret.Path = path |
| |
| if addr.Resource.Key == addrs.NoKey { |
| ret.Index = -1 |
| } else if ik, ok := addr.Resource.Key.(addrs.IntKey); ok { |
| ret.Index = int(ik) |
| } else if _, ok := addr.Resource.Key.(addrs.StringKey); ok { |
| ret.Index = -1 |
| } else { |
| panic(fmt.Errorf("cannot shim resource instance with key %#v to legacy ResourceAddress.Index", addr.Resource.Key)) |
| } |
| |
| return ret |
| } |
| |
| // AbsResourceInstanceAddr converts the receiver, a legacy resource address, to |
| // the new resource address type addrs.AbsResourceInstance. |
| // |
| // This method can be used only on an address that has a resource specification. |
| // It will panic if called on a module-path-only ResourceAddress. Use |
| // method HasResourceSpec to check before calling, in contexts where it is |
| // unclear. |
| // |
| // addrs.AbsResourceInstance does not represent the "tainted" and "deposed" |
| // states, and so if these are present on the receiver then they are discarded. |
| // |
| // This is provided for shimming purposes so that we can easily adapt functions |
| // that are returning the legacy ResourceAddress type, for situations where |
| // the new type is required. |
| func (addr *ResourceAddress) AbsResourceInstanceAddr() addrs.AbsResourceInstance { |
| if !addr.HasResourceSpec() { |
| panic("AbsResourceInstanceAddr called on ResourceAddress with no resource spec") |
| } |
| |
| ret := addrs.AbsResourceInstance{ |
| Module: addr.ModuleInstanceAddr(), |
| Resource: addrs.ResourceInstance{ |
| Resource: addrs.Resource{ |
| Type: addr.Type, |
| Name: addr.Name, |
| }, |
| }, |
| } |
| |
| switch addr.Mode { |
| case ManagedResourceMode: |
| ret.Resource.Resource.Mode = addrs.ManagedResourceMode |
| case DataResourceMode: |
| ret.Resource.Resource.Mode = addrs.DataResourceMode |
| default: |
| panic(fmt.Errorf("cannot shim %s to addrs.ResourceMode value", addr.Mode)) |
| } |
| |
| if addr.Index != -1 { |
| ret.Resource.Key = addrs.IntKey(addr.Index) |
| } |
| |
| return ret |
| } |
| |
| // ModuleInstanceAddr returns the module path portion of the receiver as a |
| // addrs.ModuleInstance value. |
| func (addr *ResourceAddress) ModuleInstanceAddr() addrs.ModuleInstance { |
| path := make(addrs.ModuleInstance, len(addr.Path)) |
| for i, name := range addr.Path { |
| path[i] = addrs.ModuleInstanceStep{Name: name} |
| } |
| return path |
| } |
| |
| // Contains returns true if and only if the given node is contained within |
| // the receiver. |
| // |
| // Containment is defined in terms of the module and resource hierarchy: |
| // a resource is contained within its module and any ancestor modules, |
| // an indexed resource instance is contained with the unindexed resource, etc. |
| func (addr *ResourceAddress) Contains(other *ResourceAddress) bool { |
| ourPath := addr.Path |
| givenPath := other.Path |
| if len(givenPath) < len(ourPath) { |
| return false |
| } |
| for i := range ourPath { |
| if ourPath[i] != givenPath[i] { |
| return false |
| } |
| } |
| |
| // If the receiver is a whole-module address then the path prefix |
| // matching is all we need. |
| if !addr.HasResourceSpec() { |
| return true |
| } |
| |
| if addr.Type != other.Type || addr.Name != other.Name || addr.Mode != other.Mode { |
| return false |
| } |
| |
| if addr.Index != -1 && addr.Index != other.Index { |
| return false |
| } |
| |
| if addr.InstanceTypeSet && (addr.InstanceTypeSet != other.InstanceTypeSet || addr.InstanceType != other.InstanceType) { |
| return false |
| } |
| |
| return true |
| } |
| |
| // Equals returns true if the receiver matches the given address. |
| // |
| // The name of this method is a misnomer, since it doesn't test for exact |
| // equality. Instead, it tests that the _specified_ parts of each |
| // address match, treating any unspecified parts as wildcards. |
| // |
| // See also Contains, which takes a more hierarchical approach to comparing |
| // addresses. |
| func (addr *ResourceAddress) Equals(raw interface{}) bool { |
| other, ok := raw.(*ResourceAddress) |
| if !ok { |
| return false |
| } |
| |
| pathMatch := len(addr.Path) == 0 && len(other.Path) == 0 || |
| reflect.DeepEqual(addr.Path, other.Path) |
| |
| indexMatch := addr.Index == -1 || |
| other.Index == -1 || |
| addr.Index == other.Index |
| |
| nameMatch := addr.Name == "" || |
| other.Name == "" || |
| addr.Name == other.Name |
| |
| typeMatch := addr.Type == "" || |
| other.Type == "" || |
| addr.Type == other.Type |
| |
| // mode is significant only when type is set |
| modeMatch := addr.Type == "" || |
| other.Type == "" || |
| addr.Mode == other.Mode |
| |
| return pathMatch && |
| indexMatch && |
| addr.InstanceType == other.InstanceType && |
| nameMatch && |
| typeMatch && |
| modeMatch |
| } |
| |
| // Less returns true if and only if the receiver should be sorted before |
| // the given address when presenting a list of resource addresses to |
| // an end-user. |
| // |
| // This sort uses lexicographic sorting for most components, but uses |
| // numeric sort for indices, thus causing index 10 to sort after |
| // index 9, rather than after index 1. |
| func (addr *ResourceAddress) Less(other *ResourceAddress) bool { |
| |
| switch { |
| |
| case len(addr.Path) != len(other.Path): |
| return len(addr.Path) < len(other.Path) |
| |
| case !reflect.DeepEqual(addr.Path, other.Path): |
| // If the two paths are the same length but don't match, we'll just |
| // cheat and compare the string forms since it's easier than |
| // comparing all of the path segments in turn, and lexicographic |
| // comparison is correct for the module path portion. |
| addrStr := addr.String() |
| otherStr := other.String() |
| return addrStr < otherStr |
| |
| case addr.Mode != other.Mode: |
| return addr.Mode == DataResourceMode |
| |
| case addr.Type != other.Type: |
| return addr.Type < other.Type |
| |
| case addr.Name != other.Name: |
| return addr.Name < other.Name |
| |
| case addr.Index != other.Index: |
| // Since "Index" is -1 for an un-indexed address, this also conveniently |
| // sorts unindexed addresses before indexed ones, should they both |
| // appear for some reason. |
| return addr.Index < other.Index |
| |
| case addr.InstanceTypeSet != other.InstanceTypeSet: |
| return !addr.InstanceTypeSet |
| |
| case addr.InstanceType != other.InstanceType: |
| // InstanceType is actually an enum, so this is just an arbitrary |
| // sort based on the enum numeric values, and thus not particularly |
| // meaningful. |
| return addr.InstanceType < other.InstanceType |
| |
| default: |
| return false |
| |
| } |
| } |
| |
| func ParseResourceIndex(s string) (int, error) { |
| if s == "" { |
| return -1, nil |
| } |
| return strconv.Atoi(s) |
| } |
| |
| func ParseResourcePath(s string) []string { |
| if s == "" { |
| return nil |
| } |
| parts := strings.Split(s, ".") |
| path := make([]string, 0, len(parts)) |
| for _, s := range parts { |
| // Due to the limitations of the regexp match below, the path match has |
| // some noise in it we have to filter out :| |
| if s == "" || s == "module" { |
| continue |
| } |
| path = append(path, s) |
| } |
| return path |
| } |
| |
| func ParseInstanceType(s string) (InstanceType, error) { |
| switch s { |
| case "", "primary": |
| return TypePrimary, nil |
| case "deposed": |
| return TypeDeposed, nil |
| case "tainted": |
| return TypeTainted, nil |
| default: |
| return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s) |
| } |
| } |
| |
| func tokenizeResourceAddress(s string) (map[string]string, error) { |
| // Example of portions of the regexp below using the |
| // string "aws_instance.web.tainted[1]" |
| re := regexp.MustCompile(`\A` + |
| // "module.foo.module.bar" (optional) |
| `(?P<path>(?:module\.(?P<module_name>[^.]+)\.?)*)` + |
| // possibly "data.", if targeting is a data resource |
| `(?P<data_prefix>(?:data\.)?)` + |
| // "aws_instance.web" (optional when module path specified) |
| `(?:(?P<type>[^.]+)\.(?P<name>[^.[]+))?` + |
| // "tainted" (optional, omission implies: "primary") |
| `(?:\.(?P<instance_type>\w+))?` + |
| // "1" (optional, omission implies: "0") |
| `(?:\[(?P<index>\d+)\])?` + |
| `\z`) |
| |
| groupNames := re.SubexpNames() |
| rawMatches := re.FindAllStringSubmatch(s, -1) |
| if len(rawMatches) != 1 { |
| return nil, fmt.Errorf("invalid resource address %q", s) |
| } |
| |
| matches := make(map[string]string) |
| for i, m := range rawMatches[0] { |
| matches[groupNames[i]] = m |
| } |
| |
| return matches, nil |
| } |