`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:
parent
035e89696c
commit
9d0d564ec7
|
@ -124,9 +124,11 @@ func Marshal(
|
||||||
}
|
}
|
||||||
|
|
||||||
// output.PriorState
|
// output.PriorState
|
||||||
output.PriorState, err = jsonstate.Marshal(sf, stateSchemas)
|
if sf != nil && !sf.State.Empty() {
|
||||||
if err != nil {
|
output.PriorState, err = jsonstate.Marshal(sf, stateSchemas)
|
||||||
return nil, fmt.Errorf("error marshaling prior state: %s", err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling prior state: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// output.Config
|
// output.Config
|
||||||
|
|
|
@ -13,8 +13,8 @@ const FormatVersion = "0.1"
|
||||||
|
|
||||||
// providers is the top-level object returned when exporting provider schemas
|
// providers is the top-level object returned when exporting provider schemas
|
||||||
type providers struct {
|
type providers struct {
|
||||||
FormatVersion string `json:"format_version"`
|
FormatVersion string `json:"format_version"`
|
||||||
Schemas map[string]Provider `json:"provider_schemas"`
|
Schemas map[string]*Provider `json:"provider_schemas,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
|
@ -24,7 +24,7 @@ type Provider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProviders() *providers {
|
func newProviders() *providers {
|
||||||
schemas := make(map[string]Provider)
|
schemas := make(map[string]*Provider)
|
||||||
return &providers{
|
return &providers{
|
||||||
FormatVersion: FormatVersion,
|
FormatVersion: FormatVersion,
|
||||||
Schemas: schemas,
|
Schemas: schemas,
|
||||||
|
@ -32,10 +32,6 @@ func newProviders() *providers {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Marshal(s *terraform.Schemas) ([]byte, error) {
|
func Marshal(s *terraform.Schemas) ([]byte, error) {
|
||||||
if len(s.Providers) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
providers := newProviders()
|
providers := newProviders()
|
||||||
|
|
||||||
for k, v := range s.Providers {
|
for k, v := range s.Providers {
|
||||||
|
@ -46,9 +42,9 @@ func Marshal(s *terraform.Schemas) ([]byte, error) {
|
||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalProvider(tps *terraform.ProviderSchema) Provider {
|
func marshalProvider(tps *terraform.ProviderSchema) *Provider {
|
||||||
if tps == nil {
|
if tps == nil {
|
||||||
return Provider{}
|
return &Provider{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ps *schema
|
var ps *schema
|
||||||
|
@ -66,7 +62,7 @@ func marshalProvider(tps *terraform.ProviderSchema) Provider {
|
||||||
ds = marshalSchemas(tps.DataSources, tps.ResourceTypeSchemaVersions)
|
ds = marshalSchemas(tps.DataSources, tps.ResourceTypeSchemaVersions)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Provider{
|
return &Provider{
|
||||||
Provider: ps,
|
Provider: ps,
|
||||||
ResourceSchemas: rs,
|
ResourceSchemas: rs,
|
||||||
DataSourceSchemas: ds,
|
DataSourceSchemas: ds,
|
||||||
|
|
|
@ -14,15 +14,15 @@ import (
|
||||||
func TestMarshalProvider(t *testing.T) {
|
func TestMarshalProvider(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Input *terraform.ProviderSchema
|
Input *terraform.ProviderSchema
|
||||||
Want Provider
|
Want *Provider
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
nil,
|
nil,
|
||||||
Provider{},
|
&Provider{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testProvider(),
|
testProvider(),
|
||||||
Provider{
|
&Provider{
|
||||||
Provider: &schema{
|
Provider: &schema{
|
||||||
Block: &block{
|
Block: &block{
|
||||||
Attributes: map[string]*attribute{
|
Attributes: map[string]*attribute{
|
||||||
|
|
|
@ -23,9 +23,9 @@ const FormatVersion = "0.1"
|
||||||
// state is the top-level representation of the json format of a terraform
|
// state is the top-level representation of the json format of a terraform
|
||||||
// state.
|
// state.
|
||||||
type state struct {
|
type state struct {
|
||||||
FormatVersion string `json:"format_version,omitempty"`
|
FormatVersion string `json:"format_version,omitempty"`
|
||||||
TerraformVersion string `json:"terraform_version"`
|
TerraformVersion string `json:"terraform_version,omitempty"`
|
||||||
Values stateValues `json:"values,omitempty"`
|
Values *stateValues `json:"values,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// stateValues is the common representation of resolved values for both the prior
|
// 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.
|
// Marshal returns the json encoding of a terraform state.
|
||||||
func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
|
func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
|
||||||
|
output := newState()
|
||||||
|
|
||||||
if sf == nil || sf.State.Empty() {
|
if sf == nil || sf.State.Empty() {
|
||||||
return nil, nil
|
ret, err := json.Marshal(output)
|
||||||
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
output := newState()
|
|
||||||
if sf.TerraformVersion != nil {
|
if sf.TerraformVersion != nil {
|
||||||
output.TerraformVersion = sf.TerraformVersion.String()
|
output.TerraformVersion = sf.TerraformVersion.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// output.StateValues
|
// output.StateValues
|
||||||
err := output.marshalStateValues(sf.State, schemas)
|
err := output.marshalStateValues(sf.State, schemas)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -155,7 +158,7 @@ func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.S
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonstate.Values = sv
|
jsonstate.Values = &sv
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
@ -31,62 +32,73 @@ func TestProvidersSchema_error(t *testing.T) {
|
||||||
func TestProvidersSchema_output(t *testing.T) {
|
func TestProvidersSchema_output(t *testing.T) {
|
||||||
// there's only one test at this time. This can be refactored to have
|
// there's only one test at this time. This can be refactored to have
|
||||||
// multiple test cases in individual directories as needed.
|
// multiple test cases in individual directories as needed.
|
||||||
inputDir := "test-fixtures/providers-schema"
|
fixtureDir := "test-fixtures/providers-schema"
|
||||||
td := tempDir(t)
|
testDirs, err := ioutil.ReadDir(fixtureDir)
|
||||||
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 {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatal(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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
type providerSchemas struct {
|
||||||
|
|
|
@ -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 plan != nil {
|
||||||
if jsonOutput == true {
|
if jsonOutput == true {
|
||||||
config := ctx.Config()
|
config := ctx.Config()
|
||||||
|
@ -188,6 +180,8 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput == true {
|
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)
|
jsonState, err := jsonstate.Marshal(stateFile, schemas)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to marshal state to json: %s", err))
|
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))
|
c.Ui.Output(string(jsonState))
|
||||||
} else {
|
} else {
|
||||||
|
if stateFile == nil {
|
||||||
|
c.Ui.Output("No state.")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
c.Ui.Output(format.State(&format.StateOpts{
|
c.Ui.Output(format.State(&format.StateOpts{
|
||||||
State: stateFile.State,
|
State: stateFile.State,
|
||||||
Color: c.Colorize(),
|
Color: c.Colorize(),
|
||||||
|
|
|
@ -2,6 +2,7 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -219,6 +220,89 @@ func TestShow_json_output(t *testing.T) {
|
||||||
}
|
}
|
||||||
json.Unmarshal([]byte(byteValue), &want)
|
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) {
|
if !cmp.Equal(got, want) {
|
||||||
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
|
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"format_version": "0.1"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"format_version": "0.1"
|
||||||
|
}
|
Loading…
Reference in New Issue