diff --git a/command/jsonconfig/config.go b/command/jsonconfig/config.go index bc83565a3..ad9d473b5 100644 --- a/command/jsonconfig/config.go +++ b/command/jsonconfig/config.go @@ -114,9 +114,9 @@ func marshalProviderConfigs( return } - for _, pc := range c.Module.ProviderConfigs { + for k, pc := range c.Module.ProviderConfigs { schema := schemas.ProviderConfig(pc.Name) - m[pc.Name] = providerConfig{ + m[k] = providerConfig{ Name: pc.Name, Alias: pc.Alias, ModuleAddress: c.Path.String(), diff --git a/command/jsonplan/plan.go b/command/jsonplan/plan.go index c5cbeb19b..6e07f0b0e 100644 --- a/command/jsonplan/plan.go +++ b/command/jsonplan/plan.go @@ -146,6 +146,7 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform } var before, after []byte + var afterUnknown cty.Value if changeV.Before != cty.NilVal { before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) if err != nil { @@ -158,31 +159,38 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform if err != nil { return err } + afterUnknown, _ = cty.Transform(changeV.After, func(path cty.Path, val cty.Value) (cty.Value, error) { + if val.IsNull() { + return cty.False, nil + } + + if !val.Type().IsPrimitiveType() { + return val, nil // just pass through non-primitives; they already contain our transform results + } + + if val.IsKnown() { + return cty.False, nil + } + + return cty.True, nil + }) + } else { + filteredAfter := omitUnknowns(changeV.After) + after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) + if err != nil { + return err + } + afterUnknown = unknownAsBool(changeV.After) } } - afterUnknown, _ := cty.Transform(changeV.After, func(path cty.Path, val cty.Value) (cty.Value, error) { - if val.IsNull() { - return cty.False, nil - } - - if !val.Type().IsPrimitiveType() { - return val, nil // just pass through non-primitives; they already contain our transform results - } - - if val.IsKnown() { - // null rather than false here so that known values - // don't appear at all in JSON serialization of our result - return cty.False, nil - } - - return cty.True, nil - }) - - a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) + a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) + if err != nil { + return err + } r.Change = change{ - Actions: []string{rc.Action.String()}, + Actions: actionString(rc.Action.String()), Before: json.RawMessage(before), After: json.RawMessage(after), AfterUnknown: a, @@ -253,7 +261,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error { a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) c := change{ - Actions: []string{oc.Action.String()}, + Actions: actionString(oc.Action.String()), Before: json.RawMessage(before), After: json.RawMessage(after), AfterUnknown: a, @@ -282,3 +290,170 @@ func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.S return nil } + +// omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, +// omitting any unknowns. +func omitUnknowns(val cty.Value) cty.Value { + if val.IsWhollyKnown() { + return val + } + + ty := val.Type() + switch { + case val.IsNull(): + return val + case !val.IsKnown(): + return cty.NilVal + case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): + if val.LengthInt() == 0 { + return val + } + + var vals []cty.Value + it := val.ElementIterator() + for it.Next() { + _, v := it.Element() + newVal := omitUnknowns(v) + if newVal != cty.NilVal { + vals = append(vals, newVal) + } else if newVal == cty.NilVal && ty.IsListType() { + // list length may be significant, so we will turn unknowns into nulls + vals = append(vals, cty.NullVal(v.Type())) + } + } + if len(vals) == 0 { + return cty.NilVal + } + switch { + case ty.IsListType(): + return cty.ListVal(vals) + case ty.IsTupleType(): + return cty.TupleVal(vals) + default: + return cty.SetVal(vals) + } + case ty.IsMapType() || ty.IsObjectType(): + var length int + switch { + case ty.IsMapType(): + length = val.LengthInt() + default: + length = len(val.Type().AttributeTypes()) + } + if length == 0 { + // If there are no elements then we can't have unknowns + return val + } + vals := make(map[string]cty.Value) + it := val.ElementIterator() + for it.Next() { + k, v := it.Element() + newVal := omitUnknowns(v) + if newVal != cty.NilVal { + vals[k.AsString()] = newVal + } + } + + if len(vals) == 0 { + return cty.NilVal + } + + switch { + case ty.IsMapType(): + return cty.MapVal(vals) + default: + return cty.ObjectVal(vals) + } + } + + return val +} + +// recursively iterate through a cty.Value, replacing known values (including +// null) with cty.True and unknown values with cty.False. +// +// TODO: +// In the future, we may choose to only return unknown values. At that point, +// this will need to convert lists/sets into tuples and maps into objects, so +// that the result will have a valid type. +func unknownAsBool(val cty.Value) cty.Value { + ty := val.Type() + switch { + case val.IsNull(): + return cty.False + case !val.IsKnown(): + if ty.IsPrimitiveType() || ty.Equals(cty.DynamicPseudoType) { + return cty.True + } + fallthrough + case ty.IsPrimitiveType(): + return cty.BoolVal(!val.IsKnown()) + case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): + length := val.LengthInt() + if length == 0 { + // If there are no elements then we can't have unknowns + return cty.False + } + vals := make([]cty.Value, 0, length) + it := val.ElementIterator() + for it.Next() { + _, v := it.Element() + vals = append(vals, unknownAsBool(v)) + } + switch { + case ty.IsListType(): + return cty.ListVal(vals) + case ty.IsTupleType(): + return cty.TupleVal(vals) + default: + return cty.SetVal(vals) + } + case ty.IsMapType() || ty.IsObjectType(): + var length int + switch { + case ty.IsMapType(): + length = val.LengthInt() + default: + length = len(val.Type().AttributeTypes()) + } + if length == 0 { + // If there are no elements then we can't have unknowns + return cty.False + } + vals := make(map[string]cty.Value) + it := val.ElementIterator() + for it.Next() { + k, v := it.Element() + vals[k.AsString()] = unknownAsBool(v) + } + switch { + case ty.IsMapType(): + return cty.MapVal(vals) + default: + return cty.ObjectVal(vals) + } + } + + return val +} + +func actionString(action string) []string { + switch { + case action == "NoOp": + return []string{"no-op"} + case action == "Create": + return []string{"create"} + case action == "Delete": + return []string{"delete"} + case action == "Update": + return []string{"update"} + case action == "CreateThenDelete": + return []string{"create", "delete"} + case action == "Read": + return []string{"read"} + case action == "DeleteThenCreate": + return []string{"delete", "create"} + default: + return []string{action} + } +} diff --git a/command/jsonplan/plan_test.go b/command/jsonplan/plan_test.go new file mode 100644 index 000000000..254021e43 --- /dev/null +++ b/command/jsonplan/plan_test.go @@ -0,0 +1,214 @@ +package jsonplan + +import ( + "reflect" + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestOmitUnknowns(t *testing.T) { + tests := []struct { + Input cty.Value + Want cty.Value + }{ + { + cty.StringVal("hello"), + cty.StringVal("hello"), + }, + { + cty.NullVal(cty.String), + cty.NullVal(cty.String), + }, + { + cty.UnknownVal(cty.String), + cty.NilVal, + }, + { + cty.ListValEmpty(cty.String), + cty.ListValEmpty(cty.String), + }, + { + cty.ListVal([]cty.Value{cty.StringVal("hello")}), + cty.ListVal([]cty.Value{cty.StringVal("hello")}), + }, + { + cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), + cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), + }, + { + cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), + cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), + }, + { + cty.ListVal([]cty.Value{cty.StringVal("hello")}), + cty.ListVal([]cty.Value{cty.StringVal("hello")}), + }, + // + { + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.UnknownVal(cty.String)}), + cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.NullVal(cty.String), + }), + }, + { + cty.MapVal(map[string]cty.Value{ + "hello": cty.True, + "world": cty.UnknownVal(cty.Bool), + }), + cty.MapVal(map[string]cty.Value{ + "hello": cty.True, + }), + }, + { + cty.SetVal([]cty.Value{ + cty.StringVal("dev"), + cty.StringVal("foo"), + cty.StringVal("stg"), + cty.UnknownVal(cty.String), + }), + cty.SetVal([]cty.Value{ + cty.StringVal("dev"), + cty.StringVal("foo"), + cty.StringVal("stg"), + }), + }, + } + + for _, test := range tests { + got := omitUnknowns(test.Input) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", + test.Input, got, test.Want, + ) + } + } +} + +func TestUnknownAsBool(t *testing.T) { + tests := []struct { + Input cty.Value + Want cty.Value + }{ + { + cty.StringVal("hello"), + cty.False, + }, + { + cty.NullVal(cty.String), + cty.False, + }, + { + cty.UnknownVal(cty.String), + cty.True, + }, + + { + cty.NullVal(cty.DynamicPseudoType), + cty.False, + }, + { + cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})), + cty.False, + }, + { + cty.DynamicVal, + cty.True, + }, + + { + cty.ListValEmpty(cty.String), + cty.False, + }, + { + cty.ListVal([]cty.Value{cty.StringVal("hello")}), + cty.ListVal([]cty.Value{cty.False}), + }, + { + cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), + cty.ListVal([]cty.Value{cty.False}), + }, + { + cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), + cty.ListVal([]cty.Value{cty.True}), + }, + { + cty.SetValEmpty(cty.String), + cty.False, + }, + { + cty.SetVal([]cty.Value{cty.StringVal("hello")}), + cty.SetVal([]cty.Value{cty.False}), + }, + { + cty.SetVal([]cty.Value{cty.NullVal(cty.String)}), + cty.SetVal([]cty.Value{cty.False}), + }, + { + cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}), + cty.SetVal([]cty.Value{cty.True}), + }, + { + cty.EmptyTupleVal, + cty.False, + }, + { + cty.TupleVal([]cty.Value{cty.StringVal("hello")}), + cty.TupleVal([]cty.Value{cty.False}), + }, + { + cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}), + cty.TupleVal([]cty.Value{cty.False}), + }, + { + cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}), + cty.TupleVal([]cty.Value{cty.True}), + }, + { + cty.MapValEmpty(cty.String), + cty.False, + }, + { + cty.MapVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}), + cty.MapVal(map[string]cty.Value{"greeting": cty.False}), + }, + { + cty.MapVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}), + cty.MapVal(map[string]cty.Value{"greeting": cty.False}), + }, + { + cty.MapVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}), + cty.MapVal(map[string]cty.Value{"greeting": cty.True}), + }, + { + cty.EmptyObjectVal, + cty.False, + }, + { + cty.ObjectVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}), + cty.ObjectVal(map[string]cty.Value{"greeting": cty.False}), + }, + { + cty.ObjectVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}), + cty.ObjectVal(map[string]cty.Value{"greeting": cty.False}), + }, + { + cty.ObjectVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}), + cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}), + }, + } + + for _, test := range tests { + got := unknownAsBool(test.Input) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %#v\ngot: %#v\nwant: %#v", + test.Input, got, test.Want, + ) + } + } +} diff --git a/command/jsonplan/values.go b/command/jsonplan/values.go index a9fd4bf45..5a28433d3 100644 --- a/command/jsonplan/values.go +++ b/command/jsonplan/values.go @@ -33,7 +33,8 @@ func marshalAttributeValues(value cty.Value, schema *configschema.Block) attribu it := value.ElementIterator() for it.Next() { k, v := it.Element() - ret[k.AsString()] = v + vJSON, _ := ctyjson.Marshal(v, v.Type()) + ret[k.AsString()] = json.RawMessage(vJSON) } return ret } @@ -80,9 +81,6 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) { func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (module, error) { var ret module - if changes.Empty() { - return ret, nil - } // build two maps: // module name -> [resource addresses] @@ -126,7 +124,7 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc for _, ri := range ris { r := changes.ResourceInstance(ri) - if r.Action == plans.Delete || r.Action == plans.NoOp { + if r.Action == plans.Delete { continue } diff --git a/command/jsonplan/values_test.go b/command/jsonplan/values_test.go index fd53b9b3a..2245a21d4 100644 --- a/command/jsonplan/values_test.go +++ b/command/jsonplan/values_test.go @@ -42,7 +42,7 @@ func TestMarshalAttributeValues(t *testing.T) { }, }, }, - attributeValues{"foo": cty.StringVal("bar")}, + attributeValues{"foo": json.RawMessage(`"bar"`)}, }, { cty.ObjectVal(map[string]cty.Value{ @@ -56,7 +56,7 @@ func TestMarshalAttributeValues(t *testing.T) { }, }, }, - attributeValues{"foo": cty.NullVal(cty.String)}, + attributeValues{"foo": json.RawMessage(`null`)}, }, { cty.ObjectVal(map[string]cty.Value{ @@ -81,13 +81,8 @@ func TestMarshalAttributeValues(t *testing.T) { }, }, attributeValues{ - "bar": cty.MapVal(map[string]cty.Value{ - "hello": cty.StringVal("world"), - }), - "baz": cty.ListVal([]cty.Value{ - cty.StringVal("goodnight"), - cty.StringVal("moon"), - }), + "bar": json.RawMessage(`{"hello":"world"}`), + "baz": json.RawMessage(`["goodnight","moon"]`), }, }, } @@ -96,7 +91,7 @@ func TestMarshalAttributeValues(t *testing.T) { 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) + t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want) } } } @@ -223,8 +218,9 @@ func TestMarshalPlanResources(t *testing.T) { ProviderName: "test", SchemaVersion: 1, AttributeValues: attributeValues{ - "woozles": cty.StringVal("baz"), - "foozles": cty.StringVal("bat"), + + "woozles": json.RawMessage(`"baz"`), + "foozles": json.RawMessage(`"bat"`), }, }}, Err: false, diff --git a/command/jsonstate/state.go b/command/jsonstate/state.go index 094bbb261..5d6e7ce2a 100644 --- a/command/jsonstate/state.go +++ b/command/jsonstate/state.go @@ -95,7 +95,8 @@ func marshalAttributeValues(value cty.Value, schema *configschema.Block) attribu it := value.ElementIterator() for it.Next() { k, v := it.Element() - ret[k.AsString()] = v + vJSON, _ := ctyjson.Marshal(v, v.Type()) + ret[k.AsString()] = json.RawMessage(vJSON) } return ret } @@ -107,7 +108,7 @@ func newState() *state { } } -// Marshal returns the json encoding of a terraform plan. +// Marshal returns the json encoding of a terraform state. func Marshal(s *states.State, schemas *terraform.Schemas) ([]byte, error) { if s.Empty() { return nil, nil @@ -121,7 +122,7 @@ func Marshal(s *states.State, schemas *terraform.Schemas) ([]byte, error) { return nil, err } - ret, err := json.Marshal(output) + ret, err := json.MarshalIndent(output, "", " ") return ret, err } diff --git a/command/jsonstate/state_test.go b/command/jsonstate/state_test.go index cb27751c1..ee7410416 100644 --- a/command/jsonstate/state_test.go +++ b/command/jsonstate/state_test.go @@ -103,7 +103,7 @@ func TestMarshalAttributeValues(t *testing.T) { }, }, }, - attributeValues{"foo": cty.StringVal("bar")}, + attributeValues{"foo": json.RawMessage(`"bar"`)}, }, { cty.ObjectVal(map[string]cty.Value{ @@ -117,7 +117,7 @@ func TestMarshalAttributeValues(t *testing.T) { }, }, }, - attributeValues{"foo": cty.NullVal(cty.String)}, + attributeValues{"foo": json.RawMessage(`null`)}, }, { cty.ObjectVal(map[string]cty.Value{ @@ -142,13 +142,8 @@ func TestMarshalAttributeValues(t *testing.T) { }, }, attributeValues{ - "bar": cty.MapVal(map[string]cty.Value{ - "hello": cty.StringVal("world"), - }), - "baz": cty.ListVal([]cty.Value{ - cty.StringVal("goodnight"), - cty.StringVal("moon"), - }), + "bar": json.RawMessage(`{"hello":"world"}`), + "baz": json.RawMessage(`["goodnight","moon"]`), }, }, } @@ -157,7 +152,7 @@ func TestMarshalAttributeValues(t *testing.T) { 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) + t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want) } } } @@ -209,8 +204,8 @@ func TestMarshalResources(t *testing.T) { ProviderName: "test", SchemaVersion: 1, AttributeValues: attributeValues{ - "foozles": cty.NullVal(cty.String), - "woozles": cty.StringVal("confuzles"), + "foozles": json.RawMessage(`null`), + "woozles": json.RawMessage(`"confuzles"`), }, }, }, diff --git a/command/show_test.go b/command/show_test.go index a64aa1385..59a7e1964 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -154,7 +154,7 @@ func TestShow_state(t *testing.T) { } } -func TestPlan_json_output(t *testing.T) { +func TestShow_json_output(t *testing.T) { fixtureDir := "test-fixtures/show-json" testDirs, err := ioutil.ReadDir(fixtureDir) if err != nil { diff --git a/command/test-fixtures/show-json/basic-create/main.tf b/command/test-fixtures/show-json/basic-create/main.tf index 52fea375d..3b857b329 100644 --- a/command/test-fixtures/show-json/basic-create/main.tf +++ b/command/test-fixtures/show-json/basic-create/main.tf @@ -1,10 +1,10 @@ variable "test_var" { - default = "bar" + default = "bar" } resource "test_instance" "test" { - ami = var.test_var + ami = var.test_var } output "test" { - value = var.test_var -} \ No newline at end of file + value = var.test_var +} diff --git a/command/test-fixtures/show-json/basic-create/output.json b/command/test-fixtures/show-json/basic-create/output.json index da287aad9..4284d821e 100644 --- a/command/test-fixtures/show-json/basic-create/output.json +++ b/command/test-fixtures/show-json/basic-create/output.json @@ -29,12 +29,15 @@ "deposed": true, "change": { "actions": [ - "Create" + "create" ], "before": null, "after_unknown": { "ami": false, "id": true + }, + "after": { + "ami": "bar" } } } @@ -42,7 +45,7 @@ "output_changes": { "test": { "actions": [ - "Create" + "create" ], "before": null, "after": "bar", diff --git a/command/test-fixtures/show-json/basic-delete/output.json b/command/test-fixtures/show-json/basic-delete/output.json index cce569d0d..297ded0e7 100644 --- a/command/test-fixtures/show-json/basic-delete/output.json +++ b/command/test-fixtures/show-json/basic-delete/output.json @@ -17,8 +17,8 @@ "provider_name": "test", "schema_version": 0, "values": { - "ami": {}, - "id": {} + "ami": "bar", + "id": null } } ] @@ -33,7 +33,7 @@ "deposed": true, "change": { "actions": [ - "Update" + "update" ], "before": { "ami": "foo", @@ -57,7 +57,7 @@ "deposed": true, "change": { "actions": [ - "Delete" + "delete" ], "before": { "ami": "foo", @@ -71,7 +71,7 @@ "output_changes": { "test": { "actions": [ - "Create" + "create" ], "before": null, "after": "bar", diff --git a/command/test-fixtures/show-json/basic-update/output.json b/command/test-fixtures/show-json/basic-update/output.json index ceca5d2b2..d065edaf0 100644 --- a/command/test-fixtures/show-json/basic-update/output.json +++ b/command/test-fixtures/show-json/basic-update/output.json @@ -7,7 +7,22 @@ "value": "bar" } }, - "root_module": {} + "root_module": { + "resources": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider_name": "test", + "schema_version": 0, + "values": { + "ami": "bar", + "id": null + } + } + ] + } }, "resource_changes": [ { @@ -18,7 +33,7 @@ "deposed": true, "change": { "actions": [ - "NoOp" + "no-op" ], "before": { "ami": "bar", @@ -38,7 +53,7 @@ "output_changes": { "test": { "actions": [ - "Create" + "create" ], "before": null, "after": "bar",