Merge pull request #29603 from hashicorp/alisdair/moved-json-output
json-output: Add support for config-driven move to plan JSON and streaming JSON UI
This commit is contained in:
commit
5386d6c5b8
|
@ -130,9 +130,25 @@ func Marshal(
|
||||||
}
|
}
|
||||||
|
|
||||||
// output.ResourceDrift
|
// output.ResourceDrift
|
||||||
output.ResourceDrift, err = output.marshalResourceChanges(p.DriftedResources, schemas)
|
if len(p.DriftedResources) > 0 {
|
||||||
if err != nil {
|
// In refresh-only mode, we render all resources marked as drifted,
|
||||||
return nil, fmt.Errorf("error in marshaling resource drift: %s", err)
|
// including those which have moved without other changes. In other plan
|
||||||
|
// modes, move-only changes will be included in the planned changes, so
|
||||||
|
// we skip them here.
|
||||||
|
var driftedResources []*plans.ResourceInstanceChangeSrc
|
||||||
|
if p.UIMode == plans.RefreshOnlyMode {
|
||||||
|
driftedResources = p.DriftedResources
|
||||||
|
} else {
|
||||||
|
for _, dr := range p.DriftedResources {
|
||||||
|
if dr.Action != plans.NoOp {
|
||||||
|
driftedResources = append(driftedResources, dr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.ResourceDrift, err = output.marshalResourceChanges(driftedResources, schemas)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error in marshaling resource drift: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// output.ResourceChanges
|
// output.ResourceChanges
|
||||||
|
@ -197,6 +213,9 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS
|
||||||
var r resourceChange
|
var r resourceChange
|
||||||
addr := rc.Addr
|
addr := rc.Addr
|
||||||
r.Address = addr.String()
|
r.Address = addr.String()
|
||||||
|
if !addr.Equal(rc.PrevRunAddr) {
|
||||||
|
r.PreviousAddress = rc.PrevRunAddr.String()
|
||||||
|
}
|
||||||
|
|
||||||
dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode
|
dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode
|
||||||
// We create "delete" actions for data resources so we can clean up
|
// We create "delete" actions for data resources so we can clean up
|
||||||
|
|
|
@ -48,6 +48,18 @@ type resourceChange struct {
|
||||||
// Address is the absolute resource address
|
// Address is the absolute resource address
|
||||||
Address string `json:"address,omitempty"`
|
Address string `json:"address,omitempty"`
|
||||||
|
|
||||||
|
// PreviousAddress is the absolute address that this resource instance had
|
||||||
|
// at the conclusion of a previous run.
|
||||||
|
//
|
||||||
|
// This will typically be omitted, but will be present if the previous
|
||||||
|
// resource instance was subject to a "moved" block that we handled in the
|
||||||
|
// process of creating this plan.
|
||||||
|
//
|
||||||
|
// Note that this behavior diverges from the internal plan data structure,
|
||||||
|
// where the previous address is set equal to the current address in the
|
||||||
|
// common case, rather than being omitted.
|
||||||
|
PreviousAddress string `json:"previous_address,omitempty"`
|
||||||
|
|
||||||
// ModuleAddress is the module portion of the above address. Omitted if the
|
// ModuleAddress is the module portion of the above address. Omitted if the
|
||||||
// instance is in the root module.
|
// instance is in the root module.
|
||||||
ModuleAddress string `json:"module_address,omitempty"`
|
ModuleAddress string `json:"module_address,omitempty"`
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# In state with `ami = "foo"`, so this should be a regular update. The provider
|
||||||
|
# should not detect changes on refresh.
|
||||||
|
resource "test_instance" "no_refresh" {
|
||||||
|
ami = "bar"
|
||||||
|
}
|
||||||
|
|
||||||
|
# In state with `ami = "refresh-me"`, but the provider will return
|
||||||
|
# `"refreshed"` after the refresh phase. The plan should show the drift
|
||||||
|
# (`"refresh-me"` to `"refreshed"`) and plan the update (`"refreshed"` to
|
||||||
|
# `"baz"`).
|
||||||
|
resource "test_instance" "should_refresh_with_move" {
|
||||||
|
ami = "baz"
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
experiments = [ config_driven_move ]
|
||||||
|
}
|
||||||
|
|
||||||
|
moved {
|
||||||
|
from = test_instance.should_refresh
|
||||||
|
to = test_instance.should_refresh_with_move
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
{
|
||||||
|
"format_version": "0.2",
|
||||||
|
"planned_values": {
|
||||||
|
"root_module": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.no_refresh",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "no_refresh",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"schema_version": 0,
|
||||||
|
"values": {
|
||||||
|
"ami": "bar",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"sensitive_values": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "test_instance.should_refresh_with_move",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "should_refresh_with_move",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"schema_version": 0,
|
||||||
|
"values": {
|
||||||
|
"ami": "baz",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"sensitive_values": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource_drift": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.should_refresh_with_move",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"previous_address": "test_instance.should_refresh",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"name": "should_refresh_with_move",
|
||||||
|
"change": {
|
||||||
|
"actions": [
|
||||||
|
"update"
|
||||||
|
],
|
||||||
|
"before": {
|
||||||
|
"ami": "refresh-me",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"ami": "refreshed",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after_sensitive": {},
|
||||||
|
"after_unknown": {},
|
||||||
|
"before_sensitive": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resource_changes": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.no_refresh",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"name": "no_refresh",
|
||||||
|
"change": {
|
||||||
|
"actions": [
|
||||||
|
"update"
|
||||||
|
],
|
||||||
|
"before": {
|
||||||
|
"ami": "foo",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"ami": "bar",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after_unknown": {},
|
||||||
|
"after_sensitive": {},
|
||||||
|
"before_sensitive": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "test_instance.should_refresh_with_move",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"previous_address": "test_instance.should_refresh",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"name": "should_refresh_with_move",
|
||||||
|
"change": {
|
||||||
|
"actions": [
|
||||||
|
"update"
|
||||||
|
],
|
||||||
|
"before": {
|
||||||
|
"ami": "refreshed",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"ami": "baz",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after_unknown": {},
|
||||||
|
"after_sensitive": {},
|
||||||
|
"before_sensitive": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prior_state": {
|
||||||
|
"format_version": "0.2",
|
||||||
|
"values": {
|
||||||
|
"root_module": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.no_refresh",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "no_refresh",
|
||||||
|
"schema_version": 0,
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"values": {
|
||||||
|
"ami": "foo",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"sensitive_values": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "test_instance.should_refresh_with_move",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "should_refresh_with_move",
|
||||||
|
"schema_version": 0,
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"values": {
|
||||||
|
"ami": "refreshed",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"sensitive_values": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"root_module": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.no_refresh",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "no_refresh",
|
||||||
|
"provider_config_key": "test",
|
||||||
|
"schema_version": 0,
|
||||||
|
"expressions": {
|
||||||
|
"ami": {
|
||||||
|
"constant_value": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "test_instance.should_refresh_with_move",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "should_refresh_with_move",
|
||||||
|
"provider_config_key": "test",
|
||||||
|
"schema_version": 0,
|
||||||
|
"expressions": {
|
||||||
|
"ami": {
|
||||||
|
"constant_value": "baz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"version": 4,
|
||||||
|
"terraform_version": "0.12.0",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "configuredUnchanged",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "no_refresh",
|
||||||
|
"provider": "provider[\"registry.terraform.io/hashicorp/test\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"ami": "foo",
|
||||||
|
"id": "placeholder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "should_refresh",
|
||||||
|
"provider": "provider[\"registry.terraform.io/hashicorp/test\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"ami": "refresh-me",
|
||||||
|
"id": "placeholder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
resource "test_instance" "baz" {
|
||||||
|
ami = "baz"
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
experiments = [ config_driven_move ]
|
||||||
|
}
|
||||||
|
|
||||||
|
moved {
|
||||||
|
from = test_instance.foo
|
||||||
|
to = test_instance.baz
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"format_version": "0.2",
|
||||||
|
"planned_values": {
|
||||||
|
"root_module": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.baz",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "baz",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"schema_version": 0,
|
||||||
|
"values": {
|
||||||
|
"ami": "baz",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"sensitive_values": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource_changes": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.baz",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"previous_address": "test_instance.foo",
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"name": "baz",
|
||||||
|
"change": {
|
||||||
|
"actions": [
|
||||||
|
"update"
|
||||||
|
],
|
||||||
|
"before": {
|
||||||
|
"ami": "foo",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"ami": "baz",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"after_unknown": {},
|
||||||
|
"after_sensitive": {},
|
||||||
|
"before_sensitive": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prior_state": {
|
||||||
|
"format_version": "0.2",
|
||||||
|
"values": {
|
||||||
|
"root_module": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.baz",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "baz",
|
||||||
|
"schema_version": 0,
|
||||||
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
|
"values": {
|
||||||
|
"ami": "foo",
|
||||||
|
"id": "placeholder"
|
||||||
|
},
|
||||||
|
"sensitive_values": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"root_module": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"address": "test_instance.baz",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "baz",
|
||||||
|
"provider_config_key": "test",
|
||||||
|
"schema_version": 0,
|
||||||
|
"expressions": {
|
||||||
|
"ami": {
|
||||||
|
"constant_value": "baz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"version": 4,
|
||||||
|
"terraform_version": "0.12.0",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "configuredUnchanged",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "test_instance",
|
||||||
|
"name": "foo",
|
||||||
|
"provider": "provider[\"registry.terraform.io/hashicorp/test\"]",
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"schema_version": 0,
|
||||||
|
"attributes": {
|
||||||
|
"ami": "foo",
|
||||||
|
"id": "placeholder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -45,32 +45,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resource_drift": [
|
|
||||||
{
|
|
||||||
"address": "test_instance.test[0]",
|
|
||||||
"mode": "managed",
|
|
||||||
"type": "test_instance",
|
|
||||||
"provider_name": "registry.terraform.io/hashicorp/test",
|
|
||||||
"name": "test",
|
|
||||||
"index": 0,
|
|
||||||
"change": {
|
|
||||||
"actions": [
|
|
||||||
"no-op"
|
|
||||||
],
|
|
||||||
"before": {
|
|
||||||
"ami": "bar",
|
|
||||||
"id": "placeholder"
|
|
||||||
},
|
|
||||||
"after": {
|
|
||||||
"ami": "bar",
|
|
||||||
"id": "placeholder"
|
|
||||||
},
|
|
||||||
"before_sensitive": {},
|
|
||||||
"after_sensitive": {},
|
|
||||||
"after_unknown": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resource_changes": [
|
"resource_changes": [
|
||||||
{
|
{
|
||||||
"address": "test_instance.test[0]",
|
"address": "test_instance.test[0]",
|
||||||
|
@ -78,6 +52,7 @@
|
||||||
"type": "test_instance",
|
"type": "test_instance",
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"index": 0,
|
"index": 0,
|
||||||
|
"previous_address": "test_instance.test",
|
||||||
"provider_name": "registry.terraform.io/hashicorp/test",
|
"provider_name": "registry.terraform.io/hashicorp/test",
|
||||||
"change": {
|
"change": {
|
||||||
"actions": [
|
"actions": [
|
||||||
|
|
|
@ -12,14 +12,22 @@ 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"`
|
||||||
Action ChangeAction `json:"action"`
|
PreviousResource *ResourceAddr `json:"previous_resource,omitempty"`
|
||||||
Reason ChangeReason `json:"reason,omitempty"`
|
Action ChangeAction `json:"action"`
|
||||||
|
Reason ChangeReason `json:"reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ResourceInstanceChange) String() string {
|
func (c *ResourceInstanceChange) String() string {
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -111,7 +111,8 @@ 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,
|
||||||
},
|
},
|
||||||
|
@ -151,7 +152,8 @@ 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
|
||||||
|
|
|
@ -479,29 +479,35 @@ func TestOperationJSON_plan(t *testing.T) {
|
||||||
Changes: &plans.Changes{
|
Changes: &plans.Changes{
|
||||||
Resources: []*plans.ResourceInstanceChangeSrc{
|
Resources: []*plans.ResourceInstanceChangeSrc{
|
||||||
{
|
{
|
||||||
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete},
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Addr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
Addr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
||||||
|
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),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
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"}`),
|
{
|
||||||
},
|
Addr: boop.Instance(addrs.NoKey).Absolute(root),
|
||||||
root.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root),
|
||||||
)
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
||||||
// Delete
|
},
|
||||||
state.SetResourceInstanceCurrent(
|
// Move-only resource drift should be present in refresh-only plans
|
||||||
beep.Instance(addrs.NoKey).Absolute(root),
|
{
|
||||||
&states.ResourceInstanceObjectSrc{
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
||||||
Status: states.ObjectReady,
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
||||||
AttrsJSON: []byte(`{"foo":"boop"}`),
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
||||||
},
|
},
|
||||||
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())
|
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,20 +990,23 @@ 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
||||||
|
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),
|
||||||
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root),
|
||||||
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Expect only two messages, as the data source deletion should be a no-op
|
// Expect only two messages, as the data source deletion should be a no-op
|
||||||
|
|
|
@ -98,9 +98,15 @@ For ease of consumption by callers, the plan representation includes a partial r
|
||||||
{
|
{
|
||||||
// "address" is the full absolute address of the resource instance this
|
// "address" is the full absolute address of the resource instance this
|
||||||
// change applies to, in the same format as addresses in a value
|
// change applies to, in the same format as addresses in a value
|
||||||
// representation
|
// representation.
|
||||||
"address": "module.child.aws_instance.foo[0]",
|
"address": "module.child.aws_instance.foo[0]",
|
||||||
|
|
||||||
|
// "previous_address" is the full absolute address of this resource
|
||||||
|
// instance as it was known after the previous Terraform run.
|
||||||
|
// Included only if the address has changed, e.g. by handling
|
||||||
|
// a "moved" block in the configuration.
|
||||||
|
"previous_address": "module.instances.aws_instance.foo[0]",
|
||||||
|
|
||||||
// "module_address", if set, is the module portion of the above address.
|
// "module_address", if set, is the module portion of the above address.
|
||||||
// Omitted if the instance is in the root module.
|
// Omitted if the instance is in the root module.
|
||||||
"module_address": "module.child",
|
"module_address": "module.child",
|
||||||
|
|
|
@ -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