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 }