Merge branch 'main' into description-metadata-language-docs
This commit is contained in:
commit
06a2fdcf9d
|
@ -10,3 +10,9 @@ type CountAttr struct {
|
|||
func (ca CountAttr) String() string {
|
||||
return "count." + ca.Name
|
||||
}
|
||||
|
||||
func (ca CountAttr) UniqueKey() UniqueKey {
|
||||
return ca // A CountAttr is its own UniqueKey
|
||||
}
|
||||
|
||||
func (ca CountAttr) uniqueKeySigil() {}
|
||||
|
|
|
@ -10,3 +10,9 @@ type ForEachAttr struct {
|
|||
func (f ForEachAttr) String() string {
|
||||
return "each." + f.Name
|
||||
}
|
||||
|
||||
func (f ForEachAttr) UniqueKey() UniqueKey {
|
||||
return f // A ForEachAttr is its own UniqueKey
|
||||
}
|
||||
|
||||
func (f ForEachAttr) uniqueKeySigil() {}
|
||||
|
|
|
@ -14,6 +14,12 @@ func (v InputVariable) String() string {
|
|||
return "var." + v.Name
|
||||
}
|
||||
|
||||
func (v InputVariable) UniqueKey() UniqueKey {
|
||||
return v // A InputVariable is its own UniqueKey
|
||||
}
|
||||
|
||||
func (v InputVariable) uniqueKeySigil() {}
|
||||
|
||||
// Absolute converts the receiver into an absolute address within the given
|
||||
// module instance.
|
||||
func (v InputVariable) Absolute(m ModuleInstance) AbsInputVariableInstance {
|
||||
|
|
|
@ -14,6 +14,12 @@ func (v LocalValue) String() string {
|
|||
return "local." + v.Name
|
||||
}
|
||||
|
||||
func (v LocalValue) UniqueKey() UniqueKey {
|
||||
return v // A LocalValue is its own UniqueKey
|
||||
}
|
||||
|
||||
func (v LocalValue) uniqueKeySigil() {}
|
||||
|
||||
// Absolute converts the receiver into an absolute address within the given
|
||||
// module instance.
|
||||
func (v LocalValue) Absolute(m ModuleInstance) AbsLocalValue {
|
||||
|
|
|
@ -15,6 +15,12 @@ func (c ModuleCall) String() string {
|
|||
return "module." + c.Name
|
||||
}
|
||||
|
||||
func (c ModuleCall) UniqueKey() UniqueKey {
|
||||
return c // A ModuleCall is its own UniqueKey
|
||||
}
|
||||
|
||||
func (c ModuleCall) uniqueKeySigil() {}
|
||||
|
||||
// Instance returns the address of an instance of the receiver identified by
|
||||
// the given key.
|
||||
func (c ModuleCall) Instance(key InstanceKey) ModuleCallInstance {
|
||||
|
@ -47,7 +53,11 @@ func (c AbsModuleCall) absMoveableSigil() {
|
|||
}
|
||||
|
||||
func (c AbsModuleCall) String() string {
|
||||
return fmt.Sprintf("%s.%s", c.Module, c.Call.Name)
|
||||
if len(c.Module) == 0 {
|
||||
return "module." + c.Call.Name
|
||||
|
||||
}
|
||||
return fmt.Sprintf("%s.module.%s", c.Module, c.Call.Name)
|
||||
}
|
||||
|
||||
func (c AbsModuleCall) Instance(key InstanceKey) ModuleInstance {
|
||||
|
@ -79,6 +89,12 @@ func (c ModuleCallInstance) String() string {
|
|||
return fmt.Sprintf("module.%s%s", c.Call.Name, c.Key)
|
||||
}
|
||||
|
||||
func (c ModuleCallInstance) UniqueKey() UniqueKey {
|
||||
return c // A ModuleCallInstance is its own UniqueKey
|
||||
}
|
||||
|
||||
func (c ModuleCallInstance) uniqueKeySigil() {}
|
||||
|
||||
func (c ModuleCallInstance) Absolute(moduleAddr ModuleInstance) ModuleInstance {
|
||||
ret := make(ModuleInstance, len(moduleAddr), len(moduleAddr)+1)
|
||||
copy(ret, moduleAddr)
|
||||
|
@ -118,6 +134,12 @@ func (m ModuleCallOutput) String() string {
|
|||
return fmt.Sprintf("%s.%s", m.Call.String(), m.Name)
|
||||
}
|
||||
|
||||
func (m ModuleCallOutput) UniqueKey() UniqueKey {
|
||||
return m // A ModuleCallOutput is its own UniqueKey
|
||||
}
|
||||
|
||||
func (m ModuleCallOutput) uniqueKeySigil() {}
|
||||
|
||||
// ModuleCallInstanceOutput is the address of a particular named output produced by
|
||||
// an instance of a module call.
|
||||
type ModuleCallInstanceOutput struct {
|
||||
|
@ -139,6 +161,12 @@ func (co ModuleCallInstanceOutput) String() string {
|
|||
return fmt.Sprintf("%s.%s", co.Call.String(), co.Name)
|
||||
}
|
||||
|
||||
func (co ModuleCallInstanceOutput) UniqueKey() UniqueKey {
|
||||
return co // A ModuleCallInstanceOutput is its own UniqueKey
|
||||
}
|
||||
|
||||
func (co ModuleCallInstanceOutput) uniqueKeySigil() {}
|
||||
|
||||
// AbsOutputValue returns the absolute output value address that corresponds
|
||||
// to the receving module call output address, once resolved in the given
|
||||
// calling module.
|
||||
|
|
|
@ -275,6 +275,14 @@ func (m ModuleInstance) String() string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
type moduleInstanceKey string
|
||||
|
||||
func (m ModuleInstance) UniqueKey() UniqueKey {
|
||||
return moduleInstanceKey(m.String())
|
||||
}
|
||||
|
||||
func (mk moduleInstanceKey) uniqueKeySigil() {}
|
||||
|
||||
// Equal returns true if the receiver and the given other value
|
||||
// contains the exact same parts.
|
||||
func (m ModuleInstance) Equal(o ModuleInstance) bool {
|
||||
|
@ -496,6 +504,28 @@ func (m ModuleInstance) absMoveableSigil() {
|
|||
// ModuleInstance is moveable
|
||||
}
|
||||
|
||||
// IsDeclaredByCall returns true if the receiver is an instance of the given
|
||||
// AbsModuleCall.
|
||||
func (m ModuleInstance) IsDeclaredByCall(other AbsModuleCall) bool {
|
||||
// Compare len(m) to len(other.Module+1) because the final module instance
|
||||
// step in other is stored in the AbsModuleCall.Call
|
||||
if len(m) > len(other.Module)+1 || len(m) == 0 && len(other.Module) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify that the other's ModuleInstance matches the receiver.
|
||||
inst, lastStep := other.Module, other.Call
|
||||
for i := range inst {
|
||||
if inst[i] != m[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Now compare the final step of the received with the other Call, where
|
||||
// only the name needs to match.
|
||||
return lastStep.Name == m[len(m)-1].Name
|
||||
}
|
||||
|
||||
func (s ModuleInstanceStep) String() string {
|
||||
if s.InstanceKey != NoKey {
|
||||
return s.Name + s.InstanceKey.String()
|
||||
|
|
|
@ -91,3 +91,80 @@ func BenchmarkStringLong(b *testing.B) {
|
|||
addr.String()
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleInstance_IsDeclaredByCall(t *testing.T) {
|
||||
tests := []struct {
|
||||
instance ModuleInstance
|
||||
call AbsModuleCall
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
ModuleInstance{},
|
||||
AbsModuleCall{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustParseModuleInstanceStr("module.child"),
|
||||
AbsModuleCall{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
ModuleInstance{},
|
||||
AbsModuleCall{
|
||||
RootModuleInstance,
|
||||
ModuleCall{Name: "child"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustParseModuleInstanceStr("module.child"),
|
||||
AbsModuleCall{ // module.child
|
||||
RootModuleInstance,
|
||||
ModuleCall{Name: "child"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
mustParseModuleInstanceStr(`module.child`),
|
||||
AbsModuleCall{ // module.kinder.module.child
|
||||
mustParseModuleInstanceStr("module.kinder"),
|
||||
ModuleCall{Name: "child"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustParseModuleInstanceStr("module.kinder"),
|
||||
// module.kinder.module.child contains module.kinder, but is not itself an instance of module.kinder
|
||||
AbsModuleCall{
|
||||
mustParseModuleInstanceStr("module.kinder"),
|
||||
ModuleCall{Name: "child"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
mustParseModuleInstanceStr("module.child"),
|
||||
AbsModuleCall{
|
||||
mustParseModuleInstanceStr(`module.kinder["a"]`),
|
||||
ModuleCall{Name: "kinder"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%q.IsCallInstance(%q)", test.instance, test.call.String()), func(t *testing.T) {
|
||||
got := test.instance.IsDeclaredByCall(test.call)
|
||||
if got != test.want {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseModuleInstanceStr(str string) ModuleInstance {
|
||||
mi, err := ParseModuleInstanceStr(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return mi
|
||||
}
|
||||
|
|
|
@ -39,6 +39,10 @@ type MoveEndpoint struct {
|
|||
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
|
||||
|
@ -73,7 +77,7 @@ func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool {
|
|||
// 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(RootModuleInstance, e, other)
|
||||
from, to := UnifyMoveEndpoints(RootModule, e, other)
|
||||
return from != nil && to != nil
|
||||
}
|
||||
|
||||
|
@ -147,11 +151,10 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost
|
|||
|
||||
// UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
|
||||
// "from" and "to" addresses in a moved block, and returns a pair of
|
||||
// AbsMoveable addresses guaranteed to be of the same dynamic type
|
||||
// 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 instance where the move
|
||||
// was declared.
|
||||
// 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
|
||||
|
@ -163,7 +166,7 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost
|
|||
// 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 ModuleInstance, relFrom, relTo *MoveEndpoint) (absFrom, absTo AbsMoveable) {
|
||||
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
|
||||
|
@ -197,17 +200,17 @@ func UnifyMoveEndpoints(moduleAddr ModuleInstance, relFrom, relTo *MoveEndpoint)
|
|||
panic("unhandled move address types")
|
||||
}
|
||||
|
||||
absFrom = relFrom.prepareAbsMoveable(moduleAddr, wantType)
|
||||
absTo = relTo.prepareAbsMoveable(moduleAddr, wantType)
|
||||
if absFrom == nil || absTo == nil {
|
||||
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 absFrom, absTo
|
||||
return modFrom, modTo
|
||||
}
|
||||
|
||||
func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType TargetableAddrType) AbsMoveable {
|
||||
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
|
||||
|
@ -216,40 +219,43 @@ func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType Ta
|
|||
case ModuleInstance:
|
||||
switch wantType {
|
||||
case ModuleInstanceAddrType:
|
||||
ret := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr))
|
||||
ret = append(ret, moduleAddr...)
|
||||
ret = append(ret, relAddr...)
|
||||
return ret
|
||||
// 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 := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr)-1)
|
||||
callerAddr = append(callerAddr, moduleAddr...)
|
||||
callerAddr = append(callerAddr, relAddr[:len(relAddr)-1]...)
|
||||
return AbsModuleCall{
|
||||
callerAddr, callAddr := relAddr.Call()
|
||||
absCallAddr := AbsModuleCall{
|
||||
Module: callerAddr,
|
||||
Call: ModuleCall{
|
||||
Name: relAddr[len(relAddr)-1].Name,
|
||||
},
|
||||
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:
|
||||
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr.Module))
|
||||
callerAddr = append(callerAddr, moduleAddr...)
|
||||
callerAddr = append(callerAddr, relAddr.Module...)
|
||||
switch wantType {
|
||||
case AbsResourceInstanceAddrType:
|
||||
return AbsResourceInstance{
|
||||
Module: callerAddr,
|
||||
Resource: relAddr.Resource,
|
||||
return &MoveEndpointInModule{
|
||||
SourceRange: e.SourceRange,
|
||||
module: moduleAddr,
|
||||
relSubject: relAddr,
|
||||
}
|
||||
case AbsResourceAddrType:
|
||||
return AbsResource{
|
||||
Module: callerAddr,
|
||||
Resource: relAddr.Resource.Resource,
|
||||
return &MoveEndpointInModule{
|
||||
SourceRange: e.SourceRange,
|
||||
module: moduleAddr,
|
||||
relSubject: relAddr.ContainingResource(),
|
||||
}
|
||||
default:
|
||||
return nil // can't make any other types from an AbsResourceInstance
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package addrs
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MoveEndpointKind represents the different kinds of object that a movable
|
||||
// address can refer to.
|
||||
type MoveEndpointKind rune
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type MoveEndpointKind
|
||||
|
||||
const (
|
||||
// MoveEndpointModule indicates that a move endpoint either refers to
|
||||
// an individual module instance or to all instances of a particular
|
||||
// module call.
|
||||
MoveEndpointModule MoveEndpointKind = 'M'
|
||||
|
||||
// MoveEndpointResource indicates that a move endpoint either refers to
|
||||
// an individual resource instance or to all instances of a particular
|
||||
// resource.
|
||||
MoveEndpointResource MoveEndpointKind = 'R'
|
||||
)
|
||||
|
||||
func absMoveableEndpointKind(addr AbsMoveable) MoveEndpointKind {
|
||||
switch addr := addr.(type) {
|
||||
case ModuleInstance, AbsModuleCall:
|
||||
return MoveEndpointModule
|
||||
case AbsResourceInstance, AbsResource:
|
||||
return MoveEndpointResource
|
||||
default:
|
||||
// The above should be exhaustive for all AbsMoveable types.
|
||||
panic(fmt.Sprintf("unsupported address type %T", addr))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package addrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// MoveEndpointInModule annotates a MoveEndpoint with the address of the
|
||||
// module where it was declared, which is the form we use for resolving
|
||||
// whether move statements chain from or are nested within other move
|
||||
// statements.
|
||||
type MoveEndpointInModule struct {
|
||||
// SourceRange is the location of the physical endpoint address
|
||||
// in configuration, if this MoveEndpoint was decoded from a
|
||||
// configuration expresson.
|
||||
SourceRange tfdiags.SourceRange
|
||||
|
||||
// The internals are unexported here because, as with MoveEndpoint,
|
||||
// we're somewhat abusing AbsMoveable here to represent an address
|
||||
// relative to the module, rather than as an absolute address.
|
||||
// Conceptually, the following two fields represent a matching pattern
|
||||
// for AbsMoveables where the elements of "module" behave as
|
||||
// ModuleInstanceStep values with a wildcard instance key, because
|
||||
// a moved block in a module affects all instances of that module.
|
||||
// Unlike MoveEndpoint, relSubject in this case can be any of the
|
||||
// address types that implement AbsMoveable.
|
||||
module Module
|
||||
relSubject AbsMoveable
|
||||
}
|
||||
|
||||
func (e *MoveEndpointInModule) ObjectKind() MoveEndpointKind {
|
||||
return absMoveableEndpointKind(e.relSubject)
|
||||
}
|
||||
|
||||
// String produces a string representation of the object matching pattern
|
||||
// represented by the reciever.
|
||||
//
|
||||
// Since there is no direct syntax for representing such an object matching
|
||||
// pattern, this function uses a splat-operator-like representation to stand
|
||||
// in for the wildcard instance keys.
|
||||
func (e *MoveEndpointInModule) String() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
var buf strings.Builder
|
||||
for _, name := range e.module {
|
||||
buf.WriteString("module.")
|
||||
buf.WriteString(name)
|
||||
buf.WriteString("[*].")
|
||||
}
|
||||
buf.WriteString(e.relSubject.String())
|
||||
|
||||
// For consistency we'll also use the splat-like wildcard syntax to
|
||||
// represent the final step being either a resource or module call
|
||||
// rather than an instance, so we can more easily distinguish the two
|
||||
// in the string representation.
|
||||
switch e.relSubject.(type) {
|
||||
case AbsModuleCall, AbsResource:
|
||||
buf.WriteString("[*]")
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SelectsModule returns true if the reciever directly selects either
|
||||
// the given module or a resource nested directly inside that module.
|
||||
//
|
||||
// This is a good function to use to decide which modules in a state
|
||||
// to consider when processing a particular move statement. For a
|
||||
// module move the given module itself is what will move, while a
|
||||
// resource move indicates that we should search each of the resources in
|
||||
// the given module to see if they match.
|
||||
func (e *MoveEndpointInModule) SelectsModule(addr ModuleInstance) bool {
|
||||
// In order to match the given module path should be at least as
|
||||
// long as the path to the module where the move endpoint was defined.
|
||||
if len(addr) < len(e.module) {
|
||||
return false
|
||||
}
|
||||
|
||||
containerPart := addr[:len(e.module)]
|
||||
relPart := addr[len(e.module):]
|
||||
|
||||
// The names of all of the steps that align with e.module must match,
|
||||
// though the instance keys are wildcards for this part.
|
||||
for i := range e.module {
|
||||
if containerPart[i].Name != e.module[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// The remaining module address steps must match both name and key.
|
||||
// The logic for all of these is similar but we will retrieve the
|
||||
// module address differently for each type.
|
||||
var relMatch ModuleInstance
|
||||
switch relAddr := e.relSubject.(type) {
|
||||
case ModuleInstance:
|
||||
relMatch = relAddr
|
||||
case AbsModuleCall:
|
||||
// This one requires a little more fuss because the call effectively
|
||||
// slices in two the final step of the module address.
|
||||
if len(relPart) != len(relAddr.Module)+1 {
|
||||
return false
|
||||
}
|
||||
callPart := relPart[len(relPart)-1]
|
||||
if callPart.Name != relAddr.Call.Name {
|
||||
return false
|
||||
}
|
||||
case AbsResource:
|
||||
relMatch = relAddr.Module
|
||||
case AbsResourceInstance:
|
||||
relMatch = relAddr.Module
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled relative address type %T", relAddr))
|
||||
}
|
||||
|
||||
if len(relPart) != len(relMatch) {
|
||||
return false
|
||||
}
|
||||
for i := range relMatch {
|
||||
if relPart[i] != relMatch[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CanChainFrom returns true if the reciever describes an address that could
|
||||
// potentially select an object that the other given address could select.
|
||||
//
|
||||
// In other words, this decides whether the move chaining rule applies, if
|
||||
// the reciever is the "to" from one statement and the other given address
|
||||
// is the "from" of another statement.
|
||||
func (e *MoveEndpointInModule) CanChainFrom(other *MoveEndpointInModule) bool {
|
||||
// TODO: implement
|
||||
return false
|
||||
}
|
||||
|
||||
// NestedWithin returns true if the reciever describes an address that is
|
||||
// contained within one of the objects that the given other address could
|
||||
// select.
|
||||
func (e *MoveEndpointInModule) NestedWithin(other *MoveEndpointInModule) bool {
|
||||
// TODO: implement
|
||||
return false
|
||||
}
|
||||
|
||||
// MoveDestination considers a an address representing a module
|
||||
// instance in the context of source and destination move endpoints and then,
|
||||
// if the module address matches the from endpoint, returns the corresponding
|
||||
// new module address that the object should move to.
|
||||
//
|
||||
// MoveDestination will return false in its second return value if the receiver
|
||||
// doesn't match fromMatch, indicating that the given move statement doesn't
|
||||
// apply to this object.
|
||||
//
|
||||
// Both of the given endpoints must be from the same move statement and thus
|
||||
// must have matching object types. If not, MoveDestination will panic.
|
||||
func (m ModuleInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (ModuleInstance, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// MoveDestination considers a an address representing a resource
|
||||
// in the context of source and destination move endpoints and then,
|
||||
// if the resource address matches the from endpoint, returns the corresponding
|
||||
// new resource address that the object should move to.
|
||||
//
|
||||
// MoveDestination will return false in its second return value if the receiver
|
||||
// doesn't match fromMatch, indicating that the given move statement doesn't
|
||||
// apply to this object.
|
||||
//
|
||||
// Both of the given endpoints must be from the same move statement and thus
|
||||
// must have matching object types. If not, MoveDestination will panic.
|
||||
func (r AbsResource) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (AbsResource, bool) {
|
||||
return AbsResource{}, false
|
||||
}
|
||||
|
||||
// MoveDestination considers a an address representing a resource
|
||||
// instance in the context of source and destination move endpoints and then,
|
||||
// if the instance address matches the from endpoint, returns the corresponding
|
||||
// new instance address that the object should move to.
|
||||
//
|
||||
// MoveDestination will return false in its second return value if the receiver
|
||||
// doesn't match fromMatch, indicating that the given move statement doesn't
|
||||
// apply to this object.
|
||||
//
|
||||
// Both of the given endpoints must be from the same move statement and thus
|
||||
// must have matching object types. If not, MoveDestination will panic.
|
||||
func (r AbsResourceInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (AbsResourceInstance, bool) {
|
||||
return AbsResourceInstance{}, false
|
||||
}
|
|
@ -5,7 +5,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
)
|
||||
|
@ -339,245 +338,127 @@ func TestParseMoveEndpoint(t *testing.T) {
|
|||
func TestUnifyMoveEndpoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
InputFrom, InputTo string
|
||||
Module ModuleInstance
|
||||
WantFrom, WantTo AbsMoveable
|
||||
Module Module
|
||||
WantFrom, WantTo string
|
||||
}{
|
||||
{
|
||||
InputFrom: `foo.bar`,
|
||||
InputTo: `foo.baz`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: AbsResource{
|
||||
Module: RootModuleInstance,
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
WantTo: AbsResource{
|
||||
Module: RootModuleInstance,
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "baz",
|
||||
},
|
||||
},
|
||||
Module: RootModule,
|
||||
WantFrom: `foo.bar[*]`,
|
||||
WantTo: `foo.baz[*]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `foo.bar`,
|
||||
InputTo: `foo.baz`,
|
||||
Module: RootModuleInstance.Child("a", NoKey),
|
||||
WantFrom: AbsResource{
|
||||
Module: RootModuleInstance.Child("a", NoKey),
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
WantTo: AbsResource{
|
||||
Module: RootModuleInstance.Child("a", NoKey),
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "baz",
|
||||
},
|
||||
},
|
||||
Module: RootModule.Child("a"),
|
||||
WantFrom: `module.a[*].foo.bar[*]`,
|
||||
WantTo: `module.a[*].foo.baz[*]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `foo.bar`,
|
||||
InputTo: `module.b[0].foo.baz`,
|
||||
Module: RootModuleInstance.Child("a", NoKey),
|
||||
WantFrom: AbsResource{
|
||||
Module: RootModuleInstance.Child("a", NoKey),
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
WantTo: AbsResource{
|
||||
Module: RootModuleInstance.Child("a", NoKey).Child("b", IntKey(0)),
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "baz",
|
||||
},
|
||||
},
|
||||
Module: RootModule.Child("a"),
|
||||
WantFrom: `module.a[*].foo.bar[*]`,
|
||||
WantTo: `module.a[*].module.b[0].foo.baz[*]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `foo.bar`,
|
||||
InputTo: `foo.bar["thing"]`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: AbsResourceInstance{
|
||||
Module: RootModuleInstance,
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
WantTo: AbsResourceInstance{
|
||||
Module: RootModuleInstance,
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Key: StringKey("thing"),
|
||||
},
|
||||
},
|
||||
Module: RootModule,
|
||||
WantFrom: `foo.bar`,
|
||||
WantTo: `foo.bar["thing"]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `foo.bar["thing"]`,
|
||||
InputTo: `foo.bar`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: AbsResourceInstance{
|
||||
Module: RootModuleInstance,
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Key: StringKey("thing"),
|
||||
},
|
||||
},
|
||||
WantTo: AbsResourceInstance{
|
||||
Module: RootModuleInstance,
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
Module: RootModule,
|
||||
WantFrom: `foo.bar["thing"]`,
|
||||
WantTo: `foo.bar`,
|
||||
},
|
||||
{
|
||||
InputFrom: `foo.bar["a"]`,
|
||||
InputTo: `foo.bar["b"]`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: AbsResourceInstance{
|
||||
Module: RootModuleInstance,
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Key: StringKey("a"),
|
||||
},
|
||||
},
|
||||
WantTo: AbsResourceInstance{
|
||||
Module: RootModuleInstance,
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Key: StringKey("b"),
|
||||
},
|
||||
},
|
||||
Module: RootModule,
|
||||
WantFrom: `foo.bar["a"]`,
|
||||
WantTo: `foo.bar["b"]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo`,
|
||||
InputTo: `module.bar`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: AbsModuleCall{
|
||||
Module: RootModuleInstance,
|
||||
Call: ModuleCall{Name: "foo"},
|
||||
},
|
||||
WantTo: AbsModuleCall{
|
||||
Module: RootModuleInstance,
|
||||
Call: ModuleCall{Name: "bar"},
|
||||
},
|
||||
Module: RootModule,
|
||||
WantFrom: `module.foo[*]`,
|
||||
WantTo: `module.bar[*]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo`,
|
||||
InputTo: `module.bar.module.baz`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: AbsModuleCall{
|
||||
Module: RootModuleInstance,
|
||||
Call: ModuleCall{Name: "foo"},
|
||||
},
|
||||
WantTo: AbsModuleCall{
|
||||
Module: RootModuleInstance.Child("bar", NoKey),
|
||||
Call: ModuleCall{Name: "baz"},
|
||||
},
|
||||
Module: RootModule,
|
||||
WantFrom: `module.foo[*]`,
|
||||
WantTo: `module.bar.module.baz[*]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo`,
|
||||
InputTo: `module.bar.module.baz`,
|
||||
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
|
||||
WantFrom: AbsModuleCall{
|
||||
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
|
||||
Call: ModuleCall{Name: "foo"},
|
||||
},
|
||||
WantTo: AbsModuleCall{
|
||||
Module: RootModuleInstance.Child("bloop", StringKey("hi")).Child("bar", NoKey),
|
||||
Call: ModuleCall{Name: "baz"},
|
||||
},
|
||||
Module: RootModule.Child("bloop"),
|
||||
WantFrom: `module.bloop[*].module.foo[*]`,
|
||||
WantTo: `module.bloop[*].module.bar.module.baz[*]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo[0]`,
|
||||
InputTo: `module.foo["a"]`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
|
||||
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
|
||||
Module: RootModule,
|
||||
WantFrom: `module.foo[0]`,
|
||||
WantTo: `module.foo["a"]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo`,
|
||||
InputTo: `module.foo["a"]`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: RootModuleInstance.Child("foo", NoKey),
|
||||
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
|
||||
Module: RootModule,
|
||||
WantFrom: `module.foo`,
|
||||
WantTo: `module.foo["a"]`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo[0]`,
|
||||
InputTo: `module.foo`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
|
||||
WantTo: RootModuleInstance.Child("foo", NoKey),
|
||||
Module: RootModule,
|
||||
WantFrom: `module.foo[0]`,
|
||||
WantTo: `module.foo`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo[0]`,
|
||||
InputTo: `module.foo`,
|
||||
Module: RootModuleInstance.Child("bloop", NoKey),
|
||||
WantFrom: RootModuleInstance.Child("bloop", NoKey).Child("foo", IntKey(0)),
|
||||
WantTo: RootModuleInstance.Child("bloop", NoKey).Child("foo", NoKey),
|
||||
Module: RootModule.Child("bloop"),
|
||||
WantFrom: `module.bloop[*].module.foo[0]`,
|
||||
WantTo: `module.bloop[*].module.foo`,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo`,
|
||||
InputTo: `foo.bar`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: nil, // Can't unify module call with resource
|
||||
WantTo: nil,
|
||||
Module: RootModule,
|
||||
WantFrom: ``, // Can't unify module call with resource
|
||||
WantTo: ``,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo[0]`,
|
||||
InputTo: `foo.bar`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: nil, // Can't unify module instance with resource
|
||||
WantTo: nil,
|
||||
Module: RootModule,
|
||||
WantFrom: ``, // Can't unify module instance with resource
|
||||
WantTo: ``,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo`,
|
||||
InputTo: `foo.bar[0]`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: nil, // Can't unify module call with resource instance
|
||||
WantTo: nil,
|
||||
Module: RootModule,
|
||||
WantFrom: ``, // Can't unify module call with resource instance
|
||||
WantTo: ``,
|
||||
},
|
||||
{
|
||||
InputFrom: `module.foo[0]`,
|
||||
InputTo: `foo.bar[0]`,
|
||||
Module: RootModuleInstance,
|
||||
WantFrom: nil, // Can't unify module instance with resource instance
|
||||
WantTo: nil,
|
||||
Module: RootModule,
|
||||
WantFrom: ``, // Can't unify module instance with resource instance
|
||||
WantTo: ``,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -604,13 +485,12 @@ func TestUnifyMoveEndpoints(t *testing.T) {
|
|||
fromEp := parseInput(test.InputFrom)
|
||||
toEp := parseInput(test.InputTo)
|
||||
|
||||
diffOpts := cmpopts.IgnoreUnexported(ModuleCall{})
|
||||
gotFrom, gotTo := UnifyMoveEndpoints(test.Module, fromEp, toEp)
|
||||
if diff := cmp.Diff(test.WantFrom, gotFrom, diffOpts); diff != "" {
|
||||
t.Errorf("wrong 'from' address\n%s", diff)
|
||||
if got, want := gotFrom.String(), test.WantFrom; got != want {
|
||||
t.Errorf("wrong 'from' result\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
if diff := cmp.Diff(test.WantTo, gotTo, diffOpts); diff != "" {
|
||||
t.Errorf("wrong 'to' address\n%s", diff)
|
||||
if got, want := gotTo.String(), test.WantTo; got != want {
|
||||
t.Errorf("wrong 'to' result\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ package addrs
|
|||
// of the configuration, which is different than the direct representation
|
||||
// of these in configuration where the author gives an address relative to
|
||||
// the current module where the address is defined. The type MoveEndpoint
|
||||
|
||||
type AbsMoveable interface {
|
||||
absMoveableSigil()
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Code generated by "stringer -type MoveEndpointKind"; DO NOT EDIT.
|
||||
|
||||
package addrs
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[MoveEndpointModule-77]
|
||||
_ = x[MoveEndpointResource-82]
|
||||
}
|
||||
|
||||
const (
|
||||
_MoveEndpointKind_name_0 = "MoveEndpointModule"
|
||||
_MoveEndpointKind_name_1 = "MoveEndpointResource"
|
||||
)
|
||||
|
||||
func (i MoveEndpointKind) String() string {
|
||||
switch {
|
||||
case i == 77:
|
||||
return _MoveEndpointKind_name_0
|
||||
case i == 82:
|
||||
return _MoveEndpointKind_name_1
|
||||
default:
|
||||
return "MoveEndpointKind(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
|
@ -10,3 +10,9 @@ type PathAttr struct {
|
|||
func (pa PathAttr) String() string {
|
||||
return "path." + pa.Name
|
||||
}
|
||||
|
||||
func (pa PathAttr) UniqueKey() UniqueKey {
|
||||
return pa // A PathAttr is its own UniqueKey
|
||||
}
|
||||
|
||||
func (pa PathAttr) uniqueKeySigil() {}
|
||||
|
|
|
@ -7,6 +7,9 @@ type Referenceable interface {
|
|||
// in lang.Scope.buildEvalContext.
|
||||
referenceableSigil()
|
||||
|
||||
// All Referenceable address types must have unique keys.
|
||||
UniqueKeyer
|
||||
|
||||
// String produces a string representation of the address that could be
|
||||
// parsed as a HCL traversal and passed to ParseRef to produce an identical
|
||||
// result.
|
||||
|
|
|
@ -32,6 +32,12 @@ func (r Resource) Equal(o Resource) bool {
|
|||
return r.Mode == o.Mode && r.Name == o.Name && r.Type == o.Type
|
||||
}
|
||||
|
||||
func (r Resource) UniqueKey() UniqueKey {
|
||||
return r // A Resource is its own UniqueKey
|
||||
}
|
||||
|
||||
func (r Resource) uniqueKeySigil() {}
|
||||
|
||||
// Instance produces the address for a specific instance of the receiver
|
||||
// that is idenfied by the given key.
|
||||
func (r Resource) Instance(key InstanceKey) ResourceInstance {
|
||||
|
@ -94,6 +100,12 @@ func (r ResourceInstance) Equal(o ResourceInstance) bool {
|
|||
return r.Key == o.Key && r.Resource.Equal(o.Resource)
|
||||
}
|
||||
|
||||
func (r ResourceInstance) UniqueKey() UniqueKey {
|
||||
return r // A ResourceInstance is its own UniqueKey
|
||||
}
|
||||
|
||||
func (r ResourceInstance) uniqueKeySigil() {}
|
||||
|
||||
// Absolute returns an AbsResourceInstance from the receiver and the given module
|
||||
// instance address.
|
||||
func (r ResourceInstance) Absolute(module ModuleInstance) AbsResourceInstance {
|
||||
|
@ -280,6 +292,14 @@ func (r AbsResourceInstance) Less(o AbsResourceInstance) bool {
|
|||
}
|
||||
}
|
||||
|
||||
type absResourceInstanceKey string
|
||||
|
||||
func (r AbsResourceInstance) UniqueKey() UniqueKey {
|
||||
return absResourceInstanceKey(r.String())
|
||||
}
|
||||
|
||||
func (r absResourceInstanceKey) uniqueKeySigil() {}
|
||||
|
||||
func (r AbsResourceInstance) absMoveableSigil() {
|
||||
// AbsResourceInstance is moveable
|
||||
}
|
||||
|
|
|
@ -44,6 +44,12 @@ func (rp ResourceInstancePhase) String() string {
|
|||
return fmt.Sprintf("%s#%s", rp.ResourceInstance, rp.Phase)
|
||||
}
|
||||
|
||||
func (rp ResourceInstancePhase) UniqueKey() UniqueKey {
|
||||
return rp // A ResourceInstancePhase is its own UniqueKey
|
||||
}
|
||||
|
||||
func (rp ResourceInstancePhase) uniqueKeySigil() {}
|
||||
|
||||
// ResourceInstancePhaseType is an enumeration used with ResourceInstancePhase.
|
||||
type ResourceInstancePhaseType string
|
||||
|
||||
|
@ -103,3 +109,9 @@ func (rp ResourcePhase) String() string {
|
|||
// because this special address type should never be exposed in the UI.
|
||||
return fmt.Sprintf("%s#%s", rp.Resource, rp.Phase)
|
||||
}
|
||||
|
||||
func (rp ResourcePhase) UniqueKey() UniqueKey {
|
||||
return rp // A ResourcePhase is its own UniqueKey
|
||||
}
|
||||
|
||||
func (rp ResourcePhase) uniqueKeySigil() {}
|
||||
|
|
|
@ -12,3 +12,9 @@ func (s selfT) referenceableSigil() {
|
|||
func (s selfT) String() string {
|
||||
return "self"
|
||||
}
|
||||
|
||||
func (s selfT) UniqueKey() UniqueKey {
|
||||
return Self // Self is its own UniqueKey
|
||||
}
|
||||
|
||||
func (s selfT) uniqueKeySigil() {}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package addrs
|
||||
|
||||
// Set represents a set of addresses of types that implement UniqueKeyer.
|
||||
type Set map[UniqueKey]UniqueKeyer
|
||||
|
||||
func (s Set) Has(addr UniqueKeyer) bool {
|
||||
_, exists := s[addr.UniqueKey()]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (s Set) Add(addr UniqueKeyer) {
|
||||
s[addr.UniqueKey()] = addr
|
||||
}
|
||||
|
||||
func (s Set) Remove(addr UniqueKeyer) {
|
||||
delete(s, addr.UniqueKey())
|
||||
}
|
||||
|
||||
func (s Set) Union(other Set) Set {
|
||||
ret := make(Set)
|
||||
for k, addr := range s {
|
||||
ret[k] = addr
|
||||
}
|
||||
for k, addr := range other {
|
||||
ret[k] = addr
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s Set) Intersection(other Set) Set {
|
||||
ret := make(Set)
|
||||
for k, addr := range s {
|
||||
if _, exists := other[k]; exists {
|
||||
ret[k] = addr
|
||||
}
|
||||
}
|
||||
for k, addr := range other {
|
||||
if _, exists := s[k]; exists {
|
||||
ret[k] = addr
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
|
@ -10,3 +10,9 @@ type TerraformAttr struct {
|
|||
func (ta TerraformAttr) String() string {
|
||||
return "terraform." + ta.Name
|
||||
}
|
||||
|
||||
func (ta TerraformAttr) UniqueKey() UniqueKey {
|
||||
return ta // A TerraformAttr is its own UniqueKey
|
||||
}
|
||||
|
||||
func (ta TerraformAttr) uniqueKeySigil() {}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package addrs
|
||||
|
||||
// UniqueKey is an interface implemented by values that serve as unique map
|
||||
// keys for particular addresses.
|
||||
//
|
||||
// All implementations of UniqueKey are comparable and can thus be used as
|
||||
// map keys. Unique keys generated from different address types are always
|
||||
// distinct. All functionally-equivalent keys for the same address type
|
||||
// always compare equal, and likewise functionally-different values do not.
|
||||
type UniqueKey interface {
|
||||
uniqueKeySigil()
|
||||
}
|
||||
|
||||
// UniqueKeyer is an interface implemented by types that can be represented
|
||||
// by a unique key.
|
||||
//
|
||||
// Some address types naturally comply with the expectations of a UniqueKey
|
||||
// and may thus be their own unique key type. However, address types that
|
||||
// are not naturally comparable can implement this interface by returning
|
||||
// proxy values.
|
||||
type UniqueKeyer interface {
|
||||
UniqueKey() UniqueKey
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package addrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUniqueKeyer aims to ensure that all of the types that have unique keys
|
||||
// will continue to meet the UniqueKeyer contract under future changes.
|
||||
//
|
||||
// If you add a new implementation of UniqueKey, consider adding a test case
|
||||
// for it here.
|
||||
func TestUniqueKeyer(t *testing.T) {
|
||||
tests := []UniqueKeyer{
|
||||
CountAttr{Name: "index"},
|
||||
ForEachAttr{Name: "key"},
|
||||
TerraformAttr{Name: "workspace"},
|
||||
PathAttr{Name: "module"},
|
||||
InputVariable{Name: "foo"},
|
||||
ModuleCall{Name: "foo"},
|
||||
ModuleCallInstance{
|
||||
Call: ModuleCall{Name: "foo"},
|
||||
Key: StringKey("a"),
|
||||
},
|
||||
ModuleCallOutput{
|
||||
Call: ModuleCall{Name: "foo"},
|
||||
Name: "bar",
|
||||
},
|
||||
ModuleCallInstanceOutput{
|
||||
Call: ModuleCallInstance{
|
||||
Call: ModuleCall{Name: "foo"},
|
||||
Key: StringKey("a"),
|
||||
},
|
||||
Name: "bar",
|
||||
},
|
||||
Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Key: IntKey(1),
|
||||
},
|
||||
RootModuleInstance,
|
||||
RootModuleInstance.Child("foo", NoKey),
|
||||
RootModuleInstance.ResourceInstance(
|
||||
DataResourceMode,
|
||||
"boop",
|
||||
"beep",
|
||||
NoKey,
|
||||
),
|
||||
Self,
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s", test), func(t *testing.T) {
|
||||
a := test.UniqueKey()
|
||||
b := test.UniqueKey()
|
||||
|
||||
// The following comparison will panic if the unique key is not
|
||||
// of a comparable type.
|
||||
if a != b {
|
||||
t.Fatalf("the two unique keys are not equal\na: %#v\b: %#v", a, b)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1814,7 +1814,27 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
|||
lcs := objchange.LongestCommonSubsequence(old, new)
|
||||
var oldI, newI, lcsI int
|
||||
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
|
||||
// We first process items in the old and new sequences which are not
|
||||
// equal to the current common sequence item. Old items are marked as
|
||||
// deletions, and new items are marked as additions.
|
||||
//
|
||||
// There is an exception for deleted & created object items, which we
|
||||
// try to render as updates where that makes sense.
|
||||
for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
|
||||
// Render this as an object update if all of these are true:
|
||||
//
|
||||
// - the current old item is an object;
|
||||
// - there's a current new item which is also an object;
|
||||
// - either there are no common items left, or the current new item
|
||||
// doesn't equal the current common item.
|
||||
//
|
||||
// Why do we need the the last clause? If we have current items in all
|
||||
// three sequences, and the current new item is equal to a common item,
|
||||
// then we should just need to advance the old item list and we'll
|
||||
// eventually find a common item matching both old and new.
|
||||
//
|
||||
// This combination of conditions allows us to render an object update
|
||||
// diff instead of a combination of delete old & create new.
|
||||
isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
|
||||
if isObjectDiff {
|
||||
ret = append(ret, &plans.Change{
|
||||
|
@ -1827,6 +1847,8 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
|||
continue
|
||||
}
|
||||
|
||||
// Otherwise, this item is not part of the common sequence, so
|
||||
// render as a deletion.
|
||||
ret = append(ret, &plans.Change{
|
||||
Action: plans.Delete,
|
||||
Before: old[oldI],
|
||||
|
@ -1842,6 +1864,9 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
|||
})
|
||||
newI++
|
||||
}
|
||||
|
||||
// When we've exhausted the old & new sequences of items which are not
|
||||
// in the common subsequence, we render a common item and continue.
|
||||
if lcsI < len(lcs) {
|
||||
ret = append(ret, &plans.Change{
|
||||
Action: plans.NoOp,
|
||||
|
|
|
@ -9,6 +9,7 @@ const (
|
|||
MessageDiagnostic MessageType = "diagnostic"
|
||||
|
||||
// Operation results
|
||||
MessageResourceDrift MessageType = "resource_drift"
|
||||
MessagePlannedChange MessageType = "planned_change"
|
||||
MessageChangeSummary MessageType = "change_summary"
|
||||
MessageOutputs MessageType = "outputs"
|
||||
|
|
|
@ -95,6 +95,14 @@ func (v *JSONView) PlannedChange(c *json.ResourceInstanceChange) {
|
|||
)
|
||||
}
|
||||
|
||||
func (v *JSONView) ResourceDrift(c *json.ResourceInstanceChange) {
|
||||
v.log.Info(
|
||||
fmt.Sprintf("%s: Drift detected (%s)", c.Resource.Addr, c.Action),
|
||||
"type", json.MessageResourceDrift,
|
||||
"change", c,
|
||||
)
|
||||
}
|
||||
|
||||
func (v *JSONView) ChangeSummary(cs *json.ChangeSummary) {
|
||||
v.log.Info(
|
||||
cs.String(),
|
||||
|
|
|
@ -141,6 +141,46 @@ func TestJSONView_PlannedChange(t *testing.T) {
|
|||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
||||
}
|
||||
|
||||
func TestJSONView_ResourceDrift(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
jv := NewJSONView(NewView(streams))
|
||||
|
||||
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
|
||||
if len(diags) > 0 {
|
||||
t.Fatal(diags.Err())
|
||||
}
|
||||
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
|
||||
cs := &plans.ResourceInstanceChangeSrc{
|
||||
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
||||
ChangeSrc: plans.ChangeSrc{
|
||||
Action: plans.Update,
|
||||
},
|
||||
}
|
||||
jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs))
|
||||
|
||||
want := []map[string]interface{}{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`,
|
||||
"@module": "terraform.ui",
|
||||
"type": "resource_drift",
|
||||
"change": map[string]interface{}{
|
||||
"action": "update",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": `module.foo.test_instance.bar["boop"]`,
|
||||
"implied_provider": "test",
|
||||
"module": "module.foo",
|
||||
"resource": `test_instance.bar["boop"]`,
|
||||
"resource_key": "boop",
|
||||
"resource_name": "bar",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
||||
}
|
||||
|
||||
func TestJSONView_ChangeSummary(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
jv := NewJSONView(NewView(streams))
|
||||
|
@ -263,12 +303,16 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
|
|||
gotLines := strings.Split(output, "\n")
|
||||
|
||||
if len(gotLines) != len(want) {
|
||||
t.Fatalf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
|
||||
t.Errorf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
|
||||
}
|
||||
|
||||
// Unmarshal each line and compare to the expected value
|
||||
for i := range gotLines {
|
||||
var gotStruct map[string]interface{}
|
||||
if i >= len(want) {
|
||||
t.Error("reached end of want messages too soon")
|
||||
break
|
||||
}
|
||||
wantStruct := want[i]
|
||||
|
||||
if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
|
||||
|
@ -283,12 +327,12 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
|
|||
|
||||
// Verify the timestamp format
|
||||
if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil {
|
||||
t.Fatalf("error parsing timestamp: %s", err)
|
||||
t.Errorf("error parsing timestamp on line %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !cmp.Equal(wantStruct, gotStruct) {
|
||||
t.Fatalf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
|
||||
t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package views
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
|
@ -10,9 +11,11 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/views/json"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type Operation interface {
|
||||
|
@ -160,6 +163,12 @@ func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
|
|||
// Log a change summary and a series of "planned" messages for the changes in
|
||||
// the plan.
|
||||
func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
if err := v.resourceDrift(plan.PrevRunState, plan.PriorState, schemas); err != nil {
|
||||
var diags tfdiags.Diagnostics
|
||||
diags = diags.Append(err)
|
||||
v.Diagnostics(diags)
|
||||
}
|
||||
|
||||
cs := &json.ChangeSummary{
|
||||
Operation: json.OperationPlanned,
|
||||
}
|
||||
|
@ -188,6 +197,92 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
|||
v.view.ChangeSummary(cs)
|
||||
}
|
||||
|
||||
func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
|
||||
if newState.ManagedResourcesEqual(oldState) {
|
||||
// Nothing to do, because we only detect and report drift for managed
|
||||
// resource instances.
|
||||
return nil
|
||||
}
|
||||
var changes []*json.ResourceInstanceChange
|
||||
for _, ms := range oldState.Modules {
|
||||
for _, rs := range ms.Resources {
|
||||
if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
|
||||
// Drift reporting is only for managed resources
|
||||
continue
|
||||
}
|
||||
|
||||
provider := rs.ProviderConfig.Provider
|
||||
for key, oldIS := range rs.Instances {
|
||||
if oldIS.Current == nil {
|
||||
// Not interested in instances that only have deposed objects
|
||||
continue
|
||||
}
|
||||
addr := rs.Addr.Instance(key)
|
||||
newIS := newState.ResourceInstance(addr)
|
||||
|
||||
schema, _ := schemas.ResourceTypeConfig(
|
||||
provider,
|
||||
addr.Resource.Resource.Mode,
|
||||
addr.Resource.Resource.Type,
|
||||
)
|
||||
if schema == nil {
|
||||
return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider)
|
||||
}
|
||||
ty := schema.ImpliedType()
|
||||
|
||||
oldObj, err := oldIS.Current.Decode(ty)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err)
|
||||
}
|
||||
|
||||
var newObj *states.ResourceInstanceObject
|
||||
if newIS != nil && newIS.Current != nil {
|
||||
newObj, err = newIS.Current.Decode(ty)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
var oldVal, newVal cty.Value
|
||||
oldVal = oldObj.Value
|
||||
if newObj != nil {
|
||||
newVal = newObj.Value
|
||||
} else {
|
||||
newVal = cty.NullVal(ty)
|
||||
}
|
||||
|
||||
if oldVal.RawEquals(newVal) {
|
||||
// No drift if the two values are semantically equivalent
|
||||
continue
|
||||
}
|
||||
|
||||
// We can only detect updates and deletes as drift.
|
||||
action := plans.Update
|
||||
if newVal.IsNull() {
|
||||
action = plans.Delete
|
||||
}
|
||||
|
||||
change := &plans.ResourceInstanceChangeSrc{
|
||||
Addr: addr,
|
||||
ChangeSrc: plans.ChangeSrc{
|
||||
Action: action,
|
||||
},
|
||||
}
|
||||
changes = append(changes, json.NewResourceInstanceChange(change))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the change structs lexically by address to give stable output
|
||||
sort.Slice(changes, func(i, j int) bool { return changes[i].Resource.Addr < changes[j].Resource.Addr })
|
||||
|
||||
for _, change := range changes {
|
||||
v.view.ResourceDrift(change)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
||||
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
||||
// Avoid rendering data sources on deletion
|
||||
|
|
|
@ -483,8 +483,8 @@ func TestOperationJSON_plan(t *testing.T) {
|
|||
if len(diags) > 0 {
|
||||
t.Fatal(diags.Err())
|
||||
}
|
||||
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"}
|
||||
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "beep"}
|
||||
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
||||
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
||||
derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"}
|
||||
|
||||
plan := &plans.Plan{
|
||||
|
@ -518,101 +518,101 @@ func TestOperationJSON_plan(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
v.Plan(plan, nil)
|
||||
v.Plan(plan, testSchemas())
|
||||
|
||||
want := []map[string]interface{}{
|
||||
// Create-then-delete should result in replace
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test_instance.boop[0]: Plan to replace",
|
||||
"@message": "test_resource.boop[0]: Plan to replace",
|
||||
"@module": "terraform.ui",
|
||||
"type": "planned_change",
|
||||
"change": map[string]interface{}{
|
||||
"action": "replace",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": `test_instance.boop[0]`,
|
||||
"addr": `test_resource.boop[0]`,
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": `test_instance.boop[0]`,
|
||||
"resource": `test_resource.boop[0]`,
|
||||
"resource_key": float64(0),
|
||||
"resource_name": "boop",
|
||||
"resource_type": "test_instance",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Simple create
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test_instance.boop[1]: Plan to create",
|
||||
"@message": "test_resource.boop[1]: Plan to create",
|
||||
"@module": "terraform.ui",
|
||||
"type": "planned_change",
|
||||
"change": map[string]interface{}{
|
||||
"action": "create",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": `test_instance.boop[1]`,
|
||||
"addr": `test_resource.boop[1]`,
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": `test_instance.boop[1]`,
|
||||
"resource": `test_resource.boop[1]`,
|
||||
"resource_key": float64(1),
|
||||
"resource_name": "boop",
|
||||
"resource_type": "test_instance",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Simple delete
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "module.vpc.test_instance.boop[0]: Plan to delete",
|
||||
"@message": "module.vpc.test_resource.boop[0]: Plan to delete",
|
||||
"@module": "terraform.ui",
|
||||
"type": "planned_change",
|
||||
"change": map[string]interface{}{
|
||||
"action": "delete",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": `module.vpc.test_instance.boop[0]`,
|
||||
"addr": `module.vpc.test_resource.boop[0]`,
|
||||
"implied_provider": "test",
|
||||
"module": "module.vpc",
|
||||
"resource": `test_instance.boop[0]`,
|
||||
"resource": `test_resource.boop[0]`,
|
||||
"resource_key": float64(0),
|
||||
"resource_name": "boop",
|
||||
"resource_type": "test_instance",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Delete-then-create is also a replace
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test_instance.beep: Plan to replace",
|
||||
"@message": "test_resource.beep: Plan to replace",
|
||||
"@module": "terraform.ui",
|
||||
"type": "planned_change",
|
||||
"change": map[string]interface{}{
|
||||
"action": "replace",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": `test_instance.beep`,
|
||||
"addr": `test_resource.beep`,
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": `test_instance.beep`,
|
||||
"resource": `test_resource.beep`,
|
||||
"resource_key": nil,
|
||||
"resource_name": "beep",
|
||||
"resource_type": "test_instance",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Simple update
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "module.vpc.test_instance.beep: Plan to update",
|
||||
"@message": "module.vpc.test_resource.beep: Plan to update",
|
||||
"@module": "terraform.ui",
|
||||
"type": "planned_change",
|
||||
"change": map[string]interface{}{
|
||||
"action": "update",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": `module.vpc.test_instance.beep`,
|
||||
"addr": `module.vpc.test_resource.beep`,
|
||||
"implied_provider": "test",
|
||||
"module": "module.vpc",
|
||||
"resource": `test_instance.beep`,
|
||||
"resource": `test_resource.beep`,
|
||||
"resource_key": nil,
|
||||
"resource_name": "beep",
|
||||
"resource_type": "test_instance",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -635,6 +635,134 @@ func TestOperationJSON_plan(t *testing.T) {
|
|||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
||||
}
|
||||
|
||||
func TestOperationJSON_planDrift(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
||||
|
||||
root := addrs.RootModuleInstance
|
||||
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
||||
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
||||
derp := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "derp"}
|
||||
|
||||
plan := &plans.Plan{
|
||||
Changes: &plans.Changes{
|
||||
Resources: []*plans.ResourceInstanceChangeSrc{},
|
||||
},
|
||||
PrevRunState: states.BuildState(func(state *states.SyncState) {
|
||||
// Update
|
||||
state.SetResourceInstanceCurrent(
|
||||
boop.Instance(addrs.NoKey).Absolute(root),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"bar"}`),
|
||||
},
|
||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
||||
)
|
||||
// Delete
|
||||
state.SetResourceInstanceCurrent(
|
||||
beep.Instance(addrs.NoKey).Absolute(root),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"boop"}`),
|
||||
},
|
||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
||||
)
|
||||
// No-op
|
||||
state.SetResourceInstanceCurrent(
|
||||
derp.Instance(addrs.NoKey).Absolute(root),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"boop"}`),
|
||||
},
|
||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
||||
)
|
||||
}),
|
||||
PriorState: states.BuildState(func(state *states.SyncState) {
|
||||
// Update
|
||||
state.SetResourceInstanceCurrent(
|
||||
boop.Instance(addrs.NoKey).Absolute(root),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"baz"}`),
|
||||
},
|
||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
||||
)
|
||||
// Delete
|
||||
state.SetResourceInstanceCurrent(
|
||||
beep.Instance(addrs.NoKey).Absolute(root),
|
||||
nil,
|
||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
||||
)
|
||||
// No-op
|
||||
state.SetResourceInstanceCurrent(
|
||||
derp.Instance(addrs.NoKey).Absolute(root),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"boop"}`),
|
||||
},
|
||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
||||
)
|
||||
}),
|
||||
}
|
||||
v.Plan(plan, testSchemas())
|
||||
|
||||
want := []map[string]interface{}{
|
||||
// Drift detected: delete
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test_resource.beep: Drift detected (delete)",
|
||||
"@module": "terraform.ui",
|
||||
"type": "resource_drift",
|
||||
"change": map[string]interface{}{
|
||||
"action": "delete",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": "test_resource.beep",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "test_resource.beep",
|
||||
"resource_key": nil,
|
||||
"resource_name": "beep",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Drift detected: update
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test_resource.boop: Drift detected (update)",
|
||||
"@module": "terraform.ui",
|
||||
"type": "resource_drift",
|
||||
"change": map[string]interface{}{
|
||||
"action": "update",
|
||||
"resource": map[string]interface{}{
|
||||
"addr": "test_resource.boop",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "test_resource.boop",
|
||||
"resource_key": nil,
|
||||
"resource_name": "boop",
|
||||
"resource_type": "test_resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
// No changes
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Plan: 0 to add, 0 to change, 0 to destroy.",
|
||||
"@module": "terraform.ui",
|
||||
"type": "change_summary",
|
||||
"changes": map[string]interface{}{
|
||||
"operation": "plan",
|
||||
"add": float64(0),
|
||||
"change": float64(0),
|
||||
"remove": float64(0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
||||
}
|
||||
|
||||
func TestOperationJSON_plannedChange(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
||||
|
|
|
@ -211,9 +211,15 @@ func (a *Attribute) decoderSpec(name string) hcldec.Spec {
|
|||
// belong to their own cty.Object definitions. It is used in other functions
|
||||
// which themselves handle that recursion.
|
||||
func listOptionalAttrsFromObject(o *Object) []string {
|
||||
var ret []string
|
||||
ret := make([]string, 0)
|
||||
|
||||
// This is unlikely to happen outside of tests.
|
||||
if o == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
for name, attr := range o.Attributes {
|
||||
if attr.Optional == true {
|
||||
if attr.Optional || attr.Computed {
|
||||
ret = append(ret, name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package configschema
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/apparentlymart/go-dump/dump"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
|
@ -885,3 +887,43 @@ func TestAttributeDecoderSpec_panic(t *testing.T) {
|
|||
attrS.decoderSpec("attr")
|
||||
t.Errorf("expected panic")
|
||||
}
|
||||
|
||||
func TestListOptionalAttrsFromObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
input *Object
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
nil,
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
&Object{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
&Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"optional": {Type: cty.String, Optional: true},
|
||||
"required": {Type: cty.Number, Required: true},
|
||||
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
||||
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
||||
},
|
||||
},
|
||||
[]string{"optional", "computed", "optional_computed"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := listOptionalAttrsFromObject(test.input)
|
||||
|
||||
// order is irrelevant
|
||||
sort.Strings(got)
|
||||
sort.Strings(test.want)
|
||||
|
||||
if diff := cmp.Diff(got, test.want); diff != "" {
|
||||
t.Fatalf("wrong result: %s\n", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ func (o *Object) ImpliedType() cty.Type {
|
|||
case NestingSet:
|
||||
return cty.Set(ret)
|
||||
default: // Should never happen
|
||||
panic("invalid Nesting")
|
||||
return cty.EmptyObject
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ func TestBlockImpliedType(t *testing.T) {
|
|||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -132,26 +133,18 @@ func TestObjectImpliedType(t *testing.T) {
|
|||
nil,
|
||||
cty.EmptyObject,
|
||||
},
|
||||
"empty": {
|
||||
&Object{},
|
||||
cty.EmptyObject,
|
||||
},
|
||||
"attributes": {
|
||||
&Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
},
|
||||
"optional": {Type: cty.String, Optional: true},
|
||||
"required": {Type: cty.Number, Required: true},
|
||||
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
||||
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
||||
},
|
||||
},
|
||||
cty.ObjectWithOptionalAttrs(
|
||||
|
@ -161,7 +154,7 @@ func TestObjectImpliedType(t *testing.T) {
|
|||
"computed": cty.List(cty.Bool),
|
||||
"optional_computed": cty.Map(cty.Bool),
|
||||
},
|
||||
[]string{"optional", "optional_computed"},
|
||||
[]string{"optional", "computed", "optional_computed"},
|
||||
),
|
||||
},
|
||||
"nested attributes": {
|
||||
|
@ -172,21 +165,42 @@ func TestObjectImpliedType(t *testing.T) {
|
|||
NestedType: &Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
"optional": {Type: cty.String, Optional: true},
|
||||
"required": {Type: cty.Number, Required: true},
|
||||
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
||||
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"nested_type": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"optional": cty.String,
|
||||
"required": cty.Number,
|
||||
"computed": cty.List(cty.Bool),
|
||||
"optional_computed": cty.Map(cty.Bool),
|
||||
}, []string{"optional", "computed", "optional_computed"}),
|
||||
}, []string{"nested_type"}),
|
||||
},
|
||||
"nested object-type attributes": {
|
||||
&Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"nested_type": {
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"optional": {Type: cty.String, Optional: true},
|
||||
"required": {Type: cty.Number, Required: true},
|
||||
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
||||
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
||||
"object": {
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"optional": cty.String,
|
||||
"required": cty.Number,
|
||||
}, []string{"optional"}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -200,7 +214,8 @@ func TestObjectImpliedType(t *testing.T) {
|
|||
"required": cty.Number,
|
||||
"computed": cty.List(cty.Bool),
|
||||
"optional_computed": cty.Map(cty.Bool),
|
||||
}, []string{"optional", "optional_computed"}),
|
||||
"object": cty.ObjectWithOptionalAttrs(map[string]cty.Type{"optional": cty.String, "required": cty.Number}, []string{"optional"}),
|
||||
}, []string{"optional", "computed", "optional_computed"}),
|
||||
}, []string{"nested_type"}),
|
||||
},
|
||||
"NestingList": {
|
||||
|
|
|
@ -55,7 +55,7 @@ func init() {
|
|||
configureRequestTimeout()
|
||||
}
|
||||
|
||||
var SupportedPluginProtocols = MustParseVersionConstraints("~> 5")
|
||||
var SupportedPluginProtocols = MustParseVersionConstraints(">= 5, <7")
|
||||
|
||||
// registryClient is a client for the provider registry protocol that is
|
||||
// specialized only for the needs of this package. It's not intended as a
|
||||
|
|
|
@ -218,6 +218,10 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
|
|||
resp.Header().Set("Content-Type", "application/json")
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(`{"versions":[{"version":"1.0.0","protocols":["0.1"]}]}`))
|
||||
case "weaksauce/protocol-six":
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(`{"versions":[{"version":"1.0.0","protocols":["6.0"]}]}`))
|
||||
case "weaksauce/no-versions":
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
resp.WriteHeader(200)
|
||||
|
@ -412,6 +416,12 @@ func TestFindClosestProtocolCompatibleVersion(t *testing.T) {
|
|||
versions.Unspecified,
|
||||
``,
|
||||
},
|
||||
"provider protocol six": {
|
||||
addrs.MustParseProviderSourceString("example.com/weaksauce/protocol-six"),
|
||||
MustParseVersion("1.0.0"),
|
||||
MustParseVersion("1.0.0"),
|
||||
``,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
|
|
@ -1457,7 +1457,7 @@ func TestProposedNew(t *testing.T) {
|
|||
"computed": cty.String,
|
||||
"optional_computed": cty.String,
|
||||
"required": cty.String,
|
||||
}, []string{"optional", "optional_computed"}),
|
||||
}, []string{"computed", "optional", "optional_computed"}),
|
||||
}))),
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
package refactoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/dag"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
)
|
||||
|
||||
type MoveResult struct {
|
||||
From, To addrs.AbsResourceInstance
|
||||
}
|
||||
|
||||
// ApplyMoves modifies in-place the given state object so that any existing
|
||||
// objects that are matched by a "from" argument of one of the move statements
|
||||
// will be moved to instead appear at the "to" argument of that statement.
|
||||
//
|
||||
// The result is a map from the unique key of each absolute address that was
|
||||
// either the source or destination of a move to a MoveResult describing
|
||||
// what happened at that address.
|
||||
//
|
||||
// ApplyMoves does not have any error situations itself, and will instead just
|
||||
// ignore any unresolvable move statements. Validation of a set of moves is
|
||||
// a separate concern applied to the configuration, because validity of
|
||||
// moves is always dependent only on the configuration, not on the state.
|
||||
//
|
||||
// ApplyMoves expects exclusive access to the given state while it's running.
|
||||
// Don't read or write any part of the state structure until ApplyMoves returns.
|
||||
func ApplyMoves(stmts []MoveStatement, state *states.State) map[addrs.UniqueKey]MoveResult {
|
||||
// The methodology here is to construct a small graph of all of the move
|
||||
// statements where the edges represent where a particular statement
|
||||
// is either chained from or nested inside the effect of another statement.
|
||||
// That then means we can traverse the graph in topological sort order
|
||||
// to gradually move objects through potentially multiple moves each.
|
||||
|
||||
g := buildMoveStatementGraph(stmts)
|
||||
|
||||
// If there are any cycles in the graph then we'll not take any action
|
||||
// at all. The separate validation step should detect this and return
|
||||
// an error.
|
||||
if len(g.Cycles()) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The starting nodes are the ones that don't depend on any other nodes.
|
||||
startNodes := make(dag.Set, len(stmts))
|
||||
for _, v := range g.Vertices() {
|
||||
if len(g.UpEdges(v)) == 0 {
|
||||
startNodes.Add(v)
|
||||
}
|
||||
}
|
||||
|
||||
results := make(map[addrs.UniqueKey]MoveResult)
|
||||
g.DepthFirstWalk(startNodes, func(v dag.Vertex, depth int) error {
|
||||
stmt := v.(*MoveStatement)
|
||||
|
||||
for _, ms := range state.Modules {
|
||||
modAddr := ms.Addr
|
||||
if !stmt.From.SelectsModule(modAddr) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We now know that the current module is relevant but what
|
||||
// we'll do with it depends on the object kind.
|
||||
switch kind := stmt.ObjectKind(); kind {
|
||||
case addrs.MoveEndpointModule:
|
||||
// For a module endpoint we just try the module address
|
||||
// directly.
|
||||
if newAddr, matches := modAddr.MoveDestination(stmt.From, stmt.To); matches {
|
||||
// We need to visit all of the resource instances in the
|
||||
// module and record them individually as results.
|
||||
for _, rs := range ms.Resources {
|
||||
relAddr := rs.Addr.Resource
|
||||
for key := range rs.Instances {
|
||||
oldInst := relAddr.Instance(key).Absolute(modAddr)
|
||||
newInst := relAddr.Instance(key).Absolute(newAddr)
|
||||
result := MoveResult{
|
||||
From: oldInst,
|
||||
To: newInst,
|
||||
}
|
||||
results[oldInst.UniqueKey()] = result
|
||||
results[newInst.UniqueKey()] = result
|
||||
}
|
||||
}
|
||||
|
||||
state.MoveModuleInstance(modAddr, newAddr)
|
||||
continue
|
||||
}
|
||||
case addrs.MoveEndpointResource:
|
||||
// For a resource endpoint we need to search each of the
|
||||
// resources and resource instances in the module.
|
||||
for _, rs := range ms.Resources {
|
||||
rAddr := rs.Addr
|
||||
if newAddr, matches := rAddr.MoveDestination(stmt.From, stmt.To); matches {
|
||||
for key := range rs.Instances {
|
||||
oldInst := rAddr.Instance(key)
|
||||
newInst := newAddr.Instance(key)
|
||||
result := MoveResult{
|
||||
From: oldInst,
|
||||
To: newInst,
|
||||
}
|
||||
results[oldInst.UniqueKey()] = result
|
||||
results[newInst.UniqueKey()] = result
|
||||
}
|
||||
state.MoveAbsResource(rAddr, newAddr)
|
||||
continue
|
||||
}
|
||||
for key := range rs.Instances {
|
||||
iAddr := rAddr.Instance(key)
|
||||
if newAddr, matches := iAddr.MoveDestination(stmt.From, stmt.To); matches {
|
||||
result := MoveResult{From: iAddr, To: newAddr}
|
||||
results[iAddr.UniqueKey()] = result
|
||||
results[newAddr.UniqueKey()] = result
|
||||
|
||||
state.MoveAbsResourceInstance(iAddr, newAddr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled move object kind %s", kind))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// FIXME: In the case of either chained or nested moves, "results" will
|
||||
// be left in a pretty interesting shape where the "old" address will
|
||||
// refer to a result that describes only the first step, while the "new"
|
||||
// address will refer to a result that describes only the last step.
|
||||
// To make that actually useful we'll need a different strategy where
|
||||
// the result describes the _effective_ source and destination, skipping
|
||||
// over any intermediate steps we took to get there, so that ultimately
|
||||
// we'll have enough information to annotate items in the plan with the
|
||||
// addresses the originally moved from.
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// buildMoveStatementGraph constructs a dependency graph of the given move
|
||||
// statements, where the nodes are all pointers to statements in the given
|
||||
// slice and the edges represent either chaining or nesting relationships.
|
||||
//
|
||||
// buildMoveStatementGraph doesn't do any validation of the graph, so it
|
||||
// may contain cycles and other sorts of invalidity.
|
||||
func buildMoveStatementGraph(stmts []MoveStatement) *dag.AcyclicGraph {
|
||||
g := &dag.AcyclicGraph{}
|
||||
for _, stmt := range stmts {
|
||||
// The graph nodes are pointers to the actual statements directly.
|
||||
g.Add(&stmt)
|
||||
}
|
||||
|
||||
// Now we'll add the edges representing chaining and nesting relationships.
|
||||
// We assume that a reasonable configuration will have at most tens of
|
||||
// move statements and thus this N*M algorithm is acceptable.
|
||||
for dependerI := range stmts {
|
||||
depender := &stmts[dependerI]
|
||||
for dependeeI := range stmts {
|
||||
dependee := &stmts[dependeeI]
|
||||
dependeeTo := dependee.To
|
||||
dependerFrom := depender.From
|
||||
if dependerFrom.CanChainFrom(dependeeTo) || dependerFrom.NestedWithin(dependeeTo) {
|
||||
g.Connect(dag.BasicEdge(depender, dependee))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package refactoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
)
|
||||
|
||||
func TestApplyMoves(t *testing.T) {
|
||||
// TODO: Renable this once we're ready to implement the intended behaviors
|
||||
// it is describing.
|
||||
t.Skip("ApplyMoves is not yet fully implemented")
|
||||
|
||||
providerAddr := addrs.AbsProviderConfig{
|
||||
Module: addrs.RootModule,
|
||||
Provider: addrs.MustParseProviderSourceString("example.com/foo/bar"),
|
||||
}
|
||||
rootNoKeyResourceAddr := [...]addrs.AbsResourceInstance{
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "from",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "to",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
}
|
||||
rootIntKeyResourceAddr := [...]addrs.AbsResourceInstance{
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "from",
|
||||
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "foo",
|
||||
Name: "to",
|
||||
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
Stmts []MoveStatement
|
||||
State *states.State
|
||||
|
||||
WantResults map[addrs.UniqueKey]MoveResult
|
||||
WantInstanceAddrs []string
|
||||
}{
|
||||
"no moves and empty state": {
|
||||
[]MoveStatement{},
|
||||
states.NewState(),
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
"no moves": {
|
||||
[]MoveStatement{},
|
||||
states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
rootNoKeyResourceAddr[0],
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{}`),
|
||||
},
|
||||
providerAddr,
|
||||
)
|
||||
}),
|
||||
nil,
|
||||
[]string{
|
||||
`foo.from`,
|
||||
},
|
||||
},
|
||||
"single move of whole singleton resource": {
|
||||
[]MoveStatement{
|
||||
testMoveStatement(t, "", "foo.from", "foo.to"),
|
||||
},
|
||||
states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
rootNoKeyResourceAddr[0],
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{}`),
|
||||
},
|
||||
providerAddr,
|
||||
)
|
||||
}),
|
||||
map[addrs.UniqueKey]MoveResult{
|
||||
rootNoKeyResourceAddr[0].UniqueKey(): {
|
||||
From: rootNoKeyResourceAddr[0],
|
||||
To: rootNoKeyResourceAddr[1],
|
||||
},
|
||||
rootNoKeyResourceAddr[1].UniqueKey(): {
|
||||
From: rootNoKeyResourceAddr[1],
|
||||
To: rootNoKeyResourceAddr[1],
|
||||
},
|
||||
},
|
||||
[]string{
|
||||
`foo.to`,
|
||||
},
|
||||
},
|
||||
"single move of whole 'count' resource": {
|
||||
[]MoveStatement{
|
||||
testMoveStatement(t, "", "foo.from", "foo.to"),
|
||||
},
|
||||
states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
rootIntKeyResourceAddr[0],
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{}`),
|
||||
},
|
||||
providerAddr,
|
||||
)
|
||||
}),
|
||||
map[addrs.UniqueKey]MoveResult{
|
||||
rootNoKeyResourceAddr[0].UniqueKey(): {
|
||||
From: rootIntKeyResourceAddr[0],
|
||||
To: rootIntKeyResourceAddr[1],
|
||||
},
|
||||
rootNoKeyResourceAddr[1].UniqueKey(): {
|
||||
From: rootIntKeyResourceAddr[0],
|
||||
To: rootIntKeyResourceAddr[1],
|
||||
},
|
||||
},
|
||||
[]string{
|
||||
`foo.to[0]`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var stmtsBuf strings.Builder
|
||||
for _, stmt := range test.Stmts {
|
||||
fmt.Fprintf(&stmtsBuf, "- from: %s\n to: %s", stmt.From, stmt.To)
|
||||
}
|
||||
t.Logf("move statements:\n%s", stmtsBuf.String())
|
||||
|
||||
t.Logf("resource instances in prior state:\n%s", spew.Sdump(allResourceInstanceAddrsInState(test.State)))
|
||||
|
||||
state := test.State.DeepCopy() // don't modify the test case in-place
|
||||
gotResults := ApplyMoves(test.Stmts, state)
|
||||
|
||||
if diff := cmp.Diff(test.WantResults, gotResults); diff != "" {
|
||||
t.Errorf("wrong results\n%s", diff)
|
||||
}
|
||||
|
||||
gotInstAddrs := allResourceInstanceAddrsInState(state)
|
||||
if diff := cmp.Diff(test.WantInstanceAddrs, gotInstAddrs); diff != "" {
|
||||
t.Errorf("wrong resource instances in final state\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testMoveStatement(t *testing.T, module string, from string, to string) MoveStatement {
|
||||
t.Helper()
|
||||
|
||||
moduleAddr := addrs.RootModule
|
||||
if len(module) != 0 {
|
||||
moduleAddr = addrs.Module(strings.Split(module, "."))
|
||||
}
|
||||
|
||||
fromTraversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(from), "from", hcl.InitialPos)
|
||||
if hclDiags.HasErrors() {
|
||||
t.Fatalf("invalid 'from' argument: %s", hclDiags.Error())
|
||||
}
|
||||
fromAddr, diags := addrs.ParseMoveEndpoint(fromTraversal)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("invalid 'from' argument: %s", diags.Err().Error())
|
||||
}
|
||||
toTraversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(to), "to", hcl.InitialPos)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("invalid 'to' argument: %s", hclDiags.Error())
|
||||
}
|
||||
toAddr, diags := addrs.ParseMoveEndpoint(toTraversal)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("invalid 'from' argument: %s", diags.Err().Error())
|
||||
}
|
||||
|
||||
fromInModule, toInModule := addrs.UnifyMoveEndpoints(moduleAddr, fromAddr, toAddr)
|
||||
if fromInModule == nil || toInModule == nil {
|
||||
t.Fatalf("incompatible endpoints")
|
||||
}
|
||||
|
||||
return MoveStatement{
|
||||
From: fromInModule,
|
||||
To: toInModule,
|
||||
|
||||
// DeclRange not populated because it's unimportant for our tests
|
||||
}
|
||||
}
|
||||
|
||||
func allResourceInstanceAddrsInState(state *states.State) []string {
|
||||
var ret []string
|
||||
for _, ms := range state.Modules {
|
||||
for _, rs := range ms.Resources {
|
||||
for key := range rs.Instances {
|
||||
ret = append(ret, rs.Addr.Instance(key).String())
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(ret)
|
||||
return ret
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package refactoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
type MoveStatement struct {
|
||||
From, To *addrs.MoveEndpointInModule
|
||||
DeclRange tfdiags.SourceRange
|
||||
}
|
||||
|
||||
// FindMoveStatements recurses through the modules of the given configuration
|
||||
// and returns a flat set of all "moved" blocks defined within, in a
|
||||
// deterministic but undefined order.
|
||||
func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
|
||||
return findMoveStatements(rootCfg, nil)
|
||||
}
|
||||
|
||||
func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
|
||||
modAddr := cfg.Path
|
||||
for _, mc := range cfg.Module.Moved {
|
||||
fromAddr, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
|
||||
if fromAddr == nil || toAddr == nil {
|
||||
// Invalid combination should've been caught during original
|
||||
// configuration decoding, in the configs package.
|
||||
panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange))
|
||||
}
|
||||
|
||||
into = append(into, MoveStatement{
|
||||
From: fromAddr,
|
||||
To: toAddr,
|
||||
DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
|
||||
})
|
||||
}
|
||||
|
||||
for _, childCfg := range cfg.Children {
|
||||
into = findMoveStatements(childCfg, into)
|
||||
}
|
||||
|
||||
return into
|
||||
}
|
||||
|
||||
func (s *MoveStatement) ObjectKind() addrs.MoveEndpointKind {
|
||||
// addrs.UnifyMoveEndpoints guarantees that both of our addresses have
|
||||
// the same kind, so we can just arbitrary use From and assume To will
|
||||
// match it.
|
||||
return s.From.ObjectKind()
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package refactoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/instances"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// ValidateMoves tests whether all of the given move statements comply with
|
||||
// both the single-statement validation rules and the "big picture" rules
|
||||
// that constrain statements in relation to one another.
|
||||
//
|
||||
// The validation rules are primarily in terms of the configuration, but
|
||||
// ValidateMoves also takes the expander that resulted from creating a plan
|
||||
// so that it can see which instances are defined for each module and resource,
|
||||
// to precisely validate move statements involving specific-instance addresses.
|
||||
//
|
||||
// Because validation depends on the planning result but move execution must
|
||||
// happen _before_ planning, we have the unusual situation where sibling
|
||||
// function ApplyMoves must run before ValidateMoves and must therefore
|
||||
// tolerate and ignore any invalid statements. The plan walk will then
|
||||
// construct in incorrect plan (because it'll be starting from the wrong
|
||||
// prior state) but ValidateMoves will block actually showing that invalid
|
||||
// plan to the user.
|
||||
func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, expander *instances.Expander) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
g := buildMoveStatementGraph(stmts)
|
||||
|
||||
if len(g.Cycles()) != 0 {
|
||||
// TODO: proper error messages for this
|
||||
diags = diags.Append(fmt.Errorf("move statement cycles"))
|
||||
}
|
||||
|
||||
// TODO: Various other validation rules
|
||||
|
||||
return diags
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package states
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
@ -296,3 +297,245 @@ func (s *State) SyncWrapper() *SyncState {
|
|||
state: s,
|
||||
}
|
||||
}
|
||||
|
||||
// MoveAbsResource moves the given src AbsResource's current state to the new
|
||||
// dst address. This will panic if the src AbsResource does not exist in state,
|
||||
// or if there is already a resource at the dst address. It is the caller's
|
||||
// responsibility to verify the validity of the move (for example, that the src
|
||||
// and dst are compatible types).
|
||||
func (s *State) MoveAbsResource(src, dst addrs.AbsResource) {
|
||||
// verify that the src address exists and the dst address does not
|
||||
rs := s.Resource(src)
|
||||
if rs == nil {
|
||||
panic(fmt.Sprintf("no state for src address %s", src.String()))
|
||||
}
|
||||
|
||||
ds := s.Resource(dst)
|
||||
if ds != nil {
|
||||
panic(fmt.Sprintf("dst resource %s already exists", dst.String()))
|
||||
}
|
||||
|
||||
ms := s.Module(src.Module)
|
||||
ms.RemoveResource(src.Resource)
|
||||
|
||||
// Remove the module if it is empty (and not root) after removing the
|
||||
// resource.
|
||||
if !ms.Addr.IsRoot() && ms.empty() {
|
||||
s.RemoveModule(src.Module)
|
||||
}
|
||||
|
||||
// Update the address before adding it to the state
|
||||
rs.Addr = dst
|
||||
s.EnsureModule(dst.Module).Resources[dst.Resource.String()] = rs
|
||||
}
|
||||
|
||||
// MaybeMoveAbsResource moves the given src AbsResource's current state to the
|
||||
// new dst address. This function will succeed if both the src address does not
|
||||
// exist in state and the dst address does; the return value indicates whether
|
||||
// or not the move occured. This function will panic if either the src does not
|
||||
// exist or the dst does exist (but not both).
|
||||
func (s *State) MaybeMoveAbsResource(src, dst addrs.AbsResource) bool {
|
||||
// Get the source and destinatation addresses from state.
|
||||
rs := s.Resource(src)
|
||||
ds := s.Resource(dst)
|
||||
|
||||
// Normal case: the src exists in state, dst does not
|
||||
if rs != nil && ds == nil {
|
||||
s.MoveAbsResource(src, dst)
|
||||
return true
|
||||
}
|
||||
|
||||
if rs == nil && ds != nil {
|
||||
// The source is not in state, the destination is. This is not
|
||||
// guaranteed to be idempotent since we aren't tracking exact moves, but
|
||||
// it's useful information for the caller.
|
||||
return false
|
||||
} else {
|
||||
panic("invalid move")
|
||||
}
|
||||
}
|
||||
|
||||
// MoveAbsResourceInstance moves the given src AbsResourceInstance's current state to
|
||||
// the new dst address. This will panic if the src AbsResourceInstance does not
|
||||
// exist in state, or if there is already a resource at the dst address. It is
|
||||
// the caller's responsibility to verify the validity of the move (for example,
|
||||
// that the src and dst are compatible types).
|
||||
func (s *State) MoveAbsResourceInstance(src, dst addrs.AbsResourceInstance) {
|
||||
srcInstanceState := s.ResourceInstance(src)
|
||||
if srcInstanceState == nil {
|
||||
panic(fmt.Sprintf("no state for src address %s", src.String()))
|
||||
}
|
||||
|
||||
dstInstanceState := s.ResourceInstance(dst)
|
||||
if dstInstanceState != nil {
|
||||
panic(fmt.Sprintf("dst resource %s already exists", dst.String()))
|
||||
}
|
||||
|
||||
srcResourceState := s.Resource(src.ContainingResource())
|
||||
srcProviderAddr := srcResourceState.ProviderConfig
|
||||
dstResourceAddr := dst.ContainingResource()
|
||||
|
||||
// Remove the source resource instance from the module's state, and then the
|
||||
// module if empty.
|
||||
ms := s.Module(src.Module)
|
||||
ms.ForgetResourceInstanceAll(src.Resource)
|
||||
if !ms.Addr.IsRoot() && ms.empty() {
|
||||
s.RemoveModule(src.Module)
|
||||
}
|
||||
|
||||
dstModule := s.EnsureModule(dst.Module)
|
||||
|
||||
// See if there is already a resource we can add this instance to.
|
||||
dstResourceState := s.Resource(dstResourceAddr)
|
||||
if dstResourceState == nil {
|
||||
// If we're moving to an address without an index then that
|
||||
// suggests the user's intent is to establish both the
|
||||
// resource and the instance at the same time (since the
|
||||
// address covers both). If there's an index in the
|
||||
// target then allow creating the new instance here.
|
||||
dstModule.SetResourceProvider(
|
||||
dstResourceAddr.Resource,
|
||||
srcProviderAddr, // in this case, we bring the provider along as if we were moving the whole resource
|
||||
)
|
||||
dstResourceState = dstModule.Resource(dstResourceAddr.Resource)
|
||||
}
|
||||
|
||||
dstResourceState.Instances[dst.Resource.Key] = srcInstanceState
|
||||
}
|
||||
|
||||
// MaybeMoveAbsResourceInstance moves the given src AbsResourceInstance's
|
||||
// current state to the new dst address. This function will succeed if both the
|
||||
// src address does not exist in state and the dst address does; the return
|
||||
// value indicates whether or not the move occured. This function will panic if
|
||||
// either the src does not exist or the dst does exist (but not both).
|
||||
func (s *State) MaybeMoveAbsResourceInstance(src, dst addrs.AbsResourceInstance) bool {
|
||||
// get the src and dst resource instances from state
|
||||
rs := s.ResourceInstance(src)
|
||||
ds := s.ResourceInstance(dst)
|
||||
|
||||
// Normal case: the src exists in state, dst does not
|
||||
if rs != nil && ds == nil {
|
||||
s.MoveAbsResourceInstance(src, dst)
|
||||
return true
|
||||
}
|
||||
|
||||
if rs == nil && ds != nil {
|
||||
// The source is not in state, the destination is. This is not
|
||||
// guaranteed to be idempotent since we aren't tracking exact moves, but
|
||||
// it's useful information.
|
||||
return false
|
||||
} else {
|
||||
panic("invalid move")
|
||||
}
|
||||
}
|
||||
|
||||
// MoveModuleInstance moves the given src ModuleInstance's current state to the
|
||||
// new dst address. This will panic if the src ModuleInstance does not
|
||||
// exist in state, or if there is already a resource at the dst address. It is
|
||||
// the caller's responsibility to verify the validity of the move.
|
||||
func (s *State) MoveModuleInstance(src, dst addrs.ModuleInstance) {
|
||||
if src.IsRoot() || dst.IsRoot() {
|
||||
panic("cannot move to or from root module")
|
||||
}
|
||||
|
||||
srcMod := s.Module(src)
|
||||
if srcMod == nil {
|
||||
panic(fmt.Sprintf("no state for src module %s", src.String()))
|
||||
}
|
||||
|
||||
dstMod := s.Module(dst)
|
||||
if dstMod != nil {
|
||||
panic(fmt.Sprintf("dst module %s already exists in state", dst.String()))
|
||||
}
|
||||
|
||||
s.RemoveModule(src)
|
||||
|
||||
srcMod.Addr = dst
|
||||
s.EnsureModule(dst)
|
||||
s.Modules[dst.String()] = srcMod
|
||||
|
||||
// Update any Resource's addresses.
|
||||
if srcMod.Resources != nil {
|
||||
for _, r := range srcMod.Resources {
|
||||
r.Addr.Module = dst
|
||||
}
|
||||
}
|
||||
|
||||
// Update any OutputValues's addresses.
|
||||
if srcMod.OutputValues != nil {
|
||||
for _, ov := range srcMod.OutputValues {
|
||||
ov.Addr.Module = dst
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaybeMoveModuleInstance moves the given src ModuleInstance's current state to
|
||||
// the new dst address. This function will succeed if both the src address does
|
||||
// not exist in state and the dst address does; the return value indicates
|
||||
// whether or not the move occured. This function will panic if either the src
|
||||
// does not exist or the dst does exist (but not both).
|
||||
func (s *State) MaybeMoveModuleInstance(src, dst addrs.ModuleInstance) bool {
|
||||
if src.IsRoot() || dst.IsRoot() {
|
||||
panic("cannot move to or from root module")
|
||||
}
|
||||
|
||||
srcMod := s.Module(src)
|
||||
dstMod := s.Module(dst)
|
||||
|
||||
// Normal case: the src exists in state, dst does not
|
||||
if srcMod != nil && dstMod == nil {
|
||||
s.MoveModuleInstance(src, dst)
|
||||
return true
|
||||
}
|
||||
|
||||
if srcMod == nil || src.IsRoot() && dstMod != nil {
|
||||
// The source is not in state, the destination is. This is not
|
||||
// guaranteed to be idempotent since we aren't tracking exact moves, but
|
||||
// it's useful information.
|
||||
return false
|
||||
} else {
|
||||
panic("invalid move")
|
||||
}
|
||||
}
|
||||
|
||||
// MoveModule takes a source and destination addrs.Module address, and moves all
|
||||
// state Modules which are contained by the src address to the new address.
|
||||
func (s *State) MoveModule(src, dst addrs.AbsModuleCall) {
|
||||
if src.Module.IsRoot() || dst.Module.IsRoot() {
|
||||
panic("cannot move to or from root module")
|
||||
}
|
||||
|
||||
// Modules only exist as ModuleInstances in state, so we need to check each
|
||||
// state Module and see if it is contained by the src address to get a full
|
||||
// list of modules to move.
|
||||
var srcMIs []*Module
|
||||
for _, module := range s.Modules {
|
||||
if !module.Addr.IsRoot() {
|
||||
if src.Module.TargetContains(module.Addr) {
|
||||
srcMIs = append(srcMIs, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(srcMIs) == 0 {
|
||||
panic(fmt.Sprintf("no matching module instances found for src module %s", src.String()))
|
||||
}
|
||||
|
||||
for _, ms := range srcMIs {
|
||||
newInst := make(addrs.ModuleInstance, len(ms.Addr))
|
||||
copy(newInst, ms.Addr)
|
||||
if ms.Addr.IsDeclaredByCall(src) {
|
||||
// Easy case: we just need to update the last step with the new name
|
||||
newInst[len(newInst)-1].Name = dst.Call.Name
|
||||
} else {
|
||||
// Trickier: this Module is a submodule. we need to find and update
|
||||
// only that appropriate step
|
||||
for s := range newInst {
|
||||
if newInst[s].Name == src.Call.Name {
|
||||
newInst[s].Name = dst.Call.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
s.MoveModuleInstance(ms.Addr, newInst)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package states
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -292,3 +293,597 @@ func TestStateDeepCopy(t *testing.T) {
|
|||
t.Fatalf("\nexpected:\n%q\ngot:\n%q\n", state, stateCopy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_MoveAbsResource(t *testing.T) {
|
||||
// Set up a starter state for the embedded tests, which should start from a copy of this state.
|
||||
state := NewState()
|
||||
rootModule := state.RootModule()
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance)
|
||||
|
||||
t.Run("basic move", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance)
|
||||
|
||||
s.MoveAbsResource(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
if len(s.RootModule().Resources) != 1 {
|
||||
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources))
|
||||
}
|
||||
|
||||
got := s.Resource(dst)
|
||||
if got.Addr.Resource != dst.Resource {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("move to new module", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("one"))
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(dstModule)
|
||||
|
||||
s.MoveAbsResource(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
if s.Module(dstModule) == nil {
|
||||
t.Fatalf("child module %s not in state", dstModule.String())
|
||||
}
|
||||
|
||||
if len(s.Module(dstModule).Resources) != 1 {
|
||||
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources))
|
||||
}
|
||||
|
||||
got := s.Resource(dst)
|
||||
if got.Addr.Resource != dst.Resource {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("from a child module to root", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
|
||||
cm := s.EnsureModule(srcModule)
|
||||
cm.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "child",
|
||||
}.Instance(addrs.IntKey(0)), // Moving the AbsResouce moves all instances
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
cm.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "child",
|
||||
}.Instance(addrs.IntKey(1)), // Moving the AbsResouce moves all instances
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(addrs.RootModuleInstance)
|
||||
s.MoveAbsResource(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
// The child module should have been removed after removing its only resource
|
||||
if s.Module(srcModule) != nil {
|
||||
t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
|
||||
}
|
||||
|
||||
if len(s.RootModule().Resources) != 2 {
|
||||
t.Fatalf("wrong number of resources in state; expected 2, found %d", len(s.RootModule().Resources))
|
||||
}
|
||||
|
||||
if len(s.Resource(dst).Instances) != 2 {
|
||||
t.Fatalf("wrong number of resource instances for dst, got %d expected 2", len(s.Resource(dst).Instances))
|
||||
}
|
||||
|
||||
got := s.Resource(dst)
|
||||
if got.Addr.Resource != dst.Resource {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("module to new module", func(t *testing.T) {
|
||||
s := NewState()
|
||||
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists"))
|
||||
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new"))
|
||||
cm := s.EnsureModule(srcModule)
|
||||
cm.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "child",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule)
|
||||
s.MoveAbsResource(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
// The child module should have been removed after removing its only resource
|
||||
if s.Module(srcModule) != nil {
|
||||
t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
|
||||
}
|
||||
|
||||
gotMod := s.Module(dstModule)
|
||||
if len(gotMod.Resources) != 1 {
|
||||
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources))
|
||||
}
|
||||
|
||||
got := s.Resource(dst)
|
||||
if got.Addr.Resource != dst.Resource {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("module to new module", func(t *testing.T) {
|
||||
s := NewState()
|
||||
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists"))
|
||||
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new"))
|
||||
cm := s.EnsureModule(srcModule)
|
||||
cm.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "child",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule)
|
||||
s.MoveAbsResource(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
// The child module should have been removed after removing its only resource
|
||||
if s.Module(srcModule) != nil {
|
||||
t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
|
||||
}
|
||||
|
||||
gotMod := s.Module(dstModule)
|
||||
if len(gotMod.Resources) != 1 {
|
||||
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources))
|
||||
}
|
||||
|
||||
got := s.Resource(dst)
|
||||
if got.Addr.Resource != dst.Resource {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_MaybeMoveAbsResource(t *testing.T) {
|
||||
state := NewState()
|
||||
rootModule := state.RootModule()
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.IntKey(0)),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance)
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance)
|
||||
|
||||
// First move, success
|
||||
t.Run("first move", func(t *testing.T) {
|
||||
moved := state.MaybeMoveAbsResource(src, dst)
|
||||
if !moved {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
})
|
||||
|
||||
// Trying to move a resource that doesn't exist in state to a resource which does exist should be a noop.
|
||||
t.Run("noop", func(t *testing.T) {
|
||||
moved := state.MaybeMoveAbsResource(src, dst)
|
||||
if moved {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_MoveAbsResourceInstance(t *testing.T) {
|
||||
state := NewState()
|
||||
rootModule := state.RootModule()
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
// src resource from the state above
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
t.Run("resource to resource instance", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
// For a little extra fun, move a resource to a resource instance: test_thing.foo to test_thing.foo[1]
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
s.MoveAbsResourceInstance(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
if len(s.RootModule().Resources) != 1 {
|
||||
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources))
|
||||
}
|
||||
|
||||
got := s.ResourceInstance(dst)
|
||||
if got == nil {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("move to new module", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
// test_thing.foo to module.kinder.test_thing.foo["baz"]
|
||||
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(dstModule)
|
||||
|
||||
s.MoveAbsResourceInstance(src, dst)
|
||||
|
||||
if s.Empty() {
|
||||
t.Fatal("unexpected empty state")
|
||||
}
|
||||
|
||||
if s.Module(dstModule) == nil {
|
||||
t.Fatalf("child module %s not in state", dstModule.String())
|
||||
}
|
||||
|
||||
if len(s.Module(dstModule).Resources) != 1 {
|
||||
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources))
|
||||
}
|
||||
|
||||
got := s.ResourceInstance(dst)
|
||||
if got == nil {
|
||||
t.Fatalf("dst resource not in state")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_MaybeMoveAbsResourceInstance(t *testing.T) {
|
||||
state := NewState()
|
||||
rootModule := state.RootModule()
|
||||
rootModule.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
// For a little extra fun, let's go from a resource to a resource instance: test_thing.foo to test_thing.bar[1]
|
||||
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
// First move, success
|
||||
t.Run("first move", func(t *testing.T) {
|
||||
moved := state.MaybeMoveAbsResourceInstance(src, dst)
|
||||
if !moved {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
got := state.ResourceInstance(dst)
|
||||
if got == nil {
|
||||
t.Fatal("destination resource instance not in state")
|
||||
}
|
||||
})
|
||||
|
||||
// Moving a resource instance that doesn't exist in state to a resource which does exist should be a noop.
|
||||
t.Run("noop", func(t *testing.T) {
|
||||
moved := state.MaybeMoveAbsResourceInstance(src, dst)
|
||||
if moved {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_MoveModuleInstance(t *testing.T) {
|
||||
state := NewState()
|
||||
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
|
||||
m := state.EnsureModule(srcModule)
|
||||
m.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
dstModule := addrs.RootModuleInstance.Child("child", addrs.IntKey(3))
|
||||
state.MoveModuleInstance(srcModule, dstModule)
|
||||
|
||||
// srcModule should have been removed, dstModule should exist and have one resource
|
||||
if len(state.Modules) != 2 { // kinder[3] and root
|
||||
t.Fatalf("wrong number of modules in state. Expected 2, got %d", len(state.Modules))
|
||||
}
|
||||
|
||||
got := state.Module(dstModule)
|
||||
if got == nil {
|
||||
t.Fatal("dstModule not found")
|
||||
}
|
||||
|
||||
gone := state.Module(srcModule)
|
||||
if gone != nil {
|
||||
t.Fatal("srcModule not removed from state")
|
||||
}
|
||||
|
||||
r := got.Resource(mustAbsResourceAddr("test_thing.foo").Resource)
|
||||
if r.Addr.Module.String() != dstModule.String() {
|
||||
fmt.Println(r.Addr.Module.String())
|
||||
t.Fatal("resource address was not updated")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestState_MaybeMoveModuleInstance(t *testing.T) {
|
||||
state := NewState()
|
||||
src := addrs.RootModuleInstance.Child("child", addrs.StringKey("a"))
|
||||
cm := state.EnsureModule(src)
|
||||
cm.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
dst := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("b"))
|
||||
|
||||
// First move, success
|
||||
t.Run("first move", func(t *testing.T) {
|
||||
moved := state.MaybeMoveModuleInstance(src, dst)
|
||||
if !moved {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
})
|
||||
|
||||
// Second move, should be a noop
|
||||
t.Run("noop", func(t *testing.T) {
|
||||
moved := state.MaybeMoveModuleInstance(src, dst)
|
||||
if moved {
|
||||
t.Fatal("wrong result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestState_MoveModule(t *testing.T) {
|
||||
// For this test, add two module instances (kinder and kinder["a"]).
|
||||
// MoveModule(kinder) should move both instances.
|
||||
state := NewState() // starter state, should be copied by the subtests.
|
||||
srcModule := addrs.RootModule.Child("kinder")
|
||||
m := state.EnsureModule(srcModule.UnkeyedInstanceShim())
|
||||
m.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
moduleInstance := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("a"))
|
||||
mi := state.EnsureModule(moduleInstance)
|
||||
mi.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
_, mc := srcModule.Call()
|
||||
src := mc.Absolute(addrs.RootModuleInstance.Child("kinder", addrs.NoKey))
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
_, dstMC := addrs.RootModule.Child("child").Call()
|
||||
dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
|
||||
s.MoveModule(src, dst)
|
||||
|
||||
// srcModule should have been removed, dstModule should exist and have one resource
|
||||
if len(s.Modules) != 3 { // child, child["a"] and root
|
||||
t.Fatalf("wrong number of modules in state. Expected 3, got %d", len(s.Modules))
|
||||
}
|
||||
|
||||
got := s.Module(dst.Module)
|
||||
if got == nil {
|
||||
t.Fatal("dstModule not found")
|
||||
}
|
||||
|
||||
got = s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a")))
|
||||
if got == nil {
|
||||
t.Fatal("dstModule instance \"a\" not found")
|
||||
}
|
||||
|
||||
gone := s.Module(srcModule.UnkeyedInstanceShim())
|
||||
if gone != nil {
|
||||
t.Fatal("srcModule not removed from state")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nested modules", func(t *testing.T) {
|
||||
s := state.DeepCopy()
|
||||
|
||||
// add a child module to module.kinder
|
||||
mi := mustParseModuleInstanceStr(`module.kinder.module.grand[1]`)
|
||||
m := s.EnsureModule(mi)
|
||||
m.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
&ResourceInstanceObjectSrc{
|
||||
Status: ObjectReady,
|
||||
SchemaVersion: 1,
|
||||
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
|
||||
_, dstMC := addrs.RootModule.Child("child").Call()
|
||||
dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
|
||||
s.MoveModule(src, dst)
|
||||
|
||||
moved := s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a")))
|
||||
if moved == nil {
|
||||
t.Fatal("dstModule not found")
|
||||
}
|
||||
|
||||
// The nested module's relative address should also have been updated
|
||||
nested := s.Module(mustParseModuleInstanceStr(`module.child.module.grand[1]`))
|
||||
if nested == nil {
|
||||
t.Fatal("nested child module of src wasn't moved")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mustParseModuleInstanceStr(str string) addrs.ModuleInstance {
|
||||
addr, diags := addrs.ParseModuleInstanceStr(str)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err())
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func mustAbsResourceAddr(s string) addrs.AbsResource {
|
||||
addr, diags := addrs.ParseAbsResourceStr(s)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err())
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
|
|
@ -554,3 +554,45 @@ func (s *SyncState) maybePruneModule(addr addrs.ModuleInstance) {
|
|||
s.state.RemoveModule(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SyncState) MoveAbsResource(src, dst addrs.AbsResource) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.state.MoveAbsResource(src, dst)
|
||||
}
|
||||
|
||||
func (s *SyncState) MaybeMoveAbsResource(src, dst addrs.AbsResource) bool {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.state.MaybeMoveAbsResource(src, dst)
|
||||
}
|
||||
|
||||
func (s *SyncState) MoveResourceInstance(src, dst addrs.AbsResourceInstance) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.state.MoveAbsResourceInstance(src, dst)
|
||||
}
|
||||
|
||||
func (s *SyncState) MaybeMoveResourceInstance(src, dst addrs.AbsResourceInstance) bool {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.state.MaybeMoveAbsResourceInstance(src, dst)
|
||||
}
|
||||
|
||||
func (s *SyncState) MoveModuleInstance(src, dst addrs.ModuleInstance) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.state.MoveModuleInstance(src, dst)
|
||||
}
|
||||
|
||||
func (s *SyncState) MaybeMoveModuleInstance(src, dst addrs.ModuleInstance) bool {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.state.MaybeMoveModuleInstance(src, dst)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/provisioners"
|
||||
"github.com/hashicorp/terraform/internal/refactoring"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
@ -608,6 +609,33 @@ The -target option is not for routine use, and is provided only for exceptional
|
|||
))
|
||||
}
|
||||
|
||||
moveStmts := refactoring.FindMoveStatements(c.config)
|
||||
if len(moveStmts) != 0 {
|
||||
// TEMP: we haven't fully implemented moving yet, so we'll just
|
||||
// reject it outright for now to reduce confusion.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Moves are not yet supported",
|
||||
"There is currently no handling of \"moved\" blocks in the configuration.",
|
||||
))
|
||||
}
|
||||
moveResults := refactoring.ApplyMoves(moveStmts, c.prevRunState)
|
||||
if len(c.targets) > 0 {
|
||||
for _, result := range moveResults {
|
||||
matchesTarget := false
|
||||
for _, targetAddr := range c.targets {
|
||||
if targetAddr.TargetContains(result.From) {
|
||||
matchesTarget = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matchesTarget {
|
||||
// TODO: Return an error stating that a targeted plan is
|
||||
// only valid if it includes this address that was moved.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var plan *plans.Plan
|
||||
var planDiags tfdiags.Diagnostics
|
||||
switch c.planMode {
|
||||
|
@ -625,6 +653,10 @@ The -target option is not for routine use, and is provided only for exceptional
|
|||
return nil, diags
|
||||
}
|
||||
|
||||
// TODO: Call refactoring.ValidateMoves, but need to figure out how to
|
||||
// get hold of the plan's expander here, or somehow otherwise export
|
||||
// the necessary subset of its data for ValidateMoves to do its work.
|
||||
|
||||
// convert the variables into the format expected for the plan
|
||||
varVals := make(map[string]plans.DynamicValue, len(c.variables))
|
||||
for k, iv := range c.variables {
|
||||
|
|
|
@ -28,3 +28,19 @@ You should preview all of your changes locally before creating a pull request. T
|
|||
1. Navigate into your local `terraform` top-level directory and run `make website`.
|
||||
2. Open `http://localhost:4567` in your web browser. While the preview is running, you can edit pages and Middleman will automatically rebuild them.
|
||||
3. When you're done with the preview, press `ctrl-C` in your terminal to stop the server.
|
||||
|
||||
## Deploying Changes
|
||||
|
||||
Merge the PR to main. The changes will appear in the next major Terraform release.
|
||||
|
||||
If you need your changes to be deployed sooner, cherry-pick them to:
|
||||
- the current release branch (e.g. `v1.0`) and push. They will be deployed in the next minor version release (once every two weeks).
|
||||
- the `stable-website` branch and push. They will be included in the next site deploy (see below). Note that the release process resets `stable-website` to match the release tag, removing any additional commits. So, we recommend always cherry-picking to the version branch first and then to `stable-website` when needed.
|
||||
|
||||
### Deployment
|
||||
Currently, HashiCorp uses a CircleCI job to deploy the [terraform.io](terraform.io) site. This job can be run manually by many people within HashiCorp, and also runs automatically whenever a user in the HashiCorp GitHub org merges changes to master in the `terraform-website` repository.
|
||||
|
||||
New commits in this repository don't automatically deploy the [terraform.io][] site, but an unrelated site deploy will usually happen within a day. If you can't wait that long, you can do a manual CircleCI build or ask someone in the #proj-terraform-docs channel to do so:
|
||||
- Log in to circleci.com, and make sure you're viewing the HashiCorp organization.
|
||||
- Go to the terraform-website project's list of workflows.
|
||||
- Find the most recent "website-deploy" workflow, and click the "Rerun workflow from start" button (which looks like a refresh button with a numeral "1" inside).
|
||||
|
|
|
@ -19,8 +19,8 @@ We refer to the `terraform` command line tool as "Terraform CLI" elsewhere
|
|||
in the documentation. This terminology is often used to distinguish it from
|
||||
other components you might use in the Terraform product family, such as
|
||||
[Terraform Cloud](/docs/cloud/) or
|
||||
the various [Terraform providers](/docs/providers/), which are developed and
|
||||
released separately from Terraform CLI.
|
||||
the various [Terraform providers](/docs/language/providers/index.html), which
|
||||
are developed and released separately from Terraform CLI.
|
||||
|
||||
To view a list of the commands available in your current Terraform version,
|
||||
run `terraform` with no additional arguments:
|
||||
|
|
|
@ -33,6 +33,10 @@ The command-line flags are all optional. The list of available flags are:
|
|||
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
||||
Ignored when [remote state](/docs/language/state/remote.html) is used.
|
||||
|
||||
-> **Note:** When using the `-json` or `-raw` command-line flag, any sensitive
|
||||
values in Terraform state will be displayed in plain text. For more information,
|
||||
see [Sensitive Data in State](/docs/language/state/sensitive-data.html).
|
||||
|
||||
## Examples
|
||||
|
||||
These examples assume the following Terraform output snippet.
|
||||
|
|
|
@ -358,6 +358,10 @@ Terraform will never itself delete a plugin from the plugin cache once it has
|
|||
been placed there. Over time, as plugins are upgraded, the cache directory may
|
||||
grow to contain several unused versions which you must delete manually.
|
||||
|
||||
-> **Note:** The plugin cache directory is not guaranteed to be concurrency
|
||||
safe. The provider installer's behavior in environments with multiple `terraform
|
||||
init` calls is undefined.
|
||||
|
||||
### Development Overrides for Provider Developers
|
||||
|
||||
-> **Note:** Development overrides work only in Terraform v0.14 and later.
|
||||
|
|
|
@ -62,6 +62,7 @@ export TF_VAR_amap='{ foo = "bar", baz = "qux" }'
|
|||
For more on how to use `TF_VAR_name` in context, check out the section on [Variable Configuration](/docs/language/values/variables.html).
|
||||
|
||||
## TF_CLI_ARGS and TF_CLI_ARGS_name
|
||||
<a id="tf-cli-args"></a>
|
||||
|
||||
The value of `TF_CLI_ARGS` will specify additional arguments to the
|
||||
command-line. This allows easier automation in CI environments as well as
|
||||
|
|
|
@ -64,8 +64,8 @@ parameter) and `NAME` (second parameter). The combination of the type
|
|||
and name must be unique.
|
||||
|
||||
Within the block (the `{ }`) is configuration for the data instance. The
|
||||
configuration is dependent on the type, and is documented for each
|
||||
data source in the [providers section](/docs/providers/index.html).
|
||||
configuration is dependent on the type; consult the [provider's documentation] (https://registry.terraform.io/browse/providers) for
|
||||
details.
|
||||
|
||||
Each data instance will export one or more attributes, which can be
|
||||
interpolated into other resources using variables of the form
|
||||
|
|
|
@ -48,8 +48,8 @@ header. For example, `provider "aws"` above is a configuration for the
|
|||
`aws` provider.
|
||||
|
||||
Within the block body (between `{ }`) is configuration for the provider.
|
||||
The configuration is dependent on the type, and is documented
|
||||
[for each provider](/docs/providers/index.html).
|
||||
The configuration is dependent on the type. Consult the [provider's documentation](https://registry.terraform.io/browse/providers) for details.
|
||||
in each provider's documentation.
|
||||
|
||||
The arguments `alias` and `version`, if present, are special arguments
|
||||
handled by Terraform Core for their respective features described above. All
|
||||
|
|
|
@ -41,9 +41,9 @@ parameter) and `NAME` (second parameter). The combination of the type
|
|||
and name must be unique.
|
||||
|
||||
Within the block (the `{ }`) is configuration for the resource. The
|
||||
configuration is dependent on the type, and is documented for each
|
||||
resource type in the
|
||||
[providers section](/docs/providers/index.html).
|
||||
configuration is dependent on the type. Consult the [provider's documentation](https://registry.terraform.io/browse/providers) for details.
|
||||
|
||||
details.
|
||||
|
||||
### Meta-parameters
|
||||
|
||||
|
|
|
@ -451,6 +451,8 @@ Each unevaluated expression in the configuration is represented with an `<expres
|
|||
}
|
||||
```
|
||||
|
||||
-> **Note:** Expressions in `dynamic` blocks are not included in the configuration representation.
|
||||
|
||||
### Block Expressions Representation
|
||||
|
||||
In some cases, it is the entire content of a block (possibly after certain special arguments have already been handled and removed) that must be represented. For that, we have an `<block-expressions-representation>` structure:
|
||||
|
|
|
@ -54,6 +54,7 @@ The following message types are supported:
|
|||
|
||||
### Operation Results
|
||||
|
||||
- `resource_drift`: describes a detected change to a single resource made outside of Terraform
|
||||
- `planned_change`: describes a planned change to a single resource
|
||||
- `change_summary`: summary of all planned or applied changes
|
||||
- `outputs`: list of all root module outputs
|
||||
|
@ -85,6 +86,39 @@ A machine-readable UI command output will always begin with a `version` message.
|
|||
}
|
||||
```
|
||||
|
||||
## Resource Drift
|
||||
|
||||
If drift is detected during planning, Terraform will emit a `resource_drift` message for each resource which has changed outside of Terraform. This message has an embedded `change` object with the following keys:
|
||||
|
||||
- `resource`: object describing the address of the resource to be changed; see [resource object](#resource-object) below for details
|
||||
- `action`: the action planned to be taken for the resource. Values: `update`, `delete`.
|
||||
|
||||
This message does not include details about the exact changes which caused the change to be planned. That information is available in [the JSON plan output](./json-format.html).
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "random_pet.animal: Drift detected (update)",
|
||||
"@module": "terraform.ui",
|
||||
"@timestamp": "2021-05-25T13:32:41.705503-04:00",
|
||||
"change": {
|
||||
"resource": {
|
||||
"addr": "random_pet.animal",
|
||||
"module": "",
|
||||
"resource": "random_pet.animal",
|
||||
"implied_provider": "random",
|
||||
"resource_type": "random_pet",
|
||||
"resource_name": "animal",
|
||||
"resource_key": null
|
||||
},
|
||||
"action": "update"
|
||||
},
|
||||
"type": "resource_drift"
|
||||
}
|
||||
```
|
||||
|
||||
## Planned Change
|
||||
|
||||
At the end of a plan or before an apply, Terraform will emit a `planned_change` message for each resource which has changes to apply. This message has an embedded `change` object with the following keys:
|
||||
|
|
|
@ -172,8 +172,10 @@ block label) and _name_ (second block label). The combination of the type
|
|||
and name must be unique.
|
||||
|
||||
Within the block (the `{ }`) is configuration for the data instance. The
|
||||
configuration is dependent on the type, and is documented for each
|
||||
data source in the [providers section](/docs/providers/index.html).
|
||||
configuration is dependent on the type; as with
|
||||
[resources](/docs/language/resources/index.html), each provider on the
|
||||
[Terraform Registry](https://registry.terraform.io/browse/providers) has its own
|
||||
documentation for configuring and using the data types it provides.
|
||||
|
||||
Each data instance will export one or more attributes, which can be
|
||||
used in other resources as reference expressions of the form
|
||||
|
|
|
@ -26,6 +26,10 @@ Supported pattern matches:
|
|||
- `[CLASS]` - matches any single non-separator character inside a class of characters (see below)
|
||||
- `[^CLASS]` - matches any single non-separator character outside a class of characters (see below)
|
||||
|
||||
Note that the doublestar (`**`) must appear as a path component by itself. A
|
||||
pattern such as /path** is invalid and will be treated the same as /path*, but
|
||||
/path*/** should achieve the desired result.
|
||||
|
||||
Character classes support the following:
|
||||
|
||||
- `[abc]` - matches any single character within the set
|
||||
|
|
|
@ -37,6 +37,21 @@ The [Terraform Registry](https://registry.terraform.io/browse/providers)
|
|||
is the main directory of publicly available Terraform providers, and hosts
|
||||
providers for most major infrastructure platforms.
|
||||
|
||||
## Provider Documentation
|
||||
|
||||
Each provider has its own documentation, describing its resource
|
||||
types and their arguments.
|
||||
|
||||
The [Terraform Registry](https://registry.terraform.io/browse/providers)
|
||||
includes documentation for a wide range of providers developed by HashiCorp, third-party vendors, and our Terraform community. Use the
|
||||
"Documentation" link in a provider's header to browse its documentation.
|
||||
|
||||
Provider documentation in the Registry is versioned; you can use the version
|
||||
menu in the header to change which version you're viewing.
|
||||
|
||||
For details about writing, generating, and previewing provider documentation,
|
||||
see the [provider publishing documentation](/docs/registry/providers/docs.html).
|
||||
|
||||
## How to Use Providers
|
||||
|
||||
To use resources from a given provider, you need to include some information
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
layout: "language"
|
||||
page_title: "Provider Documentation"
|
||||
sidebar_current: "docs-providers"
|
||||
description: "Find provider documentation on the Terraform Registry and information about creating documentation for your provider."
|
||||
---
|
||||
|
||||
# Provider Documentation
|
||||
|
||||
## Find Provider Docs
|
||||
Every Terraform provider has its own documentation on the [Terraform Registry](https://registry.terraform.io/browse/providers) that describes its resource types and their arguments.
|
||||
|
||||
## Write Provider Docs
|
||||
Learn more about [writing, generating, and rendering provider documentation](/docs/registry/providers/docs.html).
|
||||
|
|
@ -167,11 +167,6 @@
|
|||
<li>
|
||||
<a href="/docs/language/dependency-lock.html">Dependency Lock File</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/docs/providers/index.html">Provider Documentation</a>
|
||||
<!-- To be deleted when remaining 8 providers are gone, doesn't need new URL -->
|
||||
</li>
|
||||
</ul>
|
||||
</li><!-- providers -->
|
||||
|
||||
|
|
Loading…
Reference in New Issue