mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05:00
cli: Migrate show command to use command arguments and views
This commit is contained in:
@@ -2,37 +2,95 @@ package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// FIXME: this is a temporary partial definition of the view for the show
|
||||
// command, in place to allow access to the plan renderer which is now in the
|
||||
// views package.
|
||||
type Show interface {
|
||||
Plan(plan *plans.Plan, schemas *terraform.Schemas)
|
||||
// Display renders the plan, if it is available. If plan is nil, it renders the statefile.
|
||||
Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int
|
||||
|
||||
// Diagnostics renders early diagnostics, resulting from argument parsing.
|
||||
Diagnostics(diags tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// FIXME: the show view should support both human and JSON types. This code is
|
||||
// currently only used to render the plan in human-readable UI, so does not yet
|
||||
// support JSON.
|
||||
func NewShow(vt arguments.ViewType, view *View) Show {
|
||||
switch vt {
|
||||
case arguments.ViewJSON:
|
||||
return &ShowJSON{view: view}
|
||||
case arguments.ViewHuman:
|
||||
return &ShowHuman{View: *view}
|
||||
return &ShowHuman{view: view}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", vt))
|
||||
}
|
||||
}
|
||||
|
||||
type ShowHuman struct {
|
||||
View
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Show = (*ShowHuman)(nil)
|
||||
|
||||
func (v *ShowHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
renderPlan(plan, schemas, &v.View)
|
||||
func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int {
|
||||
if plan != nil {
|
||||
renderPlan(plan, schemas, v.view)
|
||||
} else {
|
||||
if stateFile == nil {
|
||||
v.view.streams.Println("No state.")
|
||||
return 0
|
||||
}
|
||||
|
||||
v.view.streams.Println(format.State(&format.StateOpts{
|
||||
State: stateFile.State,
|
||||
Color: v.view.colorize,
|
||||
Schemas: schemas,
|
||||
}))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *ShowHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
type ShowJSON struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Show = (*ShowJSON)(nil)
|
||||
|
||||
func (v *ShowJSON) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int {
|
||||
if plan != nil {
|
||||
jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas)
|
||||
|
||||
if err != nil {
|
||||
v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
|
||||
return 1
|
||||
}
|
||||
v.view.streams.Println(string(jsonPlan))
|
||||
} else {
|
||||
// It is possible that there is neither state nor a plan.
|
||||
// That's ok, we'll just return an empty object.
|
||||
jsonState, err := jsonstate.Marshal(stateFile, schemas)
|
||||
if err != nil {
|
||||
v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
|
||||
return 1
|
||||
}
|
||||
v.view.streams.Println(string(jsonState))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Diagnostics should only be called if show cannot be executed.
|
||||
// In this case, we choose to render human-readable diagnostic output,
|
||||
// primarily for backwards compatibility.
|
||||
func (v *ShowJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
184
internal/command/views/show_test.go
Normal file
184
internal/command/views/show_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/initwd"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestShowHuman(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
plan *plans.Plan
|
||||
stateFile *statefile.File
|
||||
schemas *terraform.Schemas
|
||||
wantExact bool
|
||||
wantString string
|
||||
}{
|
||||
"plan file": {
|
||||
testPlan(t),
|
||||
nil,
|
||||
testSchemas(),
|
||||
false,
|
||||
"# test_resource.foo will be created",
|
||||
},
|
||||
"statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: testState(),
|
||||
},
|
||||
testSchemas(),
|
||||
false,
|
||||
"# test_resource.foo:",
|
||||
},
|
||||
"empty statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: states.NewState(),
|
||||
},
|
||||
testSchemas(),
|
||||
true,
|
||||
"\n",
|
||||
},
|
||||
"nothing": {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
"No state.\n",
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
view.Configure(&arguments.View{NoColor: true})
|
||||
v := NewShow(arguments.ViewHuman, view)
|
||||
|
||||
code := v.Display(nil, testCase.plan, testCase.stateFile, testCase.schemas)
|
||||
if code != 0 {
|
||||
t.Errorf("expected 0 return code, got %d", code)
|
||||
}
|
||||
|
||||
output := done(t)
|
||||
got := output.Stdout()
|
||||
want := testCase.wantString
|
||||
if (testCase.wantExact && got != want) || (!testCase.wantExact && !strings.Contains(got, want)) {
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
plan *plans.Plan
|
||||
stateFile *statefile.File
|
||||
}{
|
||||
"plan file": {
|
||||
testPlan(t),
|
||||
nil,
|
||||
},
|
||||
"statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: testState(),
|
||||
},
|
||||
},
|
||||
"empty statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: states.NewState(),
|
||||
},
|
||||
},
|
||||
"nothing": {
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show")
|
||||
defer configCleanup()
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
view.Configure(&arguments.View{NoColor: true})
|
||||
v := NewShow(arguments.ViewJSON, view)
|
||||
|
||||
schemas := &terraform.Schemas{
|
||||
Providers: map[addrs.Provider]*terraform.ProviderSchema{
|
||||
addrs.NewDefaultProvider("test"): {
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"foo": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code := v.Display(config, testCase.plan, testCase.stateFile, schemas)
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("expected 0 return code, got %d", code)
|
||||
}
|
||||
|
||||
// Make sure the result looks like JSON; we comprehensively test
|
||||
// the structure of this output in the command package tests.
|
||||
var result map[string]interface{}
|
||||
got := done(t).All()
|
||||
t.Logf("output: %s", got)
|
||||
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testState returns a test State structure.
|
||||
func testState() *states.State {
|
||||
return states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"id":"bar","foo":"value"}`),
|
||||
Status: states.ObjectReady,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
// DeepCopy is used here to ensure our synthetic state matches exactly
|
||||
// with a state that will have been copied during the command
|
||||
// operation, and all fields have been copied correctly.
|
||||
}).DeepCopy()
|
||||
}
|
||||
3
internal/command/views/testdata/show/main.tf
vendored
Normal file
3
internal/command/views/testdata/show/main.tf
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
resource "test_resource" "foo" {
|
||||
foo = "value"
|
||||
}
|
||||
Reference in New Issue
Block a user