diff --git a/internal/fromproto5/validatelistresourceconfig.go b/internal/fromproto5/validatelistresourceconfig.go new file mode 100644 index 000000000..b5cbf009e --- /dev/null +++ b/internal/fromproto5/validatelistresourceconfig.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateListResourceConfigRequest returns the *fwserver.ValidateListResourceConfigRequest +// equivalent of a *tfprotov5.ValidateListResourceConfigRequest. +func ValidateListResourceConfigRequest(ctx context.Context, proto5 *tfprotov5.ValidateListResourceConfigRequest, listResource list.ListResource, listResourceSchema fwschema.Schema) (*fwserver.ValidateListResourceConfigRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + fw := &fwserver.ValidateListResourceConfigRequest{} + + config, diags := Config(ctx, proto5.Config, listResourceSchema) + + fw.Config = config + fw.ListResource = listResource + + return fw, diags +} diff --git a/internal/fromproto5/validatelistresourceconfig_test.go b/internal/fromproto5/validatelistresourceconfig_test.go new file mode 100644 index 000000000..6f5af23f6 --- /dev/null +++ b/internal/fromproto5/validatelistresourceconfig_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateListResourceConfigRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.ValidateListResourceConfigRequest + listResourceSchema fwschema.Schema + listResource list.ListResource + expected *fwserver.ValidateListResourceConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.ValidateListResourceConfigRequest{}, + expected: &fwserver.ValidateListResourceConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov5.ValidateListResourceConfigRequest{ + Config: &testProto5DynamicValue, + }, + expected: &fwserver.ValidateListResourceConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.ValidateListResourceConfigRequest{ + Config: &testProto5DynamicValue, + }, + listResourceSchema: testFwSchema, + expected: &fwserver.ValidateListResourceConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ValidateListResourceConfigRequest(context.Background(), testCase.input, testCase.listResource, testCase.listResourceSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/validatelistresourceconfig.go b/internal/fromproto6/validatelistresourceconfig.go new file mode 100644 index 000000000..d330df451 --- /dev/null +++ b/internal/fromproto6/validatelistresourceconfig.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateListResourceConfigRequest returns the *fwserver.ValidateListResourceConfigRequest +// equivalent of a *tfprotov6.ValidateListResourceConfigRequest. +func ValidateListResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.ValidateListResourceConfigRequest, listResource list.ListResource, listResourceSchema fwschema.Schema) (*fwserver.ValidateListResourceConfigRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + fw := &fwserver.ValidateListResourceConfigRequest{} + + config, diags := Config(ctx, proto6.Config, listResourceSchema) + + fw.Config = config + fw.ListResource = listResource + + return fw, diags +} diff --git a/internal/fromproto6/validatelistresourceconfig_test.go b/internal/fromproto6/validatelistresourceconfig_test.go new file mode 100644 index 000000000..8eb177bb8 --- /dev/null +++ b/internal/fromproto6/validatelistresourceconfig_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateListResourceConfigRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.ValidateListResourceConfigRequest + listResourceSchema fwschema.Schema + listResource list.ListResource + expected *fwserver.ValidateListResourceConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.ValidateListResourceConfigRequest{}, + expected: &fwserver.ValidateListResourceConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov6.ValidateListResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + expected: &fwserver.ValidateListResourceConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.ValidateListResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + listResourceSchema: testFwSchema, + expected: &fwserver.ValidateListResourceConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ValidateListResourceConfigRequest(context.Background(), testCase.input, testCase.listResource, testCase.listResourceSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index ba7c74823..adac3cafe 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -42,6 +42,11 @@ type Server struct { // to [ephemeral.ConfigureRequest.ProviderData]. EphemeralResourceConfigureData any + // ListResourceConfigureData is the + // [provider.ConfigureResponse.ListResourceData] field value which is passed + // to [list.ConfigureRequest.ProviderData]. + ListResourceConfigureData any + // ActionConfigureData is the // [provider.ConfigureResponse.ActionData] field value which is passed // to [action.ConfigureRequest.ProviderData]. diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 74301d39c..6681f9a17 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -5,7 +5,7 @@ package fwserver import ( "context" - "errors" + "github.com/hashicorp/terraform-plugin-go/tftypes" "iter" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -84,12 +84,19 @@ type ListResult struct { var NoListResults = func(func(ListResult) bool) {} // ListResource implements the framework server ListResource RPC. -func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResultsStream) error { +func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResultsStream) { listResource := fwReq.ListResource - if fwReq.Config == nil { - fwStream.Results = NoListResults - return errors.New("Invalid ListResource request: Config cannot be nil") + if fwReq.Config == nil && fwReq.ResourceSchema != nil { + fwReq.Config = &tfsdk.Config{ + Raw: tftypes.NewValue(fwReq.ResourceSchema.Type().TerraformType(ctx), nil), + Schema: fwReq.ResourceSchema, + } + } else if fwReq.Config == nil && fwReq.ResourceIdentitySchema == nil { + fwReq.Config = &tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{}, nil), + Schema: fwReq.ResourceSchema, + } } req := list.ListRequest{ @@ -112,7 +119,6 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream } fwStream.Results = processListResults(req, stream.Results) - return nil } func processListResults(req list.ListRequest, stream iter.Seq[list.ListResult]) iter.Seq[ListResult] { diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go index 55cefe779..6e9a54b7a 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -167,149 +167,20 @@ func TestServerListResource(t *testing.T) { }, }, }, - "error-on-nil-config": { + "zero-results-on-nil-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, request: &fwserver.ListRequest{ - Config: nil, + Config: nil, // Simulating a nil config ListResource: &testprovider.ListResource{ ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { - resp.Results = list.NoListResults + resp.Results = list.NoListResults // Expecting no results when config is nil }, }, }, - expectedError: "Invalid ListResource request: Config cannot be nil", expectedStreamEvents: []fwserver.ListResult{}, - }, - "error-on-nil-resource-identity": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ListRequest{ - Config: &tfsdk.Config{}, - ListResource: &testprovider.ListResource{ - ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { - resp.Results = slices.Values([]list.ListResult{ - { - Identity: nil, - Resource: &tfsdk.Resource{ - Schema: testSchema, - Raw: testResourceValue1, - }, - DisplayName: "Test Resource 1", - }, - }) - }, - }, - }, - expectedStreamEvents: []fwserver.ListResult{ - { - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic("Incomplete List Result", "..."), - }, - }, - }, - }, - "error-on-null-resource-identity": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ListRequest{ - Config: &tfsdk.Config{}, - ListResource: &testprovider.ListResource{ - ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { - resp.Results = slices.Values([]list.ListResult{ - { - Identity: &tfsdk.ResourceIdentity{}, - Resource: &tfsdk.Resource{ - Schema: testSchema, - Raw: testResourceValue1, - }, - DisplayName: "Test Resource 1", - }, - }) - }, - }, - }, - expectedStreamEvents: []fwserver.ListResult{ - { - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic("Incomplete List Result", "..."), - }, - }, - }, - }, - "warning-on-missing-resource": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ListRequest{ - Config: &tfsdk.Config{}, - IncludeResource: true, - ListResource: &testprovider.ListResource{ - ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { - resp.Results = slices.Values([]list.ListResult{ - { - Identity: &tfsdk.ResourceIdentity{ - Schema: testIdentitySchema, - Raw: testIdentityValue1, - }, - Resource: nil, - DisplayName: "Test Resource 1", - }, - }) - }, - }, - }, - expectedStreamEvents: []fwserver.ListResult{ - { - Identity: &tfsdk.ResourceIdentity{ - Schema: testIdentitySchema, - Raw: testIdentityValue1, - }, - DisplayName: "Test Resource 1", - Diagnostics: diag.Diagnostics{ - diag.NewWarningDiagnostic("Incomplete List Result", "..."), - }, - }, - }, - }, - "warning-on-null-resource": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ListRequest{ - Config: &tfsdk.Config{}, - IncludeResource: true, - ListResource: &testprovider.ListResource{ - ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { - resp.Results = slices.Values([]list.ListResult{ - { - Identity: &tfsdk.ResourceIdentity{ - Schema: testIdentitySchema, - Raw: testIdentityValue1, - }, - Resource: &tfsdk.Resource{}, - DisplayName: "Test Resource 1", - }, - }) - }, - }, - }, - expectedStreamEvents: []fwserver.ListResult{ - { - Identity: &tfsdk.ResourceIdentity{ - Schema: testIdentitySchema, - Raw: testIdentityValue1, - }, - Resource: &tfsdk.Resource{}, - DisplayName: "Test Resource 1", - Diagnostics: diag.Diagnostics{ - diag.NewWarningDiagnostic("Incomplete List Result", "..."), - }, - }, - }, + expectedError: "config cannot be nil", }, } @@ -318,14 +189,10 @@ func TestServerListResource(t *testing.T) { t.Parallel() response := &fwserver.ListResultsStream{} - err := testCase.server.ListResource(context.Background(), testCase.request, response) - if err != nil && err.Error() != testCase.expectedError { - t.Fatalf("unexpected error: %s", err) - } + testCase.server.ListResource(context.Background(), testCase.request, response) opts := cmp.Options{ cmp.Comparer(func(a, b diag.Diagnostics) bool { - // Differences in Detail() are not relevant to correctness of logic for i := range a { if a[i].Severity() != b[i].Severity() || a[i].Summary() != b[i].Summary() { return false diff --git a/internal/fwserver/server_validatelistresourceconfig.go b/internal/fwserver/server_validatelistresourceconfig.go new file mode 100644 index 000000000..65bdfabad --- /dev/null +++ b/internal/fwserver/server_validatelistresourceconfig.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateListResourceConfigRequest is the framework server request for the +// ValidateListResourceConfig RPC. +type ValidateListResourceConfigRequest struct { + Config *tfsdk.Config + ListResource list.ListResource +} + +// ValidateListResourceConfigResponse is the framework server response for the +// ValidateListResourceConfig RPC. +type ValidateListResourceConfigResponse struct { + Diagnostics diag.Diagnostics +} + +// ValidateListResourceConfig implements the framework server ValidateListResourceConfig RPC. +func (s *Server) ValidateListResourceConfig(ctx context.Context, req *ValidateListResourceConfigRequest, resp *ValidateListResourceConfigResponse) { + if req == nil || req.Config == nil { + return + } + + if listResourceWithConfigure, ok := req.ListResource.(list.ListResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "ListResource implements ListResourceWithConfigure") + + configureReq := resource.ConfigureRequest{ + ProviderData: s.ListResourceConfigureData, + } + configureResp := resource.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined ListResource Configure") + listResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined ListResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + vdscReq := list.ValidateConfigRequest{ + Config: *req.Config, + } + + if listResourceWithConfigValidators, ok := req.ListResource.(list.ListResourceWithConfigValidators); ok { + logging.FrameworkTrace(ctx, "ListResource implements ListResourceWithConfigValidators") + + for _, configValidator := range listResourceWithConfigValidators.ListResourceConfigValidators(ctx) { + vdscResp := &list.ValidateConfigResponse{} + + logging.FrameworkTrace( + ctx, + "Calling provider defined ListResourceConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + configValidator.ValidateListResourceConfig(ctx, vdscReq, vdscResp) + logging.FrameworkTrace( + ctx, + "Called provider defined ListResourceConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + } + + if listResourceWithValidateConfig, ok := req.ListResource.(list.ListResourceWithValidateConfig); ok { + logging.FrameworkTrace(ctx, "ListResource implements ListResourceWithValidateConfig") + + vdscResp := &list.ValidateConfigResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined ListResource ValidateConfig") + listResourceWithValidateConfig.ValidateListResourceConfig(ctx, vdscReq, vdscResp) + logging.FrameworkTrace(ctx, "Called provider defined ListResource ValidateConfig") + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + + validateSchemaReq := ValidateSchemaRequest{ + ClientCapabilities: schemaCapabilities, + Config: *req.Config, + } + validateSchemaResp := ValidateSchemaResponse{} + + SchemaValidate(ctx, req.Config.Schema, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics.Append(validateSchemaResp.Diagnostics...) +} diff --git a/internal/fwserver/server_validatelistresourceconfig_test.go b/internal/fwserver/server_validatelistresourceconfig_test.go new file mode 100644 index 000000000..7b2e8abc2 --- /dev/null +++ b/internal/fwserver/server_validatelistresourceconfig_test.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := tfsdk.Config{ + Raw: testValue, + Schema: testSchema, + } + + testSchemaAttributeValidator := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+req.ConfigValue.ValueString()) + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidator := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidator, + } + + testSchemaAttributeValidatorError := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeError(req.Path, "error summary", "error detail") + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorError := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorError, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.ValidateListResourceConfigRequest + expectedResponse *fwserver.ValidateListResourceConfigResponse + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfig, + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{}, + }, + "request-config-AttributeValidator": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfigAttributeValidator, + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchemaAttributeValidator + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{}, + }, + "request-config-AttributeValidator-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfigAttributeValidatorError, + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchemaAttributeValidatorError + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "error summary", + "error detail", + ), + }, + }, + }, + "request-config-ListResourceWithConfigValidators": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfig, + ListResource: &testprovider.ListResourceWithConfigValidators{ + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []list.ConfigValidator { + return []list.ConfigValidator{ + &testprovider.ListResourceConfigValidator{ + ValidateListResourceMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{}, + }, + "request-config-ListResourceWithConfigValidators-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfig, + ListResource: &testprovider.ListResourceWithConfigValidators{ + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []list.ConfigValidator { + return []list.ConfigValidator{ + &testprovider.ListResourceConfigValidator{ + ValidateListResourceMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics.AddError("error summary 1", "error detail 1") + }, + }, + &testprovider.ListResourceConfigValidator{ + ValidateListResourceMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + // Intentionally set diagnostics instead of add/append. + // The framework should not overwrite existing diagnostics. + // Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/pull/94 + resp.Diagnostics = diag.Diagnostics{ + diag.NewErrorDiagnostic("error summary 2", "error detail 2"), + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "error summary 1", + "error detail 1", + ), + diag.NewErrorDiagnostic( + "error summary 2", + "error detail 2", + ), + }}, + }, + "request-config-ListResourceWithValidateConfig": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfig, + ListResource: &testprovider.ListResourceWithValidateConfig{ + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{}, + }, + "request-config-ListResourceWithValidateConfig-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateListResourceConfigRequest{ + Config: &testConfig, + ListResource: &testprovider.ListResourceWithValidateConfig{ + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.ValidateListResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.ValidateListResourceConfigResponse{} + testCase.server.ValidateListResourceConfig(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 29fe8e50e..ef1dc7f78 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -361,6 +361,10 @@ func TestServerGetMetadata(t *testing.T) { return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName }) + sort.Slice(got.ListResources, func(i int, j int) bool { + return got.ListResources[i].TypeName < got.ListResources[j].TypeName + }) + sort.Slice(got.Functions, func(i int, j int) bool { return got.Functions[i].Name < got.Functions[j].Name }) diff --git a/internal/proto5server/server_listresource.go b/internal/proto5server/server_listresource.go index 4c630448e..54bc34b37 100644 --- a/internal/proto5server/server_listresource.go +++ b/internal/proto5server/server_listresource.go @@ -69,10 +69,7 @@ func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov5.ListResou } stream := &fwserver.ListResultsStream{} - err := s.FrameworkServer.ListResource(ctx, req, stream) - if err != nil { - return protoStream, err - } + s.FrameworkServer.ListResource(ctx, req, stream) protoStream.Results = func(push func(tfprotov5.ListResourceResult) bool) { for result := range stream.Results { diff --git a/internal/proto5server/server_validatelistresourceconfig.go b/internal/proto5server/server_validatelistresourceconfig.go index 40c47ae15..affa7327f 100644 --- a/internal/proto5server/server_validatelistresourceconfig.go +++ b/internal/proto5server/server_validatelistresourceconfig.go @@ -6,9 +6,77 @@ package proto5server import ( "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) -func (s *Server) ValidateListResourceConfig(ctx context.Context, request *tfprotov5.ValidateListResourceConfigRequest) (*tfprotov5.ValidateListResourceConfigResponse, error) { - return &tfprotov5.ValidateListResourceConfigResponse{}, nil +// ValidateListResourceConfig satisfies the tfprotov5.ProviderServer interface. +func (s *Server) ValidateListResourceConfig(ctx context.Context, proto5Req *tfprotov5.ValidateListResourceConfigRequest) (*tfprotov5.ValidateListResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + + fwResp := &fwserver.ValidateListResourceConfigResponse{} + + listResource, diags := s.FrameworkServer.ListResourceType(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + listResourceSchema, diags := s.FrameworkServer.ListResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + config, diags := fromproto5.Config(ctx, proto5Req.Config, listResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + resourceSchema, diags := s.FrameworkServer.ResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + req := &fwserver.ListRequest{ + Config: config, + ListResource: listResource, + ResourceSchema: resourceSchema, + ResourceIdentitySchema: identitySchema, + } + stream := &fwserver.ListResultsStream{} + + s.FrameworkServer.ListResource(ctx, req, stream) + + fwReq, diags := fromproto5.ValidateListResourceConfigRequest(ctx, proto5Req, listResource, listResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateListResourceConfig(ctx, fwReq, fwResp) + + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil } diff --git a/internal/proto5server/server_validatelistresourceconfig_test.go b/internal/proto5server/server_validatelistresourceconfig_test.go new file mode 100644 index 000000000..1e25b7885 --- /dev/null +++ b/internal/proto5server/server_validatelistresourceconfig_test.go @@ -0,0 +1,200 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov5.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.ValidateListResourceConfigRequest + expectedError error + expectedResponse *tfprotov5.ValidateListResourceConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) {}, + } + }, + } + }, + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateListResourceConfigRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateListResourceConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateListResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateListResourceConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResourceWithValidateConfig{ + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateListResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateListResourceConfig(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 2209429da..c67fe11d2 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -361,6 +361,10 @@ func TestServerGetMetadata(t *testing.T) { return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName }) + sort.Slice(got.ListResources, func(i int, j int) bool { + return got.ListResources[i].TypeName < got.ListResources[j].TypeName + }) + sort.Slice(got.Functions, func(i int, j int) bool { return got.Functions[i].Name < got.Functions[j].Name }) diff --git a/internal/proto6server/server_listresource.go b/internal/proto6server/server_listresource.go index 8b3352366..b180c4fbc 100644 --- a/internal/proto6server/server_listresource.go +++ b/internal/proto6server/server_listresource.go @@ -69,10 +69,7 @@ func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov6.ListResou } stream := &fwserver.ListResultsStream{} - err := s.FrameworkServer.ListResource(ctx, req, stream) - if err != nil { - return protoStream, err - } + s.FrameworkServer.ListResource(ctx, req, stream) protoStream.Results = func(push func(tfprotov6.ListResourceResult) bool) { for result := range stream.Results { diff --git a/internal/proto6server/server_validatelistresourceconfig.go b/internal/proto6server/server_validatelistresourceconfig.go index f3b282018..0cf3ca9d0 100644 --- a/internal/proto6server/server_validatelistresourceconfig.go +++ b/internal/proto6server/server_validatelistresourceconfig.go @@ -5,10 +5,77 @@ package proto6server import ( "context" - + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) -func (s *Server) ValidateListResourceConfig(ctx context.Context, request *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { - return &tfprotov6.ValidateListResourceConfigResponse{}, nil +// ValidateListResourceConfig satisfies the tfprotov6.ProviderServer interface. +func (s *Server) ValidateListResourceConfig(ctx context.Context, proto6Req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + + fwResp := &fwserver.ValidateListResourceConfigResponse{} + + listResource, diags := s.FrameworkServer.ListResourceType(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + listResourceSchema, diags := s.FrameworkServer.ListResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + config, diags := fromproto6.Config(ctx, proto6Req.Config, listResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + resourceSchema, diags := s.FrameworkServer.ResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + req := &fwserver.ListRequest{ + Config: config, + ListResource: listResource, + ResourceSchema: resourceSchema, + ResourceIdentitySchema: identitySchema, + } + stream := &fwserver.ListResultsStream{} + + s.FrameworkServer.ListResource(ctx, req, stream) + + fwReq, diags := fromproto6.ValidateListResourceConfigRequest(ctx, proto6Req, listResource, listResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateListResourceConfig(ctx, fwReq, fwResp) + + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil } diff --git a/internal/proto6server/server_validatelistresourceconfig_test.go b/internal/proto6server/server_validatelistresourceconfig_test.go new file mode 100644 index 000000000..b17a23c95 --- /dev/null +++ b/internal/proto6server/server_validatelistresourceconfig_test.go @@ -0,0 +1,200 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov6.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.ValidateListResourceConfigRequest + expectedError error + expectedResponse *tfprotov6.ValidateListResourceConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) {}, + } + }, + } + }, + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateListResourceConfigRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateListResourceConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateListResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateListResourceConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResourceWithValidateConfig{ + ListResource: &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateListResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateListResourceConfig(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/listresourceconfigvalidator.go b/internal/testing/testprovider/listresourceconfigvalidator.go new file mode 100644 index 000000000..55989c029 --- /dev/null +++ b/internal/testing/testprovider/listresourceconfigvalidator.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/list" +) + +var _ list.ConfigValidator = &ListResourceConfigValidator{} + +// Declarative list.ConfigValidator for unit testing. +type ListResourceConfigValidator struct { + // ListResourceConfigValidator interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateListResourceMethod func(context.Context, list.ValidateConfigRequest, *list.ValidateConfigResponse) +} + +// Description satisfies the list.ConfigValidator interface. +func (v *ListResourceConfigValidator) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the list.ConfigValidator interface. +func (v *ListResourceConfigValidator) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the list.ConfigValidator interface. +func (v *ListResourceConfigValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + if v.ValidateListResourceMethod == nil { + return + } + + v.ValidateListResourceMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/listresourcewithconfigure.go b/internal/testing/testprovider/listresourcewithconfigure.go new file mode 100644 index 000000000..8a0ba9386 --- /dev/null +++ b/internal/testing/testprovider/listresourcewithconfigure.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-framework/list" +) + +var _ list.ListResource = &ListResourceWithConfigure{} +var _ list.ListResourceWithConfigure = &ListResourceWithConfigure{} + +// Declarative list.ListResourceWithConfigure for unit testing. +type ListResourceWithConfigure struct { + *ListResource + + // ListResourceWithConfigure interface methods + ConfigureMethod func(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) +} + +// Configure satisfies the list.ListResourceWithConfigure interface. +func (d *ListResourceWithConfigure) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if d.ConfigureMethod == nil { + return + } + + d.ConfigureMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/listresourcewithconfigvalidators.go b/internal/testing/testprovider/listresourcewithconfigvalidators.go new file mode 100644 index 000000000..40a09eb26 --- /dev/null +++ b/internal/testing/testprovider/listresourcewithconfigvalidators.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/list" +) + +var _ list.ListResource = &ListResourceWithConfigValidators{} +var _ list.ListResourceWithConfigValidators = &ListResourceWithConfigValidators{} + +// Declarative list.ListResourceWithConfigValidators for unit testing. +type ListResourceWithConfigValidators struct { + *ListResource + + // ListResourceWithConfigValidators interface methods + ConfigValidatorsMethod func(context.Context) []list.ConfigValidator +} + +// ConfigValidators satisfies the list.ListResourceWithConfigValidators interface. +func (p *ListResourceWithConfigValidators) ListResourceConfigValidators(ctx context.Context) []list.ConfigValidator { + if p.ConfigValidatorsMethod == nil { + return nil + } + + return p.ConfigValidatorsMethod(ctx) +} diff --git a/internal/testing/testprovider/listresourcewithvalidateconfig.go b/internal/testing/testprovider/listresourcewithvalidateconfig.go new file mode 100644 index 000000000..c36816c8d --- /dev/null +++ b/internal/testing/testprovider/listresourcewithvalidateconfig.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/list" +) + +var _ list.ListResource = &ListResourceWithValidateConfig{} +var _ list.ListResourceWithValidateConfig = &ListResourceWithValidateConfig{} + +// Declarative list.ListResourceWithValidateConfig for unit testing. +type ListResourceWithValidateConfig struct { + *ListResource + + // ListResourceWithValidateConfig interface methods + ValidateConfigMethod func(context.Context, list.ValidateConfigRequest, *list.ValidateConfigResponse) +} + +// ValidateConfig satisfies the list.ListResourceWithValidateConfig interface. +func (p *ListResourceWithValidateConfig) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + if p.ValidateConfigMethod == nil { + return + } + + p.ValidateConfigMethod(ctx, req, resp) +} diff --git a/internal/toproto5/list_resource_result_test.go b/internal/toproto5/list_resource_result_test.go new file mode 100644 index 000000000..4f47b737f --- /dev/null +++ b/internal/toproto5/list_resource_result_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestListResourceResult(t *testing.T) { + t.Parallel() + + testListResultData := &fwserver.ListResult{ + Identity: nil, + Resource: &tfsdk.Resource{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + }, + DisplayName: "test-display-name", + Diagnostics: nil, + } + + testCases := map[string]struct { + input *fwserver.ListResult + expected tfprotov5.ListResourceResult + }{ + "nil": { + input: &fwserver.ListResult{ + Identity: nil, + Resource: nil, + DisplayName: "", + Diagnostics: nil, + }, + expected: tfprotov5.ListResourceResult{ + Identity: nil, + Resource: nil, + DisplayName: "", + Diagnostics: nil, + }, + }, + "valid": { + input: testListResultData, + expected: tfprotov5.ListResourceResult{ + DisplayName: "test-display-name", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ListResourceResult(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/listresourcemetadata_test.go b/internal/toproto5/listresourcemetadata_test.go new file mode 100644 index 000000000..1d8ff2055 --- /dev/null +++ b/internal/toproto5/listresourcemetadata_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestListResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.ListResourceMetadata + expected tfprotov5.ListResourceMetadata + }{ + "TypeName": { + fw: fwserver.ListResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov5.ListResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ListResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/validatelistresourceconfig.go b/internal/toproto5/validatelistresourceconfig.go new file mode 100644 index 000000000..1be62ea4a --- /dev/null +++ b/internal/toproto5/validatelistresourceconfig.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateListResourceConfigResponse returns the *tfprotov5.ValidateListResourceConfigResponse +// equivalent of a *fwserver.ValidateListResourceConfigResponse. +func ValidateListResourceConfigResponse(ctx context.Context, fw *fwserver.ValidateListResourceConfigResponse) *tfprotov5.ValidateListResourceConfigResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto5 +} diff --git a/internal/toproto5/validatelistresourceconfig_test.go b/internal/toproto5/validatelistresourceconfig_test.go new file mode 100644 index 000000000..81fff79ba --- /dev/null +++ b/internal/toproto5/validatelistresourceconfig_test.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestValidateListResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateListResourceConfigResponse + expected *tfprotov5.ValidateListResourceConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateListResourceConfigResponse{}, + expected: &tfprotov5.ValidateListResourceConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateListResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ValidateListResourceConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/list_resource_result_test.go b/internal/toproto6/list_resource_result_test.go new file mode 100644 index 000000000..fc87b8034 --- /dev/null +++ b/internal/toproto6/list_resource_result_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestListResourceResult(t *testing.T) { + t.Parallel() + + testListResultData := &fwserver.ListResult{ + Identity: nil, + Resource: &tfsdk.Resource{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + }, + DisplayName: "test-display-name", + Diagnostics: nil, + } + + testCases := map[string]struct { + input *fwserver.ListResult + expected tfprotov6.ListResourceResult + }{ + "nil": { + input: &fwserver.ListResult{ + Identity: nil, + Resource: nil, + DisplayName: "", + Diagnostics: nil, + }, + expected: tfprotov6.ListResourceResult{ + Identity: nil, + Resource: nil, + DisplayName: "", + Diagnostics: nil, + }, + }, + "valid": { + input: testListResultData, + expected: tfprotov6.ListResourceResult{ + DisplayName: "test-display-name", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ListResourceResult(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/listresourcemetadata_test.go b/internal/toproto6/listresourcemetadata_test.go new file mode 100644 index 000000000..a50938ab8 --- /dev/null +++ b/internal/toproto6/listresourcemetadata_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestListResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.ListResourceMetadata + expected tfprotov6.ListResourceMetadata + }{ + "TypeName": { + fw: fwserver.ListResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov6.ListResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ListResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/validatelistresourceconfig.go b/internal/toproto6/validatelistresourceconfig.go new file mode 100644 index 000000000..58402d1a6 --- /dev/null +++ b/internal/toproto6/validatelistresourceconfig.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateListResourceConfigResponse returns the *tfprotov6.ValidateListResourceConfigResponse +// equivalent of a *fwserver.ValidateListResourceConfigResponse. +func ValidateListResourceConfigResponse(ctx context.Context, fw *fwserver.ValidateListResourceConfigResponse) *tfprotov6.ValidateListResourceConfigResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto6 +} diff --git a/internal/toproto6/validatelistresourceconfig_test.go b/internal/toproto6/validatelistresourceconfig_test.go new file mode 100644 index 000000000..c799b9c12 --- /dev/null +++ b/internal/toproto6/validatelistresourceconfig_test.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestValidateListResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateListResourceConfigResponse + expected *tfprotov6.ValidateListResourceConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateListResourceConfigResponse{}, + expected: &tfprotov6.ValidateListResourceConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateListResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ValidateListResourceConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/configure.go b/list/configure.go new file mode 100644 index 000000000..28c2a2418 --- /dev/null +++ b/list/configure.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// ConfigureRequest represents a request for the provider to configure an +// list resource, i.e., set provider-level data or clients. An instance of +// this request struct is supplied as an argument to the ListResource type +// Configure method. +type ConfigureRequest struct { + // ProviderData is the data set in the + // [provider.ConfigureResponse.ListResourceData] field. This data is + // provider-specifc and therefore can contain any necessary remote system + // clients, custom provider data, or anything else pertinent to the + // functionality of the ListResource. + // + // This data is only set after the ConfigureProvider RPC has been called + // by Terraform. + ProviderData any +} + +// ConfigureResponse represents a response to a ConfigureRequest. An +// instance of this response struct is supplied as an argument to the +// ListResource type Configure method. +type ConfigureResponse struct { + // Diagnostics report errors or warnings related to configuring of the + // ListResource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/list/doc.go b/list/doc.go new file mode 100644 index 000000000..f190a77fd --- /dev/null +++ b/list/doc.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package list contains all interfaces, request types, and response +// types for an list resource implementation. +// +// In Terraform, an list resource is a concept which enables provider +// developers to offer practitioners list values, which will not be stored +// in any artifact produced by Terraform (plan/state). List resources can +// optionally implement renewal logic via the (ListResource).Renew method +// and cleanup logic via the (ListResource).Close method. +// +// List resources are not saved into the Terraform plan or state and can +// only be referenced in other list values, such as provider configuration +// attributes. List resources are defined by a type/name, such as "examplecloud_thing", +// a schema representing the structure and data types of configuration, and lifecycle logic. +// +// The main starting point for implementations in this package is the +// ListResource type which represents an instance of an list resource +// that has its own configuration and lifecycle logic. The [list.ListResource] +// implementations are referenced by the [provider.ProviderWithListResources] type +// ListResources method, which enables the list resource practitioner usage. +package list diff --git a/list/list_resource.go b/list/list_resource.go index 27843833b..09f46ce8d 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -66,7 +66,7 @@ type ListResourceWithConfigure interface { type ListResourceWithConfigValidators interface { ListResource - // ListResourceConfigValidators returns a list of functions which will all be performed during validation. + // ConfigValidators returns a list of functions which will all be performed during validation. ListResourceConfigValidators(context.Context) []ConfigValidator } diff --git a/list/list_resource_test.go b/list/list_resource_test.go index c2c723c5f..7426f9309 100644 --- a/list/list_resource_test.go +++ b/list/list_resource_test.go @@ -45,7 +45,4 @@ func ExampleResource_listable() { var _ list.ListResourceWithConfigValidators = &ComputeInstanceWithListResourceConfigValidators{} var _ resource.Resource = &ComputeInstanceResource{} - var _ resource.ResourceWithConfigure = &ComputeInstanceResource{} - - // Output: } diff --git a/list/metadata.go b/list/metadata.go new file mode 100644 index 000000000..8efe0b780 --- /dev/null +++ b/list/metadata.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +// MetadataRequest represents a request for the ListResource to return metadata, +// such as its type name. An instance of this request struct is supplied as +// an argument to the ListResource type Metadata method. +type MetadataRequest struct { + // ProviderTypeName is the string returned from + // [provider.MetadataResponse.TypeName], if the Provider type implements + // the Metadata method. This string should prefix the ListResource type name + // with an underscore in the response. + ProviderTypeName string +} + +// MetadataResponse represents a response to a MetadataRequest. An +// instance of this response struct is supplied as an argument to the +// ListResource type Metadata method. +type MetadataResponse struct { + // TypeName should be the full list resource type, including the provider + // type prefix and an underscore. For example, examplecloud_thing. + TypeName string +}