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 {
|
func (d diff) Moved() bool {
|
||||||
return len(d.change.PreviousAddress) > 0 && d.change.PreviousAddress != d.change.Address
|
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
|
willPrintResourceChanges := false
|
||||||
counts := make(map[plans.Action]int)
|
counts := make(map[plans.Action]int)
|
||||||
|
importingCount := 0
|
||||||
var changes []diff
|
var changes []diff
|
||||||
for _, diff := range diffs.changes {
|
for _, diff := range diffs.changes {
|
||||||
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
|
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.
|
// Don't show anything for NoOp changes.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -80,6 +81,10 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRen
|
|||||||
|
|
||||||
changes = append(changes, diff)
|
changes = append(changes, diff)
|
||||||
|
|
||||||
|
if diff.Importing() {
|
||||||
|
importingCount++
|
||||||
|
}
|
||||||
|
|
||||||
// Don't count move-only changes
|
// Don't count move-only changes
|
||||||
if action != plans.NoOp {
|
if action != plans.NoOp {
|
||||||
willPrintResourceChanges = true
|
willPrintResourceChanges = true
|
||||||
@ -219,12 +224,21 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Streams.Printf(
|
||||||
renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
|
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.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete],
|
||||||
counts[plans.Update],
|
counts[plans.Update],
|
||||||
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
|
counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(outputs) > 0 {
|
if len(outputs) > 0 {
|
||||||
renderer.Streams.Print("\nChanges to Outputs:\n")
|
renderer.Streams.Print("\nChanges to Outputs:\n")
|
||||||
@ -335,14 +349,18 @@ func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool)
|
|||||||
// the computed actions of these.
|
// the computed actions of these.
|
||||||
|
|
||||||
action := jsonplan.UnmarshalActions(diff.change.Change.Actions)
|
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.
|
// Skip resource changes that have nothing interesting to say.
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
buf.WriteString(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause)))
|
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
|
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)
|
dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, resource.Deposed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var printedMoved bool
|
||||||
|
var printedImported bool
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case plans.Create:
|
case plans.Create:
|
||||||
buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))
|
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:
|
case plans.NoOp:
|
||||||
if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address {
|
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))
|
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
|
break
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
@ -451,9 +478,27 @@ func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action
|
|||||||
}
|
}
|
||||||
buf.WriteString("\n")
|
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))
|
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()
|
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) {
|
func TestResourceChange_primitiveTypes(t *testing.T) {
|
||||||
testCases := map[string]testCase{
|
testCases := map[string]testCase{
|
||||||
"creation": {
|
"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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/mitchellh/colorstring"
|
"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/format"
|
||||||
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
|
||||||
@ -18,7 +20,6 @@ import (
|
|||||||
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
|
viewsjson "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/terminal"
|
"github.com/hashicorp/terraform/internal/terminal"
|
||||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONLogType string
|
type JSONLogType string
|
||||||
@ -49,6 +50,31 @@ const (
|
|||||||
LogVersion JSONLogType = "version"
|
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 {
|
type Renderer struct {
|
||||||
Streams *terminal.Streams
|
Streams *terminal.Streams
|
||||||
Colorize *colorstring.Colorize
|
Colorize *colorstring.Colorize
|
||||||
@ -57,11 +83,7 @@ type Renderer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...PlanRendererOpt) {
|
func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...PlanRendererOpt) {
|
||||||
// TODO(liamcervante): Tidy up this detection of version differences, we
|
if incompatibleVersions(jsonplan.FormatVersion, plan.PlanFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, plan.ProviderFormatVersion) {
|
||||||
// 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 {
|
|
||||||
renderer.Streams.Println(format.WordWrap(
|
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.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()))
|
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) {
|
func (renderer Renderer) RenderHumanState(state State) {
|
||||||
// TODO(liamcervante): Tidy up this detection of version differences, we
|
if incompatibleVersions(jsonstate.FormatVersion, state.StateFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, state.ProviderFormatVersion) {
|
||||||
// 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 {
|
|
||||||
renderer.Streams.Println(format.WordWrap(
|
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.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()))
|
renderer.Streams.Stdout.Columns()))
|
||||||
@ -94,7 +112,7 @@ func (renderer Renderer) RenderHumanState(state State) {
|
|||||||
state.renderHumanStateOutputs(renderer, opts)
|
state.renderHumanStateOutputs(renderer, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Renderer) RenderLog(log *JSONLog) error {
|
func (renderer Renderer) RenderLog(log *JSONLog) error {
|
||||||
switch log.Type {
|
switch log.Type {
|
||||||
case LogRefreshComplete,
|
case LogRefreshComplete,
|
||||||
LogVersion,
|
LogVersion,
|
||||||
@ -106,16 +124,16 @@ func (r Renderer) RenderLog(log *JSONLog) error {
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
case LogApplyStart, LogApplyComplete, LogRefreshStart, LogProvisionStart, LogResourceDrift:
|
case LogApplyStart, LogApplyComplete, LogRefreshStart, LogProvisionStart, LogResourceDrift:
|
||||||
msg := fmt.Sprintf(r.Colorize.Color("[bold]%s[reset]"), log.Message)
|
msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s[reset]"), log.Message)
|
||||||
r.Streams.Println(msg)
|
renderer.Streams.Println(msg)
|
||||||
|
|
||||||
case LogDiagnostic:
|
case LogDiagnostic:
|
||||||
diag := format.DiagnosticFromJSON(log.Diagnostic, r.Colorize, 78)
|
diag := format.DiagnosticFromJSON(log.Diagnostic, renderer.Colorize, 78)
|
||||||
r.Streams.Print(diag)
|
renderer.Streams.Print(diag)
|
||||||
|
|
||||||
case LogOutputs:
|
case LogOutputs:
|
||||||
if len(log.Outputs) > 0 {
|
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 {
|
for name, output := range log.Outputs {
|
||||||
change := structured.FromJsonViewsOutput(output)
|
change := structured.FromJsonViewsOutput(output)
|
||||||
ctype, err := ctyjson.UnmarshalType(output.Type)
|
ctype, err := ctyjson.UnmarshalType(output.Type)
|
||||||
@ -123,14 +141,14 @@ func (r Renderer) RenderLog(log *JSONLog) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := computed.NewRenderHumanOpts(r.Colorize)
|
opts := computed.NewRenderHumanOpts(renderer.Colorize)
|
||||||
opts.ShowUnchangedChildren = true
|
opts.ShowUnchangedChildren = true
|
||||||
|
|
||||||
outputDiff := differ.ComputeDiffForType(change, ctype)
|
outputDiff := differ.ComputeDiffForType(change, ctype)
|
||||||
outputStr := outputDiff.RenderHuman(0, opts)
|
outputStr := outputDiff.RenderHuman(0, opts)
|
||||||
|
|
||||||
msg := fmt.Sprintf("%s = %s", name, outputStr)
|
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{})
|
resource := log.Hook["resource"].(map[string]interface{})
|
||||||
resourceAddr := resource["addr"].(string)
|
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)
|
resourceAddr, provisioner, output)
|
||||||
r.Streams.Println(msg)
|
renderer.Streams.Println(msg)
|
||||||
|
|
||||||
case LogChangeSummary:
|
case LogChangeSummary:
|
||||||
// Normally, we will only render the apply change summary since the renderer
|
// Normally, we will only render the apply change summary since the renderer
|
||||||
// generates a plan change summary for us
|
// generates a plan change summary for us
|
||||||
msg := fmt.Sprintf(r.Colorize.Color("[bold][green]%s[reset]"), log.Message)
|
msg := fmt.Sprintf(renderer.Colorize.Color("[bold][green]%s[reset]"), log.Message)
|
||||||
r.Streams.Println("\n" + msg + "\n")
|
renderer.Streams.Println("\n" + msg + "\n")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// If the log type is not a known log type, we will just print the log message
|
// 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
|
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
|
// incremented for any change to this format that requires changes to a
|
||||||
// consuming parser.
|
// consuming parser.
|
||||||
const (
|
const (
|
||||||
FormatVersion = "1.1"
|
FormatVersion = "1.2"
|
||||||
|
|
||||||
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
|
ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update"
|
||||||
ResourceInstanceReplaceBecauseTainted = "replace_because_tainted"
|
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
|
// consists of one or more steps, each of which will be a number or a
|
||||||
// string.
|
// string.
|
||||||
ReplacePaths json.RawMessage `json:"replace_paths,omitempty"`
|
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 {
|
type output struct {
|
||||||
@ -437,6 +451,11 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var importing *Importing
|
||||||
|
if rc.Importing != nil {
|
||||||
|
importing = &Importing{ID: rc.Importing.ID}
|
||||||
|
}
|
||||||
|
|
||||||
r.Change = Change{
|
r.Change = Change{
|
||||||
Actions: actionString(rc.Action.String()),
|
Actions: actionString(rc.Action.String()),
|
||||||
Before: json.RawMessage(before),
|
Before: json.RawMessage(before),
|
||||||
@ -445,6 +464,7 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
|||||||
BeforeSensitive: json.RawMessage(beforeSensitive),
|
BeforeSensitive: json.RawMessage(beforeSensitive),
|
||||||
AfterSensitive: json.RawMessage(afterSensitive),
|
AfterSensitive: json.RawMessage(afterSensitive),
|
||||||
ReplacePaths: replacePaths,
|
ReplacePaths: replacePaths,
|
||||||
|
Importing: importing,
|
||||||
}
|
}
|
||||||
|
|
||||||
if rc.DeposedKey != states.NotDeposed {
|
if rc.DeposedKey != states.NotDeposed {
|
||||||
@ -596,6 +616,10 @@ func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
|
|||||||
AfterUnknown: a,
|
AfterUnknown: a,
|
||||||
BeforeSensitive: json.RawMessage(sensitive),
|
BeforeSensitive: json.RawMessage(sensitive),
|
||||||
AfterSensitive: 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
|
outputChanges[oc.Addr.OutputValue.Name] = c
|
||||||
|
Loading…
Reference in New Issue
Block a user