Skip to content

Commit

Permalink
Introduce providerserver package, deprecate tfsdk server functionality
Browse files Browse the repository at this point in the history
Reference: #215
Reference: #294
Reference: #296

This change represents the first half of the work necessary to extract the `tfprotov6.ProviderServer` implementation and helper functions out of the `tfsdk` package and into separate packages. The `providerserver` package will be the provider developer facing functionality, while the next iteration of this refactoring will move the actual server implementation into a separate internal package. Once in that separate internal package, efforts can be made to make that code handle terraform-plugin-go type conversions "at the edge" better.
  • Loading branch information
bflad committed Apr 27, 2022
1 parent de076e9 commit b1323a7
Show file tree
Hide file tree
Showing 13 changed files with 428 additions and 163 deletions.
11 changes: 0 additions & 11 deletions .changelog/294.txt

This file was deleted.

7 changes: 0 additions & 7 deletions .changelog/296.txt

This file was deleted.

19 changes: 19 additions & 0 deletions .changelog/pending.txt
@@ -0,0 +1,19 @@
```release-note:note
tfsdk: The `NewProtocol6Server()` function has been deprecated in preference of `providerserver.NewProtocol6()` and `providerserver.NewProtocol6WithError()` functions, which will simplify muxing and testing implementations. The `tfsdk.NewProtocol6Server()` function will be removed in the next minor version.
```

```release-note:note
tfsdk: The `Serve()` function has been deprecated in preference of the `providerserver.Serve()` function. The `tfsdk.Serve()` function will be removed in the next minor version.
```

```release-note:note
tfsdk: The `ServeOpts` type has been deprecated in preference of the `providerserver.ServeOpts` type. When migrating, the `Name` field has been replaced with `Address`. The `tfsdk.ServeOpts` type will be removed in the next minor version.
```

```release-note:note
tfsdk: The previously unexported `server` type has been temporarily exported to aid in the migration to the new `providerserver` package. It is not intended for provider developer usage and will be moved into an internal package in the next minor version.
```

```release-note:feature
Introduced `providerserver` package, which contains all functions and types necessary for serving a provider in production or acceptance testing.
```
31 changes: 31 additions & 0 deletions providerserver/provider_test.go
@@ -0,0 +1,31 @@
package providerserver

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
)

var _ tfsdk.Provider = &testProvider{}

// Provider type for testing package functionality.
//
// This is separate from tfsdk.testServeProvider to avoid changing that.
type testProvider struct{}

func (t *testProvider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{}, nil
}

func (t *testProvider) Configure(_ context.Context, _ tfsdk.ConfigureProviderRequest, _ *tfsdk.ConfigureProviderResponse) {
// intentionally empty
}

func (t *testProvider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) {
return map[string]tfsdk.DataSourceType{}, nil
}

func (t *testProvider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
return map[string]tfsdk.ResourceType{}, nil
}
59 changes: 59 additions & 0 deletions providerserver/providerserver.go
@@ -0,0 +1,59 @@
package providerserver

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server"
)

// Returns a protocol version 6 ProviderServer implementation based on the
// given Provider and suitable for usage with the terraform-plugin-go
// tf6server.Serve() function and various terraform-plugin-mux functions.
func NewProtocol6(p tfsdk.Provider) func() tfprotov6.ProviderServer {
return func() tfprotov6.ProviderServer {
return &tfsdk.Server{
Provider: p,
}
}
}

// Returns a protocol version 6 ProviderServer implementation based on the
// given Provider and suitable for usage with the terraform-plugin-sdk
// acceptance testing helper/resource.TestCase.ProtoV6ProviderFactories.
//
// The error return is not currently used, but it may be in the future.
func NewProtocol6WithError(p tfsdk.Provider) func() (tfprotov6.ProviderServer, error) {
return func() (tfprotov6.ProviderServer, error) {
return &tfsdk.Server{
Provider: p,
}, nil
}
}

// Serve serves a provider, blocking until the context is canceled.
func Serve(ctx context.Context, providerFunc func() tfsdk.Provider, opts ServeOpts) error {
err := opts.validate(ctx)

if err != nil {
return fmt.Errorf("unable to validate ServeOpts: %w", err)
}

var tf6serverOpts []tf6server.ServeOpt

if opts.Debug {
tf6serverOpts = append(tf6serverOpts, tf6server.WithManagedDebug())
}

return tf6server.Serve(
opts.Address,
func() tfprotov6.ProviderServer {
return &tfsdk.Server{
Provider: providerFunc(),
}
},
tf6serverOpts...,
)
}
39 changes: 39 additions & 0 deletions providerserver/providerserver_test.go
@@ -0,0 +1,39 @@
package providerserver

import (
"context"
"testing"

"github.com/hashicorp/terraform-plugin-go/tfprotov6"
)

func TestNewProtocol6(t *testing.T) {
provider := &testProvider{}

providerServerFunc := NewProtocol6(provider)
providerServer := providerServerFunc()

// Simple verification
_, err := providerServer.GetProviderSchema(context.Background(), &tfprotov6.GetProviderSchemaRequest{})

if err != nil {
t.Fatalf("unexpected error calling ProviderServer: %s", err)
}
}

func TestNewProtocol6WithError(t *testing.T) {
provider := &testProvider{}

providerServer, err := NewProtocol6WithError(provider)()

if err != nil {
t.Fatalf("unexpected error creating ProviderServer: %s", err)
}

// Simple verification
_, err = providerServer.GetProviderSchema(context.Background(), &tfprotov6.GetProviderSchemaRequest{})

if err != nil {
t.Fatalf("unexpected error calling ProviderServer: %s", err)
}
}
65 changes: 65 additions & 0 deletions providerserver/serve_opts.go
@@ -0,0 +1,65 @@
package providerserver

import (
"context"
"fmt"
"strings"
)

// ServeOpts are options for serving the provider.
type ServeOpts struct {
// Address is the full address of the provider. Full address form has three
// parts separated by forward slashes (/): Hostname, namespace, and
// provider type ("name").
//
// For example: registry.terraform.io/hashicorp/random.
Address string

// Debug runs the provider in a mode acceptable for debugging and testing
// processes, such as delve, by managing the process lifecycle. Information
// needed for Terraform CLI to connect to the provider is output to stdout.
// os.Interrupt (Ctrl-c) can be used to stop the provider.
Debug bool
}

// Validate a given provider address. This is only used for the Address field
// to preserve backwards compatibility for the Name field.
//
// This logic is manually implemented over importing
// github.com/hashicorp/terraform-registry-address as its functionality such as
// ParseAndInferProviderSourceString and ParseRawProviderSourceString allow
// shorter address formats, which would then require post-validation anyways.
func (opts ServeOpts) validateAddress(_ context.Context) error {
addressParts := strings.Split(opts.Address, "/")
formatErr := fmt.Errorf("expected hostname/namespace/type format, got: %s", opts.Address)

if len(addressParts) != 3 {
return formatErr
}

if addressParts[0] == "" || addressParts[1] == "" || addressParts[2] == "" {
return formatErr
}

return nil
}

// Validation checks for provider defined ServeOpts.
//
// Current checks which return errors:
//
// - If Address is not set
// - Address is a valid full provider address
func (opts ServeOpts) validate(ctx context.Context) error {
if opts.Address == "" {
return fmt.Errorf("Address must be provided")
}

err := opts.validateAddress(ctx)

if err != nil {
return fmt.Errorf("unable to validate Address: %w", err)
}

return nil
}
114 changes: 114 additions & 0 deletions providerserver/serve_opts_test.go
@@ -0,0 +1,114 @@
package providerserver

import (
"context"
"fmt"
"strings"
"testing"
)

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

testCases := map[string]struct {
serveOpts ServeOpts
expectedError error
}{
"Address": {
serveOpts: ServeOpts{
Address: "registry.terraform.io/hashicorp/testing",
},
},
"Address-missing": {
serveOpts: ServeOpts{},
expectedError: fmt.Errorf("Address must be provided"),
},
"Address-invalid-missing-hostname-and-namespace": {
serveOpts: ServeOpts{
Address: "testing",
},
expectedError: fmt.Errorf("unable to validate Address: expected hostname/namespace/type format, got: testing"),
},
"Address-invalid-missing-hostname": {
serveOpts: ServeOpts{
Address: "hashicorp/testing",
},
expectedError: fmt.Errorf("unable to validate Address: expected hostname/namespace/type format, got: hashicorp/testing"),
},
}

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

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

err := testCase.serveOpts.validate(context.Background())

if err != nil {
if testCase.expectedError == nil {
t.Fatalf("expected no error, got: %s", err)
}

if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
t.Fatalf("expected error %q, got: %s", testCase.expectedError, err)
}
}

if err == nil && testCase.expectedError != nil {
t.Fatalf("got no error, expected: %s", testCase.expectedError)
}
})
}
}

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

testCases := map[string]struct {
serveOpts ServeOpts
expectedError error
}{
"valid": {
serveOpts: ServeOpts{
Address: "registry.terraform.io/hashicorp/testing",
},
},
"invalid-missing-hostname-and-namepsace": {
serveOpts: ServeOpts{
Address: "testing",
},
expectedError: fmt.Errorf("expected hostname/namespace/type format, got: testing"),
},
"invalid-missing-hostname": {
serveOpts: ServeOpts{
Address: "hashicorp/testing",
},
expectedError: fmt.Errorf("expected hostname/namespace/type format, got: hashicorp/testing"),
},
}

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

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

err := testCase.serveOpts.validateAddress(context.Background())

if err != nil {
if testCase.expectedError == nil {
t.Fatalf("expected no error, got: %s", err)
}

if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
t.Fatalf("expected error %q, got: %s", testCase.expectedError, err)
}
}

if err == nil && testCase.expectedError != nil {
t.Fatalf("got no error, expected: %s", testCase.expectedError)
}
})
}
}

0 comments on commit b1323a7

Please sign in to comment.