diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index 1d7eff1ff..b2bf2cceb 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -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 diff --git a/internal/command/jsonplan/resource.go b/internal/command/jsonplan/resource.go index ca1299c99..1e737a626 100644 --- a/internal/command/jsonplan/resource.go +++ b/internal/command/jsonplan/resource.go @@ -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"` diff --git a/internal/command/testdata/show-json/moved-drift/main.tf b/internal/command/testdata/show-json/moved-drift/main.tf new file mode 100644 index 000000000..afdf9fe66 --- /dev/null +++ b/internal/command/testdata/show-json/moved-drift/main.tf @@ -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 +} diff --git a/internal/command/testdata/show-json/moved-drift/output.json b/internal/command/testdata/show-json/moved-drift/output.json new file mode 100644 index 000000000..0d151808f --- /dev/null +++ b/internal/command/testdata/show-json/moved-drift/output.json @@ -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" + } + } + } + ] + } + } +} diff --git a/internal/command/testdata/show-json/moved-drift/terraform.tfstate b/internal/command/testdata/show-json/moved-drift/terraform.tfstate new file mode 100644 index 000000000..02b8944d8 --- /dev/null +++ b/internal/command/testdata/show-json/moved-drift/terraform.tfstate @@ -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" + } + } + ] + } + ] +} diff --git a/internal/command/testdata/show-json/moved/main.tf b/internal/command/testdata/show-json/moved/main.tf new file mode 100644 index 000000000..0be803cbc --- /dev/null +++ b/internal/command/testdata/show-json/moved/main.tf @@ -0,0 +1,12 @@ +resource "test_instance" "baz" { + ami = "baz" +} + +terraform { + experiments = [ config_driven_move ] +} + +moved { + from = test_instance.foo + to = test_instance.baz +} diff --git a/internal/command/testdata/show-json/moved/output.json b/internal/command/testdata/show-json/moved/output.json new file mode 100644 index 000000000..3ce281983 --- /dev/null +++ b/internal/command/testdata/show-json/moved/output.json @@ -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" + } + } + } + ] + } + } +} diff --git a/internal/command/testdata/show-json/moved/terraform.tfstate b/internal/command/testdata/show-json/moved/terraform.tfstate new file mode 100644 index 000000000..b4e571887 --- /dev/null +++ b/internal/command/testdata/show-json/moved/terraform.tfstate @@ -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" + } + } + ] + } + ] +} diff --git a/internal/command/testdata/show-json/multi-resource-update/output.json b/internal/command/testdata/show-json/multi-resource-update/output.json index 247749261..262b6194b 100644 --- a/internal/command/testdata/show-json/multi-resource-update/output.json +++ b/internal/command/testdata/show-json/multi-resource-update/output.json @@ -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": [ diff --git a/website/docs/internals/json-format.html.md b/website/docs/internals/json-format.html.md index 9a3efeff5..ffe54c2cc 100644 --- a/website/docs/internals/json-format.html.md +++ b/website/docs/internals/json-format.html.md @@ -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",