Skip to content

Commit 65a9bf0

Browse files
committed
internal: Migrate ValidateResourceConfig RPC to fromproto6, fwserver, and toproto6
Reference: #215 Reference: #299 This introduces framework server caching for resource types and schemas, similar to the data source schema, data source type, and provider schema caches, which is needed with other resource RPCs.
1 parent 3c09406 commit 65a9bf0

9 files changed

+510
-108
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fromproto6
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
8+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
)
11+
12+
// ValidateResourceConfigRequest returns the *fwserver.ValidateResourceConfigRequest
13+
// equivalent of a *tfprotov6.ValidateResourceConfigRequest.
14+
func ValidateResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.ValidateResourceConfigRequest, resourceType tfsdk.ResourceType, resourceSchema *tfsdk.Schema) (*fwserver.ValidateResourceConfigRequest, diag.Diagnostics) {
15+
if proto6 == nil {
16+
return nil, nil
17+
}
18+
19+
fw := &fwserver.ValidateResourceConfigRequest{}
20+
21+
config, diags := Config(ctx, proto6.Config, resourceSchema)
22+
23+
fw.Config = config
24+
fw.ResourceType = resourceType
25+
26+
return fw, diags
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package fromproto6_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/internal/fromproto6"
10+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
11+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
14+
"github.com/hashicorp/terraform-plugin-go/tftypes"
15+
)
16+
17+
func TestValidateResourceConfigRequest(t *testing.T) {
18+
t.Parallel()
19+
20+
testProto6Type := tftypes.Object{
21+
AttributeTypes: map[string]tftypes.Type{
22+
"test_attribute": tftypes.String,
23+
},
24+
}
25+
26+
testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{
27+
"test_attribute": tftypes.NewValue(tftypes.String, "test-value"),
28+
})
29+
30+
testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value)
31+
32+
if err != nil {
33+
t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err)
34+
}
35+
36+
testFwSchema := &tfsdk.Schema{
37+
Attributes: map[string]tfsdk.Attribute{
38+
"test_attribute": {
39+
Required: true,
40+
Type: types.StringType,
41+
},
42+
},
43+
}
44+
45+
testCases := map[string]struct {
46+
input *tfprotov6.ValidateResourceConfigRequest
47+
resourceSchema *tfsdk.Schema
48+
resourceType tfsdk.ResourceType
49+
expected *fwserver.ValidateResourceConfigRequest
50+
expectedDiagnostics diag.Diagnostics
51+
}{
52+
"nil": {
53+
input: nil,
54+
expected: nil,
55+
},
56+
"empty": {
57+
input: &tfprotov6.ValidateResourceConfigRequest{},
58+
expected: &fwserver.ValidateResourceConfigRequest{},
59+
},
60+
"config-missing-schema": {
61+
input: &tfprotov6.ValidateResourceConfigRequest{
62+
Config: &testProto6DynamicValue,
63+
},
64+
expected: &fwserver.ValidateResourceConfigRequest{},
65+
expectedDiagnostics: diag.Diagnostics{
66+
diag.NewErrorDiagnostic(
67+
"Unable to Convert Configuration",
68+
"An unexpected error was encountered when converting the configuration from the protocol type. "+
69+
"This is always an issue in the Terraform Provider SDK used to implement the provider and should be reported to the provider developers.\n\n"+
70+
"Please report this to the provider developer:\n\n"+
71+
"Missing schema.",
72+
),
73+
},
74+
},
75+
"config": {
76+
input: &tfprotov6.ValidateResourceConfigRequest{
77+
Config: &testProto6DynamicValue,
78+
},
79+
resourceSchema: testFwSchema,
80+
expected: &fwserver.ValidateResourceConfigRequest{
81+
Config: &tfsdk.Config{
82+
Raw: testProto6Value,
83+
Schema: *testFwSchema,
84+
},
85+
},
86+
},
87+
}
88+
89+
for name, testCase := range testCases {
90+
name, testCase := name, testCase
91+
92+
t.Run(name, func(t *testing.T) {
93+
t.Parallel()
94+
95+
got, diags := fromproto6.ValidateResourceConfigRequest(context.Background(), testCase.input, testCase.resourceType, testCase.resourceSchema)
96+
97+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
98+
t.Errorf("unexpected difference: %s", diff)
99+
}
100+
101+
if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" {
102+
t.Errorf("unexpected diagnostics difference: %s", diff)
103+
}
104+
})
105+
}
106+
}

internal/fwserver/server.go

+123
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@ type Server struct {
5757
// providerSchemaMutex is a mutex to protect concurrent providerSchema
5858
// access from race conditions.
5959
providerSchemaMutex sync.Mutex
60+
61+
// resourceSchemas is the cached Resource Schemas for RPCs that need to
62+
// convert configuration data from the protocol. If not found, it will be
63+
// fetched from the ResourceType.GetSchema() method.
64+
resourceSchemas map[string]*tfsdk.Schema
65+
66+
// resourceSchemasDiags is the cached Diagnostics obtained while populating
67+
// resourceSchemas. This is to ensure any warnings or errors are also
68+
// returned appropriately when fetching resourceSchemas.
69+
resourceSchemasDiags diag.Diagnostics
70+
71+
// resourceSchemasMutex is a mutex to protect concurrent resourceSchemas
72+
// access from race conditions.
73+
resourceSchemasMutex sync.Mutex
74+
75+
// resourceTypes is the cached ResourceTypes for RPCs that need to
76+
// access resources. If not found, it will be fetched from the
77+
// Provider.GetResources() method.
78+
resourceTypes map[string]tfsdk.ResourceType
79+
80+
// resourceTypesDiags is the cached Diagnostics obtained while populating
81+
// resourceTypes. This is to ensure any warnings or errors are also
82+
// returned appropriately when fetching resourceTypes.
83+
resourceTypesDiags diag.Diagnostics
84+
85+
// resourceTypesMutex is a mutex to protect concurrent resourceTypes
86+
// access from race conditions.
87+
resourceTypesMutex sync.Mutex
6088
}
6189

6290
// DataSourceSchema returns the Schema associated with the DataSourceType for
@@ -174,3 +202,98 @@ func (s *Server) ProviderSchema(ctx context.Context) (*tfsdk.Schema, diag.Diagno
174202

175203
return s.providerSchema, s.providerSchemaDiags
176204
}
205+
206+
// ResourceSchema returns the Schema associated with the ResourceType for
207+
// the given type name.
208+
func (s *Server) ResourceSchema(ctx context.Context, typeName string) (*tfsdk.Schema, diag.Diagnostics) {
209+
resourceSchemas, diags := s.ResourceSchemas(ctx)
210+
211+
resourceSchema, ok := resourceSchemas[typeName]
212+
213+
if !ok {
214+
diags.AddError(
215+
"Resource Schema Not Found",
216+
fmt.Sprintf("No resource type named %q was found in the provider to fetch the schema. ", typeName)+
217+
"This is always an issue in the Terraform Provider SDK used to implement the provider and should be reported to the provider developers.",
218+
)
219+
220+
return nil, diags
221+
}
222+
223+
return resourceSchema, diags
224+
}
225+
226+
// ResourceSchemas returns the map of ResourceType Schemas. The results are
227+
// cached on first use.
228+
func (s *Server) ResourceSchemas(ctx context.Context) (map[string]*tfsdk.Schema, diag.Diagnostics) {
229+
logging.FrameworkTrace(ctx, "Checking ResourceSchemas lock")
230+
s.resourceSchemasMutex.Lock()
231+
defer s.resourceSchemasMutex.Unlock()
232+
233+
if s.resourceSchemas != nil {
234+
return s.resourceSchemas, s.resourceSchemasDiags
235+
}
236+
237+
resourceTypes, diags := s.ResourceTypes(ctx)
238+
239+
s.resourceSchemas = map[string]*tfsdk.Schema{}
240+
s.resourceSchemasDiags = diags
241+
242+
if s.resourceSchemasDiags.HasError() {
243+
return s.resourceSchemas, s.resourceSchemasDiags
244+
}
245+
246+
for resourceTypeName, resourceType := range resourceTypes {
247+
logging.FrameworkTrace(ctx, "Found resource type", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
248+
249+
logging.FrameworkDebug(ctx, "Calling provider defined ResourceType GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
250+
schema, diags := resourceType.GetSchema(ctx)
251+
logging.FrameworkDebug(ctx, "Called provider defined ResourceType GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
252+
253+
s.resourceSchemasDiags.Append(diags...)
254+
255+
if s.resourceSchemasDiags.HasError() {
256+
return s.resourceSchemas, s.resourceSchemasDiags
257+
}
258+
259+
s.resourceSchemas[resourceTypeName] = &schema
260+
}
261+
262+
return s.resourceSchemas, s.resourceSchemasDiags
263+
}
264+
265+
// ResourceType returns the ResourceType for a given type name.
266+
func (s *Server) ResourceType(ctx context.Context, typeName string) (tfsdk.ResourceType, diag.Diagnostics) {
267+
resourceTypes, diags := s.ResourceTypes(ctx)
268+
269+
resourceType, ok := resourceTypes[typeName]
270+
271+
if !ok {
272+
diags.AddError(
273+
"Resource Type Not Found",
274+
fmt.Sprintf("No resource type named %q was found in the provider.", typeName),
275+
)
276+
277+
return nil, diags
278+
}
279+
280+
return resourceType, diags
281+
}
282+
283+
// ResourceTypes returns the map of ResourceTypes. The results are cached
284+
// on first use.
285+
func (s *Server) ResourceTypes(ctx context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
286+
logging.FrameworkTrace(ctx, "Checking ResourceTypes lock")
287+
s.resourceTypesMutex.Lock()
288+
defer s.resourceTypesMutex.Unlock()
289+
290+
if s.resourceTypes != nil {
291+
return s.resourceTypes, s.resourceTypesDiags
292+
}
293+
294+
logging.FrameworkDebug(ctx, "Calling provider defined Provider GetResources")
295+
s.resourceTypes, s.resourceTypesDiags = s.Provider.GetResources(ctx)
296+
logging.FrameworkDebug(ctx, "Called provider defined Provider GetResources")
297+
298+
return s.resourceTypes, s.resourceTypesDiags
299+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package fwserver
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
8+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
9+
)
10+
11+
// ValidateResourceConfigRequest is the framework server request for the
12+
// ValidateResourceConfig RPC.
13+
type ValidateResourceConfigRequest struct {
14+
Config *tfsdk.Config
15+
ResourceType tfsdk.ResourceType
16+
}
17+
18+
// ValidateResourceConfigResponse is the framework server response for the
19+
// ValidateResourceConfig RPC.
20+
type ValidateResourceConfigResponse struct {
21+
Diagnostics diag.Diagnostics
22+
}
23+
24+
// ValidateResourceConfig implements the framework server ValidateResourceConfig RPC.
25+
func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) {
26+
if req == nil || req.Config == nil {
27+
return
28+
}
29+
30+
// Always instantiate new Resource instances.
31+
logging.FrameworkDebug(ctx, "Calling provider defined ResourceType NewResource")
32+
resource, diags := req.ResourceType.NewResource(ctx, s.Provider)
33+
logging.FrameworkDebug(ctx, "Called provider defined ResourceType NewResource")
34+
35+
resp.Diagnostics.Append(diags...)
36+
37+
if resp.Diagnostics.HasError() {
38+
return
39+
}
40+
41+
vdscReq := tfsdk.ValidateResourceConfigRequest{
42+
Config: *req.Config,
43+
}
44+
45+
if resource, ok := resource.(tfsdk.ResourceWithConfigValidators); ok {
46+
logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigValidators")
47+
48+
for _, configValidator := range resource.ConfigValidators(ctx) {
49+
vdscResp := &tfsdk.ValidateResourceConfigResponse{
50+
Diagnostics: resp.Diagnostics,
51+
}
52+
53+
logging.FrameworkDebug(
54+
ctx,
55+
"Calling provider defined ResourceConfigValidator",
56+
map[string]interface{}{
57+
logging.KeyDescription: configValidator.Description(ctx),
58+
},
59+
)
60+
configValidator.Validate(ctx, vdscReq, vdscResp)
61+
logging.FrameworkDebug(
62+
ctx,
63+
"Called provider defined ResourceConfigValidator",
64+
map[string]interface{}{
65+
logging.KeyDescription: configValidator.Description(ctx),
66+
},
67+
)
68+
69+
resp.Diagnostics = vdscResp.Diagnostics
70+
}
71+
}
72+
73+
if resource, ok := resource.(tfsdk.ResourceWithValidateConfig); ok {
74+
logging.FrameworkTrace(ctx, "Resource implements ResourceWithValidateConfig")
75+
76+
vdscResp := &tfsdk.ValidateResourceConfigResponse{
77+
Diagnostics: resp.Diagnostics,
78+
}
79+
80+
logging.FrameworkDebug(ctx, "Calling provider defined Resource ValidateConfig")
81+
resource.ValidateConfig(ctx, vdscReq, vdscResp)
82+
logging.FrameworkDebug(ctx, "Called provider defined Resource ValidateConfig")
83+
84+
resp.Diagnostics = vdscResp.Diagnostics
85+
}
86+
87+
validateSchemaReq := ValidateSchemaRequest{
88+
Config: *req.Config,
89+
}
90+
validateSchemaResp := ValidateSchemaResponse{
91+
Diagnostics: resp.Diagnostics,
92+
}
93+
94+
SchemaValidate(ctx, req.Config.Schema, validateSchemaReq, &validateSchemaResp)
95+
96+
resp.Diagnostics = validateSchemaResp.Diagnostics
97+
}

0 commit comments

Comments
 (0)