github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/move_endpoint.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package addrs
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/terramate-io/tf/tfdiags"
    11  )
    12  
    13  // MoveEndpoint is to AbsMoveable and ConfigMoveable what Target is to
    14  // Targetable: a wrapping struct that captures the result of decoding an HCL
    15  // traversal representing a relative path from the current module to
    16  // a moveable object.
    17  //
    18  // Its name reflects that its primary purpose is for the "from" and "to"
    19  // addresses in a "moved" statement in the configuration, but it's also
    20  // valid to use MoveEndpoint for other similar mechanisms that give
    21  // Terraform hints about historical configuration changes that might
    22  // prompt creating a different plan than Terraform would by default.
    23  //
    24  // To obtain a full address from a MoveEndpoint you must use
    25  // either the package function UnifyMoveEndpoints (to get an AbsMoveable) or
    26  // the method ConfigMoveable (to get a ConfigMoveable).
    27  type MoveEndpoint struct {
    28  	// SourceRange is the location of the physical endpoint address
    29  	// in configuration, if this MoveEndpoint was decoded from a
    30  	// configuration expresson.
    31  	SourceRange tfdiags.SourceRange
    32  
    33  	// Internally we (ab)use AbsMoveable as the representation of our
    34  	// relative address, even though everywhere else in Terraform
    35  	// AbsMoveable always represents a fully-absolute address.
    36  	// In practice, due to the implementation of ParseMoveEndpoint,
    37  	// this is always either a ModuleInstance or an AbsResourceInstance,
    38  	// and we only consider the possibility of interpreting it as
    39  	// a AbsModuleCall or an AbsResource in UnifyMoveEndpoints.
    40  	// This is intentionally unexported to encapsulate this unusual
    41  	// meaning of AbsMoveable.
    42  	relSubject AbsMoveable
    43  }
    44  
    45  func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
    46  	return absMoveableEndpointKind(e.relSubject)
    47  }
    48  
    49  func (e *MoveEndpoint) String() string {
    50  	// Our internal pseudo-AbsMoveable representing the relative
    51  	// address (either ModuleInstance or AbsResourceInstance) is
    52  	// a good enough proxy for the relative move endpoint address
    53  	// serialization.
    54  	return e.relSubject.String()
    55  }
    56  
    57  func (e *MoveEndpoint) Equal(other *MoveEndpoint) bool {
    58  	switch {
    59  	case (e == nil) != (other == nil):
    60  		return false
    61  	case e == nil:
    62  		return true
    63  	default:
    64  		// Since we only use ModuleInstance and AbsResourceInstance in our
    65  		// string representation, we have no ambiguity between address types
    66  		// and can safely just compare the string representations to
    67  		// compare the relSubject values.
    68  		return e.String() == other.String() && e.SourceRange == other.SourceRange
    69  	}
    70  }
    71  
    72  // MightUnifyWith returns true if it is possible that a later call to
    73  // UnifyMoveEndpoints might succeed if given the reciever and the other
    74  // given endpoint.
    75  //
    76  // This is intended for early static validation of obviously-wrong situations,
    77  // although there are still various semantic errors that this cannot catch.
    78  func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool {
    79  	// For our purposes here we'll just do a unify without a base module
    80  	// address, because the rules for whether unify can succeed depend
    81  	// only on the relative part of the addresses, not on which module
    82  	// they were declared in.
    83  	from, to := UnifyMoveEndpoints(RootModule, e, other)
    84  	return from != nil && to != nil
    85  }
    86  
    87  // ConfigMovable transforms the reciever into a ConfigMovable by resolving it
    88  // relative to the given base module, which should be the module where
    89  // the MoveEndpoint expression was found.
    90  //
    91  // The result is useful for finding the target object in the configuration,
    92  // but it's not sufficient for fully interpreting a move statement because
    93  // it lacks the specific module and resource instance keys.
    94  func (e *MoveEndpoint) ConfigMoveable(baseModule Module) ConfigMoveable {
    95  	addr := e.relSubject
    96  	switch addr := addr.(type) {
    97  	case ModuleInstance:
    98  		ret := make(Module, 0, len(baseModule)+len(addr))
    99  		ret = append(ret, baseModule...)
   100  		ret = append(ret, addr.Module()...)
   101  		return ret
   102  	case AbsResourceInstance:
   103  		moduleAddr := make(Module, 0, len(baseModule)+len(addr.Module))
   104  		moduleAddr = append(moduleAddr, baseModule...)
   105  		moduleAddr = append(moduleAddr, addr.Module.Module()...)
   106  		return ConfigResource{
   107  			Module:   moduleAddr,
   108  			Resource: addr.Resource.Resource,
   109  		}
   110  	default:
   111  		// The above should be exhaustive for all of the types
   112  		// that ParseMoveEndpoint produces as our intermediate
   113  		// address representation.
   114  		panic(fmt.Sprintf("unsupported address type %T", addr))
   115  	}
   116  
   117  }
   118  
   119  // ParseMoveEndpoint attempts to interpret the given traversal as a
   120  // "move endpoint" address, which is a relative path from the module containing
   121  // the traversal to a movable object in either the same module or in some
   122  // child module.
   123  //
   124  // This deals only with the syntactic element of a move endpoint expression
   125  // in configuration. Before the result will be useful you'll need to combine
   126  // it with the address of the module where it was declared in order to get
   127  // an absolute address relative to the root module.
   128  func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) {
   129  	path, remain, diags := parseModuleInstancePrefix(traversal)
   130  	if diags.HasErrors() {
   131  		return nil, diags
   132  	}
   133  
   134  	rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange())
   135  
   136  	if len(remain) == 0 {
   137  		return &MoveEndpoint{
   138  			relSubject:  path,
   139  			SourceRange: rng,
   140  		}, diags
   141  	}
   142  
   143  	riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
   144  	diags = diags.Append(moreDiags)
   145  	if diags.HasErrors() {
   146  		return nil, diags
   147  	}
   148  
   149  	return &MoveEndpoint{
   150  		relSubject:  riAddr,
   151  		SourceRange: rng,
   152  	}, diags
   153  }
   154  
   155  // UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
   156  // "from" and "to" addresses in a moved block, and returns a pair of
   157  // MoveEndpointInModule addresses guaranteed to be of the same dynamic type
   158  // that represent what the two MoveEndpoint addresses refer to.
   159  //
   160  // moduleAddr must be the address of the module where the move was declared.
   161  //
   162  // This function deals both with the conversion from relative to absolute
   163  // addresses and with resolving the ambiguity between no-key instance
   164  // addresses and whole-object addresses, returning the least specific
   165  // address type possible.
   166  //
   167  // Not all combinations of addresses are unifyable: the two addresses must
   168  // either both include resources or both just be modules. If the two
   169  // given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil),
   170  // in which case the caller should typically report an error to the user
   171  // stating the unification constraints.
   172  func UnifyMoveEndpoints(moduleAddr Module, relFrom, relTo *MoveEndpoint) (modFrom, modTo *MoveEndpointInModule) {
   173  
   174  	// First we'll make a decision about which address type we're
   175  	// ultimately trying to unify to. For our internal purposes
   176  	// here we're going to borrow TargetableAddrType just as a
   177  	// convenient way to talk about our address types, even though
   178  	// targetable address types are not 100% aligned with moveable
   179  	// address types.
   180  	fromType := relFrom.internalAddrType()
   181  	toType := relTo.internalAddrType()
   182  	var wantType TargetableAddrType
   183  
   184  	// Our goal here is to choose the whole-resource or whole-module-call
   185  	// addresses if both agree on it, but to use specific instance addresses
   186  	// otherwise. This is a somewhat-arbitrary way to resolve syntactic
   187  	// ambiguity between the two situations which allows both for renaming
   188  	// whole resources and for switching from a single-instance object to
   189  	// a multi-instance object.
   190  	switch {
   191  	case fromType == AbsResourceInstanceAddrType || toType == AbsResourceInstanceAddrType:
   192  		wantType = AbsResourceInstanceAddrType
   193  	case fromType == AbsResourceAddrType || toType == AbsResourceAddrType:
   194  		wantType = AbsResourceAddrType
   195  	case fromType == ModuleInstanceAddrType || toType == ModuleInstanceAddrType:
   196  		wantType = ModuleInstanceAddrType
   197  	case fromType == ModuleAddrType || toType == ModuleAddrType:
   198  		// NOTE: We're fudging a little here and using
   199  		// ModuleAddrType to represent AbsModuleCall rather
   200  		// than Module.
   201  		wantType = ModuleAddrType
   202  	default:
   203  		panic("unhandled move address types")
   204  	}
   205  
   206  	modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType)
   207  	modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType)
   208  	if modFrom == nil || modTo == nil {
   209  		// if either of them failed then they both failed, to make the
   210  		// caller's life a little easier.
   211  		return nil, nil
   212  	}
   213  	return modFrom, modTo
   214  }
   215  
   216  func (e *MoveEndpoint) prepareMoveEndpointInModule(moduleAddr Module, wantType TargetableAddrType) *MoveEndpointInModule {
   217  	// relAddr can only be either AbsResourceInstance or ModuleInstance, the
   218  	// internal intermediate representation produced by ParseMoveEndpoint.
   219  	relAddr := e.relSubject
   220  
   221  	switch relAddr := relAddr.(type) {
   222  	case ModuleInstance:
   223  		switch wantType {
   224  		case ModuleInstanceAddrType:
   225  			// Since our internal representation is already a module instance,
   226  			// we can just rewrap this one.
   227  			return &MoveEndpointInModule{
   228  				SourceRange: e.SourceRange,
   229  				module:      moduleAddr,
   230  				relSubject:  relAddr,
   231  			}
   232  		case ModuleAddrType:
   233  			// NOTE: We're fudging a little here and using
   234  			// ModuleAddrType to represent AbsModuleCall rather
   235  			// than Module.
   236  			callerAddr, callAddr := relAddr.Call()
   237  			absCallAddr := AbsModuleCall{
   238  				Module: callerAddr,
   239  				Call:   callAddr,
   240  			}
   241  			return &MoveEndpointInModule{
   242  				SourceRange: e.SourceRange,
   243  				module:      moduleAddr,
   244  				relSubject:  absCallAddr,
   245  			}
   246  		default:
   247  			return nil // can't make any other types from a ModuleInstance
   248  		}
   249  	case AbsResourceInstance:
   250  		switch wantType {
   251  		case AbsResourceInstanceAddrType:
   252  			return &MoveEndpointInModule{
   253  				SourceRange: e.SourceRange,
   254  				module:      moduleAddr,
   255  				relSubject:  relAddr,
   256  			}
   257  		case AbsResourceAddrType:
   258  			return &MoveEndpointInModule{
   259  				SourceRange: e.SourceRange,
   260  				module:      moduleAddr,
   261  				relSubject:  relAddr.ContainingResource(),
   262  			}
   263  		default:
   264  			return nil // can't make any other types from an AbsResourceInstance
   265  		}
   266  	default:
   267  		panic(fmt.Sprintf("unhandled address type %T", relAddr))
   268  	}
   269  }
   270  
   271  // internalAddrType helps facilitate our slight abuse of TargetableAddrType
   272  // as a way to talk about our different possible result address types in
   273  // UnifyMoveEndpoints.
   274  //
   275  // It's not really correct to use TargetableAddrType in this way, because
   276  // it's for Targetable rather than for AbsMoveable, but as long as the two
   277  // remain aligned enough it saves introducing yet another enumeration with
   278  // similar members that would be for internal use only anyway.
   279  func (e *MoveEndpoint) internalAddrType() TargetableAddrType {
   280  	switch addr := e.relSubject.(type) {
   281  	case ModuleInstance:
   282  		if !addr.IsRoot() && addr[len(addr)-1].InstanceKey == NoKey {
   283  			// NOTE: We're fudging a little here and using
   284  			// ModuleAddrType to represent AbsModuleCall rather
   285  			// than Module.
   286  			return ModuleAddrType
   287  		}
   288  		return ModuleInstanceAddrType
   289  	case AbsResourceInstance:
   290  		if addr.Resource.Key == NoKey {
   291  			return AbsResourceAddrType
   292  		}
   293  		return AbsResourceInstanceAddrType
   294  	default:
   295  		// The above should cover all of the address types produced
   296  		// by ParseMoveEndpoint.
   297  		panic(fmt.Sprintf("unsupported address type %T", addr))
   298  	}
   299  }