mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
[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:
parent
ddd87994bf
commit
54c1c1162f
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
57
internal/command/jsonformat/renderer_test.go
Normal file
57
internal/command/jsonformat/renderer_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user