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 {
|
func (ca CountAttr) String() string {
|
||||||
return "count." + ca.Name
|
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 {
|
func (f ForEachAttr) String() string {
|
||||||
return "each." + f.Name
|
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
|
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
|
// Absolute converts the receiver into an absolute address within the given
|
||||||
// module instance.
|
// module instance.
|
||||||
func (v InputVariable) Absolute(m ModuleInstance) AbsInputVariableInstance {
|
func (v InputVariable) Absolute(m ModuleInstance) AbsInputVariableInstance {
|
||||||
|
|
|
@ -14,6 +14,12 @@ func (v LocalValue) String() string {
|
||||||
return "local." + v.Name
|
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
|
// Absolute converts the receiver into an absolute address within the given
|
||||||
// module instance.
|
// module instance.
|
||||||
func (v LocalValue) Absolute(m ModuleInstance) AbsLocalValue {
|
func (v LocalValue) Absolute(m ModuleInstance) AbsLocalValue {
|
||||||
|
|
|
@ -15,6 +15,12 @@ func (c ModuleCall) String() string {
|
||||||
return "module." + c.Name
|
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
|
// Instance returns the address of an instance of the receiver identified by
|
||||||
// the given key.
|
// the given key.
|
||||||
func (c ModuleCall) Instance(key InstanceKey) ModuleCallInstance {
|
func (c ModuleCall) Instance(key InstanceKey) ModuleCallInstance {
|
||||||
|
@ -47,7 +53,11 @@ func (c AbsModuleCall) absMoveableSigil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c AbsModuleCall) String() string {
|
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 {
|
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)
|
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 {
|
func (c ModuleCallInstance) Absolute(moduleAddr ModuleInstance) ModuleInstance {
|
||||||
ret := make(ModuleInstance, len(moduleAddr), len(moduleAddr)+1)
|
ret := make(ModuleInstance, len(moduleAddr), len(moduleAddr)+1)
|
||||||
copy(ret, moduleAddr)
|
copy(ret, moduleAddr)
|
||||||
|
@ -118,6 +134,12 @@ func (m ModuleCallOutput) String() string {
|
||||||
return fmt.Sprintf("%s.%s", m.Call.String(), m.Name)
|
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
|
// ModuleCallInstanceOutput is the address of a particular named output produced by
|
||||||
// an instance of a module call.
|
// an instance of a module call.
|
||||||
type ModuleCallInstanceOutput struct {
|
type ModuleCallInstanceOutput struct {
|
||||||
|
@ -139,6 +161,12 @@ func (co ModuleCallInstanceOutput) String() string {
|
||||||
return fmt.Sprintf("%s.%s", co.Call.String(), co.Name)
|
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
|
// AbsOutputValue returns the absolute output value address that corresponds
|
||||||
// to the receving module call output address, once resolved in the given
|
// to the receving module call output address, once resolved in the given
|
||||||
// calling module.
|
// calling module.
|
||||||
|
|
|
@ -275,6 +275,14 @@ func (m ModuleInstance) String() string {
|
||||||
return buf.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
|
// Equal returns true if the receiver and the given other value
|
||||||
// contains the exact same parts.
|
// contains the exact same parts.
|
||||||
func (m ModuleInstance) Equal(o ModuleInstance) bool {
|
func (m ModuleInstance) Equal(o ModuleInstance) bool {
|
||||||
|
@ -496,6 +504,28 @@ func (m ModuleInstance) absMoveableSigil() {
|
||||||
// ModuleInstance is moveable
|
// 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 {
|
func (s ModuleInstanceStep) String() string {
|
||||||
if s.InstanceKey != NoKey {
|
if s.InstanceKey != NoKey {
|
||||||
return s.Name + s.InstanceKey.String()
|
return s.Name + s.InstanceKey.String()
|
||||||
|
|
|
@ -91,3 +91,80 @@ func BenchmarkStringLong(b *testing.B) {
|
||||||
addr.String()
|
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
|
relSubject AbsMoveable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
|
||||||
|
return absMoveableEndpointKind(e.relSubject)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *MoveEndpoint) String() string {
|
func (e *MoveEndpoint) String() string {
|
||||||
// Our internal pseudo-AbsMovable representing the relative
|
// Our internal pseudo-AbsMovable representing the relative
|
||||||
// address (either ModuleInstance or AbsResourceInstance) is
|
// 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
|
// address, because the rules for whether unify can succeed depend
|
||||||
// only on the relative part of the addresses, not on which module
|
// only on the relative part of the addresses, not on which module
|
||||||
// they were declared in.
|
// they were declared in.
|
||||||
from, to := UnifyMoveEndpoints(RootModuleInstance, e, other)
|
from, to := UnifyMoveEndpoints(RootModule, e, other)
|
||||||
return from != nil && to != nil
|
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
|
// UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
|
||||||
// "from" and "to" addresses in a moved block, and returns a pair of
|
// "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.
|
// that represent what the two MoveEndpoint addresses refer to.
|
||||||
//
|
//
|
||||||
// moduleAddr must be the address of the module instance where the move
|
// moduleAddr must be the address of the module where the move was declared.
|
||||||
// was declared.
|
|
||||||
//
|
//
|
||||||
// This function deals both with the conversion from relative to absolute
|
// This function deals both with the conversion from relative to absolute
|
||||||
// addresses and with resolving the ambiguity between no-key instance
|
// 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),
|
// given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil),
|
||||||
// in which case the caller should typically report an error to the user
|
// in which case the caller should typically report an error to the user
|
||||||
// stating the unification constraints.
|
// 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
|
// First we'll make a decision about which address type we're
|
||||||
// ultimately trying to unify to. For our internal purposes
|
// 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")
|
panic("unhandled move address types")
|
||||||
}
|
}
|
||||||
|
|
||||||
absFrom = relFrom.prepareAbsMoveable(moduleAddr, wantType)
|
modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType)
|
||||||
absTo = relTo.prepareAbsMoveable(moduleAddr, wantType)
|
modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType)
|
||||||
if absFrom == nil || absTo == nil {
|
if modFrom == nil || modTo == nil {
|
||||||
// if either of them failed then they both failed, to make the
|
// if either of them failed then they both failed, to make the
|
||||||
// caller's life a little easier.
|
// caller's life a little easier.
|
||||||
return nil, nil
|
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
|
// relAddr can only be either AbsResourceInstance or ModuleInstance, the
|
||||||
// internal intermediate representation produced by ParseMoveEndpoint.
|
// internal intermediate representation produced by ParseMoveEndpoint.
|
||||||
relAddr := e.relSubject
|
relAddr := e.relSubject
|
||||||
|
@ -216,40 +219,43 @@ func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType Ta
|
||||||
case ModuleInstance:
|
case ModuleInstance:
|
||||||
switch wantType {
|
switch wantType {
|
||||||
case ModuleInstanceAddrType:
|
case ModuleInstanceAddrType:
|
||||||
ret := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr))
|
// Since our internal representation is already a module instance,
|
||||||
ret = append(ret, moduleAddr...)
|
// we can just rewrap this one.
|
||||||
ret = append(ret, relAddr...)
|
return &MoveEndpointInModule{
|
||||||
return ret
|
SourceRange: e.SourceRange,
|
||||||
|
module: moduleAddr,
|
||||||
|
relSubject: relAddr,
|
||||||
|
}
|
||||||
case ModuleAddrType:
|
case ModuleAddrType:
|
||||||
// NOTE: We're fudging a little here and using
|
// NOTE: We're fudging a little here and using
|
||||||
// ModuleAddrType to represent AbsModuleCall rather
|
// ModuleAddrType to represent AbsModuleCall rather
|
||||||
// than Module.
|
// than Module.
|
||||||
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr)-1)
|
callerAddr, callAddr := relAddr.Call()
|
||||||
callerAddr = append(callerAddr, moduleAddr...)
|
absCallAddr := AbsModuleCall{
|
||||||
callerAddr = append(callerAddr, relAddr[:len(relAddr)-1]...)
|
|
||||||
return AbsModuleCall{
|
|
||||||
Module: callerAddr,
|
Module: callerAddr,
|
||||||
Call: ModuleCall{
|
Call: callAddr,
|
||||||
Name: relAddr[len(relAddr)-1].Name,
|
}
|
||||||
},
|
return &MoveEndpointInModule{
|
||||||
|
SourceRange: e.SourceRange,
|
||||||
|
module: moduleAddr,
|
||||||
|
relSubject: absCallAddr,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil // can't make any other types from a ModuleInstance
|
return nil // can't make any other types from a ModuleInstance
|
||||||
}
|
}
|
||||||
case AbsResourceInstance:
|
case AbsResourceInstance:
|
||||||
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr.Module))
|
|
||||||
callerAddr = append(callerAddr, moduleAddr...)
|
|
||||||
callerAddr = append(callerAddr, relAddr.Module...)
|
|
||||||
switch wantType {
|
switch wantType {
|
||||||
case AbsResourceInstanceAddrType:
|
case AbsResourceInstanceAddrType:
|
||||||
return AbsResourceInstance{
|
return &MoveEndpointInModule{
|
||||||
Module: callerAddr,
|
SourceRange: e.SourceRange,
|
||||||
Resource: relAddr.Resource,
|
module: moduleAddr,
|
||||||
|
relSubject: relAddr,
|
||||||
}
|
}
|
||||||
case AbsResourceAddrType:
|
case AbsResourceAddrType:
|
||||||
return AbsResource{
|
return &MoveEndpointInModule{
|
||||||
Module: callerAddr,
|
SourceRange: e.SourceRange,
|
||||||
Resource: relAddr.Resource.Resource,
|
module: moduleAddr,
|
||||||
|
relSubject: relAddr.ContainingResource(),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil // can't make any other types from an AbsResourceInstance
|
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"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
)
|
)
|
||||||
|
@ -339,245 +338,127 @@ func TestParseMoveEndpoint(t *testing.T) {
|
||||||
func TestUnifyMoveEndpoints(t *testing.T) {
|
func TestUnifyMoveEndpoints(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
InputFrom, InputTo string
|
InputFrom, InputTo string
|
||||||
Module ModuleInstance
|
Module Module
|
||||||
WantFrom, WantTo AbsMoveable
|
WantFrom, WantTo string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
InputFrom: `foo.bar`,
|
InputFrom: `foo.bar`,
|
||||||
InputTo: `foo.baz`,
|
InputTo: `foo.baz`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: AbsResource{
|
WantFrom: `foo.bar[*]`,
|
||||||
Module: RootModuleInstance,
|
WantTo: `foo.baz[*]`,
|
||||||
Resource: Resource{
|
|
||||||
Mode: ManagedResourceMode,
|
|
||||||
Type: "foo",
|
|
||||||
Name: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
WantTo: AbsResource{
|
|
||||||
Module: RootModuleInstance,
|
|
||||||
Resource: Resource{
|
|
||||||
Mode: ManagedResourceMode,
|
|
||||||
Type: "foo",
|
|
||||||
Name: "baz",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `foo.bar`,
|
InputFrom: `foo.bar`,
|
||||||
InputTo: `foo.baz`,
|
InputTo: `foo.baz`,
|
||||||
Module: RootModuleInstance.Child("a", NoKey),
|
Module: RootModule.Child("a"),
|
||||||
WantFrom: AbsResource{
|
WantFrom: `module.a[*].foo.bar[*]`,
|
||||||
Module: RootModuleInstance.Child("a", NoKey),
|
WantTo: `module.a[*].foo.baz[*]`,
|
||||||
Resource: Resource{
|
|
||||||
Mode: ManagedResourceMode,
|
|
||||||
Type: "foo",
|
|
||||||
Name: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
WantTo: AbsResource{
|
|
||||||
Module: RootModuleInstance.Child("a", NoKey),
|
|
||||||
Resource: Resource{
|
|
||||||
Mode: ManagedResourceMode,
|
|
||||||
Type: "foo",
|
|
||||||
Name: "baz",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `foo.bar`,
|
InputFrom: `foo.bar`,
|
||||||
InputTo: `module.b[0].foo.baz`,
|
InputTo: `module.b[0].foo.baz`,
|
||||||
Module: RootModuleInstance.Child("a", NoKey),
|
Module: RootModule.Child("a"),
|
||||||
WantFrom: AbsResource{
|
WantFrom: `module.a[*].foo.bar[*]`,
|
||||||
Module: RootModuleInstance.Child("a", NoKey),
|
WantTo: `module.a[*].module.b[0].foo.baz[*]`,
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `foo.bar`,
|
InputFrom: `foo.bar`,
|
||||||
InputTo: `foo.bar["thing"]`,
|
InputTo: `foo.bar["thing"]`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: AbsResourceInstance{
|
WantFrom: `foo.bar`,
|
||||||
Module: RootModuleInstance,
|
WantTo: `foo.bar["thing"]`,
|
||||||
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"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `foo.bar["thing"]`,
|
InputFrom: `foo.bar["thing"]`,
|
||||||
InputTo: `foo.bar`,
|
InputTo: `foo.bar`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: AbsResourceInstance{
|
WantFrom: `foo.bar["thing"]`,
|
||||||
Module: RootModuleInstance,
|
WantTo: `foo.bar`,
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `foo.bar["a"]`,
|
InputFrom: `foo.bar["a"]`,
|
||||||
InputTo: `foo.bar["b"]`,
|
InputTo: `foo.bar["b"]`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: AbsResourceInstance{
|
WantFrom: `foo.bar["a"]`,
|
||||||
Module: RootModuleInstance,
|
WantTo: `foo.bar["b"]`,
|
||||||
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"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo`,
|
InputFrom: `module.foo`,
|
||||||
InputTo: `module.bar`,
|
InputTo: `module.bar`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: AbsModuleCall{
|
WantFrom: `module.foo[*]`,
|
||||||
Module: RootModuleInstance,
|
WantTo: `module.bar[*]`,
|
||||||
Call: ModuleCall{Name: "foo"},
|
|
||||||
},
|
|
||||||
WantTo: AbsModuleCall{
|
|
||||||
Module: RootModuleInstance,
|
|
||||||
Call: ModuleCall{Name: "bar"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo`,
|
InputFrom: `module.foo`,
|
||||||
InputTo: `module.bar.module.baz`,
|
InputTo: `module.bar.module.baz`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: AbsModuleCall{
|
WantFrom: `module.foo[*]`,
|
||||||
Module: RootModuleInstance,
|
WantTo: `module.bar.module.baz[*]`,
|
||||||
Call: ModuleCall{Name: "foo"},
|
|
||||||
},
|
|
||||||
WantTo: AbsModuleCall{
|
|
||||||
Module: RootModuleInstance.Child("bar", NoKey),
|
|
||||||
Call: ModuleCall{Name: "baz"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo`,
|
InputFrom: `module.foo`,
|
||||||
InputTo: `module.bar.module.baz`,
|
InputTo: `module.bar.module.baz`,
|
||||||
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
|
Module: RootModule.Child("bloop"),
|
||||||
WantFrom: AbsModuleCall{
|
WantFrom: `module.bloop[*].module.foo[*]`,
|
||||||
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
|
WantTo: `module.bloop[*].module.bar.module.baz[*]`,
|
||||||
Call: ModuleCall{Name: "foo"},
|
|
||||||
},
|
|
||||||
WantTo: AbsModuleCall{
|
|
||||||
Module: RootModuleInstance.Child("bloop", StringKey("hi")).Child("bar", NoKey),
|
|
||||||
Call: ModuleCall{Name: "baz"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo[0]`,
|
InputFrom: `module.foo[0]`,
|
||||||
InputTo: `module.foo["a"]`,
|
InputTo: `module.foo["a"]`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
|
WantFrom: `module.foo[0]`,
|
||||||
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
|
WantTo: `module.foo["a"]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo`,
|
InputFrom: `module.foo`,
|
||||||
InputTo: `module.foo["a"]`,
|
InputTo: `module.foo["a"]`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: RootModuleInstance.Child("foo", NoKey),
|
WantFrom: `module.foo`,
|
||||||
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
|
WantTo: `module.foo["a"]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo[0]`,
|
InputFrom: `module.foo[0]`,
|
||||||
InputTo: `module.foo`,
|
InputTo: `module.foo`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
|
WantFrom: `module.foo[0]`,
|
||||||
WantTo: RootModuleInstance.Child("foo", NoKey),
|
WantTo: `module.foo`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo[0]`,
|
InputFrom: `module.foo[0]`,
|
||||||
InputTo: `module.foo`,
|
InputTo: `module.foo`,
|
||||||
Module: RootModuleInstance.Child("bloop", NoKey),
|
Module: RootModule.Child("bloop"),
|
||||||
WantFrom: RootModuleInstance.Child("bloop", NoKey).Child("foo", IntKey(0)),
|
WantFrom: `module.bloop[*].module.foo[0]`,
|
||||||
WantTo: RootModuleInstance.Child("bloop", NoKey).Child("foo", NoKey),
|
WantTo: `module.bloop[*].module.foo`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo`,
|
InputFrom: `module.foo`,
|
||||||
InputTo: `foo.bar`,
|
InputTo: `foo.bar`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: nil, // Can't unify module call with resource
|
WantFrom: ``, // Can't unify module call with resource
|
||||||
WantTo: nil,
|
WantTo: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo[0]`,
|
InputFrom: `module.foo[0]`,
|
||||||
InputTo: `foo.bar`,
|
InputTo: `foo.bar`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: nil, // Can't unify module instance with resource
|
WantFrom: ``, // Can't unify module instance with resource
|
||||||
WantTo: nil,
|
WantTo: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo`,
|
InputFrom: `module.foo`,
|
||||||
InputTo: `foo.bar[0]`,
|
InputTo: `foo.bar[0]`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: nil, // Can't unify module call with resource instance
|
WantFrom: ``, // Can't unify module call with resource instance
|
||||||
WantTo: nil,
|
WantTo: ``,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
InputFrom: `module.foo[0]`,
|
InputFrom: `module.foo[0]`,
|
||||||
InputTo: `foo.bar[0]`,
|
InputTo: `foo.bar[0]`,
|
||||||
Module: RootModuleInstance,
|
Module: RootModule,
|
||||||
WantFrom: nil, // Can't unify module instance with resource instance
|
WantFrom: ``, // Can't unify module instance with resource instance
|
||||||
WantTo: nil,
|
WantTo: ``,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -604,13 +485,12 @@ func TestUnifyMoveEndpoints(t *testing.T) {
|
||||||
fromEp := parseInput(test.InputFrom)
|
fromEp := parseInput(test.InputFrom)
|
||||||
toEp := parseInput(test.InputTo)
|
toEp := parseInput(test.InputTo)
|
||||||
|
|
||||||
diffOpts := cmpopts.IgnoreUnexported(ModuleCall{})
|
|
||||||
gotFrom, gotTo := UnifyMoveEndpoints(test.Module, fromEp, toEp)
|
gotFrom, gotTo := UnifyMoveEndpoints(test.Module, fromEp, toEp)
|
||||||
if diff := cmp.Diff(test.WantFrom, gotFrom, diffOpts); diff != "" {
|
if got, want := gotFrom.String(), test.WantFrom; got != want {
|
||||||
t.Errorf("wrong 'from' address\n%s", diff)
|
t.Errorf("wrong 'from' result\ngot: %s\nwant: %s", got, want)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(test.WantTo, gotTo, diffOpts); diff != "" {
|
if got, want := gotTo.String(), test.WantTo; got != want {
|
||||||
t.Errorf("wrong 'to' address\n%s", diff)
|
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 the configuration, which is different than the direct representation
|
||||||
// of these in configuration where the author gives an address relative to
|
// of these in configuration where the author gives an address relative to
|
||||||
// the current module where the address is defined. The type MoveEndpoint
|
// the current module where the address is defined. The type MoveEndpoint
|
||||||
|
|
||||||
type AbsMoveable interface {
|
type AbsMoveable interface {
|
||||||
absMoveableSigil()
|
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 {
|
func (pa PathAttr) String() string {
|
||||||
return "path." + pa.Name
|
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.
|
// in lang.Scope.buildEvalContext.
|
||||||
referenceableSigil()
|
referenceableSigil()
|
||||||
|
|
||||||
|
// All Referenceable address types must have unique keys.
|
||||||
|
UniqueKeyer
|
||||||
|
|
||||||
// String produces a string representation of the address that could be
|
// String produces a string representation of the address that could be
|
||||||
// parsed as a HCL traversal and passed to ParseRef to produce an identical
|
// parsed as a HCL traversal and passed to ParseRef to produce an identical
|
||||||
// result.
|
// 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
|
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
|
// Instance produces the address for a specific instance of the receiver
|
||||||
// that is idenfied by the given key.
|
// that is idenfied by the given key.
|
||||||
func (r Resource) Instance(key InstanceKey) ResourceInstance {
|
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)
|
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
|
// Absolute returns an AbsResourceInstance from the receiver and the given module
|
||||||
// instance address.
|
// instance address.
|
||||||
func (r ResourceInstance) Absolute(module ModuleInstance) AbsResourceInstance {
|
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() {
|
func (r AbsResourceInstance) absMoveableSigil() {
|
||||||
// AbsResourceInstance is moveable
|
// AbsResourceInstance is moveable
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,12 @@ func (rp ResourceInstancePhase) String() string {
|
||||||
return fmt.Sprintf("%s#%s", rp.ResourceInstance, rp.Phase)
|
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.
|
// ResourceInstancePhaseType is an enumeration used with ResourceInstancePhase.
|
||||||
type ResourceInstancePhaseType string
|
type ResourceInstancePhaseType string
|
||||||
|
|
||||||
|
@ -103,3 +109,9 @@ func (rp ResourcePhase) String() string {
|
||||||
// because this special address type should never be exposed in the UI.
|
// because this special address type should never be exposed in the UI.
|
||||||
return fmt.Sprintf("%s#%s", rp.Resource, rp.Phase)
|
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 {
|
func (s selfT) String() string {
|
||||||
return "self"
|
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 {
|
func (ta TerraformAttr) String() string {
|
||||||
return "terraform." + ta.Name
|
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)
|
lcs := objchange.LongestCommonSubsequence(old, new)
|
||||||
var oldI, newI, lcsI int
|
var oldI, newI, lcsI int
|
||||||
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
|
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])) {
|
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]))
|
isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
|
||||||
if isObjectDiff {
|
if isObjectDiff {
|
||||||
ret = append(ret, &plans.Change{
|
ret = append(ret, &plans.Change{
|
||||||
|
@ -1827,6 +1847,8 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, this item is not part of the common sequence, so
|
||||||
|
// render as a deletion.
|
||||||
ret = append(ret, &plans.Change{
|
ret = append(ret, &plans.Change{
|
||||||
Action: plans.Delete,
|
Action: plans.Delete,
|
||||||
Before: old[oldI],
|
Before: old[oldI],
|
||||||
|
@ -1842,6 +1864,9 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
|
||||||
})
|
})
|
||||||
newI++
|
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) {
|
if lcsI < len(lcs) {
|
||||||
ret = append(ret, &plans.Change{
|
ret = append(ret, &plans.Change{
|
||||||
Action: plans.NoOp,
|
Action: plans.NoOp,
|
||||||
|
|
|
@ -9,6 +9,7 @@ const (
|
||||||
MessageDiagnostic MessageType = "diagnostic"
|
MessageDiagnostic MessageType = "diagnostic"
|
||||||
|
|
||||||
// Operation results
|
// Operation results
|
||||||
|
MessageResourceDrift MessageType = "resource_drift"
|
||||||
MessagePlannedChange MessageType = "planned_change"
|
MessagePlannedChange MessageType = "planned_change"
|
||||||
MessageChangeSummary MessageType = "change_summary"
|
MessageChangeSummary MessageType = "change_summary"
|
||||||
MessageOutputs MessageType = "outputs"
|
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) {
|
func (v *JSONView) ChangeSummary(cs *json.ChangeSummary) {
|
||||||
v.log.Info(
|
v.log.Info(
|
||||||
cs.String(),
|
cs.String(),
|
||||||
|
|
|
@ -141,6 +141,46 @@ func TestJSONView_PlannedChange(t *testing.T) {
|
||||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
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) {
|
func TestJSONView_ChangeSummary(t *testing.T) {
|
||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
jv := NewJSONView(NewView(streams))
|
jv := NewJSONView(NewView(streams))
|
||||||
|
@ -263,12 +303,16 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
|
||||||
gotLines := strings.Split(output, "\n")
|
gotLines := strings.Split(output, "\n")
|
||||||
|
|
||||||
if len(gotLines) != len(want) {
|
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
|
// Unmarshal each line and compare to the expected value
|
||||||
for i := range gotLines {
|
for i := range gotLines {
|
||||||
var gotStruct map[string]interface{}
|
var gotStruct map[string]interface{}
|
||||||
|
if i >= len(want) {
|
||||||
|
t.Error("reached end of want messages too soon")
|
||||||
|
break
|
||||||
|
}
|
||||||
wantStruct := want[i]
|
wantStruct := want[i]
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
|
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
|
// Verify the timestamp format
|
||||||
if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil {
|
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) {
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
@ -10,9 +11,11 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/command/format"
|
"github.com/hashicorp/terraform/internal/command/format"
|
||||||
"github.com/hashicorp/terraform/internal/command/views/json"
|
"github.com/hashicorp/terraform/internal/command/views/json"
|
||||||
"github.com/hashicorp/terraform/internal/plans"
|
"github.com/hashicorp/terraform/internal/plans"
|
||||||
|
"github.com/hashicorp/terraform/internal/states"
|
||||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||||
"github.com/hashicorp/terraform/internal/terraform"
|
"github.com/hashicorp/terraform/internal/terraform"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Operation interface {
|
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
|
// Log a change summary and a series of "planned" messages for the changes in
|
||||||
// the plan.
|
// the plan.
|
||||||
func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
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{
|
cs := &json.ChangeSummary{
|
||||||
Operation: json.OperationPlanned,
|
Operation: json.OperationPlanned,
|
||||||
}
|
}
|
||||||
|
@ -188,6 +197,92 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||||
v.view.ChangeSummary(cs)
|
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) {
|
func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
||||||
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
||||||
// Avoid rendering data sources on deletion
|
// Avoid rendering data sources on deletion
|
||||||
|
|
|
@ -483,8 +483,8 @@ func TestOperationJSON_plan(t *testing.T) {
|
||||||
if len(diags) > 0 {
|
if len(diags) > 0 {
|
||||||
t.Fatal(diags.Err())
|
t.Fatal(diags.Err())
|
||||||
}
|
}
|
||||||
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"}
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
||||||
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "beep"}
|
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
||||||
derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"}
|
derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"}
|
||||||
|
|
||||||
plan := &plans.Plan{
|
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{}{
|
want := []map[string]interface{}{
|
||||||
// Create-then-delete should result in replace
|
// Create-then-delete should result in replace
|
||||||
{
|
{
|
||||||
"@level": "info",
|
"@level": "info",
|
||||||
"@message": "test_instance.boop[0]: Plan to replace",
|
"@message": "test_resource.boop[0]: Plan to replace",
|
||||||
"@module": "terraform.ui",
|
"@module": "terraform.ui",
|
||||||
"type": "planned_change",
|
"type": "planned_change",
|
||||||
"change": map[string]interface{}{
|
"change": map[string]interface{}{
|
||||||
"action": "replace",
|
"action": "replace",
|
||||||
"resource": map[string]interface{}{
|
"resource": map[string]interface{}{
|
||||||
"addr": `test_instance.boop[0]`,
|
"addr": `test_resource.boop[0]`,
|
||||||
"implied_provider": "test",
|
"implied_provider": "test",
|
||||||
"module": "",
|
"module": "",
|
||||||
"resource": `test_instance.boop[0]`,
|
"resource": `test_resource.boop[0]`,
|
||||||
"resource_key": float64(0),
|
"resource_key": float64(0),
|
||||||
"resource_name": "boop",
|
"resource_name": "boop",
|
||||||
"resource_type": "test_instance",
|
"resource_type": "test_resource",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Simple create
|
// Simple create
|
||||||
{
|
{
|
||||||
"@level": "info",
|
"@level": "info",
|
||||||
"@message": "test_instance.boop[1]: Plan to create",
|
"@message": "test_resource.boop[1]: Plan to create",
|
||||||
"@module": "terraform.ui",
|
"@module": "terraform.ui",
|
||||||
"type": "planned_change",
|
"type": "planned_change",
|
||||||
"change": map[string]interface{}{
|
"change": map[string]interface{}{
|
||||||
"action": "create",
|
"action": "create",
|
||||||
"resource": map[string]interface{}{
|
"resource": map[string]interface{}{
|
||||||
"addr": `test_instance.boop[1]`,
|
"addr": `test_resource.boop[1]`,
|
||||||
"implied_provider": "test",
|
"implied_provider": "test",
|
||||||
"module": "",
|
"module": "",
|
||||||
"resource": `test_instance.boop[1]`,
|
"resource": `test_resource.boop[1]`,
|
||||||
"resource_key": float64(1),
|
"resource_key": float64(1),
|
||||||
"resource_name": "boop",
|
"resource_name": "boop",
|
||||||
"resource_type": "test_instance",
|
"resource_type": "test_resource",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Simple delete
|
// Simple delete
|
||||||
{
|
{
|
||||||
"@level": "info",
|
"@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",
|
"@module": "terraform.ui",
|
||||||
"type": "planned_change",
|
"type": "planned_change",
|
||||||
"change": map[string]interface{}{
|
"change": map[string]interface{}{
|
||||||
"action": "delete",
|
"action": "delete",
|
||||||
"resource": map[string]interface{}{
|
"resource": map[string]interface{}{
|
||||||
"addr": `module.vpc.test_instance.boop[0]`,
|
"addr": `module.vpc.test_resource.boop[0]`,
|
||||||
"implied_provider": "test",
|
"implied_provider": "test",
|
||||||
"module": "module.vpc",
|
"module": "module.vpc",
|
||||||
"resource": `test_instance.boop[0]`,
|
"resource": `test_resource.boop[0]`,
|
||||||
"resource_key": float64(0),
|
"resource_key": float64(0),
|
||||||
"resource_name": "boop",
|
"resource_name": "boop",
|
||||||
"resource_type": "test_instance",
|
"resource_type": "test_resource",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Delete-then-create is also a replace
|
// Delete-then-create is also a replace
|
||||||
{
|
{
|
||||||
"@level": "info",
|
"@level": "info",
|
||||||
"@message": "test_instance.beep: Plan to replace",
|
"@message": "test_resource.beep: Plan to replace",
|
||||||
"@module": "terraform.ui",
|
"@module": "terraform.ui",
|
||||||
"type": "planned_change",
|
"type": "planned_change",
|
||||||
"change": map[string]interface{}{
|
"change": map[string]interface{}{
|
||||||
"action": "replace",
|
"action": "replace",
|
||||||
"resource": map[string]interface{}{
|
"resource": map[string]interface{}{
|
||||||
"addr": `test_instance.beep`,
|
"addr": `test_resource.beep`,
|
||||||
"implied_provider": "test",
|
"implied_provider": "test",
|
||||||
"module": "",
|
"module": "",
|
||||||
"resource": `test_instance.beep`,
|
"resource": `test_resource.beep`,
|
||||||
"resource_key": nil,
|
"resource_key": nil,
|
||||||
"resource_name": "beep",
|
"resource_name": "beep",
|
||||||
"resource_type": "test_instance",
|
"resource_type": "test_resource",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Simple update
|
// Simple update
|
||||||
{
|
{
|
||||||
"@level": "info",
|
"@level": "info",
|
||||||
"@message": "module.vpc.test_instance.beep: Plan to update",
|
"@message": "module.vpc.test_resource.beep: Plan to update",
|
||||||
"@module": "terraform.ui",
|
"@module": "terraform.ui",
|
||||||
"type": "planned_change",
|
"type": "planned_change",
|
||||||
"change": map[string]interface{}{
|
"change": map[string]interface{}{
|
||||||
"action": "update",
|
"action": "update",
|
||||||
"resource": map[string]interface{}{
|
"resource": map[string]interface{}{
|
||||||
"addr": `module.vpc.test_instance.beep`,
|
"addr": `module.vpc.test_resource.beep`,
|
||||||
"implied_provider": "test",
|
"implied_provider": "test",
|
||||||
"module": "module.vpc",
|
"module": "module.vpc",
|
||||||
"resource": `test_instance.beep`,
|
"resource": `test_resource.beep`,
|
||||||
"resource_key": nil,
|
"resource_key": nil,
|
||||||
"resource_name": "beep",
|
"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)
|
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) {
|
func TestOperationJSON_plannedChange(t *testing.T) {
|
||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
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
|
// belong to their own cty.Object definitions. It is used in other functions
|
||||||
// which themselves handle that recursion.
|
// which themselves handle that recursion.
|
||||||
func listOptionalAttrsFromObject(o *Object) []string {
|
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 {
|
for name, attr := range o.Attributes {
|
||||||
if attr.Optional == true {
|
if attr.Optional || attr.Computed {
|
||||||
ret = append(ret, name)
|
ret = append(ret, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package configschema
|
package configschema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/apparentlymart/go-dump/dump"
|
"github.com/apparentlymart/go-dump/dump"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hcldec"
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
@ -885,3 +887,43 @@ func TestAttributeDecoderSpec_panic(t *testing.T) {
|
||||||
attrS.decoderSpec("attr")
|
attrS.decoderSpec("attr")
|
||||||
t.Errorf("expected panic")
|
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:
|
case NestingSet:
|
||||||
return cty.Set(ret)
|
return cty.Set(ret)
|
||||||
default: // Should never happen
|
default: // Should never happen
|
||||||
panic("invalid Nesting")
|
return cty.EmptyObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ func TestBlockImpliedType(t *testing.T) {
|
||||||
"optional_computed": {
|
"optional_computed": {
|
||||||
Type: cty.Map(cty.Bool),
|
Type: cty.Map(cty.Bool),
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -132,26 +133,18 @@ func TestObjectImpliedType(t *testing.T) {
|
||||||
nil,
|
nil,
|
||||||
cty.EmptyObject,
|
cty.EmptyObject,
|
||||||
},
|
},
|
||||||
|
"empty": {
|
||||||
|
&Object{},
|
||||||
|
cty.EmptyObject,
|
||||||
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
&Object{
|
&Object{
|
||||||
Nesting: NestingSingle,
|
Nesting: NestingSingle,
|
||||||
Attributes: map[string]*Attribute{
|
Attributes: map[string]*Attribute{
|
||||||
"optional": {
|
"optional": {Type: cty.String, Optional: true},
|
||||||
Type: cty.String,
|
"required": {Type: cty.Number, Required: true},
|
||||||
Optional: true,
|
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
||||||
},
|
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
||||||
"required": {
|
|
||||||
Type: cty.Number,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"computed": {
|
|
||||||
Type: cty.List(cty.Bool),
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"optional_computed": {
|
|
||||||
Type: cty.Map(cty.Bool),
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cty.ObjectWithOptionalAttrs(
|
cty.ObjectWithOptionalAttrs(
|
||||||
|
@ -161,7 +154,7 @@ func TestObjectImpliedType(t *testing.T) {
|
||||||
"computed": cty.List(cty.Bool),
|
"computed": cty.List(cty.Bool),
|
||||||
"optional_computed": cty.Map(cty.Bool),
|
"optional_computed": cty.Map(cty.Bool),
|
||||||
},
|
},
|
||||||
[]string{"optional", "optional_computed"},
|
[]string{"optional", "computed", "optional_computed"},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"nested attributes": {
|
"nested attributes": {
|
||||||
|
@ -172,21 +165,42 @@ func TestObjectImpliedType(t *testing.T) {
|
||||||
NestedType: &Object{
|
NestedType: &Object{
|
||||||
Nesting: NestingSingle,
|
Nesting: NestingSingle,
|
||||||
Attributes: map[string]*Attribute{
|
Attributes: map[string]*Attribute{
|
||||||
"optional": {
|
"optional": {Type: cty.String, Optional: true},
|
||||||
Type: cty.String,
|
"required": {Type: cty.Number, Required: true},
|
||||||
Optional: true,
|
"computed": {Type: cty.List(cty.Bool), Computed: true},
|
||||||
},
|
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
|
||||||
"required": {
|
},
|
||||||
Type: cty.Number,
|
},
|
||||||
Required: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
"computed": {
|
},
|
||||||
Type: cty.List(cty.Bool),
|
},
|
||||||
Computed: true,
|
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||||
},
|
"nested_type": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||||
"optional_computed": {
|
"optional": cty.String,
|
||||||
Type: cty.Map(cty.Bool),
|
"required": cty.Number,
|
||||||
Optional: true,
|
"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,
|
"required": cty.Number,
|
||||||
"computed": cty.List(cty.Bool),
|
"computed": cty.List(cty.Bool),
|
||||||
"optional_computed": cty.Map(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"}),
|
}, []string{"nested_type"}),
|
||||||
},
|
},
|
||||||
"NestingList": {
|
"NestingList": {
|
||||||
|
|
|
@ -55,7 +55,7 @@ func init() {
|
||||||
configureRequestTimeout()
|
configureRequestTimeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
var SupportedPluginProtocols = MustParseVersionConstraints("~> 5")
|
var SupportedPluginProtocols = MustParseVersionConstraints(">= 5, <7")
|
||||||
|
|
||||||
// registryClient is a client for the provider registry protocol that is
|
// 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
|
// 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.Header().Set("Content-Type", "application/json")
|
||||||
resp.WriteHeader(200)
|
resp.WriteHeader(200)
|
||||||
resp.Write([]byte(`{"versions":[{"version":"1.0.0","protocols":["0.1"]}]}`))
|
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":
|
case "weaksauce/no-versions":
|
||||||
resp.Header().Set("Content-Type", "application/json")
|
resp.Header().Set("Content-Type", "application/json")
|
||||||
resp.WriteHeader(200)
|
resp.WriteHeader(200)
|
||||||
|
@ -412,6 +416,12 @@ func TestFindClosestProtocolCompatibleVersion(t *testing.T) {
|
||||||
versions.Unspecified,
|
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 {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
|
|
@ -1457,7 +1457,7 @@ func TestProposedNew(t *testing.T) {
|
||||||
"computed": cty.String,
|
"computed": cty.String,
|
||||||
"optional_computed": cty.String,
|
"optional_computed": cty.String,
|
||||||
"required": 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
|
package states
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
@ -296,3 +297,245 @@ func (s *State) SyncWrapper() *SyncState {
|
||||||
state: s,
|
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
|
package states
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -292,3 +293,597 @@ func TestStateDeepCopy(t *testing.T) {
|
||||||
t.Fatalf("\nexpected:\n%q\ngot:\n%q\n", state, stateCopy)
|
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)
|
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/plans"
|
||||||
"github.com/hashicorp/terraform/internal/providers"
|
"github.com/hashicorp/terraform/internal/providers"
|
||||||
"github.com/hashicorp/terraform/internal/provisioners"
|
"github.com/hashicorp/terraform/internal/provisioners"
|
||||||
|
"github.com/hashicorp/terraform/internal/refactoring"
|
||||||
"github.com/hashicorp/terraform/internal/states"
|
"github.com/hashicorp/terraform/internal/states"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"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 plan *plans.Plan
|
||||||
var planDiags tfdiags.Diagnostics
|
var planDiags tfdiags.Diagnostics
|
||||||
switch c.planMode {
|
switch c.planMode {
|
||||||
|
@ -625,6 +653,10 @@ The -target option is not for routine use, and is provided only for exceptional
|
||||||
return nil, diags
|
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
|
// convert the variables into the format expected for the plan
|
||||||
varVals := make(map[string]plans.DynamicValue, len(c.variables))
|
varVals := make(map[string]plans.DynamicValue, len(c.variables))
|
||||||
for k, iv := range 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`.
|
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.
|
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.
|
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
|
in the documentation. This terminology is often used to distinguish it from
|
||||||
other components you might use in the Terraform product family, such as
|
other components you might use in the Terraform product family, such as
|
||||||
[Terraform Cloud](/docs/cloud/) or
|
[Terraform Cloud](/docs/cloud/) or
|
||||||
the various [Terraform providers](/docs/providers/), which are developed and
|
the various [Terraform providers](/docs/language/providers/index.html), which
|
||||||
released separately from Terraform CLI.
|
are developed and released separately from Terraform CLI.
|
||||||
|
|
||||||
To view a list of the commands available in your current Terraform version,
|
To view a list of the commands available in your current Terraform version,
|
||||||
run `terraform` with no additional arguments:
|
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".
|
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
|
||||||
Ignored when [remote state](/docs/language/state/remote.html) is used.
|
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
|
## Examples
|
||||||
|
|
||||||
These examples assume the following Terraform output snippet.
|
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
|
been placed there. Over time, as plugins are upgraded, the cache directory may
|
||||||
grow to contain several unused versions which you must delete manually.
|
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
|
### Development Overrides for Provider Developers
|
||||||
|
|
||||||
-> **Note:** Development overrides work only in Terraform v0.14 and later.
|
-> **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).
|
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
|
## 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
|
The value of `TF_CLI_ARGS` will specify additional arguments to the
|
||||||
command-line. This allows easier automation in CI environments as well as
|
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.
|
and name must be unique.
|
||||||
|
|
||||||
Within the block (the `{ }`) is configuration for the data instance. The
|
Within the block (the `{ }`) is configuration for the data instance. The
|
||||||
configuration is dependent on the type, and is documented for each
|
configuration is dependent on the type; consult the [provider's documentation] (https://registry.terraform.io/browse/providers) for
|
||||||
data source in the [providers section](/docs/providers/index.html).
|
details.
|
||||||
|
|
||||||
Each data instance will export one or more attributes, which can be
|
Each data instance will export one or more attributes, which can be
|
||||||
interpolated into other resources using variables of the form
|
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.
|
`aws` provider.
|
||||||
|
|
||||||
Within the block body (between `{ }`) is configuration for the provider.
|
Within the block body (between `{ }`) is configuration for the provider.
|
||||||
The configuration is dependent on the type, and is documented
|
The configuration is dependent on the type. Consult the [provider's documentation](https://registry.terraform.io/browse/providers) for details.
|
||||||
[for each provider](/docs/providers/index.html).
|
in each provider's documentation.
|
||||||
|
|
||||||
The arguments `alias` and `version`, if present, are special arguments
|
The arguments `alias` and `version`, if present, are special arguments
|
||||||
handled by Terraform Core for their respective features described above. All
|
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.
|
and name must be unique.
|
||||||
|
|
||||||
Within the block (the `{ }`) is configuration for the resource. The
|
Within the block (the `{ }`) is configuration for the resource. The
|
||||||
configuration is dependent on the type, and is documented for each
|
configuration is dependent on the type. Consult the [provider's documentation](https://registry.terraform.io/browse/providers) for details.
|
||||||
resource type in the
|
|
||||||
[providers section](/docs/providers/index.html).
|
details.
|
||||||
|
|
||||||
### Meta-parameters
|
### 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
|
### 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:
|
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
|
### 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
|
- `planned_change`: describes a planned change to a single resource
|
||||||
- `change_summary`: summary of all planned or applied changes
|
- `change_summary`: summary of all planned or applied changes
|
||||||
- `outputs`: list of all root module outputs
|
- `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
|
## 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:
|
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.
|
and name must be unique.
|
||||||
|
|
||||||
Within the block (the `{ }`) is configuration for the data instance. The
|
Within the block (the `{ }`) is configuration for the data instance. The
|
||||||
configuration is dependent on the type, and is documented for each
|
configuration is dependent on the type; as with
|
||||||
data source in the [providers section](/docs/providers/index.html).
|
[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
|
Each data instance will export one or more attributes, which can be
|
||||||
used in other resources as reference expressions of the form
|
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 inside a class of characters (see below)
|
||||||
- `[^CLASS]` - matches any single non-separator character outside 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:
|
Character classes support the following:
|
||||||
|
|
||||||
- `[abc]` - matches any single character within the set
|
- `[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
|
is the main directory of publicly available Terraform providers, and hosts
|
||||||
providers for most major infrastructure platforms.
|
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
|
## How to Use Providers
|
||||||
|
|
||||||
To use resources from a given provider, you need to include some information
|
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>
|
<li>
|
||||||
<a href="/docs/language/dependency-lock.html">Dependency Lock File</a>
|
<a href="/docs/language/dependency-lock.html">Dependency Lock File</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</li><!-- providers -->
|
</li><!-- providers -->
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue