Skip to content

Commit 90a82fe

Browse files
authored
tfprotov5+tfprotov6: Replace usage of diagnostics in CallFunction RPC with function error (#380)
* tfprotov5+tfprotov6: Replace usage of diagnostics in CallFunction RPC with function error Reference: hashicorp/terraform#34603 The next version of the plugin protocol (5.5/6.5) includes support for provider defined functions. This change modifies the response returned from the CallFunction RPC, replacing diagnostics with function error. * Fix protoc version * Add changelog * Removing function argument from diagnostics * Renaming param from msg to text for function errorr * Adding change log
1 parent 0624268 commit 90a82fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3103
-2200
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: BREAKING CHANGES
2+
body: 'tfprotov5+tfprotov6: Modified the response returned from the CallFunction RPC,
3+
replacing diagnostics with function error'
4+
time: 2024-02-21T10:30:10.223878Z
5+
custom:
6+
Issue: "380"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: NOTES
2+
body: 'all: If using terraform-plugin-framework, terraform-plugin-mux, or terraform-plugin-sdk,
3+
only upgrade this Go module when upgrading those Go modules to [email protected],
4+
[email protected], and terraform-plugin-sdk/[email protected], or greater, respectively'
5+
time: 2024-02-22T08:08:18.434191Z
6+
custom:
7+
Issue: "380"

internal/logging/keys.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ const (
1111
// Attribute of the diagnostic being logged.
1212
KeyDiagnosticAttribute = "diagnostic_attribute"
1313

14-
// Function Argument of the diagnostic being logged.
15-
KeyDiagnosticFunctionArgument = "diagnostic_function_argument"
16-
1714
// Number of the error diagnostics.
1815
KeyDiagnosticErrorCount = "diagnostic_error_count"
1916

@@ -32,6 +29,15 @@ const (
3229
// Underlying error string
3330
KeyError = "error"
3431

32+
// Argument position of the function error.
33+
KeyFunctionErrorArgument = "function_error_argument"
34+
35+
// Boolean indicating presence of function error
36+
KeyFunctionErrorExists = "function_error_exists"
37+
38+
// Message of the function error.
39+
KeyFunctionErrorText = "function_error_text"
40+
3541
// Duration in milliseconds for the RPC request
3642
KeyRequestDurationMs = "tf_req_duration_ms"
3743

tfprotov5/diagnostic.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ type Diagnostic struct {
4242
// indicate that the problem is with a certain field in the resource,
4343
// which helps users find the source of the problem.
4444
Attribute *tftypes.AttributePath
45-
46-
// FunctionArgument is the positional function argument for aligning
47-
// configuration source.
48-
FunctionArgument *int64
4945
}
5046

5147
// DiagnosticSeverity represents different classes of Diagnostic which affect

tfprotov5/function.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ type CallFunctionRequest struct {
109109
// CallFunctionResponse is the response from the provider with the result of
110110
// executing the logic of the function.
111111
type CallFunctionResponse struct {
112-
// Diagnostics report errors or warnings related to the execution of the
113-
// function logic. Returning an empty slice indicates a successful response
114-
// with no warnings or errors presented to practitioners.
115-
Diagnostics []*Diagnostic
112+
// Error reports errors related to the execution of the
113+
// function logic. Returning a nil error indicates a successful response
114+
// with no errors presented to practitioners.
115+
Error *FunctionError
116116

117117
// Result is the return value from the called function, matching the result
118118
// type in the function definition.

tfprotov5/function_error.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfprotov5
5+
6+
// FunctionError is used to convey information back to the user running Terraform.
7+
type FunctionError struct {
8+
// Text is the description of the error.
9+
Text string
10+
11+
// FunctionArgument is the positional function argument for aligning
12+
// configuration source.
13+
FunctionArgument *int64
14+
}

tfprotov5/internal/diag/diagnostics.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ func (d Diagnostics) Log(ctx context.Context) {
5353
diagnosticFields[logging.KeyDiagnosticAttribute] = diagnostic.Attribute.String()
5454
}
5555

56-
if diagnostic.FunctionArgument != nil {
57-
diagnosticFields[logging.KeyDiagnosticFunctionArgument] = *diagnostic.FunctionArgument
58-
}
59-
6056
switch diagnostic.Severity {
6157
case tfprotov5.DiagnosticSeverityError:
6258
logging.ProtocolError(ctx, "Response contains error diagnostic", diagnosticFields)

tfprotov5/internal/funcerr/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
// Package funcerr contains function error helpers. These implementations are
5+
// intentionally outside the public API.
6+
package funcerr
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package funcerr
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-go/internal/logging"
10+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
11+
)
12+
13+
// FunctionError is a single FunctionError.
14+
type FunctionError tfprotov5.FunctionError
15+
16+
// HasError returns true if the FunctionError is not empty.
17+
func (e *FunctionError) HasError() bool {
18+
if e == nil {
19+
return false
20+
}
21+
22+
return e.Text != "" || e.FunctionArgument != nil
23+
}
24+
25+
// Log will log the function error:
26+
func (e *FunctionError) Log(ctx context.Context) {
27+
if e == nil {
28+
return
29+
}
30+
31+
if !e.HasError() {
32+
return
33+
}
34+
35+
switch {
36+
case e.FunctionArgument != nil && e.Text != "":
37+
logging.ProtocolError(ctx, "Response contains function error", map[string]interface{}{
38+
logging.KeyFunctionErrorText: e.Text,
39+
logging.KeyFunctionErrorArgument: *e.FunctionArgument,
40+
})
41+
case e.FunctionArgument != nil:
42+
logging.ProtocolError(ctx, "Response contains function error", map[string]interface{}{
43+
logging.KeyFunctionErrorArgument: *e.FunctionArgument,
44+
})
45+
case e.Text != "":
46+
logging.ProtocolError(ctx, "Response contains function error", map[string]interface{}{
47+
logging.KeyFunctionErrorText: e.Text,
48+
})
49+
}
50+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package funcerr_test
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
13+
"github.com/hashicorp/terraform-plugin-log/tfsdklogtest"
14+
15+
"github.com/hashicorp/terraform-plugin-go/internal/logging"
16+
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/funcerr"
17+
)
18+
19+
func TestFunctionErrorHasError(t *testing.T) {
20+
t.Parallel()
21+
22+
testCases := map[string]struct {
23+
functionError *funcerr.FunctionError
24+
expected bool
25+
}{
26+
"nil": {
27+
functionError: nil,
28+
expected: false,
29+
},
30+
"empty": {
31+
functionError: &funcerr.FunctionError{},
32+
expected: false,
33+
},
34+
"argument": {
35+
functionError: &funcerr.FunctionError{
36+
FunctionArgument: pointer(int64(0)),
37+
},
38+
expected: true,
39+
},
40+
"message": {
41+
functionError: &funcerr.FunctionError{
42+
Text: "test function error",
43+
},
44+
expected: true,
45+
},
46+
"argument-and-message": {
47+
functionError: &funcerr.FunctionError{
48+
FunctionArgument: pointer(int64(0)),
49+
Text: "test function error",
50+
},
51+
expected: true,
52+
},
53+
}
54+
55+
for name, testCase := range testCases {
56+
name, testCase := name, testCase
57+
58+
t.Run(name, func(t *testing.T) {
59+
t.Parallel()
60+
61+
got := testCase.functionError.HasError()
62+
63+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
64+
t.Errorf("unexpected difference: %s", diff)
65+
}
66+
})
67+
}
68+
}
69+
70+
func TestFunctionErrorLog(t *testing.T) {
71+
t.Parallel()
72+
73+
testCases := map[string]struct {
74+
functionError *funcerr.FunctionError
75+
expected []map[string]interface{}
76+
}{
77+
"nil": {
78+
functionError: nil,
79+
expected: nil,
80+
},
81+
"empty": {
82+
functionError: &funcerr.FunctionError{},
83+
expected: nil,
84+
},
85+
"argument": {
86+
functionError: &funcerr.FunctionError{
87+
FunctionArgument: pointer(int64(0)),
88+
},
89+
expected: []map[string]interface{}{
90+
{
91+
"@level": "error",
92+
"@message": "Response contains function error",
93+
"@module": "sdk.proto",
94+
"function_error_argument": float64(0),
95+
},
96+
},
97+
},
98+
"message": {
99+
functionError: &funcerr.FunctionError{
100+
Text: "test function error",
101+
},
102+
expected: []map[string]interface{}{
103+
{
104+
"@level": "error",
105+
"@message": "Response contains function error",
106+
"@module": "sdk.proto",
107+
"function_error_text": "test function error",
108+
},
109+
},
110+
},
111+
"argument-and-message": {
112+
functionError: &funcerr.FunctionError{
113+
FunctionArgument: pointer(int64(0)),
114+
Text: "test function error",
115+
},
116+
expected: []map[string]interface{}{
117+
{
118+
"@level": "error",
119+
"@message": "Response contains function error",
120+
"@module": "sdk.proto",
121+
"function_error_text": "test function error",
122+
"function_error_argument": float64(0),
123+
},
124+
},
125+
},
126+
}
127+
128+
for name, testCase := range testCases {
129+
name, testCase := name, testCase
130+
131+
t.Run(name, func(t *testing.T) {
132+
t.Parallel()
133+
134+
var output bytes.Buffer
135+
136+
ctx := tfsdklogtest.RootLogger(context.Background(), &output)
137+
ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{})
138+
139+
testCase.functionError.Log(ctx)
140+
141+
entries, err := tfsdklogtest.MultilineJSONDecode(&output)
142+
143+
if err != nil {
144+
t.Fatalf("unable to read multiple line JSON: %s", err)
145+
}
146+
147+
if diff := cmp.Diff(entries, testCase.expected); diff != "" {
148+
t.Errorf("unexpected difference: %s", diff)
149+
}
150+
})
151+
}
152+
}

0 commit comments

Comments
 (0)