command/json*: updating documentation and adding tests (#19944)

A few minor fixes and cleanups as a result of said tests. Hooray for
eventual consistency!
This commit is contained in:
Kristin Laemmert 2019-01-09 08:59:11 -08:00 committed by GitHub
parent e997373f44
commit cdf7cc2449
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 586 additions and 6 deletions

View File

@ -67,7 +67,7 @@ type change struct {
} }
type output struct { type output struct {
Sensitive bool `json:"sensitive,omitempty"` Sensitive bool `json:"sensitive"`
Value json.RawMessage `json:"value,omitempty"` Value json.RawMessage `json:"value,omitempty"`
} }

View File

@ -25,6 +25,9 @@ type stateValues struct {
type attributeValues map[string]interface{} type attributeValues map[string]interface{}
func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues { func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues {
if value == cty.NilVal {
return nil
}
ret := make(attributeValues) ret := make(attributeValues)
it := value.ElementIterator() it := value.ElementIterator()
@ -118,9 +121,7 @@ func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (m
return ret, nil return ret, nil
} }
// marshalPlannedValues returns two resource slices: // marshalPlanResources
// The former has attribute values populated and the latter has true/false in
// place of values indicating whether the values are known at plan time.
func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) { func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) {
var ret []resource var ret []resource

View File

@ -0,0 +1,313 @@
package jsonplan
import (
"encoding/json"
"reflect"
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestMarshalAttributeValues(t *testing.T) {
tests := []struct {
Attr cty.Value
Schema *configschema.Block
Want attributeValues
}{
{
cty.NilVal,
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
nil,
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
attributeValues{"foo": cty.StringVal("bar")},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
attributeValues{"foo": cty.NullVal(cty.String)},
},
{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
"baz": cty.ListVal([]cty.Value{
cty.StringVal("goodnight"),
cty.StringVal("moon"),
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
attributeValues{
"bar": cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
"baz": cty.ListVal([]cty.Value{
cty.StringVal("goodnight"),
cty.StringVal("moon"),
}),
},
},
}
for _, test := range tests {
got := marshalAttributeValues(test.Attr, test.Schema)
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalPlannedOutputs(t *testing.T) {
after, _ := plans.NewDynamicValue(cty.StringVal("after"), cty.DynamicPseudoType)
tests := []struct {
Changes *plans.Changes
Want map[string]output
Err bool
}{
{
&plans.Changes{},
nil,
false,
},
{
&plans.Changes{
Outputs: []*plans.OutputChangeSrc{
{
Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
After: after,
},
Sensitive: false,
},
},
},
map[string]output{
"bar": {
Sensitive: false,
Value: json.RawMessage(`"after"`),
},
},
false,
},
{ // Delete action
&plans.Changes{
Outputs: []*plans.OutputChangeSrc{
{
Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
},
Sensitive: false,
},
},
},
map[string]output{},
false,
},
}
for _, test := range tests {
got, err := marshalPlannedOutputs(test.Changes)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalPlanResources(t *testing.T) {
tests := []struct {
Action plans.Action
Before cty.Value
After cty.Value
Want []resource
Err bool
}{
{
Action: plans.Create,
Before: cty.NullVal(cty.EmptyObject),
After: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.UnknownVal(cty.String),
"foozles": cty.UnknownVal(cty.String),
}),
Want: []resource{resource{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
Name: "example",
Index: addrs.InstanceKey(nil),
ProviderName: "test",
SchemaVersion: 1,
AttributeValues: attributeValues(nil),
}},
Err: false,
},
{
Action: plans.Delete,
Before: cty.NullVal(cty.EmptyObject),
After: cty.NilVal,
Want: nil,
Err: false,
},
{
Action: plans.Update,
Before: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("foo"),
"foozles": cty.StringVal("bar"),
}),
After: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("baz"),
"foozles": cty.StringVal("bat"),
}),
Want: []resource{resource{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
Name: "example",
Index: addrs.InstanceKey(nil),
ProviderName: "test",
SchemaVersion: 1,
AttributeValues: attributeValues{
"woozles": cty.StringVal("baz"),
"foozles": cty.StringVal("bat"),
},
}},
Err: false,
},
}
for _, test := range tests {
before, err := plans.NewDynamicValue(test.Before, test.Before.Type())
if err != nil {
t.Fatal(err)
}
after, err := plans.NewDynamicValue(test.After, test.After.Type())
if err != nil {
t.Fatal(err)
}
testChange := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "example",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance),
ChangeSrc: plans.ChangeSrc{
Action: test.Action,
Before: before,
After: after,
},
},
},
}
ris := testResourceAddrs()
got, err := marshalPlanResources(testChange, ris, testSchemas())
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func testSchemas() *terraform.Schemas {
return &terraform.Schemas{
Providers: map[string]*terraform.ProviderSchema{
"test": &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"woozles": {Type: cty.String, Optional: true, Computed: true},
"foozles": {Type: cty.String, Optional: true},
},
},
},
ResourceTypeSchemaVersions: map[string]uint64{
"test_thing": 1,
},
},
},
}
}
func testResourceAddrs() []addrs.AbsResourceInstance {
return []addrs.AbsResourceInstance{
mustAddr("test_thing.example"),
}
}
func mustAddr(str string) addrs.AbsResourceInstance {
addr, diags := addrs.ParseAbsResourceInstanceStr(str)
if diags.HasErrors() {
panic(diags.Err())
}
return addr
}

View File

@ -33,7 +33,7 @@ type stateValues struct {
} }
type output struct { type output struct {
Sensitive bool `json:"sensitive,omitempty"` Sensitive bool `json:"sensitive"`
Value json.RawMessage `json:"value,omitempty"` Value json.RawMessage `json:"value,omitempty"`
} }
@ -86,6 +86,9 @@ type resource struct {
type attributeValues map[string]interface{} type attributeValues map[string]interface{}
func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues { func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues {
if value == cty.NilVal {
return nil
}
ret := make(attributeValues) ret := make(attributeValues)
it := value.ElementIterator() it := value.ElementIterator()
@ -229,7 +232,7 @@ func marshalResources(resources map[string]*states.Resource, schemas *terraform.
Address: r.Addr.String(), Address: r.Addr.String(),
Type: r.Addr.Type, Type: r.Addr.Type,
Name: r.Addr.Name, Name: r.Addr.Name,
ProviderName: r.ProviderConfig.ProviderConfig.String(), ProviderName: r.ProviderConfig.ProviderConfig.StringCompact(),
} }
switch r.Addr.Mode { switch r.Addr.Mode {

View File

@ -0,0 +1,253 @@
package jsonstate
import (
"encoding/json"
"reflect"
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestMarshalOutputs(t *testing.T) {
tests := []struct {
Outputs map[string]*states.OutputValue
Want map[string]output
Err bool
}{
{
nil,
nil,
false,
},
{
map[string]*states.OutputValue{
"test": {
Sensitive: true,
Value: cty.StringVal("sekret"),
},
},
map[string]output{
"test": {
Sensitive: true,
Value: json.RawMessage(`"sekret"`),
},
},
false,
},
{
map[string]*states.OutputValue{
"test": {
Sensitive: false,
Value: cty.StringVal("not_so_sekret"),
},
},
map[string]output{
"test": {
Sensitive: false,
Value: json.RawMessage(`"not_so_sekret"`),
},
},
false,
},
}
for _, test := range tests {
got, err := marshalOutputs(test.Outputs)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
// printing the output isn't terribly useful, but it does help indicate which test case failed
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalAttributeValues(t *testing.T) {
tests := []struct {
Attr cty.Value
Schema *configschema.Block
Want attributeValues
}{
{
cty.NilVal,
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
nil,
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
attributeValues{"foo": cty.StringVal("bar")},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
attributeValues{"foo": cty.NullVal(cty.String)},
},
{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
"baz": cty.ListVal([]cty.Value{
cty.StringVal("goodnight"),
cty.StringVal("moon"),
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
attributeValues{
"bar": cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
"baz": cty.ListVal([]cty.Value{
cty.StringVal("goodnight"),
cty.StringVal("moon"),
}),
},
},
}
for _, test := range tests {
got := marshalAttributeValues(test.Attr, test.Schema)
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalResources(t *testing.T) {
tests := []struct {
Resources map[string]*states.Resource
Schemas *terraform.Schemas
Want []resource
Err bool
}{
{
nil,
nil,
nil,
false,
},
{
map[string]*states.Resource{
"test_thing.baz": {
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "bar",
},
EachMode: states.EachList,
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
addrs.IntKey(0): {
Current: &states.ResourceInstanceObjectSrc{
SchemaVersion: 1,
Status: states.ObjectReady,
AttrsJSON: []byte(`{"woozles":"confuzles"}`),
},
},
},
ProviderConfig: addrs.ProviderConfig{
Type: "test",
}.Absolute(addrs.RootModuleInstance),
},
},
testSchemas(),
[]resource{
resource{
Address: "test_thing.bar",
Mode: "managed",
Type: "test_thing",
Name: "bar",
Index: addrs.IntKey(0),
ProviderName: "test",
SchemaVersion: 1,
AttributeValues: attributeValues{
"foozles": cty.NullVal(cty.String),
"woozles": cty.StringVal("confuzles"),
},
},
},
false,
},
}
for _, test := range tests {
got, err := marshalResources(test.Resources, test.Schemas)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func testSchemas() *terraform.Schemas {
return &terraform.Schemas{
Providers: map[string]*terraform.ProviderSchema{
"test": &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"woozles": {Type: cty.String, Optional: true, Computed: true},
"foozles": {Type: cty.String, Optional: true},
},
},
},
},
},
}
}

View File

@ -13,6 +13,15 @@ from a state or plan file. This can be used to inspect a plan to ensure
that the planned operations are expected, or to inspect the current state that the planned operations are expected, or to inspect the current state
as Terraform sees it. as Terraform sees it.
Machine-readable output is generated by adding the `-json` command-line
flag. This is only available when using `show` with a path to a Terraform
plan file.
**NOTE**
When using the `-json` command-line flag, any sensitive values in
terraform state will be displayed in plain text. For more information, see
[_Sensitive Data in State_](/docs/state/sensitive-data.html).
## Usage ## Usage
Usage: `terraform show [options] [path]` Usage: `terraform show [options] [path]`
@ -24,3 +33,4 @@ The command-line flags are all optional. The list of available flags are:
* `-no-color` - Disables output with coloring * `-no-color` - Disables output with coloring
* `-json` - Displays machine-readable output from a plan file