opentofu/internal/addrs/move_endpoint.go

300 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/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
// OpenTofu hints about historical configuration changes that might
// prompt creating a different plan than OpenTofu would by default.
//
// To obtain a full address from a MoveEndpoint you must use
// either the package function UnifyMoveEndpoints (to get an AbsMoveable) 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 AbsMoveable as the representation of our
// relative address, even though everywhere else in OpenTofu
// AbsMoveable 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 AbsMoveable.
relSubject AbsMoveable
}
func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
return absMoveableEndpointKind(e.relSubject)
}
func (e *MoveEndpoint) String() string {
// Our internal pseudo-AbsMoveable 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))
}
}