Skip to content

Commit afd18f1

Browse files
SBGoodsaustinvalle
andauthored
tfprotov5+tfprotov6: Write-only Attribute Implementation (#462)
* Upgrade Go to `v1.22` * Add support for ephemeral resources in protocol version 6 * Add support for ephemeral resources in protocol version 5 * Add ephemeral resources field to `GetMetadata_Response()` * Update tfplugin5.proto and tfplugin5.proto to support write-only attributes and generate stubs. * Implement write-only attributes in `tfprotov5` and `tfprotov6` packages * Remove `State` field from `RenewEphemeralResource` RPC response and rename `PriorState` request fields to `State`. * Update tfplugin5.proto and tfplugin5.proto to support write-only attributes and generate stubs. * Implement write-only attributes in `tfprotov5` and `tfprotov6` packages * Update protobuf stubs after rebase * Increase protocol minor version for write-only attributes * switch interfaces to be optional * removed `config` from renew request * Update protocol bindings * remove ApplyResourceChange client capabilities + add server logging for validate resource config * Update `protoc` to `v29.3` in CI * Add changelog entry * Apply suggestions from code review Co-authored-by: Austin Valle <[email protected]> --------- Co-authored-by: Austin Valle <[email protected]>
1 parent 707c7af commit afd18f1

33 files changed

+2136
-1663
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: FEATURES
2+
body: 'tfprotov5+tfprotov6: Upgraded protocols and added types to support write-only attributes'
3+
time: 2025-01-21T16:34:56.142885-05:00
4+
custom:
5+
Issue: "462"

.github/workflows/ci-protobuf.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
# pinned here to prevent unexpected differences. Follow the
3030
# https://github.com/protocolbuffers/protobuf repository for protoc
3131
# release updates.
32-
version: '27.3'
32+
version: '29.3'
3333
- run: go mod download
3434
- run: make tools
3535
- run: make protoc

internal/logging/keys.go

+3
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,7 @@ const (
8080

8181
// Whether the DeferralAllowed client capability is enabled
8282
KeyClientCapabilityDeferralAllowed = "tf_client_capability_deferral_allowed"
83+
84+
// Whether the WriteOnlyAttributesAllowed client capability is enabled
85+
KeyClientCapabilityWriteOnlyAttributesAllowed = "tf_client_capability_write_only_attributes_allowed"
8386
)

tfprotov5/client_capabilities.go

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33

44
package tfprotov5
55

6+
// ValidateResourceTypeConfigClientCapabilities allows Terraform to publish information
7+
// regarding optionally supported protocol features for the ValidateResourceTypeConfig RPC,
8+
// such as forward-compatible Terraform behavior changes.
9+
type ValidateResourceTypeConfigClientCapabilities struct {
10+
// WriteOnlyAttributesAllowed signals that the client is able to
11+
// handle write_only attributes for managed resources.
12+
WriteOnlyAttributesAllowed bool
13+
}
14+
615
// ConfigureProviderClientCapabilities allows Terraform to publish information
716
// regarding optionally supported protocol features for the ConfigureProvider RPC,
817
// such as forward-compatible Terraform behavior changes.

tfprotov5/internal/fromproto/client_capabilities.go

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ import (
88
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
99
)
1010

11+
func ValidateResourceTypeConfigClientCapabilities(in *tfplugin5.ClientCapabilities) *tfprotov5.ValidateResourceTypeConfigClientCapabilities {
12+
if in == nil {
13+
return nil
14+
}
15+
16+
resp := &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
17+
WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed,
18+
}
19+
20+
return resp
21+
}
22+
1123
func ConfigureProviderClientCapabilities(in *tfplugin5.ClientCapabilities) *tfprotov5.ConfigureProviderClientCapabilities {
1224
if in == nil {
1325
return nil

tfprotov5/internal/fromproto/client_capabilities_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,52 @@ import (
77
"testing"
88

99
"github.com/google/go-cmp/cmp"
10+
1011
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
1112
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/fromproto"
1213
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
1314
)
1415

16+
func TestValidateResourceTypeConfigClientCapabilities(t *testing.T) {
17+
t.Parallel()
18+
19+
testCases := map[string]struct {
20+
in *tfplugin5.ClientCapabilities
21+
expected *tfprotov5.ValidateResourceTypeConfigClientCapabilities
22+
}{
23+
"nil": {
24+
in: nil,
25+
expected: nil,
26+
},
27+
"zero": {
28+
in: &tfplugin5.ClientCapabilities{},
29+
expected: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{},
30+
},
31+
"WriteOnlyAttributesAllowed": {
32+
in: &tfplugin5.ClientCapabilities{
33+
WriteOnlyAttributesAllowed: true,
34+
},
35+
expected: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
36+
WriteOnlyAttributesAllowed: true,
37+
},
38+
},
39+
}
40+
41+
for name, testCase := range testCases {
42+
name, testCase := name, testCase
43+
44+
t.Run(name, func(t *testing.T) {
45+
t.Parallel()
46+
47+
got := fromproto.ValidateResourceTypeConfigClientCapabilities(testCase.in)
48+
49+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
50+
t.Errorf("unexpected difference: %s", diff)
51+
}
52+
})
53+
}
54+
}
55+
1556
func TestConfigureProviderClientCapabilities(t *testing.T) {
1657
t.Parallel()
1758

tfprotov5/internal/fromproto/resource.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ func ValidateResourceTypeConfigRequest(in *tfplugin5.ValidateResourceTypeConfig_
1414
}
1515

1616
resp := &tfprotov5.ValidateResourceTypeConfigRequest{
17-
Config: DynamicValue(in.Config),
18-
TypeName: in.TypeName,
17+
ClientCapabilities: ValidateResourceTypeConfigClientCapabilities(in.ClientCapabilities),
18+
Config: DynamicValue(in.Config),
19+
TypeName: in.TypeName,
1920
}
2021

2122
return resp

tfprotov5/internal/fromproto/resource_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/google/go-cmp/cmp"
10+
1011
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
1112
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/fromproto"
1213
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
@@ -471,6 +472,18 @@ func TestValidateResourceTypeConfigRequest(t *testing.T) {
471472
in: &tfplugin5.ValidateResourceTypeConfig_Request{},
472473
expected: &tfprotov5.ValidateResourceTypeConfigRequest{},
473474
},
475+
"ClientCapabilities": {
476+
in: &tfplugin5.ValidateResourceTypeConfig_Request{
477+
ClientCapabilities: &tfplugin5.ClientCapabilities{
478+
WriteOnlyAttributesAllowed: true,
479+
},
480+
},
481+
expected: &tfprotov5.ValidateResourceTypeConfigRequest{
482+
ClientCapabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
483+
WriteOnlyAttributesAllowed: true,
484+
},
485+
},
486+
},
474487
"Config": {
475488
in: &tfplugin5.ValidateResourceTypeConfig_Request{
476489
Config: testTfplugin5DynamicValue(),

tfprotov5/internal/tf5serverlogging/client_capabilities.go

+14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ import (
1010
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
1111
)
1212

13+
// ValidateResourceTypeConfigClientCapabilities generates a TRACE "Announced client capabilities" log.
14+
func ValidateResourceTypeConfigClientCapabilities(ctx context.Context, capabilities *tfprotov5.ValidateResourceTypeConfigClientCapabilities) {
15+
if capabilities == nil {
16+
logging.ProtocolTrace(ctx, "No announced client capabilities", map[string]interface{}{})
17+
return
18+
}
19+
20+
responseFields := map[string]interface{}{
21+
logging.KeyClientCapabilityWriteOnlyAttributesAllowed: capabilities.WriteOnlyAttributesAllowed,
22+
}
23+
24+
logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields)
25+
}
26+
1327
// ConfigureProviderClientCapabilities generates a TRACE "Announced client capabilities" log.
1428
func ConfigureProviderClientCapabilities(ctx context.Context, capabilities *tfprotov5.ConfigureProviderClientCapabilities) {
1529
if capabilities == nil {

tfprotov5/internal/tf5serverlogging/client_capabilities_test.go

+72-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,83 @@ import (
99
"testing"
1010

1111
"github.com/google/go-cmp/cmp"
12+
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
13+
"github.com/hashicorp/terraform-plugin-log/tfsdklogtest"
14+
1215
"github.com/hashicorp/terraform-plugin-go/internal/logging"
1316
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
1417
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tf5serverlogging"
15-
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
16-
"github.com/hashicorp/terraform-plugin-log/tfsdklogtest"
1718
)
1819

20+
func TestValidateResourceTypeConfigClientCapabilities(t *testing.T) {
21+
t.Parallel()
22+
23+
testCases := map[string]struct {
24+
capabilities *tfprotov5.ValidateResourceTypeConfigClientCapabilities
25+
expected []map[string]interface{}
26+
}{
27+
"nil": {
28+
capabilities: nil,
29+
expected: []map[string]interface{}{
30+
{
31+
"@level": "trace",
32+
"@message": "No announced client capabilities",
33+
"@module": "sdk.proto",
34+
},
35+
},
36+
},
37+
"empty": {
38+
capabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{},
39+
expected: []map[string]interface{}{
40+
{
41+
"@level": "trace",
42+
"@message": "Announced client capabilities",
43+
"@module": "sdk.proto",
44+
"tf_client_capability_write_only_attributes_allowed": false,
45+
},
46+
},
47+
},
48+
"write_only_attributes_allowed": {
49+
capabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
50+
WriteOnlyAttributesAllowed: true,
51+
},
52+
expected: []map[string]interface{}{
53+
{
54+
"@level": "trace",
55+
"@message": "Announced client capabilities",
56+
"@module": "sdk.proto",
57+
"tf_client_capability_write_only_attributes_allowed": true,
58+
},
59+
},
60+
},
61+
}
62+
63+
for name, testCase := range testCases {
64+
name, testCase := name, testCase
65+
66+
t.Run(name, func(t *testing.T) {
67+
t.Parallel()
68+
69+
var output bytes.Buffer
70+
71+
ctx := tfsdklogtest.RootLogger(context.Background(), &output)
72+
ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{})
73+
74+
tf5serverlogging.ValidateResourceTypeConfigClientCapabilities(ctx, testCase.capabilities)
75+
76+
entries, err := tfsdklogtest.MultilineJSONDecode(&output)
77+
78+
if err != nil {
79+
t.Fatalf("unable to read multiple line JSON: %s", err)
80+
}
81+
82+
if diff := cmp.Diff(entries, testCase.expected); diff != "" {
83+
t.Errorf("unexpected difference: %s", diff)
84+
}
85+
})
86+
}
87+
}
88+
1989
func TestConfigureProviderClientCapabilities(t *testing.T) {
2090
t.Parallel()
2191

0 commit comments

Comments
 (0)