Skip to content

Commit

Permalink
tfprotov5+tfprotov6: Update to protocol versions 5.4/6.4 with GetMeta…
Browse files Browse the repository at this point in the history
…data RPC and GetProviderSchemaOptional server capability (#311)

Reference: #310

Protocol upgrades that impose new RPCs will either require:

- The `tfprotov5.ProviderServer`/`tfprotov6.ProviderServer` interface to require a new method
- Or, new "optional" interfaces be implemented (let's make up `ProviderServerWithGetMetadata` in this case)

terraform-plugin-go is a low level abstraction which is designed to _directly implement_ the protocol rather than introduce its own abstractions. Most provider developers will directly interface with higher level Go modules, such as terraform-plugin-sdk and terraform-plugin-framework. Except for advanced provider development using this Go module directly, this type of low level "breaking" change will be hidden by also upgrading those Go modules at the same time:

- hashicorp/terraform-plugin-sdk#1235
- hashicorp/terraform-plugin-framework#829
- hashicorp/terraform-plugin-mux#186

Therefore the change is implemented on the existing interface.
  • Loading branch information
bflad authored Sep 6, 2023
1 parent 94c7a39 commit 24e6961
Show file tree
Hide file tree
Showing 42 changed files with 3,101 additions and 1,490 deletions.
7 changes: 7 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20230707-100231.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: ENHANCEMENTS
body: 'tfprotov5: Added `ServerCapabilities` type `GetProviderSchemaOptional` field,
which when enabled can signal that the provider supports RPC operations without
the `GetProviderSchema` RPC being called first'
time: 2023-07-07T10:02:31.242394-04:00
custom:
Issue: "310"
7 changes: 7 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20230707-100339.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: ENHANCEMENTS
body: 'tfprotov6: Added `ServerCapabilities` type `GetProviderSchemaOptional` field,
which when enabled can signal that the provider supports RPC operations without
the `GetProviderSchema` RPC being called first'
time: 2023-07-07T10:03:39.802189-04:00
custom:
Issue: "310"
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20230824-152200.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'tfprotov5: Upgraded protocol to 5.4 and implemented `GetMetadata` RPC'
time: 2023-08-24T15:22:00.773731-04:00
custom:
Issue: "310"
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20230824-152220.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'tfprotov6: Upgraded protocol to 6.4 and implemented `GetMetadata` RPC'
time: 2023-08-24T15:22:20.285837-04:00
custom:
Issue: "310"
7 changes: 7 additions & 0 deletions .changes/unreleased/NOTES-20230824-153956.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: NOTES
body: 'all: If using terraform-plugin-framework, terraform-plugin-mux, or terraform-plugin-sdk,
only upgrade this Go module when upgrading those Go modules or you may receive
a `missing GetMetadata method` error when compiling'
time: 2023-08-24T15:39:56.697018-04:00
custom:
Issue: "310"
6 changes: 6 additions & 0 deletions internal/logging/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ const (

// The protocol version being used, as a string, such as "6"
KeyProtocolVersion = "tf_proto_version"

// Whether the GetProviderSchemaOptional server capability is enabled
KeyServerCapabilityGetProviderSchemaOptional = "tf_server_capability_get_provider_schema_optional"

// Whether the PlanDestroy server capability is enabled
KeyServerCapabilityPlanDestroy = "tf_server_capability_plan_destroy"
)
7 changes: 7 additions & 0 deletions tfprotov5/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import (
"context"
)

// DataSourceMetadata describes metadata for a data resource in the GetMetadata
// RPC.
type DataSourceMetadata struct {
// TypeName is the name of the data resource.
TypeName string
}

// DataSourceServer is an interface containing the methods a data source
// implementation needs to fill.
type DataSourceServer interface {
Expand Down
10 changes: 10 additions & 0 deletions tfprotov5/internal/fromproto/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func DataSourceMetadata(in *tfplugin5.GetMetadata_DataSourceMetadata) *tfprotov5.DataSourceMetadata {
if in == nil {
return nil
}

return &tfprotov5.DataSourceMetadata{
TypeName: in.TypeName,
}
}

func ValidateDataSourceConfigRequest(in *tfplugin5.ValidateDataSourceConfig_Request) (*tfprotov5.ValidateDataSourceConfigRequest, error) {
resp := &tfprotov5.ValidateDataSourceConfigRequest{
TypeName: in.TypeName,
Expand Down
34 changes: 34 additions & 0 deletions tfprotov5/internal/fromproto/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func GetMetadataRequest(in *tfplugin5.GetMetadata_Request) (*tfprotov5.GetMetadataRequest, error) {
return &tfprotov5.GetMetadataRequest{}, nil
}

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

resp := &tfprotov5.GetMetadataResponse{
DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(in.DataSources)),
Resources: make([]tfprotov5.ResourceMetadata, 0, len(in.Resources)),
ServerCapabilities: ServerCapabilities(in.ServerCapabilities),
}

for _, datasource := range in.DataSources {
resp.DataSources = append(resp.DataSources, *DataSourceMetadata(datasource))
}

for _, resource := range in.Resources {
resp.Resources = append(resp.Resources, *ResourceMetadata(resource))
}

diags, err := Diagnostics(in.Diagnostics)

if err != nil {
return resp, err
}

resp.Diagnostics = diags

return resp, nil
}

func GetProviderSchemaRequest(in *tfplugin5.GetProviderSchema_Request) (*tfprotov5.GetProviderSchemaRequest, error) {
return &tfprotov5.GetProviderSchemaRequest{}, nil
}
Expand Down
10 changes: 10 additions & 0 deletions tfprotov5/internal/fromproto/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func ResourceMetadata(in *tfplugin5.GetMetadata_ResourceMetadata) *tfprotov5.ResourceMetadata {
if in == nil {
return nil
}

return &tfprotov5.ResourceMetadata{
TypeName: in.TypeName,
}
}

func ValidateResourceTypeConfigRequest(in *tfplugin5.ValidateResourceTypeConfig_Request) (*tfprotov5.ValidateResourceTypeConfigRequest, error) {
resp := &tfprotov5.ValidateResourceTypeConfigRequest{
TypeName: in.TypeName,
Expand Down
20 changes: 20 additions & 0 deletions tfprotov5/internal/fromproto/server_capabilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package fromproto

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

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

return &tfprotov5.ServerCapabilities{
GetProviderSchemaOptional: in.GetProviderSchemaOptional,
PlanDestroy: in.PlanDestroy,
}
}
26 changes: 26 additions & 0 deletions tfprotov5/internal/tf5serverlogging/server_capabilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tf5serverlogging

import (
"context"

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

// ServerCapabilities generates a TRACE "Announced server capabilities" log.
func ServerCapabilities(ctx context.Context, capabilities *tfprotov5.ServerCapabilities) {
responseFields := map[string]interface{}{
logging.KeyServerCapabilityGetProviderSchemaOptional: false,
logging.KeyServerCapabilityPlanDestroy: false,
}

if capabilities != nil {
responseFields[logging.KeyServerCapabilityGetProviderSchemaOptional] = capabilities.GetProviderSchemaOptional
responseFields[logging.KeyServerCapabilityPlanDestroy] = capabilities.PlanDestroy
}

logging.ProtocolTrace(ctx, "Announced server capabilities", responseFields)
}
104 changes: 104 additions & 0 deletions tfprotov5/internal/tf5serverlogging/server_capabilities_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tf5serverlogging_test

import (
"bytes"
"context"
"testing"

"github.com/google/go-cmp/cmp"
"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 TestServerCapabilities(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
capabilities *tfprotov5.ServerCapabilities
expected []map[string]interface{}
}{
"nil": {
capabilities: nil,
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "Announced server capabilities",
"@module": "sdk.proto",
"tf_server_capability_get_provider_schema_optional": false,
"tf_server_capability_plan_destroy": false,
},
},
},
"empty": {
capabilities: &tfprotov5.ServerCapabilities{},
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "Announced server capabilities",
"@module": "sdk.proto",
"tf_server_capability_get_provider_schema_optional": false,
"tf_server_capability_plan_destroy": false,
},
},
},
"get_provider_schema_optional": {
capabilities: &tfprotov5.ServerCapabilities{
GetProviderSchemaOptional: true,
},
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "Announced server capabilities",
"@module": "sdk.proto",
"tf_server_capability_get_provider_schema_optional": true,
"tf_server_capability_plan_destroy": false,
},
},
},
"plan_destroy": {
capabilities: &tfprotov5.ServerCapabilities{
PlanDestroy: true,
},
expected: []map[string]interface{}{
{
"@level": "trace",
"@message": "Announced server capabilities",
"@module": "sdk.proto",
"tf_server_capability_get_provider_schema_optional": false,
"tf_server_capability_plan_destroy": 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.ServerCapabilities(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)
}
})
}
}
Loading

0 comments on commit 24e6961

Please sign in to comment.