Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ignoring undefined attributes during unmarshalling #213

Merged
merged 4 commits into from Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/213.txt
@@ -0,0 +1,7 @@
```release-note:enhancement
tfprotov5: Added `RawState` type `UnmarshalWithOpts` method to facilitate configurable behaviour during unmarshalling
```

```release-note:enhancement
tfprotov6: Added `RawState` type `UnmarshalWithOpts` method to facilitate configurable behaviour during unmarshalling
```
19 changes: 19 additions & 0 deletions tfprotov5/state.go
Expand Up @@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}

// UnmarshalOpts contains options that can be used to modify the behaviour when
// unmarshalling. Currently, this only contains a struct for opts for JSON but
// could have a field for Flatmap in the future.
type UnmarshalOpts struct {
ValueFromJSONOpts tftypes.ValueFromJSONOpts
}

// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
}
if s.Flatmap != nil {
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
}
return tftypes.Value{}, ErrUnknownRawStateType
}
83 changes: 83 additions & 0 deletions tfprotov5/state_test.go
@@ -0,0 +1,83 @@
package tfprotov5_test

import (
"math/big"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestRawStateUnmarshalWithOpts(t *testing.T) {
t.Parallel()
type testCase struct {
rawState tfprotov5.RawState
value tftypes.Value
typ tftypes.Type
opts tfprotov5.UnmarshalOpts
}
tests := map[string]testCase{
"object-of-bool-number": {
rawState: tfprotov5.RawState{
JSON: []byte(`{"bool":true,"number":0}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
},
"object-with-missing-attribute": {
rawState: tfprotov5.RawState{
JSON: []byte(`{"bool":true,"number":0,"unknown":"whatever"}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
opts: tfprotov5.UnmarshalOpts{
ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
IgnoreUndefinedAttributes: true,
},
},
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()

val, err := test.rawState.UnmarshalWithOpts(test.typ, test.opts)
if err != nil {
t.Fatalf("unexpected error unmarshaling: %s", err)
}

if diff := cmp.Diff(test.value, val); diff != "" {
t.Errorf("Unexpected results (-wanted +got): %s", diff)
}
})
}
}
19 changes: 19 additions & 0 deletions tfprotov6/state.go
Expand Up @@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}

// UnmarshalOpts contains options that can be used to modify the behaviour when
// unmarshalling. Currently, this only contains a struct for opts for JSON but
// could have a field for Flatmap in the future.
type UnmarshalOpts struct {
ValueFromJSONOpts tftypes.ValueFromJSONOpts
}

// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
}
if s.Flatmap != nil {
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
}
return tftypes.Value{}, ErrUnknownRawStateType
}
83 changes: 83 additions & 0 deletions tfprotov6/state_test.go
@@ -0,0 +1,83 @@
package tfprotov6_test

import (
"math/big"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestRawStateUnmarshalWithOpts(t *testing.T) {
t.Parallel()
type testCase struct {
rawState tfprotov6.RawState
value tftypes.Value
typ tftypes.Type
opts tfprotov6.UnmarshalOpts
}
tests := map[string]testCase{
"object-of-bool-number": {
rawState: tfprotov6.RawState{
JSON: []byte(`{"bool":true,"number":0}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
},
"object-with-missing-attribute": {
rawState: tfprotov6.RawState{
JSON: []byte(`{"bool":true,"number":0,"unknown":"whatever"}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
opts: tfprotov6.UnmarshalOpts{
ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
IgnoreUndefinedAttributes: true,
},
},
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()

val, err := test.rawState.UnmarshalWithOpts(test.typ, test.opts)
if err != nil {
t.Fatalf("unexpected error unmarshaling: %s", err)
}

if diff := cmp.Diff(test.value, val); diff != "" {
t.Errorf("Unexpected results (-wanted +got): %s", diff)
}
})
}
}
66 changes: 45 additions & 21 deletions tftypes/value_json.go
Expand Up @@ -16,7 +16,24 @@ import (
// terraform-plugin-go. Third parties should not use it, and its behavior is
// not covered under the API compatibility guarantees. Don't use this.
func ValueFromJSON(data []byte, typ Type) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath())
return jsonUnmarshal(data, typ, NewAttributePath(), ValueFromJSONOpts{})
}

// ValueFromJSONOpts contains options that can be used to modify the behaviour when
// unmarshalling JSON.
type ValueFromJSONOpts struct {
// IgnoreUndefinedAttributes is used to ignore any attributes which appear in the
// JSON but do not have a corresponding entry in the schema. For example, raw state
// where an attribute has been removed from the schema.
IgnoreUndefinedAttributes bool
}

// ValueFromJSONWithOpts is identical to ValueFromJSON with the exception that it
// accepts ValueFromJSONOpts which can be used to modify the unmarshalling behaviour, such
// as ignoring undefined attributes, for instance. This can occur when the JSON
// being unmarshalled does not have a corresponding attribute in the schema.
func ValueFromJSONWithOpts(data []byte, typ Type, opts ValueFromJSONOpts) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath(), opts)
}

func jsonByteDecoder(buf []byte) *json.Decoder {
Expand All @@ -26,7 +43,7 @@ func jsonByteDecoder(buf []byte) *json.Decoder {
return dec
}

func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
func jsonUnmarshal(buf []byte, typ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand All @@ -46,18 +63,17 @@ func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
case typ.Is(Bool):
return jsonUnmarshalBool(buf, typ, p)
case typ.Is(DynamicPseudoType):
return jsonUnmarshalDynamicPseudoType(buf, typ, p)
return jsonUnmarshalDynamicPseudoType(buf, typ, p, opts)
case typ.Is(List{}):
return jsonUnmarshalList(buf, typ.(List).ElementType, p)
return jsonUnmarshalList(buf, typ.(List).ElementType, p, opts)
case typ.Is(Set{}):
return jsonUnmarshalSet(buf, typ.(Set).ElementType, p)

return jsonUnmarshalSet(buf, typ.(Set).ElementType, p, opts)
case typ.Is(Map{}):
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p)
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p, opts)
case typ.Is(Tuple{}):
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p)
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p, opts)
case typ.Is(Object{}):
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p)
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p, opts)
}
return Value{}, p.NewErrorf("unknown type %s", typ)
}
Expand Down Expand Up @@ -140,7 +156,7 @@ func jsonUnmarshalBool(buf []byte, _ Type, p *AttributePath) (Value, error) {
return Value{}, p.NewErrorf("unsupported type %T sent as %s", tok, Bool)
}

func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value, error) {
func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
if err != nil {
Expand Down Expand Up @@ -190,10 +206,10 @@ func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value
if valBody == nil {
return Value{}, p.NewErrorf("missing value in dynamically-typed value")
}
return jsonUnmarshal(valBody, t, p)
return jsonUnmarshal(valBody, t, p, opts)
}

func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -227,7 +243,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -254,7 +270,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
}, vals), nil
}

func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -284,7 +300,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -310,7 +326,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
}, vals), nil
}

func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -341,7 +357,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -360,7 +376,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
}, vals), nil
}

func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Value, error) {
func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -398,7 +414,7 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -422,7 +438,9 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
}, vals), nil
}

func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath) (Value, error) {
// jsonUnmarshalObject attempts to decode JSON object structure to tftypes.Value object.
// opts contains fields that can be used to modify the behaviour of JSON unmarshalling.
func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand All @@ -446,6 +464,12 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
}
attrType, ok := attrTypes[key]
if !ok {
if opts.IgnoreUndefinedAttributes {
// We are trying to ignore the key and value of any unsupported attribute.
_ = dec.Decode(new(json.RawMessage))
continue
}

return Value{}, innerPath.NewErrorf("unsupported attribute %q", key)
}
innerPath = p.WithAttributeName(key)
Expand All @@ -455,7 +479,7 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand Down