mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 08:00:17 -06:00
e9fe0f1118
Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
433 lines
14 KiB
Go
433 lines
14 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package addrs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// Target describes a targeted address with source location information.
|
|
type Target struct {
|
|
Subject Targetable
|
|
SourceRange tfdiags.SourceRange
|
|
}
|
|
|
|
// ParseTarget attempts to interpret the given traversal as a targetable
|
|
// address. The given traversal must be absolute, or this function will
|
|
// panic.
|
|
//
|
|
// If no error diagnostics are returned, the returned target includes the
|
|
// address that was extracted and the source range it was extracted from.
|
|
//
|
|
// If error diagnostics are returned then the Target value is invalid and
|
|
// must not be used.
|
|
func ParseTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) {
|
|
path, remain, diags := parseModuleInstancePrefix(traversal)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange())
|
|
|
|
if len(remain) == 0 {
|
|
return &Target{
|
|
Subject: path,
|
|
SourceRange: rng,
|
|
}, diags
|
|
}
|
|
|
|
riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
|
|
diags = diags.Append(moreDiags)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
var subject Targetable
|
|
switch {
|
|
case riAddr.Resource.Key == NoKey:
|
|
// We always assume that a no-key instance is meant to
|
|
// be referring to the whole resource, because the distinction
|
|
// doesn't really matter for targets anyway.
|
|
subject = riAddr.ContainingResource()
|
|
default:
|
|
subject = riAddr
|
|
}
|
|
|
|
return &Target{
|
|
Subject: subject,
|
|
SourceRange: rng,
|
|
}, diags
|
|
}
|
|
|
|
func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) {
|
|
// Note that this helper is used as part of both ParseTarget and
|
|
// ParseMoveEndpoint, so its error messages should be generic
|
|
// enough to suit both situations.
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
mode := ManagedResourceMode
|
|
if remain.RootName() == "data" {
|
|
mode = DataResourceMode
|
|
remain = remain[1:]
|
|
}
|
|
|
|
typeName, name, diags := parseResourceTypeAndName(remain, mode)
|
|
if diags.HasErrors() {
|
|
return AbsResourceInstance{}, diags
|
|
}
|
|
|
|
remain = remain[2:]
|
|
switch len(remain) {
|
|
case 0:
|
|
return moduleAddr.ResourceInstance(mode, typeName, name, NoKey), diags
|
|
case 1:
|
|
if tt, ok := remain[0].(hcl.TraverseIndex); ok {
|
|
key, err := ParseInstanceKey(tt.Key)
|
|
if err != nil {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: fmt.Sprintf("Invalid resource instance key: %s.", err),
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
})
|
|
return AbsResourceInstance{}, diags
|
|
}
|
|
|
|
return moduleAddr.ResourceInstance(mode, typeName, name, key), diags
|
|
} else {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "Resource instance key must be given in square brackets.",
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
})
|
|
return AbsResourceInstance{}, diags
|
|
}
|
|
default:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "Unexpected extra operators after address.",
|
|
Subject: remain[1].SourceRange().Ptr(),
|
|
})
|
|
return AbsResourceInstance{}, diags
|
|
}
|
|
}
|
|
|
|
// parseResourceUnderModule is a helper function that parses a traversal, which
|
|
// is an address (or a part of an address) that describes a resource (e.g.
|
|
// ["null_resource," "boop"] or ["data", "null_data_source," "bip"]), under a
|
|
// module. It returns the ConfigResource that represents the resource address.
|
|
// It does not support addresses of resources with instance keys, and will
|
|
// return an error if it encounters one (unlike
|
|
// parseResourceInstanceUnderModule).
|
|
// This function does not expect to encounter a module prefix in the traversal,
|
|
// as it should be processed by parseModulePrefix first.
|
|
func parseResourceUnderModule(moduleAddr Module, remain hcl.Traversal) (ConfigResource, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
mode := ManagedResourceMode
|
|
if remain.RootName() == "data" {
|
|
mode = DataResourceMode
|
|
remain = remain[1:]
|
|
}
|
|
|
|
typeName, name, diags := parseResourceTypeAndName(remain, mode)
|
|
if diags.HasErrors() {
|
|
return ConfigResource{}, diags
|
|
}
|
|
|
|
remain = remain[2:]
|
|
switch len(remain) {
|
|
case 0:
|
|
return moduleAddr.Resource(mode, typeName, name), diags
|
|
case 1:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Resource instance address with keys is not allowed",
|
|
Detail: "Resource address cannot be a resource instance (e.g. \"null_resource.a[0]\"), it must be a resource instead (e.g. \"null_resource.a\").",
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
})
|
|
return ConfigResource{}, diags
|
|
default:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "Unexpected extra operators after address.",
|
|
Subject: remain[1].SourceRange().Ptr(),
|
|
})
|
|
return ConfigResource{}, diags
|
|
}
|
|
}
|
|
|
|
// parseResourceTypeAndName is a helper function that parses a traversal, which
|
|
// is an address (or a part of an address) that describes a resource (e.g.
|
|
// ["null_resource," "boop"]) and returns its type and name.
|
|
// It is used in parseResourceUnderModule and parseResourceInstanceUnderModule,
|
|
// and does not expect to encounter a module prefix in the traversal.
|
|
func parseResourceTypeAndName(remain hcl.Traversal, mode ResourceMode) (typeName, name string, diags tfdiags.Diagnostics) {
|
|
if len(remain) < 2 {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "Resource specification must include a resource type and name.",
|
|
Subject: remain.SourceRange().Ptr(),
|
|
})
|
|
return typeName, name, diags
|
|
}
|
|
|
|
switch tt := remain[0].(type) {
|
|
case hcl.TraverseRoot:
|
|
typeName = tt.Name
|
|
case hcl.TraverseAttr:
|
|
typeName = tt.Name
|
|
default:
|
|
switch mode {
|
|
case ManagedResourceMode:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource type name is required.",
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
})
|
|
case DataResourceMode:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A data source name is required.",
|
|
Subject: remain[0].SourceRange().Ptr(),
|
|
})
|
|
default:
|
|
panic("unknown mode")
|
|
}
|
|
return typeName, name, diags
|
|
}
|
|
|
|
switch tt := remain[1].(type) {
|
|
case hcl.TraverseAttr:
|
|
name = tt.Name
|
|
default:
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource name is required.",
|
|
Subject: remain[1].SourceRange().Ptr(),
|
|
})
|
|
return typeName, name, diags
|
|
}
|
|
|
|
return typeName, name, diags
|
|
}
|
|
|
|
// ParseTargetStr is a helper wrapper around ParseTarget that takes a string
|
|
// and parses it with the HCL native syntax traversal parser before
|
|
// interpreting it.
|
|
//
|
|
// This should be used only in specialized situations since it will cause the
|
|
// created references to not have any meaningful source location information.
|
|
// If a target string is coming from a source that should be identified in
|
|
// error messages then the caller should instead parse it directly using a
|
|
// suitable function from the HCL API and pass the traversal itself to
|
|
// ParseTarget.
|
|
//
|
|
// Error diagnostics are returned if either the parsing fails or the analysis
|
|
// of the traversal fails. There is no way for the caller to distinguish the
|
|
// two kinds of diagnostics programmatically. If error diagnostics are returned
|
|
// the returned target may be nil or incomplete.
|
|
func ParseTargetStr(str string) (*Target, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
|
|
diags = diags.Append(parseDiags)
|
|
if parseDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
target, targetDiags := ParseTarget(traversal)
|
|
diags = diags.Append(targetDiags)
|
|
return target, diags
|
|
}
|
|
|
|
// ParseAbsResource attempts to interpret the given traversal as an absolute
|
|
// resource address, using the same syntax as expected by ParseTarget.
|
|
//
|
|
// If no error diagnostics are returned, the returned target includes the
|
|
// address that was extracted and the source range it was extracted from.
|
|
//
|
|
// If error diagnostics are returned then the AbsResource value is invalid and
|
|
// must not be used.
|
|
func ParseAbsResource(traversal hcl.Traversal) (AbsResource, tfdiags.Diagnostics) {
|
|
addr, diags := ParseTarget(traversal)
|
|
if diags.HasErrors() {
|
|
return AbsResource{}, diags
|
|
}
|
|
|
|
switch tt := addr.Subject.(type) {
|
|
|
|
case AbsResource:
|
|
return tt, diags
|
|
|
|
case AbsResourceInstance: // Catch likely user error with specialized message
|
|
// Assume that the last element of the traversal must be the index,
|
|
// since that's required for a valid resource instance address.
|
|
indexStep := traversal[len(traversal)-1]
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource address is required. This instance key identifies a specific resource instance, which is not expected here.",
|
|
Subject: indexStep.SourceRange().Ptr(),
|
|
})
|
|
return AbsResource{}, diags
|
|
|
|
case ModuleInstance: // Catch likely user error with specialized message
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource address is required here. The module path must be followed by a resource specification.",
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
})
|
|
return AbsResource{}, diags
|
|
|
|
default: // Generic message for other address types
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource address is required here.",
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
})
|
|
return AbsResource{}, diags
|
|
|
|
}
|
|
}
|
|
|
|
// ParseAbsResourceStr is a helper wrapper around ParseAbsResource that takes a
|
|
// string and parses it with the HCL native syntax traversal parser before
|
|
// interpreting it.
|
|
//
|
|
// Error diagnostics are returned if either the parsing fails or the analysis
|
|
// of the traversal fails. There is no way for the caller to distinguish the
|
|
// two kinds of diagnostics programmatically. If error diagnostics are returned
|
|
// the returned address may be incomplete.
|
|
//
|
|
// Since this function has no context about the source of the given string,
|
|
// any returned diagnostics will not have meaningful source location
|
|
// information.
|
|
func ParseAbsResourceStr(str string) (AbsResource, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
|
|
diags = diags.Append(parseDiags)
|
|
if parseDiags.HasErrors() {
|
|
return AbsResource{}, diags
|
|
}
|
|
|
|
addr, addrDiags := ParseAbsResource(traversal)
|
|
diags = diags.Append(addrDiags)
|
|
return addr, diags
|
|
}
|
|
|
|
// ParseAbsResourceInstance attempts to interpret the given traversal as an
|
|
// absolute resource instance address, using the same syntax as expected by
|
|
// ParseTarget.
|
|
//
|
|
// If no error diagnostics are returned, the returned target includes the
|
|
// address that was extracted and the source range it was extracted from.
|
|
//
|
|
// If error diagnostics are returned then the AbsResource value is invalid and
|
|
// must not be used.
|
|
func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) {
|
|
addr, diags := ParseTarget(traversal)
|
|
if diags.HasErrors() {
|
|
return AbsResourceInstance{}, diags
|
|
}
|
|
|
|
switch tt := addr.Subject.(type) {
|
|
|
|
case AbsResource:
|
|
return tt.Instance(NoKey), diags
|
|
|
|
case AbsResourceInstance:
|
|
return tt, diags
|
|
|
|
case ModuleInstance: // Catch likely user error with specialized message
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource instance address is required here. The module path must be followed by a resource instance specification.",
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
})
|
|
return AbsResourceInstance{}, diags
|
|
|
|
default: // Generic message for other address types
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid address",
|
|
Detail: "A resource address is required here.",
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
})
|
|
return AbsResourceInstance{}, diags
|
|
|
|
}
|
|
}
|
|
|
|
// ParseAbsResourceInstanceStr is a helper wrapper around
|
|
// ParseAbsResourceInstance that takes a string and parses it with the HCL
|
|
// native syntax traversal parser before interpreting it.
|
|
//
|
|
// Error diagnostics are returned if either the parsing fails or the analysis
|
|
// of the traversal fails. There is no way for the caller to distinguish the
|
|
// two kinds of diagnostics programmatically. If error diagnostics are returned
|
|
// the returned address may be incomplete.
|
|
//
|
|
// Since this function has no context about the source of the given string,
|
|
// any returned diagnostics will not have meaningful source location
|
|
// information.
|
|
func ParseAbsResourceInstanceStr(str string) (AbsResourceInstance, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
|
|
diags = diags.Append(parseDiags)
|
|
if parseDiags.HasErrors() {
|
|
return AbsResourceInstance{}, diags
|
|
}
|
|
|
|
addr, addrDiags := ParseAbsResourceInstance(traversal)
|
|
diags = diags.Append(addrDiags)
|
|
return addr, diags
|
|
}
|
|
|
|
// ModuleAddr returns the module address portion of the subject of
|
|
// the recieving target.
|
|
//
|
|
// Regardless of specific address type, all targets always include
|
|
// a module address. They might also include something in that
|
|
// module, which this method always discards if so.
|
|
func (t *Target) ModuleAddr() ModuleInstance {
|
|
switch addr := t.Subject.(type) {
|
|
case ModuleInstance:
|
|
return addr
|
|
case Module:
|
|
// We assume that a module address is really
|
|
// referring to a module path containing only
|
|
// single-instance modules.
|
|
return addr.UnkeyedInstanceShim()
|
|
case AbsResourceInstance:
|
|
return addr.Module
|
|
case AbsResource:
|
|
return addr.Module
|
|
default:
|
|
// The above cases should be exhaustive for all
|
|
// implementations of Targetable.
|
|
panic(fmt.Sprintf("unsupported target address type %T", addr))
|
|
}
|
|
}
|