Skip to content

Commit

Permalink
tfprotov5+tfprotov6: Write-only Attribute Implementation (#462)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
SBGoods and austinvalle authored Jan 21, 2025
1 parent 707c7af commit afd18f1
Show file tree
Hide file tree
Showing 33 changed files with 2,136 additions and 1,663 deletions.
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20250121-163456.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'tfprotov5+tfprotov6: Upgraded protocols and added types to support write-only attributes'
time: 2025-01-21T16:34:56.142885-05:00
custom:
Issue: "462"
2 changes: 1 addition & 1 deletion .github/workflows/ci-protobuf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
# pinned here to prevent unexpected differences. Follow the
# https://github.com/protocolbuffers/protobuf repository for protoc
# release updates.
version: '27.3'
version: '29.3'
- run: go mod download
- run: make tools
- run: make protoc
Expand Down
3 changes: 3 additions & 0 deletions internal/logging/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ const (

// Whether the DeferralAllowed client capability is enabled
KeyClientCapabilityDeferralAllowed = "tf_client_capability_deferral_allowed"

// Whether the WriteOnlyAttributesAllowed client capability is enabled
KeyClientCapabilityWriteOnlyAttributesAllowed = "tf_client_capability_write_only_attributes_allowed"
)
9 changes: 9 additions & 0 deletions tfprotov5/client_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@

package tfprotov5

// ValidateResourceTypeConfigClientCapabilities allows Terraform to publish information
// regarding optionally supported protocol features for the ValidateResourceTypeConfig RPC,
// such as forward-compatible Terraform behavior changes.
type ValidateResourceTypeConfigClientCapabilities struct {
// WriteOnlyAttributesAllowed signals that the client is able to
// handle write_only attributes for managed resources.
WriteOnlyAttributesAllowed bool
}

// ConfigureProviderClientCapabilities allows Terraform to publish information
// regarding optionally supported protocol features for the ConfigureProvider RPC,
// such as forward-compatible Terraform behavior changes.
Expand Down
12 changes: 12 additions & 0 deletions tfprotov5/internal/fromproto/client_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func ValidateResourceTypeConfigClientCapabilities(in *tfplugin5.ClientCapabilities) *tfprotov5.ValidateResourceTypeConfigClientCapabilities {
if in == nil {
return nil
}

resp := &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed,
}

return resp
}

func ConfigureProviderClientCapabilities(in *tfplugin5.ClientCapabilities) *tfprotov5.ConfigureProviderClientCapabilities {
if in == nil {
return nil
Expand Down
41 changes: 41 additions & 0 deletions tfprotov5/internal/fromproto/client_capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,52 @@ import (
"testing"

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

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/fromproto"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func TestValidateResourceTypeConfigClientCapabilities(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
in *tfplugin5.ClientCapabilities
expected *tfprotov5.ValidateResourceTypeConfigClientCapabilities
}{
"nil": {
in: nil,
expected: nil,
},
"zero": {
in: &tfplugin5.ClientCapabilities{},
expected: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{},
},
"WriteOnlyAttributesAllowed": {
in: &tfplugin5.ClientCapabilities{
WriteOnlyAttributesAllowed: true,
},
expected: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
WriteOnlyAttributesAllowed: true,
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got := fromproto.ValidateResourceTypeConfigClientCapabilities(testCase.in)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestConfigureProviderClientCapabilities(t *testing.T) {
t.Parallel()

Expand Down
5 changes: 3 additions & 2 deletions tfprotov5/internal/fromproto/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ func ValidateResourceTypeConfigRequest(in *tfplugin5.ValidateResourceTypeConfig_
}

resp := &tfprotov5.ValidateResourceTypeConfigRequest{
Config: DynamicValue(in.Config),
TypeName: in.TypeName,
ClientCapabilities: ValidateResourceTypeConfigClientCapabilities(in.ClientCapabilities),
Config: DynamicValue(in.Config),
TypeName: in.TypeName,
}

return resp
Expand Down
13 changes: 13 additions & 0 deletions tfprotov5/internal/fromproto/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

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

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/fromproto"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
Expand Down Expand Up @@ -471,6 +472,18 @@ func TestValidateResourceTypeConfigRequest(t *testing.T) {
in: &tfplugin5.ValidateResourceTypeConfig_Request{},
expected: &tfprotov5.ValidateResourceTypeConfigRequest{},
},
"ClientCapabilities": {
in: &tfplugin5.ValidateResourceTypeConfig_Request{
ClientCapabilities: &tfplugin5.ClientCapabilities{
WriteOnlyAttributesAllowed: true,
},
},
expected: &tfprotov5.ValidateResourceTypeConfigRequest{
ClientCapabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
WriteOnlyAttributesAllowed: true,
},
},
},
"Config": {
in: &tfplugin5.ValidateResourceTypeConfig_Request{
Config: testTfplugin5DynamicValue(),
Expand Down
14 changes: 14 additions & 0 deletions tfprotov5/internal/tf5serverlogging/client_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
)

// ValidateResourceTypeConfigClientCapabilities generates a TRACE "Announced client capabilities" log.
func ValidateResourceTypeConfigClientCapabilities(ctx context.Context, capabilities *tfprotov5.ValidateResourceTypeConfigClientCapabilities) {
if capabilities == nil {
logging.ProtocolTrace(ctx, "No announced client capabilities", map[string]interface{}{})
return
}

responseFields := map[string]interface{}{
logging.KeyClientCapabilityWriteOnlyAttributesAllowed: capabilities.WriteOnlyAttributesAllowed,
}

logging.ProtocolTrace(ctx, "Announced client capabilities", responseFields)
}

// ConfigureProviderClientCapabilities generates a TRACE "Announced client capabilities" log.
func ConfigureProviderClientCapabilities(ctx context.Context, capabilities *tfprotov5.ConfigureProviderClientCapabilities) {
if capabilities == nil {
Expand Down
74 changes: 72 additions & 2 deletions tfprotov5/internal/tf5serverlogging/client_capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,83 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
"github.com/hashicorp/terraform-plugin-log/tfsdklogtest"

"github.com/hashicorp/terraform-plugin-go/internal/logging"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tf5serverlogging"
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
"github.com/hashicorp/terraform-plugin-log/tfsdklogtest"
)

func TestValidateResourceTypeConfigClientCapabilities(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
capabilities *tfprotov5.ValidateResourceTypeConfigClientCapabilities
expected []map[string]interface{}
}{
"nil": {
capabilities: nil,
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "No announced client capabilities",
"@module": "sdk.proto",
},
},
},
"empty": {
capabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{},
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "Announced client capabilities",
"@module": "sdk.proto",
"tf_client_capability_write_only_attributes_allowed": false,
},
},
},
"write_only_attributes_allowed": {
capabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{
WriteOnlyAttributesAllowed: true,
},
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "Announced client capabilities",
"@module": "sdk.proto",
"tf_client_capability_write_only_attributes_allowed": true,
},
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

var output bytes.Buffer

ctx := tfsdklogtest.RootLogger(context.Background(), &output)
ctx = logging.ProtoSubsystemContext(ctx, tfsdklog.Options{})

tf5serverlogging.ValidateResourceTypeConfigClientCapabilities(ctx, testCase.capabilities)

entries, err := tfsdklogtest.MultilineJSONDecode(&output)

if err != nil {
t.Fatalf("unable to read multiple line JSON: %s", err)
}

if diff := cmp.Diff(entries, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestConfigureProviderClientCapabilities(t *testing.T) {
t.Parallel()

Expand Down
Loading

0 comments on commit afd18f1

Please sign in to comment.