Skip to content

Commit 74d4f9c

Browse files
authored
Introduce providerserver package, deprecate tfsdk server functionality (#308)
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.
1 parent de076e9 commit 74d4f9c

13 files changed

+429
-163
lines changed

.changelog/294.txt

-11
This file was deleted.

.changelog/296.txt

-7
This file was deleted.

.changelog/pending.txt

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
```release-note:note
2+
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.
3+
```
4+
5+
```release-note:note
6+
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.
7+
```
8+
9+
```release-note:note
10+
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.
11+
```
12+
13+
```release-note:note
14+
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.
15+
```
16+
17+
```release-note:feature
18+
Introduced `providerserver` package, which contains all functions and types necessary for serving a provider in production or acceptance testing.
19+
```

providerserver/provider_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package providerserver
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
8+
)
9+
10+
var _ tfsdk.Provider = &testProvider{}
11+
12+
// Provider type for testing package functionality.
13+
//
14+
// This is separate from tfsdk.testServeProvider to avoid changing that.
15+
type testProvider struct{}
16+
17+
func (t *testProvider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
18+
return tfsdk.Schema{}, nil
19+
}
20+
21+
func (t *testProvider) Configure(_ context.Context, _ tfsdk.ConfigureProviderRequest, _ *tfsdk.ConfigureProviderResponse) {
22+
// intentionally empty
23+
}
24+
25+
func (t *testProvider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) {
26+
return map[string]tfsdk.DataSourceType{}, nil
27+
}
28+
29+
func (t *testProvider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
30+
return map[string]tfsdk.ResourceType{}, nil
31+
}

providerserver/providerserver.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package providerserver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
8+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server"
10+
)
11+
12+
// NewProtocol6 returns a protocol version 6 ProviderServer implementation
13+
// based on the given Provider and suitable for usage with the
14+
// github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server.Serve()
15+
// function and various terraform-plugin-mux functions.
16+
func NewProtocol6(p tfsdk.Provider) func() tfprotov6.ProviderServer {
17+
return func() tfprotov6.ProviderServer {
18+
return &tfsdk.Server{
19+
Provider: p,
20+
}
21+
}
22+
}
23+
24+
// NewProtocol6WithError returns a protocol version 6 ProviderServer
25+
// implementation based on the given Provider and suitable for usage with
26+
// github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource.TestCase.ProtoV6ProviderFactories.
27+
//
28+
// The error return is not currently used, but it may be in the future.
29+
func NewProtocol6WithError(p tfsdk.Provider) func() (tfprotov6.ProviderServer, error) {
30+
return func() (tfprotov6.ProviderServer, error) {
31+
return &tfsdk.Server{
32+
Provider: p,
33+
}, nil
34+
}
35+
}
36+
37+
// Serve serves a provider, blocking until the context is canceled.
38+
func Serve(ctx context.Context, providerFunc func() tfsdk.Provider, opts ServeOpts) error {
39+
err := opts.validate(ctx)
40+
41+
if err != nil {
42+
return fmt.Errorf("unable to validate ServeOpts: %w", err)
43+
}
44+
45+
var tf6serverOpts []tf6server.ServeOpt
46+
47+
if opts.Debug {
48+
tf6serverOpts = append(tf6serverOpts, tf6server.WithManagedDebug())
49+
}
50+
51+
return tf6server.Serve(
52+
opts.Address,
53+
func() tfprotov6.ProviderServer {
54+
return &tfsdk.Server{
55+
Provider: providerFunc(),
56+
}
57+
},
58+
tf6serverOpts...,
59+
)
60+
}

providerserver/providerserver_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package providerserver
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
8+
)
9+
10+
func TestNewProtocol6(t *testing.T) {
11+
provider := &testProvider{}
12+
13+
providerServerFunc := NewProtocol6(provider)
14+
providerServer := providerServerFunc()
15+
16+
// Simple verification
17+
_, err := providerServer.GetProviderSchema(context.Background(), &tfprotov6.GetProviderSchemaRequest{})
18+
19+
if err != nil {
20+
t.Fatalf("unexpected error calling ProviderServer: %s", err)
21+
}
22+
}
23+
24+
func TestNewProtocol6WithError(t *testing.T) {
25+
provider := &testProvider{}
26+
27+
providerServer, err := NewProtocol6WithError(provider)()
28+
29+
if err != nil {
30+
t.Fatalf("unexpected error creating ProviderServer: %s", err)
31+
}
32+
33+
// Simple verification
34+
_, err = providerServer.GetProviderSchema(context.Background(), &tfprotov6.GetProviderSchemaRequest{})
35+
36+
if err != nil {
37+
t.Fatalf("unexpected error calling ProviderServer: %s", err)
38+
}
39+
}

providerserver/serve_opts.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package providerserver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
// ServeOpts are options for serving the provider.
10+
type ServeOpts struct {
11+
// Address is the full address of the provider. Full address form has three
12+
// parts separated by forward slashes (/): Hostname, namespace, and
13+
// provider type ("name").
14+
//
15+
// For example: registry.terraform.io/hashicorp/random.
16+
Address string
17+
18+
// Debug runs the provider in a mode acceptable for debugging and testing
19+
// processes, such as delve, by managing the process lifecycle. Information
20+
// needed for Terraform CLI to connect to the provider is output to stdout.
21+
// os.Interrupt (Ctrl-c) can be used to stop the provider.
22+
Debug bool
23+
}
24+
25+
// Validate a given provider address. This is only used for the Address field
26+
// to preserve backwards compatibility for the Name field.
27+
//
28+
// This logic is manually implemented over importing
29+
// github.com/hashicorp/terraform-registry-address as its functionality such as
30+
// ParseAndInferProviderSourceString and ParseRawProviderSourceString allow
31+
// shorter address formats, which would then require post-validation anyways.
32+
func (opts ServeOpts) validateAddress(_ context.Context) error {
33+
addressParts := strings.Split(opts.Address, "/")
34+
formatErr := fmt.Errorf("expected hostname/namespace/type format, got: %s", opts.Address)
35+
36+
if len(addressParts) != 3 {
37+
return formatErr
38+
}
39+
40+
if addressParts[0] == "" || addressParts[1] == "" || addressParts[2] == "" {
41+
return formatErr
42+
}
43+
44+
return nil
45+
}
46+
47+
// Validation checks for provider defined ServeOpts.
48+
//
49+
// Current checks which return errors:
50+
//
51+
// - If Address is not set
52+
// - Address is a valid full provider address
53+
func (opts ServeOpts) validate(ctx context.Context) error {
54+
if opts.Address == "" {
55+
return fmt.Errorf("Address must be provided")
56+
}
57+
58+
err := opts.validateAddress(ctx)
59+
60+
if err != nil {
61+
return fmt.Errorf("unable to validate Address: %w", err)
62+
}
63+
64+
return nil
65+
}

providerserver/serve_opts_test.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package providerserver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestServeOptsValidate(t *testing.T) {
11+
t.Parallel()
12+
13+
testCases := map[string]struct {
14+
serveOpts ServeOpts
15+
expectedError error
16+
}{
17+
"Address": {
18+
serveOpts: ServeOpts{
19+
Address: "registry.terraform.io/hashicorp/testing",
20+
},
21+
},
22+
"Address-missing": {
23+
serveOpts: ServeOpts{},
24+
expectedError: fmt.Errorf("Address must be provided"),
25+
},
26+
"Address-invalid-missing-hostname-and-namespace": {
27+
serveOpts: ServeOpts{
28+
Address: "testing",
29+
},
30+
expectedError: fmt.Errorf("unable to validate Address: expected hostname/namespace/type format, got: testing"),
31+
},
32+
"Address-invalid-missing-hostname": {
33+
serveOpts: ServeOpts{
34+
Address: "hashicorp/testing",
35+
},
36+
expectedError: fmt.Errorf("unable to validate Address: expected hostname/namespace/type format, got: hashicorp/testing"),
37+
},
38+
}
39+
40+
for name, testCase := range testCases {
41+
name, testCase := name, testCase
42+
43+
t.Run(name, func(t *testing.T) {
44+
t.Parallel()
45+
46+
err := testCase.serveOpts.validate(context.Background())
47+
48+
if err != nil {
49+
if testCase.expectedError == nil {
50+
t.Fatalf("expected no error, got: %s", err)
51+
}
52+
53+
if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
54+
t.Fatalf("expected error %q, got: %s", testCase.expectedError, err)
55+
}
56+
}
57+
58+
if err == nil && testCase.expectedError != nil {
59+
t.Fatalf("got no error, expected: %s", testCase.expectedError)
60+
}
61+
})
62+
}
63+
}
64+
65+
func TestServeOptsValidateAddress(t *testing.T) {
66+
t.Parallel()
67+
68+
testCases := map[string]struct {
69+
serveOpts ServeOpts
70+
expectedError error
71+
}{
72+
"valid": {
73+
serveOpts: ServeOpts{
74+
Address: "registry.terraform.io/hashicorp/testing",
75+
},
76+
},
77+
"invalid-missing-hostname-and-namepsace": {
78+
serveOpts: ServeOpts{
79+
Address: "testing",
80+
},
81+
expectedError: fmt.Errorf("expected hostname/namespace/type format, got: testing"),
82+
},
83+
"invalid-missing-hostname": {
84+
serveOpts: ServeOpts{
85+
Address: "hashicorp/testing",
86+
},
87+
expectedError: fmt.Errorf("expected hostname/namespace/type format, got: hashicorp/testing"),
88+
},
89+
}
90+
91+
for name, testCase := range testCases {
92+
name, testCase := name, testCase
93+
94+
t.Run(name, func(t *testing.T) {
95+
t.Parallel()
96+
97+
err := testCase.serveOpts.validateAddress(context.Background())
98+
99+
if err != nil {
100+
if testCase.expectedError == nil {
101+
t.Fatalf("expected no error, got: %s", err)
102+
}
103+
104+
if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
105+
t.Fatalf("expected error %q, got: %s", testCase.expectedError, err)
106+
}
107+
}
108+
109+
if err == nil && testCase.expectedError != nil {
110+
t.Fatalf("got no error, expected: %s", testCase.expectedError)
111+
}
112+
})
113+
}
114+
}

0 commit comments

Comments
 (0)