[Plannable Import] Implement human-readable plan rendering (#33113)

* [plannable import] embed the resource id within the changes

* add the plannable imports to the json and human plans

* latest importing struct
This commit is contained in:
Liam Cervante 2023-05-03 18:50:04 +02:00 committed by GitHub
parent ddd87994bf
commit 54c1c1162f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 494 additions and 34 deletions

View File

@ -100,3 +100,7 @@ type diff struct {
func (d diff) Moved() bool {
return len(d.change.PreviousAddress) > 0 && d.change.PreviousAddress != d.change.Address
}
func (d diff) Importing() bool {
return d.change.Change.Importing != nil
}

View File

@ -66,10 +66,11 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRen
willPrintResourceChanges := false
counts := make(map[plans.Action]int)
importingCount := 0
var changes []diff
for _, diff := range diffs.changes {
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
if action == plans.NoOp && !diff.Moved() {
if action == plans.NoOp && !diff.Moved() && !diff.Importing() {
// Don't show anything for NoOp changes.
continue
}
@ -80,6 +81,10 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRen
changes = append(changes, diff)
if diff.Importing() {
importingCount++
}
// Don't count move-only changes
if action != plans.NoOp {
willPrintResourceChanges = true
@ -219,11 +224,20 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRen
}
}
renderer.Streams.Printf(
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
counts[plans.Update],
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
if importingCount > 0 {
renderer.Streams.Printf(
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to import, %d to change, %d to destroy.\n"),
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
importingCount,
counts[plans.Update],
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
} else {
renderer.Streams.Printf(
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
counts[plans.Update],
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
}
}
if len(outputs) > 0 {
@ -335,14 +349,18 @@ func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool)
// the computed actions of these.
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
if action == plans.NoOp && (len(diff.change.PreviousAddress) == 0 || diff.change.PreviousAddress == diff.change.Address) {
if action == plans.NoOp && !diff.Moved() && !diff.Importing() {
// Skip resource changes that have nothing interesting to say.
return "", false
}
var buf bytes.Buffer
buf.WriteString(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause)))
buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize))))
opts := computed.NewRenderHumanOpts(renderer.Colorize)
opts.ShowUnchangedChildren = diff.Importing()
buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, opts)))
return buf.String(), true
}
@ -354,6 +372,9 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action
dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, resource.Deposed)
}
var printedMoved bool
var printedImported bool
switch action {
case plans.Create:
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))
@ -442,6 +463,12 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action
case plans.NoOp:
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address {
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has moved to [bold]%s[reset]", resource.PreviousAddress, dispAddr))
printedMoved = true
break
}
if resource.Change.Importing != nil {
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be imported", dispAddr))
printedImported = true
break
}
fallthrough
@ -451,9 +478,27 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action
}
buf.WriteString("\n")
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address && action != plans.NoOp {
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address && !printedMoved {
buf.WriteString(fmt.Sprintf(" # [reset](moved from %s)\n", resource.PreviousAddress))
}
if resource.Change.Importing != nil && !printedImported {
// We want to make this as forward compatible as possible, and we know
// the ID may be removed from the Importing metadata in favour of
// something else.
// As Importing metadata is loaded from a JSON struct, the effect of it
// being removed in the future will mean this renderer will receive it
// as an empty string
if len(resource.Change.Importing.ID) > 0 {
buf.WriteString(fmt.Sprintf(" # [reset](imported from \"%s\")\n", resource.Change.Importing.ID))
} else {
// This means we're trying to render a plan from a future version
// and we didn't get given the ID. So we'll do our best.
buf.WriteString(" # [reset](will be imported first)\n")
}
}
if resource.Change.Importing != nil && (action == plans.CreateThenDelete || action == plans.DeleteThenCreate) {
buf.WriteString(" # [reset][yellow]Warning: this will destroy the imported resource[reset]\n")
}
return buf.String()
}

View File

@ -80,6 +80,310 @@ and found no differences, so no changes are needed.
}
}
func TestRenderHuman_Imports(t *testing.T) {
color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
schemas := map[string]*jsonprovider.Provider{
"test": {
ResourceSchemas: map[string]*jsonprovider.Schema{
"test_resource": {
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"id": {
AttributeType: marshalJson(t, "string"),
},
"value": {
AttributeType: marshalJson(t, "string"),
},
},
},
},
},
},
}
tcs := map[string]struct {
plan Plan
output string
}{
"simple_import": {
plan: Plan{
ResourceChanges: []jsonplan.ResourceChange{
{
Address: "test_resource.resource",
Mode: "managed",
Type: "test_resource",
Name: "resource",
ProviderName: "test",
Change: jsonplan.Change{
Actions: []string{"no-op"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
Importing: &jsonplan.Importing{
ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
},
},
},
},
},
output: `
Terraform will perform the following actions:
# test_resource.resource will be imported
resource "test_resource" "resource" {
id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
value = "Hello, World!"
}
Plan: 0 to add, 1 to import, 0 to change, 0 to destroy.
`,
},
"import_and_move": {
plan: Plan{
ResourceChanges: []jsonplan.ResourceChange{
{
Address: "test_resource.after",
PreviousAddress: "test_resource.before",
Mode: "managed",
Type: "test_resource",
Name: "after",
ProviderName: "test",
Change: jsonplan.Change{
Actions: []string{"no-op"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
Importing: &jsonplan.Importing{
ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
},
},
},
},
},
output: `
Terraform will perform the following actions:
# test_resource.before has moved to test_resource.after
# (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
resource "test_resource" "after" {
id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
value = "Hello, World!"
}
Plan: 0 to add, 1 to import, 0 to change, 0 to destroy.
`,
},
"import_move_and_update": {
plan: Plan{
ResourceChanges: []jsonplan.ResourceChange{
{
Address: "test_resource.after",
PreviousAddress: "test_resource.before",
Mode: "managed",
Type: "test_resource",
Name: "after",
ProviderName: "test",
Change: jsonplan.Change{
Actions: []string{"update"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, Universe!",
}),
Importing: &jsonplan.Importing{
ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
},
},
},
},
},
output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# test_resource.after will be updated in-place
# (moved from test_resource.before)
# (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
~ resource "test_resource" "after" {
id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
~ value = "Hello, World!" -> "Hello, Universe!"
}
Plan: 0 to add, 1 to import, 1 to change, 0 to destroy.
`,
},
"import_and_update": {
plan: Plan{
ResourceChanges: []jsonplan.ResourceChange{
{
Address: "test_resource.resource",
Mode: "managed",
Type: "test_resource",
Name: "resource",
ProviderName: "test",
Change: jsonplan.Change{
Actions: []string{"update"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, Universe!",
}),
Importing: &jsonplan.Importing{
ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
},
},
},
},
},
output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# test_resource.resource will be updated in-place
# (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
~ resource "test_resource" "resource" {
id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
~ value = "Hello, World!" -> "Hello, Universe!"
}
Plan: 0 to add, 1 to import, 1 to change, 0 to destroy.
`,
},
"import_and_update_with_no_id": {
plan: Plan{
ResourceChanges: []jsonplan.ResourceChange{
{
Address: "test_resource.resource",
Mode: "managed",
Type: "test_resource",
Name: "resource",
ProviderName: "test",
Change: jsonplan.Change{
Actions: []string{"update"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, Universe!",
}),
Importing: &jsonplan.Importing{},
},
},
},
},
output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# test_resource.resource will be updated in-place
# (will be imported first)
~ resource "test_resource" "resource" {
id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
~ value = "Hello, World!" -> "Hello, Universe!"
}
Plan: 0 to add, 1 to import, 1 to change, 0 to destroy.
`,
},
"import_and_replace": {
plan: Plan{
ResourceChanges: []jsonplan.ResourceChange{
{
Address: "test_resource.resource",
Mode: "managed",
Type: "test_resource",
Name: "resource",
ProviderName: "test",
Change: jsonplan.Change{
Actions: []string{"create", "delete"},
Before: marshalJson(t, map[string]interface{}{
"id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
"value": "Hello, World!",
}),
After: marshalJson(t, map[string]interface{}{
"id": "9794FB1F-7260-442F-830C-F2D450E90CE3",
"value": "Hello, World!",
}),
ReplacePaths: marshalJson(t, [][]string{{"id"}}),
Importing: &jsonplan.Importing{
ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
},
},
ActionReason: "",
},
},
},
output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+/- create replacement and then destroy
Terraform will perform the following actions:
# test_resource.resource must be replaced
# (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
# Warning: this will destroy the imported resource
+/- resource "test_resource" "resource" {
~ id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" -> "9794FB1F-7260-442F-830C-F2D450E90CE3" # forces replacement
value = "Hello, World!"
}
Plan: 1 to add, 1 to import, 0 to change, 1 to destroy.
`,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
plan := tc.plan
plan.PlanFormatVersion = jsonplan.FormatVersion
plan.ProviderFormatVersion = jsonprovider.FormatVersion
plan.ProviderSchemas = schemas
renderer := Renderer{
Colorize: color,
Streams: streams,
}
plan.renderHuman(renderer, plans.NormalMode)
got := done(t).Stdout()
want := tc.output
if diff := cmp.Diff(want, got); len(diff) > 0 {
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
})
}
}
func TestResourceChange_primitiveTypes(t *testing.T) {
testCases := map[string]testCase{
"creation": {
@ -6957,3 +7261,11 @@ func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block {
},
}
}
func marshalJson(t *testing.T, data interface{}) json.RawMessage {
result, err := json.Marshal(data)
if err != nil {
t.Fatalf("failed to marshal json: %v", err)
}
return result
}

View File

@ -5,8 +5,10 @@ package jsonformat
import (
"fmt"
"strconv"
"github.com/mitchellh/colorstring"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/command/format"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
@ -18,7 +20,6 @@ import (
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/terminal"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
type JSONLogType string
@ -49,6 +50,31 @@ const (
LogVersion JSONLogType = "version"
)
func incompatibleVersions(localVersion, remoteVersion string) bool {
var parsedLocal, parsedRemote float64
var err error
if parsedLocal, err = strconv.ParseFloat(localVersion, 64); err != nil {
return false
}
if parsedRemote, err = strconv.ParseFloat(remoteVersion, 64); err != nil {
return false
}
// If the local version is less than the remote version then the remote
// version might contain things the local version doesn't know about, so
// we're going to say they are incompatible.
//
// So far, we have built the renderer and the json packages to be backwards
// compatible so if the local version is greater than the remote version
// then that is okay, we'll still render a complete and correct plan.
//
// Note, this might change in the future. For example, if we introduce a
// new major version in one of the formats the renderer may no longer be
// backward compatible.
return parsedLocal < parsedRemote
}
type Renderer struct {
Streams *terminal.Streams
Colorize *colorstring.Colorize
@ -57,11 +83,7 @@ type Renderer struct {
}
func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...PlanRendererOpt) {
// TODO(liamcervante): Tidy up this detection of version differences, we
// should only report warnings when the plan is generated using a newer
// version then we are executing. We could also look into major vs minor
// version differences. This should work for alpha testing in the meantime.
if plan.PlanFormatVersion != jsonplan.FormatVersion || plan.ProviderFormatVersion != jsonprovider.FormatVersion {
if incompatibleVersions(jsonplan.FormatVersion, plan.PlanFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, plan.ProviderFormatVersion) {
renderer.Streams.Println(format.WordWrap(
renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of Terraform, the diff presented here may be missing representations of recent features."),
renderer.Streams.Stdout.Columns()))
@ -71,11 +93,7 @@ func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...Pla
}
func (renderer Renderer) RenderHumanState(state State) {
// TODO(liamcervante): Tidy up this detection of version differences, we
// should only report warnings when the plan is generated using a newer
// version then we are executing. We could also look into major vs minor
// version differences. This should work for alpha testing in the meantime.
if state.StateFormatVersion != jsonstate.FormatVersion || state.ProviderFormatVersion != jsonprovider.FormatVersion {
if incompatibleVersions(jsonstate.FormatVersion, state.StateFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, state.ProviderFormatVersion) {
renderer.Streams.Println(format.WordWrap(
renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This state was retrieved using a different version of Terraform, the state presented here maybe missing representations of recent features."),
renderer.Streams.Stdout.Columns()))
@ -94,7 +112,7 @@ func (renderer Renderer) RenderHumanState(state State) {
state.renderHumanStateOutputs(renderer, opts)
}
func (r Renderer) RenderLog(log *JSONLog) error {
func (renderer Renderer) RenderLog(log *JSONLog) error {
switch log.Type {
case LogRefreshComplete,
LogVersion,
@ -106,16 +124,16 @@ func (r Renderer) RenderLog(log *JSONLog) error {
return nil
case LogApplyStart, LogApplyComplete, LogRefreshStart, LogProvisionStart, LogResourceDrift:
msg := fmt.Sprintf(r.Colorize.Color("[bold]%s[reset]"), log.Message)
r.Streams.Println(msg)
msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s[reset]"), log.Message)
renderer.Streams.Println(msg)
case LogDiagnostic:
diag := format.DiagnosticFromJSON(log.Diagnostic, r.Colorize, 78)
r.Streams.Print(diag)
diag := format.DiagnosticFromJSON(log.Diagnostic, renderer.Colorize, 78)
renderer.Streams.Print(diag)
case LogOutputs:
if len(log.Outputs) > 0 {
r.Streams.Println(r.Colorize.Color("[bold][green]Outputs:[reset]"))
renderer.Streams.Println(renderer.Colorize.Color("[bold][green]Outputs:[reset]"))
for name, output := range log.Outputs {
change := structured.FromJsonViewsOutput(output)
ctype, err := ctyjson.UnmarshalType(output.Type)
@ -123,14 +141,14 @@ func (r Renderer) RenderLog(log *JSONLog) error {
return err
}
opts := computed.NewRenderHumanOpts(r.Colorize)
opts := computed.NewRenderHumanOpts(renderer.Colorize)
opts.ShowUnchangedChildren = true
outputDiff := differ.ComputeDiffForType(change, ctype)
outputStr := outputDiff.RenderHuman(0, opts)
msg := fmt.Sprintf("%s = %s", name, outputStr)
r.Streams.Println(msg)
renderer.Streams.Println(msg)
}
}
@ -140,19 +158,19 @@ func (r Renderer) RenderLog(log *JSONLog) error {
resource := log.Hook["resource"].(map[string]interface{})
resourceAddr := resource["addr"].(string)
msg := fmt.Sprintf(r.Colorize.Color("[bold]%s: (%s):[reset] %s"),
msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s: (%s):[reset] %s"),
resourceAddr, provisioner, output)
r.Streams.Println(msg)
renderer.Streams.Println(msg)
case LogChangeSummary:
// Normally, we will only render the apply change summary since the renderer
// generates a plan change summary for us
msg := fmt.Sprintf(r.Colorize.Color("[bold][green]%s[reset]"), log.Message)
r.Streams.Println("\n" + msg + "\n")
msg := fmt.Sprintf(renderer.Colorize.Color("[bold][green]%s[reset]"), log.Message)
renderer.Streams.Println("\n" + msg + "\n")
default:
// If the log type is not a known log type, we will just print the log message
r.Streams.Println(log.Message)
renderer.Streams.Println(log.Message)
}
return nil

View File

@ -0,0 +1,57 @@
package jsonformat
import (
"testing"
"github.com/hashicorp/terraform/internal/command/jsonplan"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/command/jsonstate"
)
func TestIncompatibleVersions(t *testing.T) {
tcs := map[string]struct {
local string
remote string
expected bool
}{
"matching": {
local: "1.1",
remote: "1.1",
expected: false,
},
"local_latest": {
local: "1.2",
remote: "1.1",
expected: false,
},
"local_earliest": {
local: "1.1",
remote: "1.2",
expected: true,
},
"parses_state_version": {
local: jsonstate.FormatVersion,
remote: jsonstate.FormatVersion,
expected: false,
},
"parses_provider_version": {
local: jsonprovider.FormatVersion,
remote: jsonprovider.FormatVersion,
expected: false,
},
"parses_plan_version": {
local: jsonplan.FormatVersion,
remote: jsonplan.FormatVersion,
expected: false,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
actual := incompatibleVersions(tc.local, tc.remote)
if actual != tc.expected {
t.Errorf("expected %t but found %t", tc.expected, actual)
}
})
}
}

View File

@ -29,7 +29,7 @@ import (
// incremented for any change to this format that requires changes to a
// consuming parser.
const (
FormatVersion = "1.1"
FormatVersion = "1.2"
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
ResourceInstanceReplaceBecauseTainted = "replace_because_tainted"
@ -125,6 +125,20 @@ type Change struct {
// consists of one or more steps, each of which will be a number or a
// string.
ReplacePaths json.RawMessage `json:"replace_paths,omitempty"`
// Importing contains the import metadata about this operation. If importing
// is present (ie. not null) then the change is an import operation in
// addition to anything mentioned in the actions field. The actual contents
// of the Importing struct is subject to change, so downstream consumers
// should treat any values in here as strictly optional.
Importing *Importing `json:"importing,omitempty"`
}
// Importing is a nested object for the resource import metadata.
type Importing struct {
// The original ID of this resource used to target it as part of planned
// import operation.
ID string `json:"id,omitempty"`
}
type output struct {
@ -437,6 +451,11 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
return nil, err
}
var importing *Importing
if rc.Importing != nil {
importing = &Importing{ID: rc.Importing.ID}
}
r.Change = Change{
Actions: actionString(rc.Action.String()),
Before: json.RawMessage(before),
@ -445,6 +464,7 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
BeforeSensitive: json.RawMessage(beforeSensitive),
AfterSensitive: json.RawMessage(afterSensitive),
ReplacePaths: replacePaths,
Importing: importing,
}
if rc.DeposedKey != states.NotDeposed {
@ -596,6 +616,10 @@ func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
AfterUnknown: a,
BeforeSensitive: json.RawMessage(sensitive),
AfterSensitive: json.RawMessage(sensitive),
// Just to be explicit, outputs cannot be imported so this is always
// nil.
Importing: nil,
}
outputChanges[oc.Addr.OutputValue.Name] = c