Skip to content

Commit

Permalink
tftypes: Allow DynamicPseudoType with known values (#136)
Browse files Browse the repository at this point in the history
Reference: #94
Reference: #99
Reference: #100
Reference: #128
Reference: #133

Reverts incorrect logic for handling DynamicPseudoType values in `tftypes`. This type information must be preserved when traversing the protocol, as Terraform CLI decodes values based on the schema information. If a concrete value type is used where DynamicPseudoType is expected, Terraform CLI will return errors such as (given an object of 4 attributes, when DynamicPseudoType is expected):

```
│ Error: ["manifest"]: msgpack: invalid code=84 decoding array length
```
  • Loading branch information
bflad authored Jan 13, 2022
1 parent 1014e7b commit 5ad95e8
Show file tree
Hide file tree
Showing 15 changed files with 179 additions and 73 deletions.
3 changes: 3 additions & 0 deletions .changelog/136.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
tftypes: Fixed regression with DynamicPseudoType handling since v0.4.0, allowing usage of known values again and preventing msgpack decoding errors in Terraform CLI
```
3 changes: 3 additions & 0 deletions tftypes/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ func (val1 Value) Diff(val2 Value) ([]ValueDiff, error) {
// if we have the same keys, we can just let recursion
// from the walk check the sub-values match
return true, nil
case value1.Type().Is(DynamicPseudoType):
// Let recursion from the walk check the sub-values match
return true, nil
}
return false, fmt.Errorf("unexpected type %v in Diff at %s", value1.Type(), path)
})
Expand Down
4 changes: 1 addition & 3 deletions tftypes/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ func valueFromList(typ Type, in interface{}) (Value, error) {
case []Value:
var valType Type
for pos, v := range value {
if v.Type().Is(DynamicPseudoType) && v.IsKnown() {
return Value{}, NewAttributePath().WithElementKeyInt(pos).NewErrorf("invalid value %s for %s", v, v.Type())
} else if !v.Type().Is(DynamicPseudoType) && !v.Type().UsableAs(typ) {
if !v.Type().UsableAs(typ) {
return Value{}, NewAttributePath().WithElementKeyInt(pos).NewErrorf("can't use %s as %s", v.Type(), typ)
}
if valType == nil {
Expand Down
4 changes: 1 addition & 3 deletions tftypes/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ func valueFromMap(typ Type, in interface{}) (Value, error) {
var elType Type
for _, k := range keys {
v := value[k]
if v.Type().Is(DynamicPseudoType) && v.IsKnown() {
return Value{}, NewAttributePath().WithElementKeyString(k).NewErrorf("invalid value %s for %s", v, v.Type())
} else if !v.Type().Is(DynamicPseudoType) && !v.Type().UsableAs(typ) {
if !v.Type().UsableAs(typ) {
return Value{}, NewAttributePath().WithElementKeyString(k).NewErrorf("can't use %s as %s", v.Type(), typ)
}
if elType == nil {
Expand Down
4 changes: 1 addition & 3 deletions tftypes/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,7 @@ func valueFromObject(types map[string]Type, optionalAttrs map[string]struct{}, i
if v.Type() == nil {
return Value{}, NewAttributePath().WithAttributeName(k).NewErrorf("missing value type")
}
if v.Type().Is(DynamicPseudoType) && v.IsKnown() && !v.IsNull() {
return Value{}, NewAttributePath().WithAttributeName(k).NewErrorf("invalid value %s for %s", v, v.Type())
} else if !v.Type().Is(DynamicPseudoType) && !v.Type().UsableAs(typ) {
if !v.Type().UsableAs(typ) {
return Value{}, NewAttributePath().WithAttributeName(k).NewErrorf("can't use %s as %s", v.Type(), typ)
}
}
Expand Down
53 changes: 52 additions & 1 deletion tftypes/primitive.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,16 @@ func (p primitive) supportedGoTypes() []string {
case Bool.name:
return []string{"bool", "*bool"}
case DynamicPseudoType.name:
return []string{"nil", "UnknownValue"}
// List/Set is covered by Tuple, Map is covered by Object
possibleTypes := []Type{
String, Bool, Number,
Tuple{}, Object{},
}
results := []string{}
for _, t := range possibleTypes {
results = append(results, t.supportedGoTypes()...)
}
return results
}
panic(fmt.Sprintf("unknown primitive type %q", p.name))
}
Expand Down Expand Up @@ -346,3 +355,45 @@ func valueFromNumber(in interface{}) (Value, error) {
return Value{}, fmt.Errorf("tftypes.NewValue can't use %T as a tftypes.Number; expected types are: %s", in, formattedSupportedGoTypes(Number))
}
}

func valueFromDynamicPseudoType(val interface{}) (Value, error) {
switch val := val.(type) {
case string, *string:
v, err := valueFromString(val)
if err != nil {
return Value{}, err
}
v.typ = DynamicPseudoType
return v, nil
case *big.Float, float64, *float64, int, *int, int8, *int8, int16, *int16, int32, *int32, int64, *int64, uint, *uint, uint8, *uint8, uint16, *uint16, uint32, *uint32, uint64, *uint64:
v, err := valueFromNumber(val)
if err != nil {
return Value{}, err
}
v.typ = DynamicPseudoType
return v, nil
case bool, *bool:
v, err := valueFromBool(val)
if err != nil {
return Value{}, err
}
v.typ = DynamicPseudoType
return v, nil
case map[string]Value:
v, err := valueFromObject(nil, nil, val)
if err != nil {
return Value{}, err
}
v.typ = DynamicPseudoType
return v, nil
case []Value:
v, err := valueFromTuple(nil, val)
if err != nil {
return Value{}, err
}
v.typ = DynamicPseudoType
return v, nil
default:
return Value{}, fmt.Errorf("tftypes.NewValue can't use %T as a tftypes.DynamicPseudoType; expected types are: %s", val, formattedSupportedGoTypes(DynamicPseudoType))
}
}
4 changes: 1 addition & 3 deletions tftypes/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ func valueFromSet(typ Type, in interface{}) (Value, error) {
case []Value:
var elType Type
for _, v := range value {
if v.Type().Is(DynamicPseudoType) && v.IsKnown() {
return Value{}, NewAttributePath().WithElementKeyValue(v).NewErrorf("invalid value %s for %s", v, v.Type())
} else if !v.Type().Is(DynamicPseudoType) && !v.Type().UsableAs(typ) {
if !v.Type().UsableAs(typ) {
return Value{}, NewAttributePath().WithElementKeyValue(v).NewErrorf("can't use %s as %s", v.Type(), typ)
}
if elType == nil {
Expand Down
4 changes: 1 addition & 3 deletions tftypes/tuple.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ func valueFromTuple(types []Type, in interface{}) (Value, error) {
}
for pos, v := range value {
typ := types[pos]
if v.Type().Is(DynamicPseudoType) && v.IsKnown() {
return Value{}, NewAttributePath().WithElementKeyInt(pos).NewErrorf("invalid value %s for %s", v, v.Type())
} else if !v.Type().Is(DynamicPseudoType) && !v.Type().UsableAs(typ) {
if !v.Type().UsableAs(typ) {
return Value{}, NewAttributePath().WithElementKeyInt(pos).NewErrorf("can't use %s as %s", v.Type(), typ)
}
}
Expand Down
11 changes: 6 additions & 5 deletions tftypes/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package tftypes

import (
"bytes"
"errors"
"fmt"
"math/big"
"sort"
Expand Down Expand Up @@ -296,10 +295,6 @@ func newValue(t Type, val interface{}) (Value, error) {
}, nil
}

if t.Is(DynamicPseudoType) {
return Value{}, errors.New("cannot have DynamicPseudoType with known value, DynamicPseudoType can only contain null or unknown values")
}

if creator, ok := val.(ValueCreator); ok {
var err error
val, err = creator.ToTerraform5Value()
Expand Down Expand Up @@ -357,6 +352,12 @@ func newValue(t Type, val interface{}) (Value, error) {
return Value{}, err
}
return v, nil
case t.Is(DynamicPseudoType):
v, err := valueFromDynamicPseudoType(val)
if err != nil {
return Value{}, err
}
return v, nil
default:
return Value{}, fmt.Errorf("unknown type %s passed to tftypes.NewValue", t)
}
Expand Down
90 changes: 87 additions & 3 deletions tftypes/value_dpt_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tftypes

import (
"math/big"
"regexp"
"testing"
)
Expand All @@ -14,10 +15,93 @@ func Test_newValue_dpt(t *testing.T) {
expected Value
}
tests := map[string]testCase{
"known": {
"*big.Float": {
typ: DynamicPseudoType,
val: "hello",
err: regexp.MustCompile(`cannot have DynamicPseudoType with known value, DynamicPseudoType can only contain null or unknown values`),
val: big.NewFloat(123),
expected: Value{
typ: DynamicPseudoType,
value: Value{
typ: Number,
value: big.NewFloat(123),
},
},
},
"bool": {
typ: DynamicPseudoType,
val: true,
expected: Value{
typ: DynamicPseudoType,
value: true,
},
},
"float64": {
typ: DynamicPseudoType,
val: float64(123),
expected: Value{
typ: DynamicPseudoType,
value: Value{
typ: Number,
value: big.NewFloat(123),
},
},
},
"int": {
typ: DynamicPseudoType,
val: 123,
expected: Value{
typ: DynamicPseudoType,
value: Value{
typ: Number,
value: big.NewFloat(123),
},
},
},
"int64": {
typ: DynamicPseudoType,
val: int64(123),
expected: Value{
typ: DynamicPseudoType,
value: Value{
typ: Number,
value: big.NewFloat(123),
},
},
},
"object": {
typ: DynamicPseudoType,
val: map[string]Value{
"testkey": NewValue(String, "testvalue"),
},
expected: Value{
typ: DynamicPseudoType,
value: map[string]Value{
"testkey": {
typ: String,
value: "testvalue",
},
},
},
},
"string": {
typ: DynamicPseudoType,
val: "test",
expected: Value{
typ: DynamicPseudoType,
value: "test",
},
},
"tuple": {
typ: DynamicPseudoType,
val: []Value{NewValue(String, "test")},
expected: Value{
typ: DynamicPseudoType,
value: []Value{
{
typ: String,
value: "test",
},
},
},
},
"null": {
typ: DynamicPseudoType,
Expand Down
22 changes: 3 additions & 19 deletions tftypes/value_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,20 +355,8 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
return Value{}, p.NewErrorf("invalid JSON, expected %q, got %q", json.Delim('}'), tok)
}

elTyp := attrType
if attrType.Is(DynamicPseudoType) {
var elements []Value
for _, val := range vals {
elements = append(elements, val)
}
elTyp, err = TypeFromElements(elements)
if err != nil {
return Value{}, p.NewErrorf("invalid elements for map: %w", err)
}
}

return NewValue(Map{
ElementType: elTyp,
ElementType: attrType,
}, vals), nil
}

Expand All @@ -393,7 +381,6 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
// while generally in Go it's undesirable to treat empty and nil slices
// separately, in this case we're surfacing a non-Go-in-origin
// distinction, so we'll allow it.
types := []Type{}
vals := []Value{}

var idx int
Expand All @@ -415,7 +402,6 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
if err != nil {
return Value{}, err
}
types = append(types, val.Type())
vals = append(vals, val)
}

Expand All @@ -432,7 +418,7 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
}

return NewValue(Tuple{
ElementTypes: types,
ElementTypes: elementTypes,
}, vals), nil
}

Expand All @@ -447,7 +433,6 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
return Value{}, p.NewErrorf("invalid JSON, expected %q, got %q", json.Delim('{'), tok)
}

types := map[string]Type{}
vals := map[string]Value{}
for dec.More() {
innerPath := p.WithElementKeyValue(NewValue(String, UnknownValue))
Expand All @@ -474,7 +459,6 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
if err != nil {
return Value{}, err
}
types[key] = val.Type()
vals[key] = val
}

Expand All @@ -494,6 +478,6 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
}

return NewValue(Object{
AttributeTypes: types,
AttributeTypes: attrTypes,
}, vals), nil
}
6 changes: 3 additions & 3 deletions tftypes/value_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func TestValueFromJSON(t *testing.T) {
},
"tuple-of-dynamic-bools": {
value: NewValue(Tuple{
ElementTypes: []Type{Bool, Bool},
ElementTypes: []Type{DynamicPseudoType, DynamicPseudoType},
}, []Value{
NewValue(Bool, true),
NewValue(Bool, false),
Expand All @@ -324,7 +324,7 @@ func TestValueFromJSON(t *testing.T) {
},
"map-of-dynamic-bools": {
value: NewValue(Map{
ElementType: Bool,
ElementType: DynamicPseudoType,
}, map[string]Value{
"true": NewValue(Bool, true),
"false": NewValue(Bool, false),
Expand All @@ -338,7 +338,7 @@ func TestValueFromJSON(t *testing.T) {
value: NewValue(Object{
AttributeTypes: map[string]Type{
"static": Bool,
"dynamic": Bool,
"dynamic": DynamicPseudoType,
},
}, map[string]Value{
"static": NewValue(Bool, true),
Expand Down
Loading

0 comments on commit 5ad95e8

Please sign in to comment.