mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
300 lines
11 KiB
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))
|
|
}
|
|
}
|