json-output: Previous address for resource changes

Configuration-driven moves are represented in the plan file by setting
the resource's `PrevRunAddr` to a different value than its `Addr`. For
JSON plan output, we here add a new field to resource changes,
`previous_address`, which is present and non-empty only if the resource
is planned to be moved.

Like the CLI UI, refresh-only plans will include move-only changes in
the resource drift JSON output. In normal plan mode, these are elided to
avoid redundancy with planned changes.
This commit is contained in:
Alisdair McDiarmid 2021-09-17 13:18:34 -04:00
parent 78705f4f10
commit 78c4a8c461
10 changed files with 403 additions and 30 deletions

View File

@ -130,9 +130,25 @@ func Marshal(
}
// output.ResourceDrift
output.ResourceDrift, err = output.marshalResourceChanges(p.DriftedResources, schemas)
if err != nil {
return nil, fmt.Errorf("error in marshaling resource drift: %s", err)
if len(p.DriftedResources) > 0 {
// In refresh-only mode, we render all resources marked as drifted,
// 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
@ -197,6 +213,9 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS
var r resourceChange
addr := rc.Addr
r.Address = addr.String()
if !addr.Equal(rc.PrevRunAddr) {
r.PreviousAddress = rc.PrevRunAddr.String()
}
dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode
// We create "delete" actions for data resources so we can clean up

View File

@ -48,6 +48,18 @@ type resourceChange struct {
// Address is the absolute resource address
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
// instance is in the root module.
ModuleAddress string `json:"module_address,omitempty"`

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
resource "test_instance" "baz" {
ami = "baz"
}
terraform {
experiments = [ config_driven_move ]
}
moved {
from = test_instance.foo
to = test_instance.baz
}

View File

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

View File

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

View File

@ -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": [
{
"address": "test_instance.test[0]",
@ -78,6 +52,7 @@
"type": "test_instance",
"name": "test",
"index": 0,
"previous_address": "test_instance.test",
"provider_name": "registry.terraform.io/hashicorp/test",
"change": {
"actions": [

View File

@ -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
// change applies to, in the same format as addresses in a value
// representation
// representation.
"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.
// Omitted if the instance is in the root module.
"module_address": "module.child",