command/jsonplan: Don't panic with mixtures of known/unknown/empty

The omitUnknowns and unknownAsBool functions were previously trying hard
to preserve the same collection types in the output as they had in the
input, by attempting to keep everything matched up so that the results
would be valid.

Unfortunately, this turns out to be a harder problem than we originally
thought: it was possible for a collection value going in to produce
inconsistent element types out (and thus a panic) in the following
situations:
- when a collection with mixed known and unknown values was passed in
  to omitUnknowns.
- when a collection of collections where the inner collections are a
  mixture of empty and not empty in unknownAsNull.

The results of these functions are only used to marshal to JSON anyway,
and JSON serialization can't distinguish between the three sequence types
or the two mapping types, so in practice we can just standardize on
converting all sequences to tuple and all mappings to object here and not
change the resulting output at all, and then we don't have to worry about
making sure all of the inner types get preserved exactly.

A nice consequence of that relaxation is that we can now do what we
originally wanted to do with unknownAsBool, and omit map keys and
object attributes altogether if their values would've been false,
producing a much more compact result. This is easiest to do now when
there's only one known user of this JSON plan output, and we know that
user will treat both false and omitted as the same here.
This commit is contained in:
Martin Atkins 2019-05-24 16:30:58 -07:00
parent 3865b74780
commit d512584497
5 changed files with 167 additions and 143 deletions

View File

@ -351,22 +351,21 @@ func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.S
// omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, // omitUnknowns recursively walks the src cty.Value and returns a new cty.Value,
// omitting any unknowns. // omitting any unknowns.
//
// The result also normalizes some types: all sequence types are turned into
// tuple types and all mapping types are converted to object types, since we
// assume the result of this is just going to be serialized as JSON (and thus
// lose those distinctions) anyway.
func omitUnknowns(val cty.Value) cty.Value { func omitUnknowns(val cty.Value) cty.Value {
if val.IsWhollyKnown() {
return val
}
ty := val.Type() ty := val.Type()
switch { switch {
case val.IsNull(): case val.IsNull():
return val return val
case !val.IsKnown(): case !val.IsKnown():
return cty.NilVal return cty.NilVal
case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): case ty.IsPrimitiveType():
if val.LengthInt() == 0 {
return val return val
} case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
var vals []cty.Value var vals []cty.Value
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
@ -379,29 +378,12 @@ func omitUnknowns(val cty.Value) cty.Value {
vals = append(vals, cty.NullVal(v.Type())) vals = append(vals, cty.NullVal(v.Type()))
} }
} }
if len(vals) == 0 { // We use tuple types always here, because the work we did above
return cty.NilVal // may have caused the individual elements to have different types,
} // and we're doing this work to produce JSON anyway and JSON marshalling
switch { // represents all of these sequence types as an array.
case ty.IsListType():
return cty.ListVal(vals)
case ty.IsTupleType():
return cty.TupleVal(vals) return cty.TupleVal(vals)
default:
return cty.SetVal(vals)
}
case ty.IsMapType() || ty.IsObjectType(): 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) vals := make(map[string]cty.Value)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
@ -411,29 +393,24 @@ func omitUnknowns(val cty.Value) cty.Value {
vals[k.AsString()] = newVal vals[k.AsString()] = newVal
} }
} }
// We use object types always here, because the work we did above
if len(vals) == 0 { // may have caused the individual elements to have different types,
return cty.NilVal // and we're doing this work to produce JSON anyway and JSON marshalling
} // represents both of these mapping types as an object.
switch {
case ty.IsMapType():
return cty.MapVal(vals)
default:
return cty.ObjectVal(vals) return cty.ObjectVal(vals)
default:
// Should never happen, since the above should cover all types
panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val))
} }
} }
return val // recursively iterate through a cty.Value, replacing unknown values (including
} // null) with cty.True and known values with cty.False.
// recursively iterate through a cty.Value, replacing known values (including
// null) with cty.True and unknown values with cty.False.
// //
// TODO: // The result also normalizes some types: all sequence types are turned into
// In the future, we may choose to only return unknown values. At that point, // tuple types and all mapping types are converted to object types, since we
// this will need to convert lists/sets into tuples and maps into objects, so // assume the result of this is just going to be serialized as JSON (and thus
// that the result will have a valid type. // lose those distinctions) anyway.
func unknownAsBool(val cty.Value) cty.Value { func unknownAsBool(val cty.Value) cty.Value {
ty := val.Type() ty := val.Type()
switch { switch {
@ -450,7 +427,7 @@ func unknownAsBool(val cty.Value) cty.Value {
length := val.LengthInt() length := val.LengthInt()
if length == 0 { if length == 0 {
// If there are no elements then we can't have unknowns // If there are no elements then we can't have unknowns
return cty.False return cty.EmptyTupleVal
} }
vals := make([]cty.Value, 0, length) vals := make([]cty.Value, 0, length)
it := val.ElementIterator() it := val.ElementIterator()
@ -458,14 +435,12 @@ func unknownAsBool(val cty.Value) cty.Value {
_, v := it.Element() _, v := it.Element()
vals = append(vals, unknownAsBool(v)) vals = append(vals, unknownAsBool(v))
} }
switch { // The above transform may have changed the types of some of the
case ty.IsListType(): // elements, so we'll always use a tuple here in case we've now made
return cty.ListVal(vals) // different elements have different types. Our ultimate goal is to
case ty.IsTupleType(): // marshal to JSON anyway, and all of these sequence types are
// indistinguishable in JSON.
return cty.TupleVal(vals) return cty.TupleVal(vals)
default:
return cty.SetVal(vals)
}
case ty.IsMapType() || ty.IsObjectType(): case ty.IsMapType() || ty.IsObjectType():
var length int var length int
switch { switch {
@ -476,25 +451,29 @@ func unknownAsBool(val cty.Value) cty.Value {
} }
if length == 0 { if length == 0 {
// If there are no elements then we can't have unknowns // If there are no elements then we can't have unknowns
return cty.False return cty.EmptyObjectVal
} }
vals := make(map[string]cty.Value) vals := make(map[string]cty.Value)
it := val.ElementIterator() it := val.ElementIterator()
for it.Next() { for it.Next() {
k, v := it.Element() k, v := it.Element()
vAsBool := unknownAsBool(v)
if !vAsBool.RawEquals(cty.False) { // all of the "false"s for known values for more compact serialization
vals[k.AsString()] = unknownAsBool(v) vals[k.AsString()] = unknownAsBool(v)
} }
switch { }
case ty.IsMapType(): // The above transform may have changed the types of some of the
return cty.MapVal(vals) // elements, so we'll always use an object here in case we've now made
default: // different elements have different types. Our ultimate goal is to
// marshal to JSON anyway, and all of these mapping types are
// indistinguishable in JSON.
return cty.ObjectVal(vals) return cty.ObjectVal(vals)
default:
// Should never happen, since the above should cover all types
panic(fmt.Sprintf("unknownAsBool cannot handle %#v", val))
} }
} }
return val
}
func actionString(action string) []string { func actionString(action string) []string {
switch { switch {
case action == "NoOp": case action == "NoOp":

View File

@ -26,30 +26,30 @@ func TestOmitUnknowns(t *testing.T) {
}, },
{ {
cty.ListValEmpty(cty.String), cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String), cty.EmptyTupleVal,
}, },
{ {
cty.ListVal([]cty.Value{cty.StringVal("hello")}), cty.ListVal([]cty.Value{cty.StringVal("hello")}),
cty.ListVal([]cty.Value{cty.StringVal("hello")}), cty.TupleVal([]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.NullVal(cty.String)}), cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
}, },
{ {
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), cty.TupleVal([]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.TupleVal([]cty.Value{cty.StringVal("hello")}),
}, },
// //
{ {
cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{
cty.StringVal("hello"), cty.StringVal("hello"),
cty.UnknownVal(cty.String)}), cty.UnknownVal(cty.String)}),
cty.ListVal([]cty.Value{ cty.TupleVal([]cty.Value{
cty.StringVal("hello"), cty.StringVal("hello"),
cty.NullVal(cty.String), cty.NullVal(cty.String),
}), }),
@ -59,7 +59,7 @@ func TestOmitUnknowns(t *testing.T) {
"hello": cty.True, "hello": cty.True,
"world": cty.UnknownVal(cty.Bool), "world": cty.UnknownVal(cty.Bool),
}), }),
cty.MapVal(map[string]cty.Value{ cty.ObjectVal(map[string]cty.Value{
"hello": cty.True, "hello": cty.True,
}), }),
}, },
@ -70,12 +70,28 @@ func TestOmitUnknowns(t *testing.T) {
cty.StringVal("stg"), cty.StringVal("stg"),
cty.UnknownVal(cty.String), cty.UnknownVal(cty.String),
}), }),
cty.SetVal([]cty.Value{ cty.TupleVal([]cty.Value{
cty.StringVal("dev"), cty.StringVal("dev"),
cty.StringVal("foo"), cty.StringVal("foo"),
cty.StringVal("stg"), cty.StringVal("stg"),
}), }),
}, },
{
cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("known"),
}),
}),
cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("known"),
}),
cty.EmptyObjectVal,
}),
},
} }
for _, test := range tests { for _, test := range tests {
@ -122,39 +138,39 @@ func TestUnknownAsBool(t *testing.T) {
{ {
cty.ListValEmpty(cty.String), cty.ListValEmpty(cty.String),
cty.False, cty.EmptyTupleVal,
}, },
{ {
cty.ListVal([]cty.Value{cty.StringVal("hello")}), cty.ListVal([]cty.Value{cty.StringVal("hello")}),
cty.ListVal([]cty.Value{cty.False}), cty.TupleVal([]cty.Value{cty.False}),
}, },
{ {
cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
cty.ListVal([]cty.Value{cty.False}), cty.TupleVal([]cty.Value{cty.False}),
}, },
{ {
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
cty.ListVal([]cty.Value{cty.True}), cty.TupleVal([]cty.Value{cty.True}),
}, },
{ {
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
cty.False, cty.EmptyTupleVal,
}, },
{ {
cty.SetVal([]cty.Value{cty.StringVal("hello")}), cty.SetVal([]cty.Value{cty.StringVal("hello")}),
cty.SetVal([]cty.Value{cty.False}), cty.TupleVal([]cty.Value{cty.False}),
}, },
{ {
cty.SetVal([]cty.Value{cty.NullVal(cty.String)}), cty.SetVal([]cty.Value{cty.NullVal(cty.String)}),
cty.SetVal([]cty.Value{cty.False}), cty.TupleVal([]cty.Value{cty.False}),
}, },
{ {
cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}), cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}),
cty.SetVal([]cty.Value{cty.True}), cty.TupleVal([]cty.Value{cty.True}),
}, },
{ {
cty.EmptyTupleVal, cty.EmptyTupleVal,
cty.False, cty.EmptyTupleVal,
}, },
{ {
cty.TupleVal([]cty.Value{cty.StringVal("hello")}), cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
@ -170,36 +186,70 @@ func TestUnknownAsBool(t *testing.T) {
}, },
{ {
cty.MapValEmpty(cty.String), cty.MapValEmpty(cty.String),
cty.False, cty.EmptyObjectVal,
}, },
{ {
cty.MapVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}), cty.MapVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
cty.MapVal(map[string]cty.Value{"greeting": cty.False}), cty.EmptyObjectVal,
}, },
{ {
cty.MapVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}), cty.MapVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
cty.MapVal(map[string]cty.Value{"greeting": cty.False}), cty.EmptyObjectVal,
}, },
{ {
cty.MapVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}), cty.MapVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
cty.MapVal(map[string]cty.Value{"greeting": cty.True}), cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
}, },
{ {
cty.EmptyObjectVal, cty.EmptyObjectVal,
cty.False, cty.EmptyObjectVal,
}, },
{ {
cty.ObjectVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}), cty.ObjectVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
cty.ObjectVal(map[string]cty.Value{"greeting": cty.False}), cty.EmptyObjectVal,
}, },
{ {
cty.ObjectVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}), cty.ObjectVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
cty.ObjectVal(map[string]cty.Value{"greeting": cty.False}), cty.EmptyObjectVal,
}, },
{ {
cty.ObjectVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}), cty.ObjectVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}), cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
}, },
{
cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("known"),
}),
}),
cty.TupleVal([]cty.Value{
cty.EmptyObjectVal,
cty.ObjectVal(map[string]cty.Value{
"a": cty.True,
}),
}),
},
{
cty.SetVal([]cty.Value{
cty.MapValEmpty(cty.String),
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("known"),
}),
cty.MapVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
}),
cty.TupleVal([]cty.Value{
cty.EmptyObjectVal,
cty.ObjectVal(map[string]cty.Value{
"a": cty.True,
}),
cty.EmptyObjectVal,
}),
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -166,14 +166,14 @@ func TestMarshalPlannedOutputs(t *testing.T) {
} }
func TestMarshalPlanResources(t *testing.T) { func TestMarshalPlanResources(t *testing.T) {
tests := []struct { tests := map[string]struct {
Action plans.Action Action plans.Action
Before cty.Value Before cty.Value
After cty.Value After cty.Value
Want []resource Want []resource
Err bool Err bool
}{ }{
{ "create with unkonwns": {
Action: plans.Create, Action: plans.Create,
Before: cty.NullVal(cty.EmptyObject), Before: cty.NullVal(cty.EmptyObject),
After: cty.ObjectVal(map[string]cty.Value{ After: cty.ObjectVal(map[string]cty.Value{
@ -188,18 +188,18 @@ func TestMarshalPlanResources(t *testing.T) {
Index: addrs.InstanceKey(nil), Index: addrs.InstanceKey(nil),
ProviderName: "test", ProviderName: "test",
SchemaVersion: 1, SchemaVersion: 1,
AttributeValues: attributeValues(nil), AttributeValues: attributeValues{},
}}, }},
Err: false, Err: false,
}, },
{ "delete": {
Action: plans.Delete, Action: plans.Delete,
Before: cty.NullVal(cty.EmptyObject), Before: cty.NullVal(cty.EmptyObject),
After: cty.NilVal, After: cty.NilVal,
Want: nil, Want: nil,
Err: false, Err: false,
}, },
{ "update without unknowns": {
Action: plans.Update, Action: plans.Update,
Before: cty.ObjectVal(map[string]cty.Value{ Before: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("foo"), "woozles": cty.StringVal("foo"),
@ -227,7 +227,8 @@ func TestMarshalPlanResources(t *testing.T) {
}, },
} }
for _, test := range tests { for name, test := range tests {
t.Run(name, func(t *testing.T) {
before, err := plans.NewDynamicValue(test.Before, test.Before.Type()) before, err := plans.NewDynamicValue(test.Before, test.Before.Type())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -271,6 +272,7 @@ func TestMarshalPlanResources(t *testing.T) {
if !eq { 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)
} }
})
} }
} }

View File

@ -67,7 +67,6 @@
], ],
"before": null, "before": null,
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
}, },
"after": { "after": {
@ -88,7 +87,6 @@
], ],
"before": null, "before": null,
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
}, },
"after": { "after": {
@ -109,7 +107,6 @@
], ],
"before": null, "before": null,
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
}, },
"after": { "after": {

View File

@ -86,7 +86,6 @@
"ami": "bar-var" "ami": "bar-var"
}, },
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
} }
} }
@ -108,7 +107,6 @@
"ami": "baz" "ami": "baz"
}, },
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
} }
} }
@ -130,7 +128,6 @@
"ami": "baz" "ami": "baz"
}, },
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
} }
} }
@ -152,7 +149,6 @@
"ami": "baz" "ami": "baz"
}, },
"after_unknown": { "after_unknown": {
"ami": false,
"id": true "id": true
} }
} }