`terraform show` and `terraform providers schema -json` should return valid json (#20697)

* command/providers schemas: return empty json object if config parses successfully but no providers found
* command/show (state): return an empty object if state is nil
This commit is contained in:
Kristin Laemmert 2019-03-14 14:52:07 -07:00 committed by GitHub
parent 035e89696c
commit 9d0d564ec7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 184 additions and 83 deletions

View File

@ -124,9 +124,11 @@ func Marshal(
}
// output.PriorState
output.PriorState, err = jsonstate.Marshal(sf, stateSchemas)
if err != nil {
return nil, fmt.Errorf("error marshaling prior state: %s", err)
if sf != nil && !sf.State.Empty() {
output.PriorState, err = jsonstate.Marshal(sf, stateSchemas)
if err != nil {
return nil, fmt.Errorf("error marshaling prior state: %s", err)
}
}
// output.Config

View File

@ -13,8 +13,8 @@ const FormatVersion = "0.1"
// providers is the top-level object returned when exporting provider schemas
type providers struct {
FormatVersion string `json:"format_version"`
Schemas map[string]Provider `json:"provider_schemas"`
FormatVersion string `json:"format_version"`
Schemas map[string]*Provider `json:"provider_schemas,omitempty"`
}
type Provider struct {
@ -24,7 +24,7 @@ type Provider struct {
}
func newProviders() *providers {
schemas := make(map[string]Provider)
schemas := make(map[string]*Provider)
return &providers{
FormatVersion: FormatVersion,
Schemas: schemas,
@ -32,10 +32,6 @@ func newProviders() *providers {
}
func Marshal(s *terraform.Schemas) ([]byte, error) {
if len(s.Providers) == 0 {
return nil, nil
}
providers := newProviders()
for k, v := range s.Providers {
@ -46,9 +42,9 @@ func Marshal(s *terraform.Schemas) ([]byte, error) {
return ret, err
}
func marshalProvider(tps *terraform.ProviderSchema) Provider {
func marshalProvider(tps *terraform.ProviderSchema) *Provider {
if tps == nil {
return Provider{}
return &Provider{}
}
var ps *schema
@ -66,7 +62,7 @@ func marshalProvider(tps *terraform.ProviderSchema) Provider {
ds = marshalSchemas(tps.DataSources, tps.ResourceTypeSchemaVersions)
}
return Provider{
return &Provider{
Provider: ps,
ResourceSchemas: rs,
DataSourceSchemas: ds,

View File

@ -14,15 +14,15 @@ import (
func TestMarshalProvider(t *testing.T) {
tests := []struct {
Input *terraform.ProviderSchema
Want Provider
Want *Provider
}{
{
nil,
Provider{},
&Provider{},
},
{
testProvider(),
Provider{
&Provider{
Provider: &schema{
Block: &block{
Attributes: map[string]*attribute{

View File

@ -23,9 +23,9 @@ const FormatVersion = "0.1"
// state is the top-level representation of the json format of a terraform
// state.
type state struct {
FormatVersion string `json:"format_version,omitempty"`
TerraformVersion string `json:"terraform_version"`
Values stateValues `json:"values,omitempty"`
FormatVersion string `json:"format_version,omitempty"`
TerraformVersion string `json:"terraform_version,omitempty"`
Values *stateValues `json:"values,omitempty"`
}
// stateValues is the common representation of resolved values for both the prior
@ -121,14 +121,17 @@ func newState() *state {
// Marshal returns the json encoding of a terraform state.
func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
output := newState()
if sf == nil || sf.State.Empty() {
return nil, nil
ret, err := json.Marshal(output)
return ret, err
}
output := newState()
if sf.TerraformVersion != nil {
output.TerraformVersion = sf.TerraformVersion.String()
}
// output.StateValues
err := output.marshalStateValues(sf.State, schemas)
if err != nil {
@ -155,7 +158,7 @@ func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.S
return err
}
jsonstate.Values = sv
jsonstate.Values = &sv
return nil
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
@ -31,62 +32,73 @@ func TestProvidersSchema_error(t *testing.T) {
func TestProvidersSchema_output(t *testing.T) {
// there's only one test at this time. This can be refactored to have
// multiple test cases in individual directories as needed.
inputDir := "test-fixtures/providers-schema"
td := tempDir(t)
copy.CopyDir(inputDir, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := showFixtureProvider()
ui := new(cli.MockUi)
m := Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
}
// `terrafrom init`
ic := &InitCommand{
Meta: m,
providerInstaller: &mockProviderInstaller{
Providers: map[string][]string{
"test": []string{"1.2.3"},
},
Dir: m.pluginDir(),
},
}
if code := ic.Run([]string{}); code != 0 {
t.Fatalf("init failed\n%s", ui.ErrorWriter)
}
// flush the init output from the mock ui
ui.OutputWriter.Reset()
// `terraform provider schemas` command
pc := &ProvidersSchemaCommand{Meta: m}
if code := pc.Run([]string{"-json"}); code != 0 {
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
}
var got, want providerSchemas
gotString := ui.OutputWriter.String()
json.Unmarshal([]byte(gotString), &got)
wantFile, err := os.Open("output.json")
fixtureDir := "test-fixtures/providers-schema"
testDirs, err := ioutil.ReadDir(fixtureDir)
if err != nil {
t.Fatalf("err: %s", err)
}
defer wantFile.Close()
byteValue, err := ioutil.ReadAll(wantFile)
if err != nil {
t.Fatalf("err: %s", err)
}
json.Unmarshal([]byte(byteValue), &want)
if !cmp.Equal(got, want) {
fmt.Println(gotString)
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
t.Fatal(err)
}
for _, entry := range testDirs {
if !entry.IsDir() {
continue
}
t.Run(entry.Name(), func(t *testing.T) {
td := tempDir(t)
inputDir := filepath.Join(fixtureDir, entry.Name())
copy.CopyDir(inputDir, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := showFixtureProvider()
ui := new(cli.MockUi)
m := Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
}
// `terrafrom init`
ic := &InitCommand{
Meta: m,
providerInstaller: &mockProviderInstaller{
Providers: map[string][]string{
"test": []string{"1.2.3"},
},
Dir: m.pluginDir(),
},
}
if code := ic.Run([]string{}); code != 0 {
t.Fatalf("init failed\n%s", ui.ErrorWriter)
}
// flush the init output from the mock ui
ui.OutputWriter.Reset()
// `terraform provider schemas` command
pc := &ProvidersSchemaCommand{Meta: m}
if code := pc.Run([]string{"-json"}); code != 0 {
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
}
var got, want providerSchemas
gotString := ui.OutputWriter.String()
json.Unmarshal([]byte(gotString), &got)
wantFile, err := os.Open("output.json")
if err != nil {
t.Fatalf("err: %s", err)
}
defer wantFile.Close()
byteValue, err := ioutil.ReadAll(wantFile)
if err != nil {
t.Fatalf("err: %s", err)
}
json.Unmarshal([]byte(byteValue), &want)
if !cmp.Equal(got, want) {
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}
})
}
}
type providerSchemas struct {

View File

@ -140,14 +140,6 @@ func (c *ShowCommand) Run(args []string) int {
}
}
// This is an odd-looking check, because it's ok if we have a plan and an
// empty state, and we've already validated that any command-line arguments
// have been read successfully
if plan == nil && stateFile == nil {
c.Ui.Output("No state.")
return 0
}
if plan != nil {
if jsonOutput == true {
config := ctx.Config()
@ -188,6 +180,8 @@ func (c *ShowCommand) Run(args []string) int {
}
if jsonOutput == true {
// At this point, 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 {
c.Ui.Error(fmt.Sprintf("Failed to marshal state to json: %s", err))
@ -195,6 +189,10 @@ func (c *ShowCommand) Run(args []string) int {
}
c.Ui.Output(string(jsonState))
} else {
if stateFile == nil {
c.Ui.Output("No state.")
return 0
}
c.Ui.Output(format.State(&format.StateOpts{
State: stateFile.State,
Color: c.Colorize(),

View File

@ -2,6 +2,7 @@ package command
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@ -219,6 +220,89 @@ func TestShow_json_output(t *testing.T) {
}
json.Unmarshal([]byte(byteValue), &want)
if !cmp.Equal(got, want) {
fmt.Println(ui.OutputWriter.String())
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}
})
}
}
// similar test as above, without the plan
func TestShow_json_output_state(t *testing.T) {
fixtureDir := "test-fixtures/show-json-state"
testDirs, err := ioutil.ReadDir(fixtureDir)
if err != nil {
t.Fatal(err)
}
for _, entry := range testDirs {
if !entry.IsDir() {
continue
}
t.Run(entry.Name(), func(t *testing.T) {
td := tempDir(t)
inputDir := filepath.Join(fixtureDir, entry.Name())
copy.CopyDir(inputDir, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := showFixtureProvider()
ui := new(cli.MockUi)
m := Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
}
// init
ic := &InitCommand{
Meta: m,
providerInstaller: &mockProviderInstaller{
Providers: map[string][]string{
"test": []string{"1.2.3"},
},
Dir: m.pluginDir(),
},
}
if code := ic.Run([]string{}); code != 0 {
t.Fatalf("init failed\n%s", ui.ErrorWriter)
}
// flush the plan output from the mock ui
ui.OutputWriter.Reset()
sc := &ShowCommand{
Meta: m,
}
if code := sc.Run([]string{"-json"}); code != 0 {
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
}
// compare ui output to wanted output
type state struct {
FormatVersion string `json:"format_version,omitempty"`
TerraformVersion string `json:"terraform_version"`
Values map[string]interface{} `json:"values,omitempty"`
}
var got, want state
gotString := ui.OutputWriter.String()
json.Unmarshal([]byte(gotString), &got)
wantFile, err := os.Open("output.json")
if err != nil {
t.Fatalf("err: %s", err)
}
defer wantFile.Close()
byteValue, err := ioutil.ReadAll(wantFile)
if err != nil {
t.Fatalf("err: %s", err)
}
json.Unmarshal([]byte(byteValue), &want)
if !cmp.Equal(got, want) {
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}

View File

@ -0,0 +1,3 @@
{
"format_version": "0.1"
}

View File

@ -0,0 +1,3 @@
{
"format_version": "0.1"
}