Skip to content

Commit

Permalink
tftypes: Guarantee Type interface MarshalJSON error safety (#371)
Browse files Browse the repository at this point in the history
Reference: #238
Reference: #365

This change removes the necessity for protocol type conversion to handle JSON encoding errors, therefore greatly simplifying those packages. Type system conversions into the protocol should always be possible, albeit potentially not valid for usage in Terraform. The protocol conversion code is not the place to handle that consideration, rather if developers want to validate their schema/function/type definitions before its sent across the protocol with respect to the validation rules imposed by Terraform, separate validation functionality should be introduced. This is not conceptually new, does not introduce behavior changes, and uses fuzz testing to verify that a panic is not possible today in the one way that developers could potentially introduce a new panic.

The fuzz testing has been run for 60 seconds on a currently powerful workstation without generating any failure cases:

```console
$ go test -fuzz=Fuzz -fuzztime=60s ./tftypes
fuzz: elapsed: 0s, gathering baseline coverage: 0/138 completed
fuzz: elapsed: 0s, gathering baseline coverage: 138/138 completed, now fuzzing with 10 workers
fuzz: elapsed: 3s, execs: 406242 (135409/sec), new interesting: 6 (total: 144)
fuzz: elapsed: 6s, execs: 833268 (142312/sec), new interesting: 7 (total: 145)
fuzz: elapsed: 9s, execs: 1269960 (145562/sec), new interesting: 7 (total: 145)
fuzz: elapsed: 12s, execs: 1684371 (138143/sec), new interesting: 8 (total: 146)
fuzz: elapsed: 15s, execs: 2137768 (151160/sec), new interesting: 9 (total: 147)
fuzz: elapsed: 18s, execs: 2574729 (145641/sec), new interesting: 11 (total: 149)
fuzz: elapsed: 21s, execs: 3011973 (145722/sec), new interesting: 12 (total: 150)
fuzz: elapsed: 24s, execs: 3442147 (143418/sec), new interesting: 12 (total: 150)
fuzz: elapsed: 27s, execs: 3868833 (142210/sec), new interesting: 12 (total: 150)
fuzz: elapsed: 30s, execs: 4313780 (148320/sec), new interesting: 14 (total: 152)
fuzz: elapsed: 33s, execs: 4763813 (150034/sec), new interesting: 14 (total: 152)
fuzz: elapsed: 36s, execs: 5201686 (145936/sec), new interesting: 15 (total: 153)
fuzz: elapsed: 39s, execs: 5613562 (137285/sec), new interesting: 15 (total: 153)
fuzz: elapsed: 42s, execs: 6051164 (145875/sec), new interesting: 15 (total: 153)
fuzz: elapsed: 45s, execs: 6485230 (144677/sec), new interesting: 15 (total: 153)
fuzz: elapsed: 48s, execs: 6912045 (142294/sec), new interesting: 15 (total: 153)
fuzz: elapsed: 51s, execs: 7349416 (145756/sec), new interesting: 16 (total: 154)
fuzz: elapsed: 54s, execs: 7792201 (147626/sec), new interesting: 16 (total: 154)
fuzz: elapsed: 57s, execs: 8225683 (144471/sec), new interesting: 18 (total: 156)
fuzz: elapsed: 1m0s, execs: 8637257 (137227/sec), new interesting: 18 (total: 156)
fuzz: elapsed: 1m0s, execs: 8637257 (0/sec), new interesting: 18 (total: 156)
PASS
ok      github.com/hashicorp/terraform-plugin-go/tftypes        60.883s
```
  • Loading branch information
bflad committed Jan 24, 2024
1 parent 73243d5 commit c9a4f80
Show file tree
Hide file tree
Showing 25 changed files with 386 additions and 729 deletions.
19 changes: 7 additions & 12 deletions tfprotov5/internal/toproto/dynamic_value.go
Expand Up @@ -4,8 +4,6 @@
package toproto

import (
"fmt"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
"github.com/hashicorp/terraform-plugin-go/tftypes"
Expand All @@ -24,17 +22,14 @@ func DynamicValue(in *tfprotov5.DynamicValue) *tfplugin5.DynamicValue {
return resp
}

func CtyType(in tftypes.Type) ([]byte, error) {
func CtyType(in tftypes.Type) []byte {
if in == nil {
return nil, nil
return nil
}

switch {
case in.Is(tftypes.String), in.Is(tftypes.Bool), in.Is(tftypes.Number),
in.Is(tftypes.List{}), in.Is(tftypes.Map{}),
in.Is(tftypes.Set{}), in.Is(tftypes.Object{}),
in.Is(tftypes.Tuple{}), in.Is(tftypes.DynamicPseudoType):
return in.MarshalJSON() //nolint:staticcheck
}
return nil, fmt.Errorf("unknown type %s", in)
// MarshalJSON is always error safe.
// nolint:staticcheck // Intended first-party usage
resp, _ := in.MarshalJSON()

return resp
}
101 changes: 21 additions & 80 deletions tfprotov5/internal/toproto/function.go
Expand Up @@ -4,8 +4,6 @@
package toproto

import (
"fmt"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)
Expand All @@ -23,61 +21,31 @@ func CallFunction_Response(in *tfprotov5.CallFunctionResponse) *tfplugin5.CallFu
return resp
}

func Function(in *tfprotov5.Function) (*tfplugin5.Function, error) {
func Function(in *tfprotov5.Function) *tfplugin5.Function {
if in == nil {
return nil, nil
return nil
}

resp := &tfplugin5.Function{
Description: in.Description,
DescriptionKind: StringKind(in.DescriptionKind),
DeprecationMessage: in.DeprecationMessage,
Parameters: make([]*tfplugin5.Function_Parameter, 0, len(in.Parameters)),
Return: Function_Return(in.Return),
Summary: in.Summary,
VariadicParameter: Function_Parameter(in.VariadicParameter),
}

for position, parameter := range in.Parameters {
if parameter == nil {
return nil, fmt.Errorf("missing function parameter definition at position: %d", position)
}

functionParameter, err := Function_Parameter(parameter)

if err != nil {
return nil, fmt.Errorf("unable to marshal function parameter at position %d: %w", position, err)
}

resp.Parameters = append(resp.Parameters, functionParameter)
}

if in.Return == nil {
return nil, fmt.Errorf("missing function return definition")
}

functionReturn, err := Function_Return(in.Return)

if err != nil {
return nil, fmt.Errorf("unable to marshal function return: %w", err)
for _, parameter := range in.Parameters {
resp.Parameters = append(resp.Parameters, Function_Parameter(parameter))
}

resp.Return = functionReturn

if in.VariadicParameter != nil {
variadicParameter, err := Function_Parameter(in.VariadicParameter)

if err != nil {
return nil, fmt.Errorf("unable to marshal variadic function parameter: %w", err)
}

resp.VariadicParameter = variadicParameter
}

return resp, nil
return resp
}

func Function_Parameter(in *tfprotov5.FunctionParameter) (*tfplugin5.Function_Parameter, error) {
func Function_Parameter(in *tfprotov5.FunctionParameter) *tfplugin5.Function_Parameter {
if in == nil {
return nil, nil
return nil
}

resp := &tfplugin5.Function_Parameter{
Expand All @@ -86,66 +54,39 @@ func Function_Parameter(in *tfprotov5.FunctionParameter) (*tfplugin5.Function_Pa
Description: in.Description,
DescriptionKind: StringKind(in.DescriptionKind),
Name: in.Name,
Type: CtyType(in.Type),
}

if in.Type == nil {
return nil, fmt.Errorf("missing function parameter type definition")
}

ctyType, err := CtyType(in.Type)

if err != nil {
return resp, fmt.Errorf("error marshaling function parameter type: %w", err)
}

resp.Type = ctyType

return resp, nil
return resp
}

func Function_Return(in *tfprotov5.FunctionReturn) (*tfplugin5.Function_Return, error) {
func Function_Return(in *tfprotov5.FunctionReturn) *tfplugin5.Function_Return {
if in == nil {
return nil, nil
}

resp := &tfplugin5.Function_Return{}

if in.Type == nil {
return nil, fmt.Errorf("missing function return type definition")
return nil
}

ctyType, err := CtyType(in.Type)

if err != nil {
return resp, fmt.Errorf("error marshaling function return type: %w", err)
resp := &tfplugin5.Function_Return{
Type: CtyType(in.Type),
}

resp.Type = ctyType

return resp, nil
return resp
}

func GetFunctions_Response(in *tfprotov5.GetFunctionsResponse) (*tfplugin5.GetFunctions_Response, error) {
func GetFunctions_Response(in *tfprotov5.GetFunctionsResponse) *tfplugin5.GetFunctions_Response {
if in == nil {
return nil, nil
return nil
}

resp := &tfplugin5.GetFunctions_Response{
Diagnostics: Diagnostics(in.Diagnostics),
Functions: make(map[string]*tfplugin5.Function, len(in.Functions)),
}

for name, functionPtr := range in.Functions {
function, err := Function(functionPtr)

if err != nil {
return nil, fmt.Errorf("error marshaling function definition for %q: %w", name, err)
}

resp.Functions[name] = function
for name, function := range in.Functions {
resp.Functions[name] = Function(function)
}

return resp, nil
return resp
}

func GetMetadata_FunctionMetadata(in *tfprotov5.FunctionMetadata) *tfplugin5.GetMetadata_FunctionMetadata {
Expand Down

0 comments on commit c9a4f80

Please sign in to comment.