Merge branch 'main' into description-metadata-language-docs

This commit is contained in:
Laura Pacilio 2021-07-21 16:24:59 -04:00
commit 06a2fdcf9d
58 changed files with 2616 additions and 304 deletions

View File

@ -10,3 +10,9 @@ type CountAttr struct {
func (ca CountAttr) String() string {
return "count." + ca.Name
}
func (ca CountAttr) UniqueKey() UniqueKey {
return ca // A CountAttr is its own UniqueKey
}
func (ca CountAttr) uniqueKeySigil() {}

View File

@ -10,3 +10,9 @@ type ForEachAttr struct {
func (f ForEachAttr) String() string {
return "each." + f.Name
}
func (f ForEachAttr) UniqueKey() UniqueKey {
return f // A ForEachAttr is its own UniqueKey
}
func (f ForEachAttr) uniqueKeySigil() {}

View File

@ -14,6 +14,12 @@ func (v InputVariable) String() string {
return "var." + v.Name
}
func (v InputVariable) UniqueKey() UniqueKey {
return v // A InputVariable is its own UniqueKey
}
func (v InputVariable) uniqueKeySigil() {}
// Absolute converts the receiver into an absolute address within the given
// module instance.
func (v InputVariable) Absolute(m ModuleInstance) AbsInputVariableInstance {

View File

@ -14,6 +14,12 @@ func (v LocalValue) String() string {
return "local." + v.Name
}
func (v LocalValue) UniqueKey() UniqueKey {
return v // A LocalValue is its own UniqueKey
}
func (v LocalValue) uniqueKeySigil() {}
// Absolute converts the receiver into an absolute address within the given
// module instance.
func (v LocalValue) Absolute(m ModuleInstance) AbsLocalValue {

View File

@ -15,6 +15,12 @@ func (c ModuleCall) String() string {
return "module." + c.Name
}
func (c ModuleCall) UniqueKey() UniqueKey {
return c // A ModuleCall is its own UniqueKey
}
func (c ModuleCall) uniqueKeySigil() {}
// Instance returns the address of an instance of the receiver identified by
// the given key.
func (c ModuleCall) Instance(key InstanceKey) ModuleCallInstance {
@ -47,7 +53,11 @@ func (c AbsModuleCall) absMoveableSigil() {
}
func (c AbsModuleCall) String() string {
return fmt.Sprintf("%s.%s", c.Module, c.Call.Name)
if len(c.Module) == 0 {
return "module." + c.Call.Name
}
return fmt.Sprintf("%s.module.%s", c.Module, c.Call.Name)
}
func (c AbsModuleCall) Instance(key InstanceKey) ModuleInstance {
@ -79,6 +89,12 @@ func (c ModuleCallInstance) String() string {
return fmt.Sprintf("module.%s%s", c.Call.Name, c.Key)
}
func (c ModuleCallInstance) UniqueKey() UniqueKey {
return c // A ModuleCallInstance is its own UniqueKey
}
func (c ModuleCallInstance) uniqueKeySigil() {}
func (c ModuleCallInstance) Absolute(moduleAddr ModuleInstance) ModuleInstance {
ret := make(ModuleInstance, len(moduleAddr), len(moduleAddr)+1)
copy(ret, moduleAddr)
@ -118,6 +134,12 @@ func (m ModuleCallOutput) String() string {
return fmt.Sprintf("%s.%s", m.Call.String(), m.Name)
}
func (m ModuleCallOutput) UniqueKey() UniqueKey {
return m // A ModuleCallOutput is its own UniqueKey
}
func (m ModuleCallOutput) uniqueKeySigil() {}
// ModuleCallInstanceOutput is the address of a particular named output produced by
// an instance of a module call.
type ModuleCallInstanceOutput struct {
@ -139,6 +161,12 @@ func (co ModuleCallInstanceOutput) String() string {
return fmt.Sprintf("%s.%s", co.Call.String(), co.Name)
}
func (co ModuleCallInstanceOutput) UniqueKey() UniqueKey {
return co // A ModuleCallInstanceOutput is its own UniqueKey
}
func (co ModuleCallInstanceOutput) uniqueKeySigil() {}
// AbsOutputValue returns the absolute output value address that corresponds
// to the receving module call output address, once resolved in the given
// calling module.

View File

@ -275,6 +275,14 @@ func (m ModuleInstance) String() string {
return buf.String()
}
type moduleInstanceKey string
func (m ModuleInstance) UniqueKey() UniqueKey {
return moduleInstanceKey(m.String())
}
func (mk moduleInstanceKey) uniqueKeySigil() {}
// Equal returns true if the receiver and the given other value
// contains the exact same parts.
func (m ModuleInstance) Equal(o ModuleInstance) bool {
@ -496,6 +504,28 @@ func (m ModuleInstance) absMoveableSigil() {
// ModuleInstance is moveable
}
// IsDeclaredByCall returns true if the receiver is an instance of the given
// AbsModuleCall.
func (m ModuleInstance) IsDeclaredByCall(other AbsModuleCall) bool {
// Compare len(m) to len(other.Module+1) because the final module instance
// step in other is stored in the AbsModuleCall.Call
if len(m) > len(other.Module)+1 || len(m) == 0 && len(other.Module) == 0 {
return false
}
// Verify that the other's ModuleInstance matches the receiver.
inst, lastStep := other.Module, other.Call
for i := range inst {
if inst[i] != m[i] {
return false
}
}
// Now compare the final step of the received with the other Call, where
// only the name needs to match.
return lastStep.Name == m[len(m)-1].Name
}
func (s ModuleInstanceStep) String() string {
if s.InstanceKey != NoKey {
return s.Name + s.InstanceKey.String()

View File

@ -91,3 +91,80 @@ func BenchmarkStringLong(b *testing.B) {
addr.String()
}
}
func TestModuleInstance_IsDeclaredByCall(t *testing.T) {
tests := []struct {
instance ModuleInstance
call AbsModuleCall
want bool
}{
{
ModuleInstance{},
AbsModuleCall{},
false,
},
{
mustParseModuleInstanceStr("module.child"),
AbsModuleCall{},
false,
},
{
ModuleInstance{},
AbsModuleCall{
RootModuleInstance,
ModuleCall{Name: "child"},
},
false,
},
{
mustParseModuleInstanceStr("module.child"),
AbsModuleCall{ // module.child
RootModuleInstance,
ModuleCall{Name: "child"},
},
true,
},
{
mustParseModuleInstanceStr(`module.child`),
AbsModuleCall{ // module.kinder.module.child
mustParseModuleInstanceStr("module.kinder"),
ModuleCall{Name: "child"},
},
false,
},
{
mustParseModuleInstanceStr("module.kinder"),
// module.kinder.module.child contains module.kinder, but is not itself an instance of module.kinder
AbsModuleCall{
mustParseModuleInstanceStr("module.kinder"),
ModuleCall{Name: "child"},
},
false,
},
{
mustParseModuleInstanceStr("module.child"),
AbsModuleCall{
mustParseModuleInstanceStr(`module.kinder["a"]`),
ModuleCall{Name: "kinder"},
},
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%q.IsCallInstance(%q)", test.instance, test.call.String()), func(t *testing.T) {
got := test.instance.IsDeclaredByCall(test.call)
if got != test.want {
t.Fatal("wrong result")
}
})
}
}
func mustParseModuleInstanceStr(str string) ModuleInstance {
mi, err := ParseModuleInstanceStr(str)
if err != nil {
panic(err)
}
return mi
}

View File

@ -39,6 +39,10 @@ type MoveEndpoint struct {
relSubject AbsMoveable
}
func (e *MoveEndpoint) ObjectKind() MoveEndpointKind {
return absMoveableEndpointKind(e.relSubject)
}
func (e *MoveEndpoint) String() string {
// Our internal pseudo-AbsMovable representing the relative
// address (either ModuleInstance or AbsResourceInstance) is
@ -73,7 +77,7 @@ func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool {
// address, because the rules for whether unify can succeed depend
// only on the relative part of the addresses, not on which module
// they were declared in.
from, to := UnifyMoveEndpoints(RootModuleInstance, e, other)
from, to := UnifyMoveEndpoints(RootModule, e, other)
return from != nil && to != nil
}
@ -147,11 +151,10 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost
// UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
// "from" and "to" addresses in a moved block, and returns a pair of
// AbsMoveable addresses guaranteed to be of the same dynamic type
// MoveEndpointInModule addresses guaranteed to be of the same dynamic type
// that represent what the two MoveEndpoint addresses refer to.
//
// moduleAddr must be the address of the module instance where the move
// was declared.
// moduleAddr must be the address of the module where the move was declared.
//
// This function deals both with the conversion from relative to absolute
// addresses and with resolving the ambiguity between no-key instance
@ -163,7 +166,7 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost
// given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil),
// in which case the caller should typically report an error to the user
// stating the unification constraints.
func UnifyMoveEndpoints(moduleAddr ModuleInstance, relFrom, relTo *MoveEndpoint) (absFrom, absTo AbsMoveable) {
func UnifyMoveEndpoints(moduleAddr Module, relFrom, relTo *MoveEndpoint) (modFrom, modTo *MoveEndpointInModule) {
// First we'll make a decision about which address type we're
// ultimately trying to unify to. For our internal purposes
@ -197,17 +200,17 @@ func UnifyMoveEndpoints(moduleAddr ModuleInstance, relFrom, relTo *MoveEndpoint)
panic("unhandled move address types")
}
absFrom = relFrom.prepareAbsMoveable(moduleAddr, wantType)
absTo = relTo.prepareAbsMoveable(moduleAddr, wantType)
if absFrom == nil || absTo == nil {
modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType)
modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType)
if modFrom == nil || modTo == nil {
// if either of them failed then they both failed, to make the
// caller's life a little easier.
return nil, nil
}
return absFrom, absTo
return modFrom, modTo
}
func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType TargetableAddrType) AbsMoveable {
func (e *MoveEndpoint) prepareMoveEndpointInModule(moduleAddr Module, wantType TargetableAddrType) *MoveEndpointInModule {
// relAddr can only be either AbsResourceInstance or ModuleInstance, the
// internal intermediate representation produced by ParseMoveEndpoint.
relAddr := e.relSubject
@ -216,40 +219,43 @@ func (e *MoveEndpoint) prepareAbsMoveable(moduleAddr ModuleInstance, wantType Ta
case ModuleInstance:
switch wantType {
case ModuleInstanceAddrType:
ret := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr))
ret = append(ret, moduleAddr...)
ret = append(ret, relAddr...)
return ret
// Since our internal representation is already a module instance,
// we can just rewrap this one.
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr,
}
case ModuleAddrType:
// NOTE: We're fudging a little here and using
// ModuleAddrType to represent AbsModuleCall rather
// than Module.
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr)-1)
callerAddr = append(callerAddr, moduleAddr...)
callerAddr = append(callerAddr, relAddr[:len(relAddr)-1]...)
return AbsModuleCall{
callerAddr, callAddr := relAddr.Call()
absCallAddr := AbsModuleCall{
Module: callerAddr,
Call: ModuleCall{
Name: relAddr[len(relAddr)-1].Name,
},
Call: callAddr,
}
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: absCallAddr,
}
default:
return nil // can't make any other types from a ModuleInstance
}
case AbsResourceInstance:
callerAddr := make(ModuleInstance, 0, len(moduleAddr)+len(relAddr.Module))
callerAddr = append(callerAddr, moduleAddr...)
callerAddr = append(callerAddr, relAddr.Module...)
switch wantType {
case AbsResourceInstanceAddrType:
return AbsResourceInstance{
Module: callerAddr,
Resource: relAddr.Resource,
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr,
}
case AbsResourceAddrType:
return AbsResource{
Module: callerAddr,
Resource: relAddr.Resource.Resource,
return &MoveEndpointInModule{
SourceRange: e.SourceRange,
module: moduleAddr,
relSubject: relAddr.ContainingResource(),
}
default:
return nil // can't make any other types from an AbsResourceInstance

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -5,7 +5,6 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
@ -339,245 +338,127 @@ func TestParseMoveEndpoint(t *testing.T) {
func TestUnifyMoveEndpoints(t *testing.T) {
tests := []struct {
InputFrom, InputTo string
Module ModuleInstance
WantFrom, WantTo AbsMoveable
Module Module
WantFrom, WantTo string
}{
{
InputFrom: `foo.bar`,
InputTo: `foo.baz`,
Module: RootModuleInstance,
WantFrom: AbsResource{
Module: RootModuleInstance,
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
WantTo: AbsResource{
Module: RootModuleInstance,
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "baz",
},
},
Module: RootModule,
WantFrom: `foo.bar[*]`,
WantTo: `foo.baz[*]`,
},
{
InputFrom: `foo.bar`,
InputTo: `foo.baz`,
Module: RootModuleInstance.Child("a", NoKey),
WantFrom: AbsResource{
Module: RootModuleInstance.Child("a", NoKey),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
WantTo: AbsResource{
Module: RootModuleInstance.Child("a", NoKey),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "baz",
},
},
Module: RootModule.Child("a"),
WantFrom: `module.a[*].foo.bar[*]`,
WantTo: `module.a[*].foo.baz[*]`,
},
{
InputFrom: `foo.bar`,
InputTo: `module.b[0].foo.baz`,
Module: RootModuleInstance.Child("a", NoKey),
WantFrom: AbsResource{
Module: RootModuleInstance.Child("a", NoKey),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
WantTo: AbsResource{
Module: RootModuleInstance.Child("a", NoKey).Child("b", IntKey(0)),
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "baz",
},
},
Module: RootModule.Child("a"),
WantFrom: `module.a[*].foo.bar[*]`,
WantTo: `module.a[*].module.b[0].foo.baz[*]`,
},
{
InputFrom: `foo.bar`,
InputTo: `foo.bar["thing"]`,
Module: RootModuleInstance,
WantFrom: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
},
WantTo: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("thing"),
},
},
Module: RootModule,
WantFrom: `foo.bar`,
WantTo: `foo.bar["thing"]`,
},
{
InputFrom: `foo.bar["thing"]`,
InputTo: `foo.bar`,
Module: RootModuleInstance,
WantFrom: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("thing"),
},
},
WantTo: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
},
},
Module: RootModule,
WantFrom: `foo.bar["thing"]`,
WantTo: `foo.bar`,
},
{
InputFrom: `foo.bar["a"]`,
InputTo: `foo.bar["b"]`,
Module: RootModuleInstance,
WantFrom: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("a"),
},
},
WantTo: AbsResourceInstance{
Module: RootModuleInstance,
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
},
Key: StringKey("b"),
},
},
Module: RootModule,
WantFrom: `foo.bar["a"]`,
WantTo: `foo.bar["b"]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.bar`,
Module: RootModuleInstance,
WantFrom: AbsModuleCall{
Module: RootModuleInstance,
Call: ModuleCall{Name: "foo"},
},
WantTo: AbsModuleCall{
Module: RootModuleInstance,
Call: ModuleCall{Name: "bar"},
},
Module: RootModule,
WantFrom: `module.foo[*]`,
WantTo: `module.bar[*]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.bar.module.baz`,
Module: RootModuleInstance,
WantFrom: AbsModuleCall{
Module: RootModuleInstance,
Call: ModuleCall{Name: "foo"},
},
WantTo: AbsModuleCall{
Module: RootModuleInstance.Child("bar", NoKey),
Call: ModuleCall{Name: "baz"},
},
Module: RootModule,
WantFrom: `module.foo[*]`,
WantTo: `module.bar.module.baz[*]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.bar.module.baz`,
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
WantFrom: AbsModuleCall{
Module: RootModuleInstance.Child("bloop", StringKey("hi")),
Call: ModuleCall{Name: "foo"},
},
WantTo: AbsModuleCall{
Module: RootModuleInstance.Child("bloop", StringKey("hi")).Child("bar", NoKey),
Call: ModuleCall{Name: "baz"},
},
Module: RootModule.Child("bloop"),
WantFrom: `module.bloop[*].module.foo[*]`,
WantTo: `module.bloop[*].module.bar.module.baz[*]`,
},
{
InputFrom: `module.foo[0]`,
InputTo: `module.foo["a"]`,
Module: RootModuleInstance,
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
Module: RootModule,
WantFrom: `module.foo[0]`,
WantTo: `module.foo["a"]`,
},
{
InputFrom: `module.foo`,
InputTo: `module.foo["a"]`,
Module: RootModuleInstance,
WantFrom: RootModuleInstance.Child("foo", NoKey),
WantTo: RootModuleInstance.Child("foo", StringKey("a")),
Module: RootModule,
WantFrom: `module.foo`,
WantTo: `module.foo["a"]`,
},
{
InputFrom: `module.foo[0]`,
InputTo: `module.foo`,
Module: RootModuleInstance,
WantFrom: RootModuleInstance.Child("foo", IntKey(0)),
WantTo: RootModuleInstance.Child("foo", NoKey),
Module: RootModule,
WantFrom: `module.foo[0]`,
WantTo: `module.foo`,
},
{
InputFrom: `module.foo[0]`,
InputTo: `module.foo`,
Module: RootModuleInstance.Child("bloop", NoKey),
WantFrom: RootModuleInstance.Child("bloop", NoKey).Child("foo", IntKey(0)),
WantTo: RootModuleInstance.Child("bloop", NoKey).Child("foo", NoKey),
Module: RootModule.Child("bloop"),
WantFrom: `module.bloop[*].module.foo[0]`,
WantTo: `module.bloop[*].module.foo`,
},
{
InputFrom: `module.foo`,
InputTo: `foo.bar`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module call with resource
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module call with resource
WantTo: ``,
},
{
InputFrom: `module.foo[0]`,
InputTo: `foo.bar`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module instance with resource
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module instance with resource
WantTo: ``,
},
{
InputFrom: `module.foo`,
InputTo: `foo.bar[0]`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module call with resource instance
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module call with resource instance
WantTo: ``,
},
{
InputFrom: `module.foo[0]`,
InputTo: `foo.bar[0]`,
Module: RootModuleInstance,
WantFrom: nil, // Can't unify module instance with resource instance
WantTo: nil,
Module: RootModule,
WantFrom: ``, // Can't unify module instance with resource instance
WantTo: ``,
},
}
@ -604,13 +485,12 @@ func TestUnifyMoveEndpoints(t *testing.T) {
fromEp := parseInput(test.InputFrom)
toEp := parseInput(test.InputTo)
diffOpts := cmpopts.IgnoreUnexported(ModuleCall{})
gotFrom, gotTo := UnifyMoveEndpoints(test.Module, fromEp, toEp)
if diff := cmp.Diff(test.WantFrom, gotFrom, diffOpts); diff != "" {
t.Errorf("wrong 'from' address\n%s", diff)
if got, want := gotFrom.String(), test.WantFrom; got != want {
t.Errorf("wrong 'from' result\ngot: %s\nwant: %s", got, want)
}
if diff := cmp.Diff(test.WantTo, gotTo, diffOpts); diff != "" {
t.Errorf("wrong 'to' address\n%s", diff)
if got, want := gotTo.String(), test.WantTo; got != want {
t.Errorf("wrong 'to' result\ngot: %s\nwant: %s", got, want)
}
})
}

View File

@ -9,7 +9,6 @@ package addrs
// of the configuration, which is different than the direct representation
// of these in configuration where the author gives an address relative to
// the current module where the address is defined. The type MoveEndpoint
type AbsMoveable interface {
absMoveableSigil()

View File

@ -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) + ")"
}
}

View File

@ -10,3 +10,9 @@ type PathAttr struct {
func (pa PathAttr) String() string {
return "path." + pa.Name
}
func (pa PathAttr) UniqueKey() UniqueKey {
return pa // A PathAttr is its own UniqueKey
}
func (pa PathAttr) uniqueKeySigil() {}

View File

@ -7,6 +7,9 @@ type Referenceable interface {
// in lang.Scope.buildEvalContext.
referenceableSigil()
// All Referenceable address types must have unique keys.
UniqueKeyer
// String produces a string representation of the address that could be
// parsed as a HCL traversal and passed to ParseRef to produce an identical
// result.

View File

@ -32,6 +32,12 @@ func (r Resource) Equal(o Resource) bool {
return r.Mode == o.Mode && r.Name == o.Name && r.Type == o.Type
}
func (r Resource) UniqueKey() UniqueKey {
return r // A Resource is its own UniqueKey
}
func (r Resource) uniqueKeySigil() {}
// Instance produces the address for a specific instance of the receiver
// that is idenfied by the given key.
func (r Resource) Instance(key InstanceKey) ResourceInstance {
@ -94,6 +100,12 @@ func (r ResourceInstance) Equal(o ResourceInstance) bool {
return r.Key == o.Key && r.Resource.Equal(o.Resource)
}
func (r ResourceInstance) UniqueKey() UniqueKey {
return r // A ResourceInstance is its own UniqueKey
}
func (r ResourceInstance) uniqueKeySigil() {}
// Absolute returns an AbsResourceInstance from the receiver and the given module
// instance address.
func (r ResourceInstance) Absolute(module ModuleInstance) AbsResourceInstance {
@ -280,6 +292,14 @@ func (r AbsResourceInstance) Less(o AbsResourceInstance) bool {
}
}
type absResourceInstanceKey string
func (r AbsResourceInstance) UniqueKey() UniqueKey {
return absResourceInstanceKey(r.String())
}
func (r absResourceInstanceKey) uniqueKeySigil() {}
func (r AbsResourceInstance) absMoveableSigil() {
// AbsResourceInstance is moveable
}

View File

@ -44,6 +44,12 @@ func (rp ResourceInstancePhase) String() string {
return fmt.Sprintf("%s#%s", rp.ResourceInstance, rp.Phase)
}
func (rp ResourceInstancePhase) UniqueKey() UniqueKey {
return rp // A ResourceInstancePhase is its own UniqueKey
}
func (rp ResourceInstancePhase) uniqueKeySigil() {}
// ResourceInstancePhaseType is an enumeration used with ResourceInstancePhase.
type ResourceInstancePhaseType string
@ -103,3 +109,9 @@ func (rp ResourcePhase) String() string {
// because this special address type should never be exposed in the UI.
return fmt.Sprintf("%s#%s", rp.Resource, rp.Phase)
}
func (rp ResourcePhase) UniqueKey() UniqueKey {
return rp // A ResourcePhase is its own UniqueKey
}
func (rp ResourcePhase) uniqueKeySigil() {}

View File

@ -12,3 +12,9 @@ func (s selfT) referenceableSigil() {
func (s selfT) String() string {
return "self"
}
func (s selfT) UniqueKey() UniqueKey {
return Self // Self is its own UniqueKey
}
func (s selfT) uniqueKeySigil() {}

43
internal/addrs/set.go Normal file
View File

@ -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
}

View File

@ -10,3 +10,9 @@ type TerraformAttr struct {
func (ta TerraformAttr) String() string {
return "terraform." + ta.Name
}
func (ta TerraformAttr) UniqueKey() UniqueKey {
return ta // A TerraformAttr is its own UniqueKey
}
func (ta TerraformAttr) uniqueKeySigil() {}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -1814,7 +1814,27 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
lcs := objchange.LongestCommonSubsequence(old, new)
var oldI, newI, lcsI int
for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
// We first process items in the old and new sequences which are not
// equal to the current common sequence item. Old items are marked as
// deletions, and new items are marked as additions.
//
// There is an exception for deleted & created object items, which we
// try to render as updates where that makes sense.
for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
// Render this as an object update if all of these are true:
//
// - the current old item is an object;
// - there's a current new item which is also an object;
// - either there are no common items left, or the current new item
// doesn't equal the current common item.
//
// Why do we need the the last clause? If we have current items in all
// three sequences, and the current new item is equal to a common item,
// then we should just need to advance the old item list and we'll
// eventually find a common item matching both old and new.
//
// This combination of conditions allows us to render an object update
// diff instead of a combination of delete old & create new.
isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
if isObjectDiff {
ret = append(ret, &plans.Change{
@ -1827,6 +1847,8 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
continue
}
// Otherwise, this item is not part of the common sequence, so
// render as a deletion.
ret = append(ret, &plans.Change{
Action: plans.Delete,
Before: old[oldI],
@ -1842,6 +1864,9 @@ func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
})
newI++
}
// When we've exhausted the old & new sequences of items which are not
// in the common subsequence, we render a common item and continue.
if lcsI < len(lcs) {
ret = append(ret, &plans.Change{
Action: plans.NoOp,

View File

@ -9,6 +9,7 @@ const (
MessageDiagnostic MessageType = "diagnostic"
// Operation results
MessageResourceDrift MessageType = "resource_drift"
MessagePlannedChange MessageType = "planned_change"
MessageChangeSummary MessageType = "change_summary"
MessageOutputs MessageType = "outputs"

View File

@ -95,6 +95,14 @@ func (v *JSONView) PlannedChange(c *json.ResourceInstanceChange) {
)
}
func (v *JSONView) ResourceDrift(c *json.ResourceInstanceChange) {
v.log.Info(
fmt.Sprintf("%s: Drift detected (%s)", c.Resource.Addr, c.Action),
"type", json.MessageResourceDrift,
"change", c,
)
}
func (v *JSONView) ChangeSummary(cs *json.ChangeSummary) {
v.log.Info(
cs.String(),

View File

@ -141,6 +141,46 @@ func TestJSONView_PlannedChange(t *testing.T) {
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestJSONView_ResourceDrift(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
foo, diags := addrs.ParseModuleInstanceStr("module.foo")
if len(diags) > 0 {
t.Fatal(diags.Err())
}
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
cs := &plans.ResourceInstanceChangeSrc{
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
ChangeSrc: plans.ChangeSrc{
Action: plans.Update,
},
}
jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs))
want := []map[string]interface{}{
{
"@level": "info",
"@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`,
"@module": "terraform.ui",
"type": "resource_drift",
"change": map[string]interface{}{
"action": "update",
"resource": map[string]interface{}{
"addr": `module.foo.test_instance.bar["boop"]`,
"implied_provider": "test",
"module": "module.foo",
"resource": `test_instance.bar["boop"]`,
"resource_key": "boop",
"resource_name": "bar",
"resource_type": "test_instance",
},
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestJSONView_ChangeSummary(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
@ -263,12 +303,16 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
gotLines := strings.Split(output, "\n")
if len(gotLines) != len(want) {
t.Fatalf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
t.Errorf("unexpected number of messages. got %d, want %d", len(gotLines), len(want))
}
// Unmarshal each line and compare to the expected value
for i := range gotLines {
var gotStruct map[string]interface{}
if i >= len(want) {
t.Error("reached end of want messages too soon")
break
}
wantStruct := want[i]
if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
@ -283,12 +327,12 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
// Verify the timestamp format
if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil {
t.Fatalf("error parsing timestamp: %s", err)
t.Errorf("error parsing timestamp on line %d: %s", i, err)
}
}
if !cmp.Equal(wantStruct, gotStruct) {
t.Fatalf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
}
}
}

View File

@ -3,6 +3,7 @@ package views
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/internal/addrs"
@ -10,9 +11,11 @@ import (
"github.com/hashicorp/terraform/internal/command/format"
"github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type Operation interface {
@ -160,6 +163,12 @@ func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
// Log a change summary and a series of "planned" messages for the changes in
// the plan.
func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
if err := v.resourceDrift(plan.PrevRunState, plan.PriorState, schemas); err != nil {
var diags tfdiags.Diagnostics
diags = diags.Append(err)
v.Diagnostics(diags)
}
cs := &json.ChangeSummary{
Operation: json.OperationPlanned,
}
@ -188,6 +197,92 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
v.view.ChangeSummary(cs)
}
func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
if newState.ManagedResourcesEqual(oldState) {
// Nothing to do, because we only detect and report drift for managed
// resource instances.
return nil
}
var changes []*json.ResourceInstanceChange
for _, ms := range oldState.Modules {
for _, rs := range ms.Resources {
if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
// Drift reporting is only for managed resources
continue
}
provider := rs.ProviderConfig.Provider
for key, oldIS := range rs.Instances {
if oldIS.Current == nil {
// Not interested in instances that only have deposed objects
continue
}
addr := rs.Addr.Instance(key)
newIS := newState.ResourceInstance(addr)
schema, _ := schemas.ResourceTypeConfig(
provider,
addr.Resource.Resource.Mode,
addr.Resource.Resource.Type,
)
if schema == nil {
return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider)
}
ty := schema.ImpliedType()
oldObj, err := oldIS.Current.Decode(ty)
if err != nil {
return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err)
}
var newObj *states.ResourceInstanceObject
if newIS != nil && newIS.Current != nil {
newObj, err = newIS.Current.Decode(ty)
if err != nil {
return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err)
}
}
var oldVal, newVal cty.Value
oldVal = oldObj.Value
if newObj != nil {
newVal = newObj.Value
} else {
newVal = cty.NullVal(ty)
}
if oldVal.RawEquals(newVal) {
// No drift if the two values are semantically equivalent
continue
}
// We can only detect updates and deletes as drift.
action := plans.Update
if newVal.IsNull() {
action = plans.Delete
}
change := &plans.ResourceInstanceChangeSrc{
Addr: addr,
ChangeSrc: plans.ChangeSrc{
Action: action,
},
}
changes = append(changes, json.NewResourceInstanceChange(change))
}
}
}
// Sort the change structs lexically by address to give stable output
sort.Slice(changes, func(i, j int) bool { return changes[i].Resource.Addr < changes[j].Resource.Addr })
for _, change := range changes {
v.view.ResourceDrift(change)
}
return nil
}
func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
// Avoid rendering data sources on deletion

View File

@ -483,8 +483,8 @@ func TestOperationJSON_plan(t *testing.T) {
if len(diags) > 0 {
t.Fatal(diags.Err())
}
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"}
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "beep"}
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"}
plan := &plans.Plan{
@ -518,101 +518,101 @@ func TestOperationJSON_plan(t *testing.T) {
},
},
}
v.Plan(plan, nil)
v.Plan(plan, testSchemas())
want := []map[string]interface{}{
// Create-then-delete should result in replace
{
"@level": "info",
"@message": "test_instance.boop[0]: Plan to replace",
"@message": "test_resource.boop[0]: Plan to replace",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "replace",
"resource": map[string]interface{}{
"addr": `test_instance.boop[0]`,
"addr": `test_resource.boop[0]`,
"implied_provider": "test",
"module": "",
"resource": `test_instance.boop[0]`,
"resource": `test_resource.boop[0]`,
"resource_key": float64(0),
"resource_name": "boop",
"resource_type": "test_instance",
"resource_type": "test_resource",
},
},
},
// Simple create
{
"@level": "info",
"@message": "test_instance.boop[1]: Plan to create",
"@message": "test_resource.boop[1]: Plan to create",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "create",
"resource": map[string]interface{}{
"addr": `test_instance.boop[1]`,
"addr": `test_resource.boop[1]`,
"implied_provider": "test",
"module": "",
"resource": `test_instance.boop[1]`,
"resource": `test_resource.boop[1]`,
"resource_key": float64(1),
"resource_name": "boop",
"resource_type": "test_instance",
"resource_type": "test_resource",
},
},
},
// Simple delete
{
"@level": "info",
"@message": "module.vpc.test_instance.boop[0]: Plan to delete",
"@message": "module.vpc.test_resource.boop[0]: Plan to delete",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "delete",
"resource": map[string]interface{}{
"addr": `module.vpc.test_instance.boop[0]`,
"addr": `module.vpc.test_resource.boop[0]`,
"implied_provider": "test",
"module": "module.vpc",
"resource": `test_instance.boop[0]`,
"resource": `test_resource.boop[0]`,
"resource_key": float64(0),
"resource_name": "boop",
"resource_type": "test_instance",
"resource_type": "test_resource",
},
},
},
// Delete-then-create is also a replace
{
"@level": "info",
"@message": "test_instance.beep: Plan to replace",
"@message": "test_resource.beep: Plan to replace",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "replace",
"resource": map[string]interface{}{
"addr": `test_instance.beep`,
"addr": `test_resource.beep`,
"implied_provider": "test",
"module": "",
"resource": `test_instance.beep`,
"resource": `test_resource.beep`,
"resource_key": nil,
"resource_name": "beep",
"resource_type": "test_instance",
"resource_type": "test_resource",
},
},
},
// Simple update
{
"@level": "info",
"@message": "module.vpc.test_instance.beep: Plan to update",
"@message": "module.vpc.test_resource.beep: Plan to update",
"@module": "terraform.ui",
"type": "planned_change",
"change": map[string]interface{}{
"action": "update",
"resource": map[string]interface{}{
"addr": `module.vpc.test_instance.beep`,
"addr": `module.vpc.test_resource.beep`,
"implied_provider": "test",
"module": "module.vpc",
"resource": `test_instance.beep`,
"resource": `test_resource.beep`,
"resource_key": nil,
"resource_name": "beep",
"resource_type": "test_instance",
"resource_type": "test_resource",
},
},
},
@ -635,6 +635,134 @@ func TestOperationJSON_plan(t *testing.T) {
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestOperationJSON_planDrift(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}
root := addrs.RootModuleInstance
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
derp := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "derp"}
plan := &plans.Plan{
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{},
},
PrevRunState: states.BuildState(func(state *states.SyncState) {
// Update
state.SetResourceInstanceCurrent(
boop.Instance(addrs.NoKey).Absolute(root),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"bar"}`),
},
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
// Delete
state.SetResourceInstanceCurrent(
beep.Instance(addrs.NoKey).Absolute(root),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"boop"}`),
},
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
// No-op
state.SetResourceInstanceCurrent(
derp.Instance(addrs.NoKey).Absolute(root),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"boop"}`),
},
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
}),
PriorState: states.BuildState(func(state *states.SyncState) {
// Update
state.SetResourceInstanceCurrent(
boop.Instance(addrs.NoKey).Absolute(root),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"baz"}`),
},
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
// Delete
state.SetResourceInstanceCurrent(
beep.Instance(addrs.NoKey).Absolute(root),
nil,
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
// No-op
state.SetResourceInstanceCurrent(
derp.Instance(addrs.NoKey).Absolute(root),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"boop"}`),
},
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
}),
}
v.Plan(plan, testSchemas())
want := []map[string]interface{}{
// Drift detected: delete
{
"@level": "info",
"@message": "test_resource.beep: Drift detected (delete)",
"@module": "terraform.ui",
"type": "resource_drift",
"change": map[string]interface{}{
"action": "delete",
"resource": map[string]interface{}{
"addr": "test_resource.beep",
"implied_provider": "test",
"module": "",
"resource": "test_resource.beep",
"resource_key": nil,
"resource_name": "beep",
"resource_type": "test_resource",
},
},
},
// Drift detected: update
{
"@level": "info",
"@message": "test_resource.boop: Drift detected (update)",
"@module": "terraform.ui",
"type": "resource_drift",
"change": map[string]interface{}{
"action": "update",
"resource": map[string]interface{}{
"addr": "test_resource.boop",
"implied_provider": "test",
"module": "",
"resource": "test_resource.boop",
"resource_key": nil,
"resource_name": "boop",
"resource_type": "test_resource",
},
},
},
// No changes
{
"@level": "info",
"@message": "Plan: 0 to add, 0 to change, 0 to destroy.",
"@module": "terraform.ui",
"type": "change_summary",
"changes": map[string]interface{}{
"operation": "plan",
"add": float64(0),
"change": float64(0),
"remove": float64(0),
},
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}
func TestOperationJSON_plannedChange(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
v := &OperationJSON{view: NewJSONView(NewView(streams))}

View File

@ -211,9 +211,15 @@ func (a *Attribute) decoderSpec(name string) hcldec.Spec {
// belong to their own cty.Object definitions. It is used in other functions
// which themselves handle that recursion.
func listOptionalAttrsFromObject(o *Object) []string {
var ret []string
ret := make([]string, 0)
// This is unlikely to happen outside of tests.
if o == nil {
return ret
}
for name, attr := range o.Attributes {
if attr.Optional == true {
if attr.Optional || attr.Computed {
ret = append(ret, name)
}
}

View File

@ -1,10 +1,12 @@
package configschema
import (
"sort"
"testing"
"github.com/apparentlymart/go-dump/dump"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
@ -885,3 +887,43 @@ func TestAttributeDecoderSpec_panic(t *testing.T) {
attrS.decoderSpec("attr")
t.Errorf("expected panic")
}
func TestListOptionalAttrsFromObject(t *testing.T) {
tests := []struct {
input *Object
want []string
}{
{
nil,
[]string{},
},
{
&Object{},
[]string{},
},
{
&Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
},
},
[]string{"optional", "computed", "optional_computed"},
},
}
for _, test := range tests {
got := listOptionalAttrsFromObject(test.input)
// order is irrelevant
sort.Strings(got)
sort.Strings(test.want)
if diff := cmp.Diff(got, test.want); diff != "" {
t.Fatalf("wrong result: %s\n", diff)
}
}
}

View File

@ -79,7 +79,7 @@ func (o *Object) ImpliedType() cty.Type {
case NestingSet:
return cty.Set(ret)
default: // Should never happen
panic("invalid Nesting")
return cty.EmptyObject
}
}

View File

@ -37,6 +37,7 @@ func TestBlockImpliedType(t *testing.T) {
"optional_computed": {
Type: cty.Map(cty.Bool),
Optional: true,
Computed: true,
},
},
},
@ -132,26 +133,18 @@ func TestObjectImpliedType(t *testing.T) {
nil,
cty.EmptyObject,
},
"empty": {
&Object{},
cty.EmptyObject,
},
"attributes": {
&Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {
Type: cty.String,
Optional: true,
},
"required": {
Type: cty.Number,
Required: true,
},
"computed": {
Type: cty.List(cty.Bool),
Computed: true,
},
"optional_computed": {
Type: cty.Map(cty.Bool),
Optional: true,
},
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
},
},
cty.ObjectWithOptionalAttrs(
@ -161,7 +154,7 @@ func TestObjectImpliedType(t *testing.T) {
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
},
[]string{"optional", "optional_computed"},
[]string{"optional", "computed", "optional_computed"},
),
},
"nested attributes": {
@ -172,21 +165,42 @@ func TestObjectImpliedType(t *testing.T) {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {
Type: cty.String,
Optional: true,
},
"required": {
Type: cty.Number,
Required: true,
},
"computed": {
Type: cty.List(cty.Bool),
Computed: true,
},
"optional_computed": {
Type: cty.Map(cty.Bool),
Optional: true,
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
},
},
Optional: true,
},
},
},
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"nested_type": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
}, []string{"optional", "computed", "optional_computed"}),
}, []string{"nested_type"}),
},
"nested object-type attributes": {
&Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"nested_type": {
NestedType: &Object{
Nesting: NestingSingle,
Attributes: map[string]*Attribute{
"optional": {Type: cty.String, Optional: true},
"required": {Type: cty.Number, Required: true},
"computed": {Type: cty.List(cty.Bool), Computed: true},
"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
"object": {
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"optional": cty.String,
"required": cty.Number,
}, []string{"optional"}),
},
},
},
@ -200,7 +214,8 @@ func TestObjectImpliedType(t *testing.T) {
"required": cty.Number,
"computed": cty.List(cty.Bool),
"optional_computed": cty.Map(cty.Bool),
}, []string{"optional", "optional_computed"}),
"object": cty.ObjectWithOptionalAttrs(map[string]cty.Type{"optional": cty.String, "required": cty.Number}, []string{"optional"}),
}, []string{"optional", "computed", "optional_computed"}),
}, []string{"nested_type"}),
},
"NestingList": {

View File

@ -55,7 +55,7 @@ func init() {
configureRequestTimeout()
}
var SupportedPluginProtocols = MustParseVersionConstraints("~> 5")
var SupportedPluginProtocols = MustParseVersionConstraints(">= 5, <7")
// registryClient is a client for the provider registry protocol that is
// specialized only for the needs of this package. It's not intended as a

View File

@ -218,6 +218,10 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
resp.Write([]byte(`{"versions":[{"version":"1.0.0","protocols":["0.1"]}]}`))
case "weaksauce/protocol-six":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
resp.Write([]byte(`{"versions":[{"version":"1.0.0","protocols":["6.0"]}]}`))
case "weaksauce/no-versions":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
@ -412,6 +416,12 @@ func TestFindClosestProtocolCompatibleVersion(t *testing.T) {
versions.Unspecified,
``,
},
"provider protocol six": {
addrs.MustParseProviderSourceString("example.com/weaksauce/protocol-six"),
MustParseVersion("1.0.0"),
MustParseVersion("1.0.0"),
``,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {

View File

@ -1457,7 +1457,7 @@ func TestProposedNew(t *testing.T) {
"computed": cty.String,
"optional_computed": cty.String,
"required": cty.String,
}, []string{"optional", "optional_computed"}),
}, []string{"computed", "optional", "optional_computed"}),
}))),
}),
},

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -1,6 +1,7 @@
package states
import (
"fmt"
"sort"
"github.com/zclconf/go-cty/cty"
@ -296,3 +297,245 @@ func (s *State) SyncWrapper() *SyncState {
state: s,
}
}
// MoveAbsResource moves the given src AbsResource's current state to the new
// dst address. This will panic if the src AbsResource does not exist in state,
// or if there is already a resource at the dst address. It is the caller's
// responsibility to verify the validity of the move (for example, that the src
// and dst are compatible types).
func (s *State) MoveAbsResource(src, dst addrs.AbsResource) {
// verify that the src address exists and the dst address does not
rs := s.Resource(src)
if rs == nil {
panic(fmt.Sprintf("no state for src address %s", src.String()))
}
ds := s.Resource(dst)
if ds != nil {
panic(fmt.Sprintf("dst resource %s already exists", dst.String()))
}
ms := s.Module(src.Module)
ms.RemoveResource(src.Resource)
// Remove the module if it is empty (and not root) after removing the
// resource.
if !ms.Addr.IsRoot() && ms.empty() {
s.RemoveModule(src.Module)
}
// Update the address before adding it to the state
rs.Addr = dst
s.EnsureModule(dst.Module).Resources[dst.Resource.String()] = rs
}
// MaybeMoveAbsResource moves the given src AbsResource's current state to the
// new dst address. This function will succeed if both the src address does not
// exist in state and the dst address does; the return value indicates whether
// or not the move occured. This function will panic if either the src does not
// exist or the dst does exist (but not both).
func (s *State) MaybeMoveAbsResource(src, dst addrs.AbsResource) bool {
// Get the source and destinatation addresses from state.
rs := s.Resource(src)
ds := s.Resource(dst)
// Normal case: the src exists in state, dst does not
if rs != nil && ds == nil {
s.MoveAbsResource(src, dst)
return true
}
if rs == nil && ds != nil {
// The source is not in state, the destination is. This is not
// guaranteed to be idempotent since we aren't tracking exact moves, but
// it's useful information for the caller.
return false
} else {
panic("invalid move")
}
}
// MoveAbsResourceInstance moves the given src AbsResourceInstance's current state to
// the new dst address. This will panic if the src AbsResourceInstance does not
// exist in state, or if there is already a resource at the dst address. It is
// the caller's responsibility to verify the validity of the move (for example,
// that the src and dst are compatible types).
func (s *State) MoveAbsResourceInstance(src, dst addrs.AbsResourceInstance) {
srcInstanceState := s.ResourceInstance(src)
if srcInstanceState == nil {
panic(fmt.Sprintf("no state for src address %s", src.String()))
}
dstInstanceState := s.ResourceInstance(dst)
if dstInstanceState != nil {
panic(fmt.Sprintf("dst resource %s already exists", dst.String()))
}
srcResourceState := s.Resource(src.ContainingResource())
srcProviderAddr := srcResourceState.ProviderConfig
dstResourceAddr := dst.ContainingResource()
// Remove the source resource instance from the module's state, and then the
// module if empty.
ms := s.Module(src.Module)
ms.ForgetResourceInstanceAll(src.Resource)
if !ms.Addr.IsRoot() && ms.empty() {
s.RemoveModule(src.Module)
}
dstModule := s.EnsureModule(dst.Module)
// See if there is already a resource we can add this instance to.
dstResourceState := s.Resource(dstResourceAddr)
if dstResourceState == nil {
// If we're moving to an address without an index then that
// suggests the user's intent is to establish both the
// resource and the instance at the same time (since the
// address covers both). If there's an index in the
// target then allow creating the new instance here.
dstModule.SetResourceProvider(
dstResourceAddr.Resource,
srcProviderAddr, // in this case, we bring the provider along as if we were moving the whole resource
)
dstResourceState = dstModule.Resource(dstResourceAddr.Resource)
}
dstResourceState.Instances[dst.Resource.Key] = srcInstanceState
}
// MaybeMoveAbsResourceInstance moves the given src AbsResourceInstance's
// current state to the new dst address. This function will succeed if both the
// src address does not exist in state and the dst address does; the return
// value indicates whether or not the move occured. This function will panic if
// either the src does not exist or the dst does exist (but not both).
func (s *State) MaybeMoveAbsResourceInstance(src, dst addrs.AbsResourceInstance) bool {
// get the src and dst resource instances from state
rs := s.ResourceInstance(src)
ds := s.ResourceInstance(dst)
// Normal case: the src exists in state, dst does not
if rs != nil && ds == nil {
s.MoveAbsResourceInstance(src, dst)
return true
}
if rs == nil && ds != nil {
// The source is not in state, the destination is. This is not
// guaranteed to be idempotent since we aren't tracking exact moves, but
// it's useful information.
return false
} else {
panic("invalid move")
}
}
// MoveModuleInstance moves the given src ModuleInstance's current state to the
// new dst address. This will panic if the src ModuleInstance does not
// exist in state, or if there is already a resource at the dst address. It is
// the caller's responsibility to verify the validity of the move.
func (s *State) MoveModuleInstance(src, dst addrs.ModuleInstance) {
if src.IsRoot() || dst.IsRoot() {
panic("cannot move to or from root module")
}
srcMod := s.Module(src)
if srcMod == nil {
panic(fmt.Sprintf("no state for src module %s", src.String()))
}
dstMod := s.Module(dst)
if dstMod != nil {
panic(fmt.Sprintf("dst module %s already exists in state", dst.String()))
}
s.RemoveModule(src)
srcMod.Addr = dst
s.EnsureModule(dst)
s.Modules[dst.String()] = srcMod
// Update any Resource's addresses.
if srcMod.Resources != nil {
for _, r := range srcMod.Resources {
r.Addr.Module = dst
}
}
// Update any OutputValues's addresses.
if srcMod.OutputValues != nil {
for _, ov := range srcMod.OutputValues {
ov.Addr.Module = dst
}
}
}
// MaybeMoveModuleInstance moves the given src ModuleInstance's current state to
// the new dst address. This function will succeed if both the src address does
// not exist in state and the dst address does; the return value indicates
// whether or not the move occured. This function will panic if either the src
// does not exist or the dst does exist (but not both).
func (s *State) MaybeMoveModuleInstance(src, dst addrs.ModuleInstance) bool {
if src.IsRoot() || dst.IsRoot() {
panic("cannot move to or from root module")
}
srcMod := s.Module(src)
dstMod := s.Module(dst)
// Normal case: the src exists in state, dst does not
if srcMod != nil && dstMod == nil {
s.MoveModuleInstance(src, dst)
return true
}
if srcMod == nil || src.IsRoot() && dstMod != nil {
// The source is not in state, the destination is. This is not
// guaranteed to be idempotent since we aren't tracking exact moves, but
// it's useful information.
return false
} else {
panic("invalid move")
}
}
// MoveModule takes a source and destination addrs.Module address, and moves all
// state Modules which are contained by the src address to the new address.
func (s *State) MoveModule(src, dst addrs.AbsModuleCall) {
if src.Module.IsRoot() || dst.Module.IsRoot() {
panic("cannot move to or from root module")
}
// Modules only exist as ModuleInstances in state, so we need to check each
// state Module and see if it is contained by the src address to get a full
// list of modules to move.
var srcMIs []*Module
for _, module := range s.Modules {
if !module.Addr.IsRoot() {
if src.Module.TargetContains(module.Addr) {
srcMIs = append(srcMIs, module)
}
}
}
if len(srcMIs) == 0 {
panic(fmt.Sprintf("no matching module instances found for src module %s", src.String()))
}
for _, ms := range srcMIs {
newInst := make(addrs.ModuleInstance, len(ms.Addr))
copy(newInst, ms.Addr)
if ms.Addr.IsDeclaredByCall(src) {
// Easy case: we just need to update the last step with the new name
newInst[len(newInst)-1].Name = dst.Call.Name
} else {
// Trickier: this Module is a submodule. we need to find and update
// only that appropriate step
for s := range newInst {
if newInst[s].Name == src.Call.Name {
newInst[s].Name = dst.Call.Name
}
}
}
s.MoveModuleInstance(ms.Addr, newInst)
}
}

View File

@ -1,6 +1,7 @@
package states
import (
"fmt"
"reflect"
"testing"
@ -292,3 +293,597 @@ func TestStateDeepCopy(t *testing.T) {
t.Fatalf("\nexpected:\n%q\ngot:\n%q\n", state, stateCopy)
}
}
func TestState_MoveAbsResource(t *testing.T) {
// Set up a starter state for the embedded tests, which should start from a copy of this state.
state := NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.IntKey(0)),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance)
t.Run("basic move", func(t *testing.T) {
s := state.DeepCopy()
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance)
s.MoveAbsResource(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
if len(s.RootModule().Resources) != 1 {
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources))
}
got := s.Resource(dst)
if got.Addr.Resource != dst.Resource {
t.Fatalf("dst resource not in state")
}
})
t.Run("move to new module", func(t *testing.T) {
s := state.DeepCopy()
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("one"))
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(dstModule)
s.MoveAbsResource(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
if s.Module(dstModule) == nil {
t.Fatalf("child module %s not in state", dstModule.String())
}
if len(s.Module(dstModule).Resources) != 1 {
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources))
}
got := s.Resource(dst)
if got.Addr.Resource != dst.Resource {
t.Fatalf("dst resource not in state")
}
})
t.Run("from a child module to root", func(t *testing.T) {
s := state.DeepCopy()
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
cm := s.EnsureModule(srcModule)
cm.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "child",
}.Instance(addrs.IntKey(0)), // Moving the AbsResouce moves all instances
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
cm.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "child",
}.Instance(addrs.IntKey(1)), // Moving the AbsResouce moves all instances
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(addrs.RootModuleInstance)
s.MoveAbsResource(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
// The child module should have been removed after removing its only resource
if s.Module(srcModule) != nil {
t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
}
if len(s.RootModule().Resources) != 2 {
t.Fatalf("wrong number of resources in state; expected 2, found %d", len(s.RootModule().Resources))
}
if len(s.Resource(dst).Instances) != 2 {
t.Fatalf("wrong number of resource instances for dst, got %d expected 2", len(s.Resource(dst).Instances))
}
got := s.Resource(dst)
if got.Addr.Resource != dst.Resource {
t.Fatalf("dst resource not in state")
}
})
t.Run("module to new module", func(t *testing.T) {
s := NewState()
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists"))
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new"))
cm := s.EnsureModule(srcModule)
cm.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "child",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule)
s.MoveAbsResource(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
// The child module should have been removed after removing its only resource
if s.Module(srcModule) != nil {
t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
}
gotMod := s.Module(dstModule)
if len(gotMod.Resources) != 1 {
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources))
}
got := s.Resource(dst)
if got.Addr.Resource != dst.Resource {
t.Fatalf("dst resource not in state")
}
})
t.Run("module to new module", func(t *testing.T) {
s := NewState()
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists"))
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new"))
cm := s.EnsureModule(srcModule)
cm.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "child",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule)
s.MoveAbsResource(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
// The child module should have been removed after removing its only resource
if s.Module(srcModule) != nil {
t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
}
gotMod := s.Module(dstModule)
if len(gotMod.Resources) != 1 {
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources))
}
got := s.Resource(dst)
if got.Addr.Resource != dst.Resource {
t.Fatalf("dst resource not in state")
}
})
}
func TestState_MaybeMoveAbsResource(t *testing.T) {
state := NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.IntKey(0)),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance)
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance)
// First move, success
t.Run("first move", func(t *testing.T) {
moved := state.MaybeMoveAbsResource(src, dst)
if !moved {
t.Fatal("wrong result")
}
})
// Trying to move a resource that doesn't exist in state to a resource which does exist should be a noop.
t.Run("noop", func(t *testing.T) {
moved := state.MaybeMoveAbsResource(src, dst)
if moved {
t.Fatal("wrong result")
}
})
}
func TestState_MoveAbsResourceInstance(t *testing.T) {
state := NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
// src resource from the state above
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
t.Run("resource to resource instance", func(t *testing.T) {
s := state.DeepCopy()
// For a little extra fun, move a resource to a resource instance: test_thing.foo to test_thing.foo[1]
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance)
s.MoveAbsResourceInstance(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
if len(s.RootModule().Resources) != 1 {
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources))
}
got := s.ResourceInstance(dst)
if got == nil {
t.Fatalf("dst resource not in state")
}
})
t.Run("move to new module", func(t *testing.T) {
s := state.DeepCopy()
// test_thing.foo to module.kinder.test_thing.foo["baz"]
dstModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(dstModule)
s.MoveAbsResourceInstance(src, dst)
if s.Empty() {
t.Fatal("unexpected empty state")
}
if s.Module(dstModule) == nil {
t.Fatalf("child module %s not in state", dstModule.String())
}
if len(s.Module(dstModule).Resources) != 1 {
t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources))
}
got := s.ResourceInstance(dst)
if got == nil {
t.Fatalf("dst resource not in state")
}
})
}
func TestState_MaybeMoveAbsResourceInstance(t *testing.T) {
state := NewState()
rootModule := state.RootModule()
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
// For a little extra fun, let's go from a resource to a resource instance: test_thing.foo to test_thing.bar[1]
src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance)
// First move, success
t.Run("first move", func(t *testing.T) {
moved := state.MaybeMoveAbsResourceInstance(src, dst)
if !moved {
t.Fatal("wrong result")
}
got := state.ResourceInstance(dst)
if got == nil {
t.Fatal("destination resource instance not in state")
}
})
// Moving a resource instance that doesn't exist in state to a resource which does exist should be a noop.
t.Run("noop", func(t *testing.T) {
moved := state.MaybeMoveAbsResourceInstance(src, dst)
if moved {
t.Fatal("wrong result")
}
})
}
func TestState_MoveModuleInstance(t *testing.T) {
state := NewState()
srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
m := state.EnsureModule(srcModule)
m.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
dstModule := addrs.RootModuleInstance.Child("child", addrs.IntKey(3))
state.MoveModuleInstance(srcModule, dstModule)
// srcModule should have been removed, dstModule should exist and have one resource
if len(state.Modules) != 2 { // kinder[3] and root
t.Fatalf("wrong number of modules in state. Expected 2, got %d", len(state.Modules))
}
got := state.Module(dstModule)
if got == nil {
t.Fatal("dstModule not found")
}
gone := state.Module(srcModule)
if gone != nil {
t.Fatal("srcModule not removed from state")
}
r := got.Resource(mustAbsResourceAddr("test_thing.foo").Resource)
if r.Addr.Module.String() != dstModule.String() {
fmt.Println(r.Addr.Module.String())
t.Fatal("resource address was not updated")
}
}
func TestState_MaybeMoveModuleInstance(t *testing.T) {
state := NewState()
src := addrs.RootModuleInstance.Child("child", addrs.StringKey("a"))
cm := state.EnsureModule(src)
cm.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
dst := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("b"))
// First move, success
t.Run("first move", func(t *testing.T) {
moved := state.MaybeMoveModuleInstance(src, dst)
if !moved {
t.Fatal("wrong result")
}
})
// Second move, should be a noop
t.Run("noop", func(t *testing.T) {
moved := state.MaybeMoveModuleInstance(src, dst)
if moved {
t.Fatal("wrong result")
}
})
}
func TestState_MoveModule(t *testing.T) {
// For this test, add two module instances (kinder and kinder["a"]).
// MoveModule(kinder) should move both instances.
state := NewState() // starter state, should be copied by the subtests.
srcModule := addrs.RootModule.Child("kinder")
m := state.EnsureModule(srcModule.UnkeyedInstanceShim())
m.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
moduleInstance := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("a"))
mi := state.EnsureModule(moduleInstance)
mi.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
_, mc := srcModule.Call()
src := mc.Absolute(addrs.RootModuleInstance.Child("kinder", addrs.NoKey))
t.Run("basic", func(t *testing.T) {
s := state.DeepCopy()
_, dstMC := addrs.RootModule.Child("child").Call()
dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
s.MoveModule(src, dst)
// srcModule should have been removed, dstModule should exist and have one resource
if len(s.Modules) != 3 { // child, child["a"] and root
t.Fatalf("wrong number of modules in state. Expected 3, got %d", len(s.Modules))
}
got := s.Module(dst.Module)
if got == nil {
t.Fatal("dstModule not found")
}
got = s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a")))
if got == nil {
t.Fatal("dstModule instance \"a\" not found")
}
gone := s.Module(srcModule.UnkeyedInstanceShim())
if gone != nil {
t.Fatal("srcModule not removed from state")
}
})
t.Run("nested modules", func(t *testing.T) {
s := state.DeepCopy()
// add a child module to module.kinder
mi := mustParseModuleInstanceStr(`module.kinder.module.grand[1]`)
m := s.EnsureModule(mi)
m.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&ResourceInstanceObjectSrc{
Status: ObjectReady,
SchemaVersion: 1,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
_, dstMC := addrs.RootModule.Child("child").Call()
dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
s.MoveModule(src, dst)
moved := s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a")))
if moved == nil {
t.Fatal("dstModule not found")
}
// The nested module's relative address should also have been updated
nested := s.Module(mustParseModuleInstanceStr(`module.child.module.grand[1]`))
if nested == nil {
t.Fatal("nested child module of src wasn't moved")
}
})
}
func mustParseModuleInstanceStr(str string) addrs.ModuleInstance {
addr, diags := addrs.ParseModuleInstanceStr(str)
if diags.HasErrors() {
panic(diags.Err())
}
return addr
}
func mustAbsResourceAddr(s string) addrs.AbsResource {
addr, diags := addrs.ParseAbsResourceStr(s)
if diags.HasErrors() {
panic(diags.Err())
}
return addr
}

View File

@ -554,3 +554,45 @@ func (s *SyncState) maybePruneModule(addr addrs.ModuleInstance) {
s.state.RemoveModule(addr)
}
}
func (s *SyncState) MoveAbsResource(src, dst addrs.AbsResource) {
s.lock.Lock()
defer s.lock.Unlock()
s.state.MoveAbsResource(src, dst)
}
func (s *SyncState) MaybeMoveAbsResource(src, dst addrs.AbsResource) bool {
s.lock.Lock()
defer s.lock.Unlock()
return s.state.MaybeMoveAbsResource(src, dst)
}
func (s *SyncState) MoveResourceInstance(src, dst addrs.AbsResourceInstance) {
s.lock.Lock()
defer s.lock.Unlock()
s.state.MoveAbsResourceInstance(src, dst)
}
func (s *SyncState) MaybeMoveResourceInstance(src, dst addrs.AbsResourceInstance) bool {
s.lock.Lock()
defer s.lock.Unlock()
return s.state.MaybeMoveAbsResourceInstance(src, dst)
}
func (s *SyncState) MoveModuleInstance(src, dst addrs.ModuleInstance) {
s.lock.Lock()
defer s.lock.Unlock()
s.state.MoveModuleInstance(src, dst)
}
func (s *SyncState) MaybeMoveModuleInstance(src, dst addrs.ModuleInstance) bool {
s.lock.Lock()
defer s.lock.Unlock()
return s.state.MaybeMoveModuleInstance(src, dst)
}

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
@ -608,6 +609,33 @@ The -target option is not for routine use, and is provided only for exceptional
))
}
moveStmts := refactoring.FindMoveStatements(c.config)
if len(moveStmts) != 0 {
// TEMP: we haven't fully implemented moving yet, so we'll just
// reject it outright for now to reduce confusion.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Moves are not yet supported",
"There is currently no handling of \"moved\" blocks in the configuration.",
))
}
moveResults := refactoring.ApplyMoves(moveStmts, c.prevRunState)
if len(c.targets) > 0 {
for _, result := range moveResults {
matchesTarget := false
for _, targetAddr := range c.targets {
if targetAddr.TargetContains(result.From) {
matchesTarget = true
break
}
}
if !matchesTarget {
// TODO: Return an error stating that a targeted plan is
// only valid if it includes this address that was moved.
}
}
}
var plan *plans.Plan
var planDiags tfdiags.Diagnostics
switch c.planMode {
@ -625,6 +653,10 @@ The -target option is not for routine use, and is provided only for exceptional
return nil, diags
}
// TODO: Call refactoring.ValidateMoves, but need to figure out how to
// get hold of the plan's expander here, or somehow otherwise export
// the necessary subset of its data for ValidateMoves to do its work.
// convert the variables into the format expected for the plan
varVals := make(map[string]plans.DynamicValue, len(c.variables))
for k, iv := range c.variables {

View File

@ -28,3 +28,19 @@ You should preview all of your changes locally before creating a pull request. T
1. Navigate into your local `terraform` top-level directory and run `make website`.
2. Open `http://localhost:4567` in your web browser. While the preview is running, you can edit pages and Middleman will automatically rebuild them.
3. When you're done with the preview, press `ctrl-C` in your terminal to stop the server.
## Deploying Changes
Merge the PR to main. The changes will appear in the next major Terraform release.
If you need your changes to be deployed sooner, cherry-pick them to:
- the current release branch (e.g. `v1.0`) and push. They will be deployed in the next minor version release (once every two weeks).
- the `stable-website` branch and push. They will be included in the next site deploy (see below). Note that the release process resets `stable-website` to match the release tag, removing any additional commits. So, we recommend always cherry-picking to the version branch first and then to `stable-website` when needed.
### Deployment
Currently, HashiCorp uses a CircleCI job to deploy the [terraform.io](terraform.io) site. This job can be run manually by many people within HashiCorp, and also runs automatically whenever a user in the HashiCorp GitHub org merges changes to master in the `terraform-website` repository.
New commits in this repository don't automatically deploy the [terraform.io][] site, but an unrelated site deploy will usually happen within a day. If you can't wait that long, you can do a manual CircleCI build or ask someone in the #proj-terraform-docs channel to do so:
- Log in to circleci.com, and make sure you're viewing the HashiCorp organization.
- Go to the terraform-website project's list of workflows.
- Find the most recent "website-deploy" workflow, and click the "Rerun workflow from start" button (which looks like a refresh button with a numeral "1" inside).

View File

@ -19,8 +19,8 @@ We refer to the `terraform` command line tool as "Terraform CLI" elsewhere
in the documentation. This terminology is often used to distinguish it from
other components you might use in the Terraform product family, such as
[Terraform Cloud](/docs/cloud/) or
the various [Terraform providers](/docs/providers/), which are developed and
released separately from Terraform CLI.
the various [Terraform providers](/docs/language/providers/index.html), which
are developed and released separately from Terraform CLI.
To view a list of the commands available in your current Terraform version,
run `terraform` with no additional arguments:

View File

@ -33,6 +33,10 @@ The command-line flags are all optional. The list of available flags are:
* `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
Ignored when [remote state](/docs/language/state/remote.html) is used.
-> **Note:** When using the `-json` or `-raw` command-line flag, any sensitive
values in Terraform state will be displayed in plain text. For more information,
see [Sensitive Data in State](/docs/language/state/sensitive-data.html).
## Examples
These examples assume the following Terraform output snippet.

View File

@ -358,6 +358,10 @@ Terraform will never itself delete a plugin from the plugin cache once it has
been placed there. Over time, as plugins are upgraded, the cache directory may
grow to contain several unused versions which you must delete manually.
-> **Note:** The plugin cache directory is not guaranteed to be concurrency
safe. The provider installer's behavior in environments with multiple `terraform
init` calls is undefined.
### Development Overrides for Provider Developers
-> **Note:** Development overrides work only in Terraform v0.14 and later.

View File

@ -62,6 +62,7 @@ export TF_VAR_amap='{ foo = "bar", baz = "qux" }'
For more on how to use `TF_VAR_name` in context, check out the section on [Variable Configuration](/docs/language/values/variables.html).
## TF_CLI_ARGS and TF_CLI_ARGS_name
<a id="tf-cli-args"></a>
The value of `TF_CLI_ARGS` will specify additional arguments to the
command-line. This allows easier automation in CI environments as well as

View File

@ -64,8 +64,8 @@ parameter) and `NAME` (second parameter). The combination of the type
and name must be unique.
Within the block (the `{ }`) is configuration for the data instance. The
configuration is dependent on the type, and is documented for each
data source in the [providers section](/docs/providers/index.html).
configuration is dependent on the type; consult the [provider's documentation] (https://registry.terraform.io/browse/providers) for
details.
Each data instance will export one or more attributes, which can be
interpolated into other resources using variables of the form

View File

@ -48,8 +48,8 @@ header. For example, `provider "aws"` above is a configuration for the
`aws` provider.
Within the block body (between `{ }`) is configuration for the provider.
The configuration is dependent on the type, and is documented
[for each provider](/docs/providers/index.html).
The configuration is dependent on the type. Consult the [provider's documentation](https://registry.terraform.io/browse/providers) for details.
in each provider's documentation.
The arguments `alias` and `version`, if present, are special arguments
handled by Terraform Core for their respective features described above. All

View File

@ -41,9 +41,9 @@ parameter) and `NAME` (second parameter). The combination of the type
and name must be unique.
Within the block (the `{ }`) is configuration for the resource. The
configuration is dependent on the type, and is documented for each
resource type in the
[providers section](/docs/providers/index.html).
configuration is dependent on the type. Consult the [provider's documentation](https://registry.terraform.io/browse/providers) for details.
details.
### Meta-parameters

View File

@ -451,6 +451,8 @@ Each unevaluated expression in the configuration is represented with an `<expres
}
```
-> **Note:** Expressions in `dynamic` blocks are not included in the configuration representation.
### Block Expressions Representation
In some cases, it is the entire content of a block (possibly after certain special arguments have already been handled and removed) that must be represented. For that, we have an `<block-expressions-representation>` structure:

View File

@ -54,6 +54,7 @@ The following message types are supported:
### Operation Results
- `resource_drift`: describes a detected change to a single resource made outside of Terraform
- `planned_change`: describes a planned change to a single resource
- `change_summary`: summary of all planned or applied changes
- `outputs`: list of all root module outputs
@ -85,6 +86,39 @@ A machine-readable UI command output will always begin with a `version` message.
}
```
## Resource Drift
If drift is detected during planning, Terraform will emit a `resource_drift` message for each resource which has changed outside of Terraform. This message has an embedded `change` object with the following keys:
- `resource`: object describing the address of the resource to be changed; see [resource object](#resource-object) below for details
- `action`: the action planned to be taken for the resource. Values: `update`, `delete`.
This message does not include details about the exact changes which caused the change to be planned. That information is available in [the JSON plan output](./json-format.html).
### Example
```json
{
"@level": "info",
"@message": "random_pet.animal: Drift detected (update)",
"@module": "terraform.ui",
"@timestamp": "2021-05-25T13:32:41.705503-04:00",
"change": {
"resource": {
"addr": "random_pet.animal",
"module": "",
"resource": "random_pet.animal",
"implied_provider": "random",
"resource_type": "random_pet",
"resource_name": "animal",
"resource_key": null
},
"action": "update"
},
"type": "resource_drift"
}
```
## Planned Change
At the end of a plan or before an apply, Terraform will emit a `planned_change` message for each resource which has changes to apply. This message has an embedded `change` object with the following keys:

View File

@ -172,8 +172,10 @@ block label) and _name_ (second block label). The combination of the type
and name must be unique.
Within the block (the `{ }`) is configuration for the data instance. The
configuration is dependent on the type, and is documented for each
data source in the [providers section](/docs/providers/index.html).
configuration is dependent on the type; as with
[resources](/docs/language/resources/index.html), each provider on the
[Terraform Registry](https://registry.terraform.io/browse/providers) has its own
documentation for configuring and using the data types it provides.
Each data instance will export one or more attributes, which can be
used in other resources as reference expressions of the form

View File

@ -26,6 +26,10 @@ Supported pattern matches:
- `[CLASS]` - matches any single non-separator character inside a class of characters (see below)
- `[^CLASS]` - matches any single non-separator character outside a class of characters (see below)
Note that the doublestar (`**`) must appear as a path component by itself. A
pattern such as /path** is invalid and will be treated the same as /path*, but
/path*/** should achieve the desired result.
Character classes support the following:
- `[abc]` - matches any single character within the set

View File

@ -37,6 +37,21 @@ The [Terraform Registry](https://registry.terraform.io/browse/providers)
is the main directory of publicly available Terraform providers, and hosts
providers for most major infrastructure platforms.
## Provider Documentation
Each provider has its own documentation, describing its resource
types and their arguments.
The [Terraform Registry](https://registry.terraform.io/browse/providers)
includes documentation for a wide range of providers developed by HashiCorp, third-party vendors, and our Terraform community. Use the
"Documentation" link in a provider's header to browse its documentation.
Provider documentation in the Registry is versioned; you can use the version
menu in the header to change which version you're viewing.
For details about writing, generating, and previewing provider documentation,
see the [provider publishing documentation](/docs/registry/providers/docs.html).
## How to Use Providers
To use resources from a given provider, you need to include some information

View File

@ -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).

View File

@ -167,11 +167,6 @@
<li>
<a href="/docs/language/dependency-lock.html">Dependency Lock File</a>
</li>
<li>
<a href="/docs/providers/index.html">Provider Documentation</a>
<!-- To be deleted when remaining 8 providers are gone, doesn't need new URL -->
</li>
</ul>
</li><!-- providers -->