From 9d0d564ec7c041d859ea4eb0b504fddfcc9b3bda Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 14 Mar 2019 14:52:07 -0700 Subject: [PATCH] `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 --- command/jsonplan/plan.go | 8 +- command/jsonprovider/provider.go | 16 +-- command/jsonprovider/provider_test.go | 6 +- command/jsonstate/state.go | 15 ++- command/providers_schema_test.go | 118 ++++++++++-------- command/show.go | 14 +-- command/show_test.go | 84 +++++++++++++ .../providers-schema/{ => basic}/output.json | 0 .../providers-schema/{ => basic}/provider.tf | 0 .../providers-schema/empty/main.tf | 0 .../providers-schema/empty/output.json | 3 + .../show-json-state/empty/output.json | 3 + 12 files changed, 184 insertions(+), 83 deletions(-) rename command/test-fixtures/providers-schema/{ => basic}/output.json (100%) rename command/test-fixtures/providers-schema/{ => basic}/provider.tf (100%) create mode 100644 command/test-fixtures/providers-schema/empty/main.tf create mode 100644 command/test-fixtures/providers-schema/empty/output.json create mode 100644 command/test-fixtures/show-json-state/empty/output.json diff --git a/command/jsonplan/plan.go b/command/jsonplan/plan.go index 14107baa0..f6cf36668 100644 --- a/command/jsonplan/plan.go +++ b/command/jsonplan/plan.go @@ -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 diff --git a/command/jsonprovider/provider.go b/command/jsonprovider/provider.go index a6db90e93..5c45fd472 100644 --- a/command/jsonprovider/provider.go +++ b/command/jsonprovider/provider.go @@ -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, diff --git a/command/jsonprovider/provider_test.go b/command/jsonprovider/provider_test.go index 57ca05bb3..64b21d746 100644 --- a/command/jsonprovider/provider_test.go +++ b/command/jsonprovider/provider_test.go @@ -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{ diff --git a/command/jsonstate/state.go b/command/jsonstate/state.go index 5718129de..68ed520a0 100644 --- a/command/jsonstate/state.go +++ b/command/jsonstate/state.go @@ -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 } diff --git a/command/providers_schema_test.go b/command/providers_schema_test.go index 78bbd2df8..fbc44f215 100644 --- a/command/providers_schema_test.go +++ b/command/providers_schema_test.go @@ -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 { diff --git a/command/show.go b/command/show.go index 2a32a120d..b43a4a1c1 100644 --- a/command/show.go +++ b/command/show.go @@ -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(), diff --git a/command/show_test.go b/command/show_test.go index 9ea3c5c87..30f5bba71 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -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)) } diff --git a/command/test-fixtures/providers-schema/output.json b/command/test-fixtures/providers-schema/basic/output.json similarity index 100% rename from command/test-fixtures/providers-schema/output.json rename to command/test-fixtures/providers-schema/basic/output.json diff --git a/command/test-fixtures/providers-schema/provider.tf b/command/test-fixtures/providers-schema/basic/provider.tf similarity index 100% rename from command/test-fixtures/providers-schema/provider.tf rename to command/test-fixtures/providers-schema/basic/provider.tf diff --git a/command/test-fixtures/providers-schema/empty/main.tf b/command/test-fixtures/providers-schema/empty/main.tf new file mode 100644 index 000000000..e69de29bb diff --git a/command/test-fixtures/providers-schema/empty/output.json b/command/test-fixtures/providers-schema/empty/output.json new file mode 100644 index 000000000..d457a374f --- /dev/null +++ b/command/test-fixtures/providers-schema/empty/output.json @@ -0,0 +1,3 @@ +{ + "format_version": "0.1" +} \ No newline at end of file diff --git a/command/test-fixtures/show-json-state/empty/output.json b/command/test-fixtures/show-json-state/empty/output.json new file mode 100644 index 000000000..d457a374f --- /dev/null +++ b/command/test-fixtures/show-json-state/empty/output.json @@ -0,0 +1,3 @@ +{ + "format_version": "0.1" +} \ No newline at end of file