opentofu/internal/addrs/move_endpoint.go
Martin Atkins 22eee529e3 addrs: MoveEndpointInModule
We previously built out addrs.UnifyMoveEndpoints with a different
implementation strategy in mind, but that design turns out to not be
viable because it forces us to move to AbsMoveable addresses too soon,
before we've done the analysis required to identify chained and nested
moves.

Instead, UnifyMoveEndpoints will return a new type MoveEndpointInModule
which conceptually represents a matching pattern which either matches or
doesn't match a particular AbsMoveable. It does this by just binding the
unified relative address from the MoveEndpoint to the module where it
was declared, and thus allows us to distinguish between the part of the
module path which applies to any instances of the given modules vs. the
user-specified part which must identify particular module instances.
2021-07-14 17:37:48 -07:00

297 lines
11 KiB
Go

package addrs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// MoveEndpoint is to AbsMoveable and ConfigMoveable what Target is to
// Targetable: a wrapping struct that captures the result of decoding an HCL
// traversal representing a relative path from the current module to
// a moveable object.
//
// Its name reflects that its primary purpose is for the "from" and "to"
// addresses in a "moved" statement in the configuration, but it's also
// valid to use MoveEndpoint for other similar mechanisms that give
// Terraform hints about historical configuration changes that might
// prompt creating a different plan than Terraform would by default.
//
// To obtain a full address from a MoveEndpoint you must use
// either the package function UnifyMoveEndpoints (to get an AbsMovable) or
// the method ConfigMoveable (to get a ConfigMoveable).
type MoveEndpoint struct {
// SourceRange is the location of the physical endpoint address
// in configuration, if this MoveEndpoint was decoded from a
// configuration expresson.
SourceRange tfdiags.SourceRange
// Internally we (ab)use AbsMovable as the representation of our
// relative address, even though everywhere else in Terraform
// AbsMovable always represents a fully-absolute address.
// In practice, due to the implementation of ParseMoveEndpoint,
// this is always either a ModuleInstance or an AbsResourceInstance,
// and we only consider the possibility of interpreting it as
// a AbsModuleCall or an AbsResource in UnifyMoveEndpoints.
// This is intentionally unexported to encapsulate this unusual
// meaning of AbsMovable.
relSubject AbsMoveable
}
func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
return absMoveableEndpointKind(e.relSubject)
}
func (e *MoveEndpoint) String() string {
// Our internal pseudo-AbsMovable representing the relative
// address (either ModuleInstance or AbsResourceInstance) is
// a good enough proxy for the relative move endpoint address
// serialization.
return e.relSubject.String()
}
func (e *MoveEndpoint) Equal(other *MoveEndpoint) bool {
switch {
case (e == nil) != (other == nil):
return false
case e == nil:
return true
default:
// Since we only use ModuleInstance and AbsResourceInstance in our
// string representation, we have no ambiguity between address types
// and can safely just compare the string representations to
// compare the relSubject values.
return e.String() == other.String() && e.SourceRange == other.SourceRange
}
}
// MightUnifyWith returns true if it is possible that a later call to
// UnifyMoveEndpoints might succeed if given the reciever and the other
// given endpoint.
//
// This is intended for early static validation of obviously-wrong situations,
// although there are still various semantic errors that this cannot catch.
func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool {
// For our purposes here we'll just do a unify without a base module
// address, because the rules for whether unify can succeed depend
// only on the relative part of the addresses, not on which module
// they were declared in.
from, to := UnifyMoveEndpoints(RootModule, e, other)
return from != nil && to != nil
}
// ConfigMovable transforms the reciever into a ConfigMovable by resolving it
// relative to the given base module, which should be the module where
// the MoveEndpoint expression was found.
//
// The result is useful for finding the target object in the configuration,
// but it's not sufficient for fully interpreting a move statement because
// it lacks the specific module and resource instance keys.
func (e *MoveEndpoint) ConfigMoveable(baseModule Module) ConfigMoveable {
addr := e.relSubject
switch addr := addr.(type) {
case ModuleInstance:
ret := make(Module, 0, len(baseModule)+len(addr))
ret = append(ret, baseModule...)
ret = append(ret, addr.Module()...)
return ret
case AbsResourceInstance:
moduleAddr := make(Module, 0, len(baseModule)+len(addr.Module))
moduleAddr = append(moduleAddr, baseModule...)
moduleAddr = append(moduleAddr, addr.Module.Module()...)
return ConfigResource{
Module: moduleAddr,
Resource: addr.Resource.Resource,
}
default:
// The above should be exhaustive for all of the types
// that ParseMoveEndpoint produces as our intermediate
// address representation.
panic(fmt.Sprintf("unsupported address type %T", addr))
}
}
// ParseMoveEndpoint attempts to interpret the given traversal as a
// "move endpoint" address, which is a relative path from the module containing
// the traversal to a movable object in either the same module or in some
// child module.
//
// This deals only with the syntactic element of a move endpoint expression
// in configuration. Before the result will be useful you'll need to combine
// it with the address of the module where it was declared in order to get
// an absolute address relative to the root module.
func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) {
path, remain, diags := parseModuleInstancePrefix(traversal)
if diags.HasErrors() {
return nil, diags
}
rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange())
if len(remain) == 0 {
return &MoveEndpoint{
relSubject: path,
SourceRange: rng,
}, diags
}
riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return nil, diags
}
return &MoveEndpoint{
relSubject: riAddr,
SourceRange: rng,
}, diags
}
// UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
// "from" and "to" addresses in a moved block, and returns a pair of
// MoveEndpointInModule addresses guaranteed to be of the same dynamic type
// that represent what the two MoveEndpoint addresses refer to.
//
// moduleAddr must be the address of the module where the move was declared.
//
// This function deals both with the conversion from relative to absolute
// addresses and with resolving the ambiguity between no-key instance
// addresses and whole-object addresses, returning the least specific
// address type possible.
//
// Not all combinations of addresses are unifyable: the two addresses must
// either both include resources or both just be modules. If the two
// given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil),
// in which case the caller should typically report an error to the user
// stating the unification constraints.
func UnifyMoveEndpoints(moduleAddr Module, relFrom, relTo *MoveEndpoint) (modFrom, modTo *MoveEndpointInModule) {
// First we'll make a decision about which address type we're
// ultimately trying to unify to. For our internal purposes
// here we're going to borrow TargetableAddrType just as a
// convenient way to talk about our address types, even though
// targetable address types are not 100% aligned with moveable
// address types.
fromType := relFrom.internalAddrType()
toType := relTo.internalAddrType()
var wantType TargetableAddrType
// Our goal here is to choose the whole-resource or whole-module-call
// addresses if both agree on it, but to use specific instance addresses
// otherwise. This is a somewhat-arbitrary way to resolve syntactic
// ambiguity between the two situations which allows both for renaming
// whole resources and for switching from a single-instance object to
// a multi-instance object.
switch {
case fromType == AbsResourceInstanceAddrType || toType == AbsResourceInstanceAddrType:
wantType = AbsResourceInstanceAddrType
case fromType == AbsResourceAddrType || toType == AbsResourceAddrType:
wantType = AbsResourceAddrType
case fromType == ModuleInstanceAddrType || toType == ModuleInstanceAddrType:
wantType = ModuleInstanceAddrType
case fromType == ModuleAddrType || toType == ModuleAddrType:
// NOTE: We're fudging a little here and using
// ModuleAddrType to represent AbsModuleCall rather
// than Module.
wantType = ModuleAddrType
default:
panic("unhandled move address types")
}
modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType)
modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType)
if modFrom == nil || modTo == nil {
// if either of them failed then they both failed, to make the
// caller's life a little easier.
return nil, nil
}
return modFrom, modTo
}
func (e *MoveEndpoint) prepareMoveEndpointInModule(moduleAddr Module, wantType TargetableAddrType) *MoveEndpointInModule {
// relAddr can only be either AbsResourceInstance or ModuleInstance, the
// internal intermediate representation produced by ParseMoveEndpoint.
relAddr := e.relSubject
switch relAddr := relAddr.(type) {
case ModuleInstance:
switch wantType {
case ModuleInstanceAddrType:
// Since our internal representation is already a module instance,
// we can just rewrap this one.
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr,
}
case ModuleAddrType:
// NOTE: We're fudging a little here and using
// ModuleAddrType to represent AbsModuleCall rather
// than Module.
callerAddr, callAddr := relAddr.Call()
absCallAddr := AbsModuleCall{
Module: callerAddr,
Call: callAddr,
}
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: absCallAddr,
}
default:
return nil // can't make any other types from a ModuleInstance
}
case AbsResourceInstance:
switch wantType {
case AbsResourceInstanceAddrType:
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr,
}
case AbsResourceAddrType:
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr.ContainingResource(),
}
default:
return nil // can't make any other types from an AbsResourceInstance
}
default:
panic(fmt.Sprintf("unhandled address type %T", relAddr))
}
}
// internalAddrType helps facilitate our slight abuse of TargetableAddrType
// as a way to talk about our different possible result address types in
// UnifyMoveEndpoints.
//
// It's not really correct to use TargetableAddrType in this way, because
// it's for Targetable rather than for AbsMoveable, but as long as the two
// remain aligned enough it saves introducing yet another enumeration with
// similar members that would be for internal use only anyway.
func (e *MoveEndpoint) internalAddrType() TargetableAddrType {
switch addr := e.relSubject.(type) {
case ModuleInstance:
if !addr.IsRoot() && addr[len(addr)-1].InstanceKey == NoKey {
// NOTE: We're fudging a little here and using
// ModuleAddrType to represent AbsModuleCall rather
// than Module.
return ModuleAddrType
}
return ModuleInstanceAddrType
case AbsResourceInstance:
if addr.Resource.Key == NoKey {
return AbsResourceAddrType
}
return AbsResourceInstanceAddrType
default:
// The above should cover all of the address types produced
// by ParseMoveEndpoint.
panic(fmt.Sprintf("unsupported address type %T", addr))
}
}