From 46062681410d53e745d7ecd42e831262e9e2abfd Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 8 Jul 2025 11:50:11 -0400 Subject: [PATCH 01/16] Added List Resource Config Validation --- go.mod | 2 +- go.sum | 4 +- .../fromproto5/validatelistresourceconfig.go | 31 ++ .../validatelistresourceconfig_test.go | 108 +++++++ .../fromproto6/validatelistresourceconfig.go | 31 ++ .../validatelistresourceconfig_test.go | 108 +++++++ internal/fwserver/server.go | 5 + .../server_validatelistresourceconfig.go | 108 +++++++ .../server_validatelistresourceconfig_test.go | 306 ++++++++++++++++++ .../server_validatelistresourceconfig.go | 60 +++- .../server_validatelistresourceconfig_test.go | 167 ++++++++++ .../server_validatelistresourceconfig.go | 60 +++- .../server_validatelistresourceconfig_test.go | 167 ++++++++++ .../{list_resource.go => listresource.go} | 0 .../listresourceconfigvalidator.go | 47 +++ .../testprovider/listresourcewithconfigure.go | 30 ++ .../listresourcewithconfigvalidators.go | 30 ++ .../listresourcewithvalidateconfig.go | 30 ++ .../toproto5/list_resource_result_test.go | 76 +++++ .../toproto5/listresourcemetadata_test.go | 44 +++ .../toproto5/validatelistresourceconfig.go | 25 ++ .../validatelistresourceconfig_test.go | 67 ++++ .../toproto6/list_resource_result_test.go | 76 +++++ .../toproto6/listresourcemetadata_test.go | 44 +++ .../toproto6/validatelistresourceconfig.go | 25 ++ .../validatelistresourceconfig_test.go | 67 ++++ list/configure.go | 34 ++ list/doc.go | 23 ++ list/list_resource.go | 4 +- list/list_resource_test.go | 5 +- list/metadata.go | 24 ++ 31 files changed, 1795 insertions(+), 13 deletions(-) create mode 100644 internal/fromproto5/validatelistresourceconfig.go create mode 100644 internal/fromproto5/validatelistresourceconfig_test.go create mode 100644 internal/fromproto6/validatelistresourceconfig.go create mode 100644 internal/fromproto6/validatelistresourceconfig_test.go create mode 100644 internal/fwserver/server_validatelistresourceconfig.go create mode 100644 internal/fwserver/server_validatelistresourceconfig_test.go create mode 100644 internal/proto5server/server_validatelistresourceconfig_test.go create mode 100644 internal/proto6server/server_validatelistresourceconfig_test.go rename internal/testing/testprovider/{list_resource.go => listresource.go} (100%) create mode 100644 internal/testing/testprovider/listresourceconfigvalidator.go create mode 100644 internal/testing/testprovider/listresourcewithconfigure.go create mode 100644 internal/testing/testprovider/listresourcewithconfigvalidators.go create mode 100644 internal/testing/testprovider/listresourcewithvalidateconfig.go create mode 100644 internal/toproto5/list_resource_result_test.go create mode 100644 internal/toproto5/listresourcemetadata_test.go create mode 100644 internal/toproto5/validatelistresourceconfig.go create mode 100644 internal/toproto5/validatelistresourceconfig_test.go create mode 100644 internal/toproto6/list_resource_result_test.go create mode 100644 internal/toproto6/listresourcemetadata_test.go create mode 100644 internal/toproto6/validatelistresourceconfig.go create mode 100644 internal/toproto6/validatelistresourceconfig_test.go create mode 100644 list/configure.go create mode 100644 list/doc.go create mode 100644 list/metadata.go diff --git a/go.mod b/go.mod index 9ad4347fc..d24f6a7b8 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250616135123-a19df43120ea + github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250703143221-06cc08e56c87 github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index 33da5983d..80745b6ac 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0U github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250616135123-a19df43120ea h1:U9EAAeQtszGlR7mDS7rY77B/a4/XiMDB8HfAtqLAuAQ= -github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250616135123-a19df43120ea/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= +github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250703143221-06cc08e56c87 h1:7GqdqQtDa2XSOL80U8cfgrnmisAfzZtH2FPe0SxvCJM= +github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250703143221-06cc08e56c87/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= 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 f178b21aa..d09c80ce2 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -40,6 +40,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 + // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. diff --git a/internal/fwserver/server_validatelistresourceconfig.go b/internal/fwserver/server_validatelistresourceconfig.go new file mode 100644 index 000000000..1da10c33d --- /dev/null +++ b/internal/fwserver/server_validatelistresourceconfig.go @@ -0,0 +1,108 @@ +// 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/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 := list.ConfigureRequest{ + ProviderData: s.ListResourceConfigureData, + } + configureResp := list.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_validatelistresourceconfig.go b/internal/proto5server/server_validatelistresourceconfig.go index 40c47ae15..743437b44 100644 --- a/internal/proto5server/server_validatelistresourceconfig.go +++ b/internal/proto5server/server_validatelistresourceconfig.go @@ -6,9 +6,65 @@ 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) + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + listResourceSchema, diags := s.FrameworkServer.ListResourceSchema(ctx, proto5Req.TypeName) + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + config, diags := fromproto5.Config(ctx, proto5Req.Config, listResourceSchema) + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + resourceSchema, diags := s.FrameworkServer.ResourceSchema(ctx, proto5Req.TypeName) + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + if diags.HasError() { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + req := &fwserver.ListRequest{ + Config: config, + ListResource: listResource, + ResourceSchema: resourceSchema, + ResourceIdentitySchema: identitySchema, + } + stream := &fwserver.ListResultsStream{} + + err := s.FrameworkServer.ListResource(ctx, req, stream) + if err != nil { + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), err + } + + 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..4da591bea --- /dev/null +++ b/internal/proto5server/server_validatelistresourceconfig_test.go @@ -0,0 +1,167 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource" + "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" + "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"), + }) + + testDynamicValue, err := tfprotov5.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + 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{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.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" + }, + } + }, + } + }, + }, + }, + }, + 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") + }, + } + }, + } + }, + }, + }, + }, + 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_validatelistresourceconfig.go b/internal/proto6server/server_validatelistresourceconfig.go index f3b282018..f84155ab3 100644 --- a/internal/proto6server/server_validatelistresourceconfig.go +++ b/internal/proto6server/server_validatelistresourceconfig.go @@ -6,9 +6,65 @@ 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) + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + listResourceSchema, diags := s.FrameworkServer.ListResourceSchema(ctx, proto6Req.TypeName) + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + config, diags := fromproto6.Config(ctx, proto6Req.Config, listResourceSchema) + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + resourceSchema, diags := s.FrameworkServer.ResourceSchema(ctx, proto6Req.TypeName) + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + if diags.HasError() { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil + } + + req := &fwserver.ListRequest{ + Config: config, + ListResource: listResource, + ResourceSchema: resourceSchema, + ResourceIdentitySchema: identitySchema, + } + stream := &fwserver.ListResultsStream{} + + err := s.FrameworkServer.ListResource(ctx, req, stream) + if err != nil { + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), err + } + + 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..2ec55431b --- /dev/null +++ b/internal/proto6server/server_validatelistresourceconfig_test.go @@ -0,0 +1,167 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource" + "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" + "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"), + }) + + testDynamicValue, err := tfprotov6.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + 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{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.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" + }, + } + }, + } + }, + }, + }, + }, + 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") + }, + } + }, + } + }, + }, + }, + }, + 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/list_resource.go b/internal/testing/testprovider/listresource.go similarity index 100% rename from internal/testing/testprovider/list_resource.go rename to internal/testing/testprovider/listresource.go 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..c19820a3f --- /dev/null +++ b/internal/testing/testprovider/listresourcewithconfigure.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 = &ListResourceWithConfigure{} +var _ list.ListResourceWithConfigure = &ListResourceWithConfigure{} + +// Declarative list.ListResourceWithConfigure for unit testing. +type ListResourceWithConfigure struct { + *ListResource + + // ListResourceWithConfigure interface methods + ConfigureMethod func(context.Context, list.ConfigureRequest, *list.ConfigureResponse) +} + +// Configure satisfies the list.ListResourceWithConfigure interface. +func (d *ListResourceWithConfigure) Configure(ctx context.Context, req list.ConfigureRequest, resp *list.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 75fdbca07..3e9185e8d 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -50,7 +50,7 @@ type ListResourceWithConfigure interface { // signature is intended to be compatible with the Configure method // signature in the Resource interface. One implementation of Configure can // satisfy both interfaces. - Configure(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) + Configure(context.Context, ConfigureRequest, *ConfigureResponse) } // ListResourceWithConfigValidators is an interface type that extends @@ -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..354a3a2aa 100644 --- a/list/list_resource_test.go +++ b/list/list_resource_test.go @@ -23,7 +23,7 @@ type ComputeInstanceWithListResourceConfigValidators struct { ComputeInstanceResource } -func (c *ComputeInstanceResource) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { +func (c *ComputeInstanceResource) Configure(_ context.Context, _ list.ConfigureRequest, _ *list.ConfigureResponse) { } func (c *ComputeInstanceResource) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { @@ -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 +} From 53ff1ec57d5fd074e921b9a840997920dd4efd05 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 8 Jul 2025 13:10:17 -0400 Subject: [PATCH 02/16] Updating ListResource to return diag.Diagnotics instead of error --- internal/fwserver/server_listresource.go | 11 ++++++++--- internal/fwserver/server_listresource_test.go | 10 ++++++---- internal/proto5server/server_listresource.go | 4 ++-- .../proto5server/server_listresource_test.go | 2 +- .../server_validatelistresourceconfig.go | 17 ++++++++++++++++- internal/proto6server/server_listresource.go | 4 ++-- .../proto6server/server_listresource_test.go | 2 +- .../server_validatelistresourceconfig.go | 18 ++++++++++++++++-- 8 files changed, 52 insertions(+), 16 deletions(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 0acff71c1..5ae7f3d93 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -5,7 +5,7 @@ package fwserver import ( "context" - "errors" + "fmt" "iter" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -80,12 +80,17 @@ 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) diag.Diagnostics { listResource := fwReq.ListResource if fwReq.Config == nil { fwStream.Results = NoListResults - return errors.New("Invalid ListResource request: Config cannot be nil") + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid ListResource Request", + fmt.Sprintf("Config cannot be nil"), + ), + } } req := list.ListRequest{ diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go index 55cefe779..54e967862 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -73,7 +73,7 @@ func TestServerListResource(t *testing.T) { server *fwserver.Server request *fwserver.ListRequest expectedStreamEvents []fwserver.ListResult - expectedError string + expectedError diag.Diagnostics }{ "success-with-zero-results": { server: &fwserver.Server{ @@ -179,7 +179,9 @@ func TestServerListResource(t *testing.T) { }, }, }, - expectedError: "Invalid ListResource request: Config cannot be nil", + expectedError: diag.Diagnostics{ + diag.NewErrorDiagnostic("Invalid ListResource Request", "Config cannot be nil"), + }, expectedStreamEvents: []fwserver.ListResult{}, }, "error-on-nil-resource-identity": { @@ -319,8 +321,8 @@ func TestServerListResource(t *testing.T) { 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) + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) } opts := cmp.Options{ diff --git a/internal/proto5server/server_listresource.go b/internal/proto5server/server_listresource.go index af85693c4..03d21337b 100644 --- a/internal/proto5server/server_listresource.go +++ b/internal/proto5server/server_listresource.go @@ -16,7 +16,7 @@ import ( // ListRequestErrorDiagnostics returns a value suitable for // [ListResourceServerStream.Results]. It yields a single result that contains // the given error diagnostics. -func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov5.ListResourceServerStream, error) { +func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov5.ListResourceServerStream, diag.Diagnostics) { protoDiags := toproto5.Diagnostics(ctx, diags) return &tfprotov5.ListResourceServerStream{ Results: func(push func(tfprotov5.ListResourceResult) bool) { @@ -25,7 +25,7 @@ func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) }, nil } -func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov5.ListResourceRequest) (*tfprotov5.ListResourceServerStream, error) { +func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov5.ListResourceRequest) (*tfprotov5.ListResourceServerStream, diag.Diagnostics) { protoStream := &tfprotov5.ListResourceServerStream{Results: tfprotov5.NoListResults} allDiags := diag.Diagnostics{} diff --git a/internal/proto5server/server_listresource_test.go b/internal/proto5server/server_listresource_test.go index 1b68ec8c3..4d30ee5a5 100644 --- a/internal/proto5server/server_listresource_test.go +++ b/internal/proto5server/server_listresource_test.go @@ -173,7 +173,7 @@ func TestServerListResource(t *testing.T) { testCases := map[string]struct { server *Server request *tfprotov5.ListResourceRequest - expectedError error + expectedError diag.Diagnostics expectedDiagnostics diag.Diagnostics expectedResults []tfprotov5.ListResourceResult }{ diff --git a/internal/proto5server/server_validatelistresourceconfig.go b/internal/proto5server/server_validatelistresourceconfig.go index 743437b44..a2d1eb5a5 100644 --- a/internal/proto5server/server_validatelistresourceconfig.go +++ b/internal/proto5server/server_validatelistresourceconfig.go @@ -19,26 +19,41 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, proto5Req *tfpr 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 } @@ -53,7 +68,7 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, proto5Req *tfpr err := s.FrameworkServer.ListResource(ctx, req, stream) if err != nil { - return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), err + return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil } fwReq, diags := fromproto5.ValidateListResourceConfigRequest(ctx, proto5Req, listResource, listResourceSchema) diff --git a/internal/proto6server/server_listresource.go b/internal/proto6server/server_listresource.go index 5a5bf0b4c..0e92271b8 100644 --- a/internal/proto6server/server_listresource.go +++ b/internal/proto6server/server_listresource.go @@ -16,7 +16,7 @@ import ( // ListRequestErrorDiagnostics returns a value suitable for // [ListResourceServerStream.Results]. It yields a single result that contains // the given error diagnostics. -func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov6.ListResourceServerStream, error) { +func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov6.ListResourceServerStream, diag.Diagnostics) { protoDiags := toproto6.Diagnostics(ctx, diags) return &tfprotov6.ListResourceServerStream{ Results: func(push func(tfprotov6.ListResourceResult) bool) { @@ -25,7 +25,7 @@ func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) }, nil } -func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, error) { +func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, diag.Diagnostics) { protoStream := &tfprotov6.ListResourceServerStream{Results: tfprotov6.NoListResults} allDiags := diag.Diagnostics{} diff --git a/internal/proto6server/server_listresource_test.go b/internal/proto6server/server_listresource_test.go index 831f6e4a6..17a50b871 100644 --- a/internal/proto6server/server_listresource_test.go +++ b/internal/proto6server/server_listresource_test.go @@ -193,7 +193,7 @@ func TestServerListResource(t *testing.T) { testCases := map[string]struct { server *Server request *tfprotov6.ListResourceRequest - expectedError error + expectedError diag.Diagnostics expectedDiagnostics diag.Diagnostics expectedResults []tfprotov6.ListResourceResult }{ diff --git a/internal/proto6server/server_validatelistresourceconfig.go b/internal/proto6server/server_validatelistresourceconfig.go index f84155ab3..c8d7b24ab 100644 --- a/internal/proto6server/server_validatelistresourceconfig.go +++ b/internal/proto6server/server_validatelistresourceconfig.go @@ -5,7 +5,6 @@ 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" @@ -19,26 +18,41 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, proto6Req *tfpr 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 } @@ -53,7 +67,7 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, proto6Req *tfpr err := s.FrameworkServer.ListResource(ctx, req, stream) if err != nil { - return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), err + return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil } fwReq, diags := fromproto6.ValidateListResourceConfigRequest(ctx, proto6Req, listResource, listResourceSchema) From ff6d36707137f00f55bc89975dd5b7160eaa805f Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 8 Jul 2025 13:15:47 -0400 Subject: [PATCH 03/16] Fix linter complaint --- internal/fwserver/server_listresource.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 5ae7f3d93..3c6349154 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -5,7 +5,6 @@ package fwserver import ( "context" - "fmt" "iter" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -88,7 +87,7 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream return diag.Diagnostics{ diag.NewErrorDiagnostic( "Invalid ListResource Request", - fmt.Sprintf("Config cannot be nil"), + "Config cannot be nil", ), } } From 0fe81afd88e2e14a63330aede96a12e52a84f791 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 11 Jul 2025 13:30:56 -0400 Subject: [PATCH 04/16] Updated ListResource to return diags inside ListResult --- internal/fwserver/server_listresource.go | 13 +- internal/fwserver/server_listresource_test.go | 168 +----------------- internal/proto5server/server_listresource.go | 9 +- .../proto5server/server_listresource_test.go | 2 +- .../server_validatelistresourceconfig.go | 5 +- .../server_validatelistresourceconfig_test.go | 35 ++-- internal/proto6server/server_listresource.go | 9 +- .../proto6server/server_listresource_test.go | 2 +- .../server_validatelistresourceconfig.go | 5 +- 9 files changed, 41 insertions(+), 207 deletions(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 3c6349154..3cf3a7e93 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -28,6 +28,10 @@ type ListRequest struct { // Resource field in the ListResult struct. IncludeResource bool + // Limit specifies the maximum number of results that Terraform is + // expecting. + Limit int64 + ResourceSchema fwschema.Schema ResourceIdentitySchema fwschema.Schema } @@ -79,17 +83,11 @@ 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) diag.Diagnostics { +func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResultsStream) { listResource := fwReq.ListResource if fwReq.Config == nil { fwStream.Results = NoListResults - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Invalid ListResource Request", - "Config cannot be nil", - ), - } } req := list.ListRequest{ @@ -111,7 +109,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 54e967862..4af522ed3 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -73,7 +73,7 @@ func TestServerListResource(t *testing.T) { server *fwserver.Server request *fwserver.ListRequest expectedStreamEvents []fwserver.ListResult - expectedError diag.Diagnostics + expectedError string }{ "success-with-zero-results": { server: &fwserver.Server{ @@ -89,20 +89,6 @@ func TestServerListResource(t *testing.T) { }, expectedStreamEvents: []fwserver.ListResult{}, }, - "success-with-nil-results": { - 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) { - // Do nothing, so that resp.Results is nil - }, - }, - }, - expectedStreamEvents: []fwserver.ListResult{}, - }, "success-with-multiple-results": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -167,152 +153,6 @@ func TestServerListResource(t *testing.T) { }, }, }, - "error-on-nil-config": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.ListRequest{ - Config: nil, - ListResource: &testprovider.ListResource{ - ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { - resp.Results = list.NoListResults - }, - }, - }, - expectedError: diag.Diagnostics{ - diag.NewErrorDiagnostic("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", "..."), - }, - }, - }, - }, } for name, testCase := range testCases { @@ -320,14 +160,10 @@ func TestServerListResource(t *testing.T) { t.Parallel() response := &fwserver.ListResultsStream{} - err := testCase.server.ListResource(context.Background(), testCase.request, response) - if diff := cmp.Diff(testCase.expectedError, err); diff != "" { - t.Errorf("unexpected error difference: %s", diff) - } + 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/proto5server/server_listresource.go b/internal/proto5server/server_listresource.go index 03d21337b..c585f086e 100644 --- a/internal/proto5server/server_listresource.go +++ b/internal/proto5server/server_listresource.go @@ -16,7 +16,7 @@ import ( // ListRequestErrorDiagnostics returns a value suitable for // [ListResourceServerStream.Results]. It yields a single result that contains // the given error diagnostics. -func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov5.ListResourceServerStream, diag.Diagnostics) { +func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov5.ListResourceServerStream, error) { protoDiags := toproto5.Diagnostics(ctx, diags) return &tfprotov5.ListResourceServerStream{ Results: func(push func(tfprotov5.ListResourceResult) bool) { @@ -25,7 +25,7 @@ func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) }, nil } -func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov5.ListResourceRequest) (*tfprotov5.ListResourceServerStream, diag.Diagnostics) { +func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov5.ListResourceRequest) (*tfprotov5.ListResourceServerStream, error) { protoStream := &tfprotov5.ListResourceServerStream{Results: tfprotov5.NoListResults} allDiags := diag.Diagnostics{} @@ -68,10 +68,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_listresource_test.go b/internal/proto5server/server_listresource_test.go index 4d30ee5a5..1b68ec8c3 100644 --- a/internal/proto5server/server_listresource_test.go +++ b/internal/proto5server/server_listresource_test.go @@ -173,7 +173,7 @@ func TestServerListResource(t *testing.T) { testCases := map[string]struct { server *Server request *tfprotov5.ListResourceRequest - expectedError diag.Diagnostics + expectedError error expectedDiagnostics diag.Diagnostics expectedResults []tfprotov5.ListResourceResult }{ diff --git a/internal/proto5server/server_validatelistresourceconfig.go b/internal/proto5server/server_validatelistresourceconfig.go index a2d1eb5a5..affa7327f 100644 --- a/internal/proto5server/server_validatelistresourceconfig.go +++ b/internal/proto5server/server_validatelistresourceconfig.go @@ -66,10 +66,7 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, proto5Req *tfpr } stream := &fwserver.ListResultsStream{} - err := s.FrameworkServer.ListResource(ctx, req, stream) - if err != nil { - return toproto5.ValidateListResourceConfigResponse(ctx, fwResp), nil - } + s.FrameworkServer.ListResource(ctx, req, stream) fwReq, diags := fromproto5.ValidateListResourceConfigRequest(ctx, proto5Req, listResource, listResourceSchema) diff --git a/internal/proto5server/server_validatelistresourceconfig_test.go b/internal/proto5server/server_validatelistresourceconfig_test.go index 4da591bea..fc429e4bf 100644 --- a/internal/proto5server/server_validatelistresourceconfig_test.go +++ b/internal/proto5server/server_validatelistresourceconfig_test.go @@ -6,6 +6,7 @@ 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" @@ -14,12 +15,22 @@ import ( "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/list/schema" "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerValidateListResourceConfig(t *testing.T) { t.Parallel() + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_optional": schema.StringAttribute{ + Optional: false, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + testType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test": tftypes.String, @@ -33,15 +44,7 @@ func TestServerValidateListResourceConfig(t *testing.T) { testDynamicValue, err := tfprotov5.NewDynamicValue(testType, testValue) if err != nil { - t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) - } - - testSchema := schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test": schema.StringAttribute{ - Required: true, - }, - }, + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } testCases := map[string]struct { @@ -58,10 +61,20 @@ func TestServerValidateListResourceConfig(t *testing.T) { return []func() list.ListResource{ func() list.ListResource { return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) {}, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + }, + ListMethod: func(_ context.Context, _ list.ListRequest, resp *list.ListResultsStream) { + }, } }, } diff --git a/internal/proto6server/server_listresource.go b/internal/proto6server/server_listresource.go index 0e92271b8..f990eb742 100644 --- a/internal/proto6server/server_listresource.go +++ b/internal/proto6server/server_listresource.go @@ -16,7 +16,7 @@ import ( // ListRequestErrorDiagnostics returns a value suitable for // [ListResourceServerStream.Results]. It yields a single result that contains // the given error diagnostics. -func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov6.ListResourceServerStream, diag.Diagnostics) { +func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) (*tfprotov6.ListResourceServerStream, error) { protoDiags := toproto6.Diagnostics(ctx, diags) return &tfprotov6.ListResourceServerStream{ Results: func(push func(tfprotov6.ListResourceResult) bool) { @@ -25,7 +25,7 @@ func ListRequestErrorDiagnostics(ctx context.Context, diags ...diag.Diagnostic) }, nil } -func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, diag.Diagnostics) { +func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, error) { protoStream := &tfprotov6.ListResourceServerStream{Results: tfprotov6.NoListResults} allDiags := diag.Diagnostics{} @@ -68,10 +68,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_listresource_test.go b/internal/proto6server/server_listresource_test.go index 17a50b871..831f6e4a6 100644 --- a/internal/proto6server/server_listresource_test.go +++ b/internal/proto6server/server_listresource_test.go @@ -193,7 +193,7 @@ func TestServerListResource(t *testing.T) { testCases := map[string]struct { server *Server request *tfprotov6.ListResourceRequest - expectedError diag.Diagnostics + expectedError error expectedDiagnostics diag.Diagnostics expectedResults []tfprotov6.ListResourceResult }{ diff --git a/internal/proto6server/server_validatelistresourceconfig.go b/internal/proto6server/server_validatelistresourceconfig.go index c8d7b24ab..0cf3ca9d0 100644 --- a/internal/proto6server/server_validatelistresourceconfig.go +++ b/internal/proto6server/server_validatelistresourceconfig.go @@ -65,10 +65,7 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, proto6Req *tfpr } stream := &fwserver.ListResultsStream{} - err := s.FrameworkServer.ListResource(ctx, req, stream) - if err != nil { - return toproto6.ValidateListResourceConfigResponse(ctx, fwResp), nil - } + s.FrameworkServer.ListResource(ctx, req, stream) fwReq, diags := fromproto6.ValidateListResourceConfigRequest(ctx, proto6Req, listResource, listResourceSchema) From b1bf9e91e3e9cca0e966f3d15b126a3a3f58e9d6 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 11 Jul 2025 13:39:55 -0400 Subject: [PATCH 05/16] Added List Resource to tests in getmetadata --- internal/proto5server/server_getmetadata_test.go | 4 ++++ internal/proto6server/server_getmetadata_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 01e0dc43c..17a4378d0 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -806,6 +806,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_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 36c0f39d0..1a6af5df1 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -806,6 +806,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 }) From 99229ad9651e204fe18f29baed0997e5e5b256fa Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 11 Jul 2025 13:40:35 -0400 Subject: [PATCH 06/16] Added List Resource to tests in getmetadata --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 5285a8e49..3996d6843 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,7 @@ toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 - github.com/hashicorp/terraform-plugin-log v0.9.0 ) From 177467d4267db0faec9e46dc87f85b3a6a21dbdd Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 15 Jul 2025 23:26:58 -0400 Subject: [PATCH 07/16] Added ResourcesMethod to tests in server_validatelistresourceconfig_test.go --- internal/fwserver/server_listresource.go | 1 + .../server_validatelistresourceconfig_test.go | 48 ++++++++++++----- .../server_validatelistresourceconfig_test.go | 53 +++++++++++++++---- 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index f8e8a16eb..d3f34b393 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -88,6 +88,7 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream if fwReq.Config == nil { fwStream.Results = NoListResults + return } req := list.ListRequest{ diff --git a/internal/proto5server/server_validatelistresourceconfig_test.go b/internal/proto5server/server_validatelistresourceconfig_test.go index fc429e4bf..1e25b7885 100644 --- a/internal/proto5server/server_validatelistresourceconfig_test.go +++ b/internal/proto5server/server_validatelistresourceconfig_test.go @@ -22,10 +22,7 @@ func TestServerValidateListResourceConfig(t *testing.T) { testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ - "test_optional": schema.StringAttribute{ - Optional: false, - }, - "test_required": schema.StringAttribute{ + "test": schema.StringAttribute{ Required: true, }, }, @@ -64,16 +61,17 @@ func TestServerValidateListResourceConfig(t *testing.T) { MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = "test_resource" }, - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "example_attribute": schema.StringAttribute{ - Required: true, - }, - }, - } - }, - ListMethod: func(_ context.Context, _ list.ListRequest, resp *list.ListResultsStream) { + 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" }, } }, @@ -105,6 +103,17 @@ func TestServerValidateListResourceConfig(t *testing.T) { }, } }, + 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" + }, + } + }, + } + }, }, }, }, @@ -138,6 +147,17 @@ func TestServerValidateListResourceConfig(t *testing.T) { }, } }, + 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" + }, + } + }, + } + }, }, }, }, diff --git a/internal/proto6server/server_validatelistresourceconfig_test.go b/internal/proto6server/server_validatelistresourceconfig_test.go index 2ec55431b..b17a23c95 100644 --- a/internal/proto6server/server_validatelistresourceconfig_test.go +++ b/internal/proto6server/server_validatelistresourceconfig_test.go @@ -6,6 +6,7 @@ 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" @@ -14,12 +15,19 @@ import ( "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/list/schema" "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) 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, @@ -36,14 +44,6 @@ func TestServerValidateListResourceConfig(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } - testSchema := schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test": schema.StringAttribute{ - Required: true, - }, - }, - } - testCases := map[string]struct { server *Server request *tfprotov6.ValidateListResourceConfigRequest @@ -58,10 +58,21 @@ func TestServerValidateListResourceConfig(t *testing.T) { return []func() list.ListResource{ func() list.ListResource { return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) {}, 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" + }, } }, } @@ -92,6 +103,17 @@ func TestServerValidateListResourceConfig(t *testing.T) { }, } }, + 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" + }, + } + }, + } + }, }, }, }, @@ -125,6 +147,17 @@ func TestServerValidateListResourceConfig(t *testing.T) { }, } }, + 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" + }, + } + }, + } + }, }, }, }, From cdcb3c704c94ef2bc7812657492cf3e3d0ed4f88 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 15 Jul 2025 23:34:48 -0400 Subject: [PATCH 08/16] gofmt error --- internal/fwserver/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 5a2677ec2..13e39c973 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -41,7 +41,6 @@ type Server struct { // to [ephemeral.ConfigureRequest.ProviderData]. EphemeralResourceConfigureData any - // ListResourceConfigureData is the // [provider.ConfigureResponse.ListResourceData] field value which is passed // to [list.ConfigureRequest.ProviderData]. @@ -70,7 +69,6 @@ type Server struct { // access from race conditions. actionFuncsMutex sync.Mutex - // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. From 394e375f74fcba5e35ac673e5d5783be1affbede Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 8 Jul 2025 11:50:11 -0400 Subject: [PATCH 09/16] Added actions --- internal/fwserver/server.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index e3eb6eeb6..97d739155 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -70,6 +70,11 @@ type Server struct { // access from race conditions. actionFuncsMutex sync.Mutex + // ListResourceConfigureData is the + // [provider.ConfigureResponse.ListResourceData] field value which is passed + // to [list.ConfigureRequest.ProviderData]. + ListResourceConfigureData any + // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. From 94c6c14d65fd26169f349d32acdfea4ea7e4a626 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 15 Jul 2025 23:34:48 -0400 Subject: [PATCH 10/16] rebasing --- internal/fwserver/server.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 97d739155..e3eb6eeb6 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -70,11 +70,6 @@ type Server struct { // access from race conditions. actionFuncsMutex sync.Mutex - // ListResourceConfigureData is the - // [provider.ConfigureResponse.ListResourceData] field value which is passed - // to [list.ConfigureRequest.ProviderData]. - ListResourceConfigureData any - // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. From bdb63f1be4c87f5946e50935f25f4dc4dc594de8 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 8 Jul 2025 11:50:11 -0400 Subject: [PATCH 11/16] Added actions --- internal/fwserver/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index e3eb6eeb6..a5d0456fc 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -8,8 +8,6 @@ import ( "fmt" "sync" - "github.com/hashicorp/terraform-plugin-framework/action" - actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" From c066bd836af88201593f264bb4149f4a5cb42962 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 15 Jul 2025 23:56:03 -0400 Subject: [PATCH 12/16] updated with more actions --- internal/fwserver/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index a5d0456fc..060f9cd47 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -6,6 +6,8 @@ package fwserver import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "sync" "github.com/hashicorp/terraform-plugin-framework/datasource" From 710ed93c2f7b2877d3095b0438172d9e9ec77640 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 17 Jul 2025 13:01:51 -0400 Subject: [PATCH 13/16] Addressed some PR comments --- internal/fwserver/server.go | 4 ++-- internal/fwserver/server_listresource.go | 7 +++++-- internal/fwserver/server_listresource_test.go | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index b9d2debdf..6ec16b3d3 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -6,10 +6,10 @@ package fwserver import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/action" - actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "sync" + "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index d3f34b393..afd84f441 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -5,6 +5,7 @@ package fwserver import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" "iter" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -87,8 +88,10 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream listResource := fwReq.ListResource if fwReq.Config == nil { - fwStream.Results = NoListResults - return + fwReq.Config = &tfsdk.Config{ + Raw: tftypes.NewValue(fwReq.ResourceSchema.Type().TerraformType(ctx), nil), + Schema: fwReq.ResourceSchema, + } } req := list.ListRequest{ diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go index 4af522ed3..cc26c5480 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -89,6 +89,20 @@ func TestServerListResource(t *testing.T) { }, expectedStreamEvents: []fwserver.ListResult{}, }, + "success-with-nil-results": { + 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) { + // Do nothing, so that resp.Results is nil + }, + }, + }, + expectedStreamEvents: []fwserver.ListResult{}, + }, "success-with-multiple-results": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, From 6a6f19c0d2a7ccf4217ddd2c269e17a2b4e0d1e6 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 17 Jul 2025 13:10:53 -0400 Subject: [PATCH 14/16] Updated Configure to be resource instead of List --- internal/fwserver/server_validatelistresourceconfig.go | 5 +++-- internal/testing/testprovider/listresourcewithconfigure.go | 5 +++-- list/list_resource.go | 2 +- list/list_resource_test.go | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/fwserver/server_validatelistresourceconfig.go b/internal/fwserver/server_validatelistresourceconfig.go index 1da10c33d..65bdfabad 100644 --- a/internal/fwserver/server_validatelistresourceconfig.go +++ b/internal/fwserver/server_validatelistresourceconfig.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -34,10 +35,10 @@ func (s *Server) ValidateListResourceConfig(ctx context.Context, req *ValidateLi if listResourceWithConfigure, ok := req.ListResource.(list.ListResourceWithConfigure); ok { logging.FrameworkTrace(ctx, "ListResource implements ListResourceWithConfigure") - configureReq := list.ConfigureRequest{ + configureReq := resource.ConfigureRequest{ ProviderData: s.ListResourceConfigureData, } - configureResp := list.ConfigureResponse{} + configureResp := resource.ConfigureResponse{} logging.FrameworkTrace(ctx, "Calling provider defined ListResource Configure") listResourceWithConfigure.Configure(ctx, configureReq, &configureResp) diff --git a/internal/testing/testprovider/listresourcewithconfigure.go b/internal/testing/testprovider/listresourcewithconfigure.go index c19820a3f..8a0ba9386 100644 --- a/internal/testing/testprovider/listresourcewithconfigure.go +++ b/internal/testing/testprovider/listresourcewithconfigure.go @@ -5,6 +5,7 @@ package testprovider import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/list" ) @@ -17,11 +18,11 @@ type ListResourceWithConfigure struct { *ListResource // ListResourceWithConfigure interface methods - ConfigureMethod func(context.Context, list.ConfigureRequest, *list.ConfigureResponse) + ConfigureMethod func(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) } // Configure satisfies the list.ListResourceWithConfigure interface. -func (d *ListResourceWithConfigure) Configure(ctx context.Context, req list.ConfigureRequest, resp *list.ConfigureResponse) { +func (d *ListResourceWithConfigure) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if d.ConfigureMethod == nil { return } diff --git a/list/list_resource.go b/list/list_resource.go index 61a7a95f5..09f46ce8d 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -50,7 +50,7 @@ type ListResourceWithConfigure interface { // signature is intended to be compatible with the Configure method // signature in the Resource interface. One implementation of Configure can // satisfy both interfaces. - Configure(context.Context, ConfigureRequest, *ConfigureResponse) + Configure(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) } // ListResourceWithConfigValidators is an interface type that extends diff --git a/list/list_resource_test.go b/list/list_resource_test.go index 354a3a2aa..7426f9309 100644 --- a/list/list_resource_test.go +++ b/list/list_resource_test.go @@ -23,7 +23,7 @@ type ComputeInstanceWithListResourceConfigValidators struct { ComputeInstanceResource } -func (c *ComputeInstanceResource) Configure(_ context.Context, _ list.ConfigureRequest, _ *list.ConfigureResponse) { +func (c *ComputeInstanceResource) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { } func (c *ComputeInstanceResource) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { From c9060a6974e59a0c6afa5206c40060fdf417be2e Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 17 Jul 2025 13:14:11 -0400 Subject: [PATCH 15/16] Updated for linter --- internal/fwserver/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 6ec16b3d3..adac3cafe 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -42,7 +42,6 @@ type Server struct { // to [ephemeral.ConfigureRequest.ProviderData]. EphemeralResourceConfigureData any - // ListResourceConfigureData is the // [provider.ConfigureResponse.ListResourceData] field value which is passed // to [list.ConfigureRequest.ProviderData]. @@ -53,7 +52,6 @@ type Server struct { // to [action.ConfigureRequest.ProviderData]. ActionConfigureData any - // actionSchemas is the cached Action Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the Action.Schema() method. From a78febbbf89cac50527200a7fcec661db9cd4916 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 17 Jul 2025 14:06:26 -0400 Subject: [PATCH 16/16] Updated to add test as suggested in PR --- internal/fwserver/server_listresource.go | 7 ++++++- internal/fwserver/server_listresource_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index afd84f441..6681f9a17 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -87,11 +87,16 @@ var NoListResults = func(func(ListResult) bool) {} func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResultsStream) { listResource := fwReq.ListResource - if fwReq.Config == 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{ diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go index cc26c5480..6e9a54b7a 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -167,6 +167,21 @@ func TestServerListResource(t *testing.T) { }, }, }, + "zero-results-on-nil-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ListRequest{ + Config: nil, // Simulating a nil config + ListResource: &testprovider.ListResource{ + ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { + resp.Results = list.NoListResults // Expecting no results when config is nil + }, + }, + }, + expectedStreamEvents: []fwserver.ListResult{}, + expectedError: "config cannot be nil", + }, } for name, testCase := range testCases {