json-output: Config-driven move support in JSON UI
Add previous address information to the `planned_change` and `resource_drift` messages for the streaming JSON UI output of plan and apply operations. Here we also add a "move" action value to the `change` object of these messages, to represent a move-only operation. As part of this work we also simplify this code to use the plan's DriftedResources values instead of recomputing the drift from state.
This commit is contained in:
parent
78c4a8c461
commit
b59b057591
|
@ -12,12 +12,20 @@ func NewResourceInstanceChange(change *plans.ResourceInstanceChangeSrc) *Resourc
|
||||||
Action: changeAction(change.Action),
|
Action: changeAction(change.Action),
|
||||||
Reason: changeReason(change.ActionReason),
|
Reason: changeReason(change.ActionReason),
|
||||||
}
|
}
|
||||||
|
if !change.Addr.Equal(change.PrevRunAddr) {
|
||||||
|
if c.Action == ActionNoOp {
|
||||||
|
c.Action = ActionMove
|
||||||
|
}
|
||||||
|
pr := newResourceAddr(change.PrevRunAddr)
|
||||||
|
c.PreviousResource = &pr
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceInstanceChange struct {
|
type ResourceInstanceChange struct {
|
||||||
Resource ResourceAddr `json:"resource"`
|
Resource ResourceAddr `json:"resource"`
|
||||||
|
PreviousResource *ResourceAddr `json:"previous_resource,omitempty"`
|
||||||
Action ChangeAction `json:"action"`
|
Action ChangeAction `json:"action"`
|
||||||
Reason ChangeReason `json:"reason,omitempty"`
|
Reason ChangeReason `json:"reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -30,6 +38,7 @@ type ChangeAction string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionNoOp ChangeAction = "noop"
|
ActionNoOp ChangeAction = "noop"
|
||||||
|
ActionMove ChangeAction = "move"
|
||||||
ActionCreate ChangeAction = "create"
|
ActionCreate ChangeAction = "create"
|
||||||
ActionRead ChangeAction = "read"
|
ActionRead ChangeAction = "read"
|
||||||
ActionUpdate ChangeAction = "update"
|
ActionUpdate ChangeAction = "update"
|
||||||
|
|
|
@ -112,6 +112,7 @@ func TestJSONView_PlannedChange(t *testing.T) {
|
||||||
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
|
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
|
||||||
cs := &plans.ResourceInstanceChangeSrc{
|
cs := &plans.ResourceInstanceChangeSrc{
|
||||||
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
||||||
|
PrevRunAddr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
||||||
ChangeSrc: plans.ChangeSrc{
|
ChangeSrc: plans.ChangeSrc{
|
||||||
Action: plans.Create,
|
Action: plans.Create,
|
||||||
},
|
},
|
||||||
|
@ -152,6 +153,7 @@ func TestJSONView_ResourceDrift(t *testing.T) {
|
||||||
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
|
managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"}
|
||||||
cs := &plans.ResourceInstanceChangeSrc{
|
cs := &plans.ResourceInstanceChangeSrc{
|
||||||
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
||||||
|
PrevRunAddr: managed.Instance(addrs.StringKey("boop")).Absolute(foo),
|
||||||
ChangeSrc: plans.ChangeSrc{
|
ChangeSrc: plans.ChangeSrc{
|
||||||
Action: plans.Update,
|
Action: plans.Update,
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,6 @@ package views
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
@ -11,11 +10,9 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/command/format"
|
"github.com/hashicorp/terraform/internal/command/format"
|
||||||
"github.com/hashicorp/terraform/internal/command/views/json"
|
"github.com/hashicorp/terraform/internal/command/views/json"
|
||||||
"github.com/hashicorp/terraform/internal/plans"
|
"github.com/hashicorp/terraform/internal/plans"
|
||||||
"github.com/hashicorp/terraform/internal/states"
|
|
||||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||||
"github.com/hashicorp/terraform/internal/terraform"
|
"github.com/hashicorp/terraform/internal/terraform"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Operation interface {
|
type Operation interface {
|
||||||
|
@ -163,10 +160,14 @@ func (v *OperationJSON) EmergencyDumpState(stateFile *statefile.File) error {
|
||||||
// Log a change summary and a series of "planned" messages for the changes in
|
// Log a change summary and a series of "planned" messages for the changes in
|
||||||
// the plan.
|
// the plan.
|
||||||
func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||||
if err := v.resourceDrift(plan.PrevRunState, plan.PriorState, schemas); err != nil {
|
for _, dr := range plan.DriftedResources {
|
||||||
var diags tfdiags.Diagnostics
|
// In refresh-only mode, we output all resources marked as drifted,
|
||||||
diags = diags.Append(err)
|
// including those which have moved without other changes. In other plan
|
||||||
v.Diagnostics(diags)
|
// modes, move-only changes will be included in the planned changes, so
|
||||||
|
// we skip them here.
|
||||||
|
if dr.Action != plans.NoOp || plan.UIMode == plans.RefreshOnlyMode {
|
||||||
|
v.view.ResourceDrift(json.NewResourceInstanceChange(dr))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cs := &json.ChangeSummary{
|
cs := &json.ChangeSummary{
|
||||||
|
@ -189,7 +190,7 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||||
cs.Remove++
|
cs.Remove++
|
||||||
}
|
}
|
||||||
|
|
||||||
if change.Action != plans.NoOp {
|
if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) {
|
||||||
v.view.PlannedChange(json.NewResourceInstanceChange(change))
|
v.view.PlannedChange(json.NewResourceInstanceChange(change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,92 +209,6 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *OperationJSON) resourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error {
|
|
||||||
if newState.ManagedResourcesEqual(oldState) {
|
|
||||||
// Nothing to do, because we only detect and report drift for managed
|
|
||||||
// resource instances.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var changes []*json.ResourceInstanceChange
|
|
||||||
for _, ms := range oldState.Modules {
|
|
||||||
for _, rs := range ms.Resources {
|
|
||||||
if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
|
|
||||||
// Drift reporting is only for managed resources
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := rs.ProviderConfig.Provider
|
|
||||||
for key, oldIS := range rs.Instances {
|
|
||||||
if oldIS.Current == nil {
|
|
||||||
// Not interested in instances that only have deposed objects
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addr := rs.Addr.Instance(key)
|
|
||||||
newIS := newState.ResourceInstance(addr)
|
|
||||||
|
|
||||||
schema, _ := schemas.ResourceTypeConfig(
|
|
||||||
provider,
|
|
||||||
addr.Resource.Resource.Mode,
|
|
||||||
addr.Resource.Resource.Type,
|
|
||||||
)
|
|
||||||
if schema == nil {
|
|
||||||
return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider)
|
|
||||||
}
|
|
||||||
ty := schema.ImpliedType()
|
|
||||||
|
|
||||||
oldObj, err := oldIS.Current.Decode(ty)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var newObj *states.ResourceInstanceObject
|
|
||||||
if newIS != nil && newIS.Current != nil {
|
|
||||||
newObj, err = newIS.Current.Decode(ty)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldVal, newVal cty.Value
|
|
||||||
oldVal = oldObj.Value
|
|
||||||
if newObj != nil {
|
|
||||||
newVal = newObj.Value
|
|
||||||
} else {
|
|
||||||
newVal = cty.NullVal(ty)
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldVal.RawEquals(newVal) {
|
|
||||||
// No drift if the two values are semantically equivalent
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can only detect updates and deletes as drift.
|
|
||||||
action := plans.Update
|
|
||||||
if newVal.IsNull() {
|
|
||||||
action = plans.Delete
|
|
||||||
}
|
|
||||||
|
|
||||||
change := &plans.ResourceInstanceChangeSrc{
|
|
||||||
Addr: addr,
|
|
||||||
ChangeSrc: plans.ChangeSrc{
|
|
||||||
Action: action,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
changes = append(changes, json.NewResourceInstanceChange(change))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the change structs lexically by address to give stable output
|
|
||||||
sort.Slice(changes, func(i, j int) bool { return changes[i].Resource.Addr < changes[j].Resource.Addr })
|
|
||||||
|
|
||||||
for _, change := range changes {
|
|
||||||
v.view.ResourceDrift(change)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
||||||
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
||||||
// Avoid rendering data sources on deletion
|
// Avoid rendering data sources on deletion
|
||||||
|
|
|
@ -480,27 +480,33 @@ func TestOperationJSON_plan(t *testing.T) {
|
||||||
Resources: []*plans.ResourceInstanceChangeSrc{
|
Resources: []*plans.ResourceInstanceChangeSrc{
|
||||||
{
|
{
|
||||||
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete},
|
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
||||||
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
Addr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
||||||
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
||||||
},
|
},
|
||||||
// Data source deletion should not show up in the logs
|
// Data source deletion should not show up in the logs
|
||||||
{
|
{
|
||||||
Addr: derp.Instance(addrs.NoKey).Absolute(root),
|
Addr: derp.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -623,74 +629,175 @@ func TestOperationJSON_plan(t *testing.T) {
|
||||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOperationJSON_planDrift(t *testing.T) {
|
func TestOperationJSON_planDriftWithMove(t *testing.T) {
|
||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
||||||
|
|
||||||
root := addrs.RootModuleInstance
|
root := addrs.RootModuleInstance
|
||||||
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
||||||
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
||||||
derp := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "derp"}
|
blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"}
|
||||||
|
honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"}
|
||||||
|
|
||||||
plan := &plans.Plan{
|
plan := &plans.Plan{
|
||||||
|
UIMode: plans.NormalMode,
|
||||||
|
Changes: &plans.Changes{
|
||||||
|
Resources: []*plans.ResourceInstanceChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
||||||
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DriftedResources: []*plans.ResourceInstanceChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Addr: boop.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
||||||
|
},
|
||||||
|
// Move-only resource drift should not be present in normal mode plans
|
||||||
|
{
|
||||||
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
||||||
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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 with move
|
||||||
|
{
|
||||||
|
"@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",
|
||||||
|
},
|
||||||
|
"previous_resource": map[string]interface{}{
|
||||||
|
"addr": "test_resource.blep",
|
||||||
|
"implied_provider": "test",
|
||||||
|
"module": "",
|
||||||
|
"resource": "test_resource.blep",
|
||||||
|
"resource_key": nil,
|
||||||
|
"resource_name": "blep",
|
||||||
|
"resource_type": "test_resource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Move-only change
|
||||||
|
{
|
||||||
|
"@level": "info",
|
||||||
|
"@message": `test_resource.honk["bonk"]: Plan to move`,
|
||||||
|
"@module": "terraform.ui",
|
||||||
|
"type": "planned_change",
|
||||||
|
"change": map[string]interface{}{
|
||||||
|
"action": "move",
|
||||||
|
"resource": map[string]interface{}{
|
||||||
|
"addr": `test_resource.honk["bonk"]`,
|
||||||
|
"implied_provider": "test",
|
||||||
|
"module": "",
|
||||||
|
"resource": `test_resource.honk["bonk"]`,
|
||||||
|
"resource_key": "bonk",
|
||||||
|
"resource_name": "honk",
|
||||||
|
"resource_type": "test_resource",
|
||||||
|
},
|
||||||
|
"previous_resource": map[string]interface{}{
|
||||||
|
"addr": `test_resource.honk[0]`,
|
||||||
|
"implied_provider": "test",
|
||||||
|
"module": "",
|
||||||
|
"resource": `test_resource.honk[0]`,
|
||||||
|
"resource_key": float64(0),
|
||||||
|
"resource_name": "honk",
|
||||||
|
"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_planDriftWithMoveRefreshOnly(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"}
|
||||||
|
blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"}
|
||||||
|
honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"}
|
||||||
|
|
||||||
|
plan := &plans.Plan{
|
||||||
|
UIMode: plans.RefreshOnlyMode,
|
||||||
Changes: &plans.Changes{
|
Changes: &plans.Changes{
|
||||||
Resources: []*plans.ResourceInstanceChangeSrc{},
|
Resources: []*plans.ResourceInstanceChangeSrc{},
|
||||||
},
|
},
|
||||||
PrevRunState: states.BuildState(func(state *states.SyncState) {
|
DriftedResources: []*plans.ResourceInstanceChangeSrc{
|
||||||
// Update
|
{
|
||||||
state.SetResourceInstanceCurrent(
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
boop.Instance(addrs.NoKey).Absolute(root),
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
&states.ResourceInstanceObjectSrc{
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
Status: states.ObjectReady,
|
|
||||||
AttrsJSON: []byte(`{"foo":"bar"}`),
|
|
||||||
},
|
},
|
||||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
{
|
||||||
)
|
Addr: boop.Instance(addrs.NoKey).Absolute(root),
|
||||||
// Delete
|
PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root),
|
||||||
state.SetResourceInstanceCurrent(
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
||||||
beep.Instance(addrs.NoKey).Absolute(root),
|
|
||||||
&states.ResourceInstanceObjectSrc{
|
|
||||||
Status: states.ObjectReady,
|
|
||||||
AttrsJSON: []byte(`{"foo":"boop"}`),
|
|
||||||
},
|
},
|
||||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
// Move-only resource drift should be present in refresh-only plans
|
||||||
)
|
{
|
||||||
// No-op
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
||||||
state.SetResourceInstanceCurrent(
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
derp.Instance(addrs.NoKey).Absolute(root),
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
||||||
&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())
|
v.Plan(plan, testSchemas())
|
||||||
|
|
||||||
|
@ -731,6 +838,43 @@ func TestOperationJSON_planDrift(t *testing.T) {
|
||||||
"resource_name": "boop",
|
"resource_name": "boop",
|
||||||
"resource_type": "test_resource",
|
"resource_type": "test_resource",
|
||||||
},
|
},
|
||||||
|
"previous_resource": map[string]interface{}{
|
||||||
|
"addr": "test_resource.blep",
|
||||||
|
"implied_provider": "test",
|
||||||
|
"module": "",
|
||||||
|
"resource": "test_resource.blep",
|
||||||
|
"resource_key": nil,
|
||||||
|
"resource_name": "blep",
|
||||||
|
"resource_type": "test_resource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Drift detected: Move-only change
|
||||||
|
{
|
||||||
|
"@level": "info",
|
||||||
|
"@message": `test_resource.honk["bonk"]: Drift detected (move)`,
|
||||||
|
"@module": "terraform.ui",
|
||||||
|
"type": "resource_drift",
|
||||||
|
"change": map[string]interface{}{
|
||||||
|
"action": "move",
|
||||||
|
"resource": map[string]interface{}{
|
||||||
|
"addr": `test_resource.honk["bonk"]`,
|
||||||
|
"implied_provider": "test",
|
||||||
|
"module": "",
|
||||||
|
"resource": `test_resource.honk["bonk"]`,
|
||||||
|
"resource_key": "bonk",
|
||||||
|
"resource_name": "honk",
|
||||||
|
"resource_type": "test_resource",
|
||||||
|
},
|
||||||
|
"previous_resource": map[string]interface{}{
|
||||||
|
"addr": `test_resource.honk[0]`,
|
||||||
|
"implied_provider": "test",
|
||||||
|
"module": "",
|
||||||
|
"resource": `test_resource.honk[0]`,
|
||||||
|
"resource_key": float64(0),
|
||||||
|
"resource_name": "honk",
|
||||||
|
"resource_type": "test_resource",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// No changes
|
// No changes
|
||||||
|
@ -846,6 +990,7 @@ func TestOperationJSON_plannedChange(t *testing.T) {
|
||||||
// Replace requested by user
|
// Replace requested by user
|
||||||
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
||||||
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
||||||
ActionReason: plans.ResourceInstanceReplaceByRequest,
|
ActionReason: plans.ResourceInstanceReplaceByRequest,
|
||||||
})
|
})
|
||||||
|
@ -853,12 +998,14 @@ func TestOperationJSON_plannedChange(t *testing.T) {
|
||||||
// Simple create
|
// Simple create
|
||||||
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
||||||
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Data source deletion
|
// Data source deletion
|
||||||
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
||||||
Addr: derp.Instance(addrs.NoKey).Absolute(root),
|
Addr: derp.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,8 @@ This message does not include details about the exact changes which caused the c
|
||||||
At the end of a plan or before an apply, Terraform will emit a `planned_change` message for each resource which has changes to apply. This message has an embedded `change` object with the following keys:
|
At the end of a plan or before an apply, Terraform will emit a `planned_change` message for each resource which has changes to apply. This message has an embedded `change` object with the following keys:
|
||||||
|
|
||||||
- `resource`: object describing the address of the resource to be changed; see [resource object](#resource-object) below for details
|
- `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: `noop`, `create`, `read`, `update`, `replace`, `delete`.
|
- `previous_resource`: object describing the previous address of the resource, if this change includes a configuration-driven move
|
||||||
|
- `action`: the action planned to be taken for the resource. Values: `noop`, `create`, `read`, `update`, `replace`, `delete`, `move`.
|
||||||
- `reason`: an optional reason for the change, currently only used when the action is `replace`. Values:
|
- `reason`: an optional reason for the change, currently only used when the action is `replace`. Values:
|
||||||
- `tainted`: resource was marked as tainted
|
- `tainted`: resource was marked as tainted
|
||||||
- `requested`: user requested that the resource be replaced, for example via the `-replace` plan flag
|
- `requested`: user requested that the resource be replaced, for example via the `-replace` plan flag
|
||||||
|
|
Loading…
Reference in New Issue