From 80104b4e0864d9899f64934008782382c7a2b376 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 10 Aug 2021 10:03:22 -0400 Subject: [PATCH] tfsdk: Initial support for Data Source, Provider, and Resource validation (#75) Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/17 Reference: https://github.com/hashicorp/terraform-plugin-framework/pull/65 - Adds `AttributeValidator` interface type and `Validators` field to `Attributes` type. - Adds `DataSourceConfigValidator`, `DataSourceWithConfigValidators`, and `DataSourceWithValidateConfig` interface types - Adds `ProviderConfigValidator`, `ProviderWithConfigValidators`, and `ProviderWithValidateConfig` interface types - Adds `ResourceConfigValidator`, `ResourceWithConfigValidators`, and `ResourceWithValidateConfig` interface types Response diagnostics are passed through all functionality to allow overriding previous diagnostics. --- .changelog/75.txt | 3 + tfsdk/attribute.go | 142 +++ tfsdk/attribute_test.go | 554 ++++++++++++ tfsdk/attribute_validation.go | 49 + tfsdk/attribute_validation_test.go | 52 ++ tfsdk/data_source_validation.go | 54 ++ tfsdk/provider_validation.go | 53 ++ tfsdk/request_validation.go | 40 + tfsdk/resource_validation.go | 53 ++ tfsdk/response_validation.go | 38 + tfsdk/schema.go | 17 + tfsdk/schema_test.go | 130 +++ tfsdk/schema_validation.go | 23 + tfsdk/serve.go | 268 +++++- ...erve_data_source_config_validators_test.go | 92 ++ .../serve_data_source_validate_config_test.go | 67 ++ .../serve_provider_config_validators_test.go | 58 ++ tfsdk/serve_provider_test.go | 23 +- tfsdk/serve_provider_validate_config_test.go | 34 + .../serve_resource_config_validators_test.go | 98 ++ tfsdk/serve_resource_validate_config_test.go | 73 ++ tfsdk/serve_test.go | 843 +++++++++++++++++- 22 files changed, 2713 insertions(+), 51 deletions(-) create mode 100644 .changelog/75.txt create mode 100644 tfsdk/attribute_validation.go create mode 100644 tfsdk/attribute_validation_test.go create mode 100644 tfsdk/data_source_validation.go create mode 100644 tfsdk/provider_validation.go create mode 100644 tfsdk/request_validation.go create mode 100644 tfsdk/resource_validation.go create mode 100644 tfsdk/response_validation.go create mode 100644 tfsdk/schema_validation.go create mode 100644 tfsdk/serve_data_source_config_validators_test.go create mode 100644 tfsdk/serve_data_source_validate_config_test.go create mode 100644 tfsdk/serve_provider_config_validators_test.go create mode 100644 tfsdk/serve_provider_validate_config_test.go create mode 100644 tfsdk/serve_resource_config_validators_test.go create mode 100644 tfsdk/serve_resource_validate_config_test.go diff --git a/.changelog/75.txt b/.changelog/75.txt new file mode 100644 index 000000000..5c0ae3b74 --- /dev/null +++ b/.changelog/75.txt @@ -0,0 +1,3 @@ +```release-note:feature +tfsdk: Attributes, Data Sources, Providers, and Resources now support configuration validation +``` diff --git a/tfsdk/attribute.go b/tfsdk/attribute.go index 5b750949e..596df8582 100644 --- a/tfsdk/attribute.go +++ b/tfsdk/attribute.go @@ -3,9 +3,11 @@ package tfsdk import ( "context" "errors" + "fmt" "sort" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -67,6 +69,9 @@ type Attribute struct { // using this attribute, warning them that it is deprecated and // instructing them on what upgrade steps to take. DeprecationMessage string + + // Validators defines validation functionality for the attribute. + Validators []AttributeValidator } // ApplyTerraform5AttributePathStep transparently calls @@ -207,3 +212,140 @@ func (a Attribute) tfprotov6SchemaAttribute(ctx context.Context, name string, pa return schemaAttribute, nil } + +// validate performs all Attribute validation. +func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + if (a.Attributes == nil || len(a.Attributes.GetAttributes()) == 0) && a.Type == nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Attribute Definition", + Detail: "Attribute must define either Attributes or Type. This is always a problem with the provider and should be reported to the provider developer.", + Attribute: req.AttributePath, + }) + + return + } + + if a.Attributes != nil && len(a.Attributes.GetAttributes()) > 0 && a.Type != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Attribute Definition", + Detail: "Attribute cannot define both Attributes and Type. This is always a problem with the provider and should be reported to the provider developer.", + Attribute: req.AttributePath, + }) + + return + } + + attributeConfig, err := req.Config.GetAttribute(ctx, req.AttributePath) + + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Value Error", + Detail: "Attribute validation cannot read configuration value. Report this to the provider developer:\n\n" + err.Error(), + Attribute: req.AttributePath, + }) + + return + } + + req.AttributeConfig = attributeConfig + + for _, validator := range a.Validators { + validator.Validate(ctx, req, resp) + } + + if a.Attributes != nil { + nm := a.Attributes.GetNestingMode() + switch nm { + case NestingModeList: + l, ok := req.AttributeConfig.(types.List) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Validation Error", + Detail: "Attribute validation cannot walk schema. Report this to the provider developer:\n\n" + err.Error(), + Attribute: req.AttributePath, + }) + + return + } + + for idx := range l.Elems { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyInt(int64(idx)).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + case NestingModeSet: + // TODO: Set implementation + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/53 + case NestingModeMap: + m, ok := req.AttributeConfig.(types.Map) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Validation Error", + Detail: "Attribute validation cannot walk schema. Report this to the provider developer:\n\n" + err.Error(), + Attribute: req.AttributePath, + }) + + return + } + + for key := range m.Elems { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyString(key).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + case NestingModeSingle: + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + default: + err := fmt.Errorf("unknown attribute validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Validation Error", + Detail: "Attribute validation cannot walk schema. Report this to the provider developer:\n\n" + err.Error(), + Attribute: req.AttributePath, + }) + + return + } + } +} diff --git a/tfsdk/attribute_test.go b/tfsdk/attribute_test.go index b2368442d..7f0e9d5bc 100644 --- a/tfsdk/attribute_test.go +++ b/tfsdk/attribute_test.go @@ -671,3 +671,557 @@ func TestAttributeTfprotov6SchemaAttribute(t *testing.T) { }) } } + +func TestAttributeValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + req ValidateAttributeRequest + resp ValidateAttributeResponse + }{ + "no-attributes-or-type": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Attribute Definition", + Detail: "Attribute must define either Attributes or Type. This is always a problem with the provider and should be reported to the provider developer.", + Attribute: tftypes.NewAttributePath().WithAttributeName("test"), + }, + }, + }, + }, + "both-attributes-and-type": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + }, + }), + Type: types.StringType, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Attribute Definition", + Detail: "Attribute cannot define both Attributes and Type. This is always a problem with the provider and should be reported to the provider developer.", + Attribute: tftypes.NewAttributePath().WithAttributeName("test"), + }, + }, + }, + }, + "config-error": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nottest": tftypes.String, + }, + }, map[string]tftypes.Value{ + "nottest": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Value Error", + Detail: "Attribute validation cannot read configuration value. Report this to the provider developer:\n\nerror walking config: AttributeName(\"test\") still remains in the path: step cannot be applied to this value", + Attribute: tftypes.NewAttributePath().WithAttributeName("test"), + }, + }, + }, + }, + "no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "warnings": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testWarningAttributeValidator{}, + testWarningAttributeValidator{}, + }, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testWarningDiagnostic, + testWarningDiagnostic, + }, + }, + }, + "errors": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + testErrorAttributeValidator{}, + }, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testErrorDiagnostic, + testErrorDiagnostic, + }, + }, + }, + "nested-attr-list-no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "nested-attr-list-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testErrorDiagnostic, + }, + }, + }, + "nested-attr-map-no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + AttributeType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "nested-attr-map-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + AttributeType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testErrorDiagnostic, + }, + }, + }, + "nested-attr-single-no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "nested-attr-single-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testErrorDiagnostic, + }, + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + var got ValidateAttributeResponse + attribute, err := tc.req.Config.Schema.AttributeAtPath(tc.req.AttributePath) + + if err != nil { + t.Fatalf("Unexpected error getting Attribute: %s", err) + } + + attribute.validate(context.Background(), tc.req, &got) + + if diff := cmp.Diff(got, tc.resp); diff != "" { + t.Errorf("Unexpected response (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/tfsdk/attribute_validation.go b/tfsdk/attribute_validation.go new file mode 100644 index 000000000..1192b826b --- /dev/null +++ b/tfsdk/attribute_validation.go @@ -0,0 +1,49 @@ +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// AttributeValidator describes reusable Attribute validation functionality. +type AttributeValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to schema plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to schema Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // Validate performs the validation. + Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) +} + +// ValidateAttributeRequest repesents a request for +type ValidateAttributeRequest struct { + // AttributePath contains the path of the attribute. + AttributePath *tftypes.AttributePath + + // AttributeConfig contains the value of the attribute in the configuration. + AttributeConfig attr.Value + + // Config contains the entire configuration of the data source, provider, or resource. + Config Config +} + +// ValidateAttributeResponse represents a response to a +// ValidateAttributeRequest. An instance of this response struct is +// automatically passed through to each AttributeValidator. +type ValidateAttributeResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/tfsdk/attribute_validation_test.go b/tfsdk/attribute_validation_test.go new file mode 100644 index 000000000..a897ffe7e --- /dev/null +++ b/tfsdk/attribute_validation_test.go @@ -0,0 +1,52 @@ +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +var ( + testErrorDiagnostic = &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error Diagnostic", + Detail: "This is an error.", + } + testWarningDiagnostic = &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "Warning Diagnostic", + Detail: "This is a warning.", + } +) + +type testErrorAttributeValidator struct { + AttributeValidator +} + +func (v testErrorAttributeValidator) Description(ctx context.Context) string { + return "validation that always returns an error" +} + +func (v testErrorAttributeValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v testErrorAttributeValidator) Validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + resp.Diagnostics = append(resp.Diagnostics, testErrorDiagnostic) +} + +type testWarningAttributeValidator struct { + AttributeValidator +} + +func (v testWarningAttributeValidator) Description(ctx context.Context) string { + return "validation that always returns a warning" +} + +func (v testWarningAttributeValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v testWarningAttributeValidator) Validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + resp.Diagnostics = append(resp.Diagnostics, testWarningDiagnostic) +} diff --git a/tfsdk/data_source_validation.go b/tfsdk/data_source_validation.go new file mode 100644 index 000000000..fcb20d5e9 --- /dev/null +++ b/tfsdk/data_source_validation.go @@ -0,0 +1,54 @@ +package tfsdk + +import ( + "context" +) + +// DataSourceConfigValidator describes reusable data source configuration validation functionality. +type DataSourceConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to data source plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to data source Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // Validate performs the validation. + Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. +// +// Declaring validation using this methodology simplifies implmentation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type DataSourceWithConfigValidators interface { + DataSource + + // ConfigValidators returns a list of DataSourceConfigValidators. Each DataSourceConfigValidator's Validate method will be called when validating the data source. + ConfigValidators(context.Context) []DataSourceConfigValidator +} + +// DataSourceWithValidateConfig is an interface type that extends DataSource to include imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single data source. Any +// documentation of this functionality must be manually added into schema +// descriptions. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type DataSourceWithValidateConfig interface { + DataSource + + // ValidateConfig performs the validation. + ValidateConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} diff --git a/tfsdk/provider_validation.go b/tfsdk/provider_validation.go new file mode 100644 index 000000000..b1fced26d --- /dev/null +++ b/tfsdk/provider_validation.go @@ -0,0 +1,53 @@ +package tfsdk + +import ( + "context" +) + +// ProviderConfigValidator describes reusable Provider configuration validation functionality. +type ProviderConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to provider plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to provider Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // Validate performs the validation. + Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} + +// ProviderWithConfigValidators is an interface type that extends Provider to include declarative validations. +// +// Declaring validation using this methodology simplifies implementation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type ProviderWithConfigValidators interface { + Provider + + // ConfigValidators returns a list of functions which will all be performed during validation. + ConfigValidators(context.Context) []ProviderConfigValidator +} + +// ProviderWithValidateConfig is an interface type that extends Provider to include imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single provider. Any documentation +// of this functionality must be manually added into schema descriptions. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type ProviderWithValidateConfig interface { + Provider + + // ValidateConfig performs the validation. + ValidateConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} diff --git a/tfsdk/request_validation.go b/tfsdk/request_validation.go new file mode 100644 index 000000000..293d0d29a --- /dev/null +++ b/tfsdk/request_validation.go @@ -0,0 +1,40 @@ +package tfsdk + +// ValidateDataSourceConfigRequest represents a request to validate the +// configuration of a data source. An instance of this request struct is +// supplied as an argument to the DataSource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateDataSourceConfigRequest struct { + // Config is the configuration the user supplied for the data source. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config Config +} + +// ValidateProviderConfigRequest represents a request to validate the +// configuration of a provider. An instance of this request struct is +// supplied as an argument to the Provider ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateProviderConfigRequest struct { + // Config is the configuration the user supplied for the provider. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config Config +} + +// ValidateResourceConfigRequest represents a request to validate the +// configuration of a resource. An instance of this request struct is +// supplied as an argument to the Resource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateResourceConfigRequest struct { + // Config is the configuration the user supplied for the resource. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config Config +} diff --git a/tfsdk/resource_validation.go b/tfsdk/resource_validation.go new file mode 100644 index 000000000..d50fd1488 --- /dev/null +++ b/tfsdk/resource_validation.go @@ -0,0 +1,53 @@ +package tfsdk + +import ( + "context" +) + +// ResourceConfigValidator describes reusable Resource configuration validation functionality. +type ResourceConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to resource plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to resource Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // Validate performs the validation. + Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} + +// ResourceWithConfigValidators is an interface type that extends Resource to include declarative validations. +// +// Declaring validation using this methodology simplifies implmentation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type ResourceWithConfigValidators interface { + Resource + + // ConfigValidators returns a list of functions which will all be performed during validation. + ConfigValidators(context.Context) []ResourceConfigValidator +} + +// ResourceWithValidateConfig is an interface type that extends Resource to include imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single resource. Any documentation +// of this functionality must be manually added into schema descriptions. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type ResourceWithValidateConfig interface { + Resource + + // ValidateConfig performs the validation. + ValidateConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} diff --git a/tfsdk/response_validation.go b/tfsdk/response_validation.go new file mode 100644 index 000000000..b4931a125 --- /dev/null +++ b/tfsdk/response_validation.go @@ -0,0 +1,38 @@ +package tfsdk + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateDataSourceConfigResponse represents a response to a +// ValidateDataSourceConfigRequest. An instance of this response struct is +// supplied as an argument to the DataSource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateDataSourceConfigResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics []*tfprotov6.Diagnostic +} + +// ValidateResourceConfigResponse represents a response to a +// ValidateResourceConfigRequest. An instance of this response struct is +// supplied as an argument to the Resource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateResourceConfigResponse struct { + // Diagnostics report errors or warnings related to validating the resource + // configuration. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics []*tfprotov6.Diagnostic +} + +// ValidateProviderConfigResponse represents a response to a +// ValidateProviderConfigRequest. An instance of this response struct is +// supplied as an argument to the Provider ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateProviderConfigResponse struct { + // Diagnostics report errors or warnings related to validating the provider + // configuration. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/tfsdk/schema.go b/tfsdk/schema.go index 9962fdd85..cef316126 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -183,3 +183,20 @@ func (s Schema) tfprotov6Schema(ctx context.Context) (*tfprotov6.Schema, error) return result, nil } + +// validate performs all Attribute validation. +func (s Schema) validate(ctx context.Context, req ValidateSchemaRequest, resp *ValidateSchemaResponse) { + for name, attribute := range s.Attributes { + attributeReq := ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName(name), + Config: req.Config, + } + attributeResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + attribute.validate(ctx, attributeReq, attributeResp) + + resp.Diagnostics = attributeResp.Diagnostics + } +} diff --git a/tfsdk/schema_test.go b/tfsdk/schema_test.go index 253fa7caa..ecb14610a 100644 --- a/tfsdk/schema_test.go +++ b/tfsdk/schema_test.go @@ -520,3 +520,133 @@ func TestSchemaTfprotov6Schema(t *testing.T) { }) } } + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + req ValidateSchemaRequest + resp ValidateSchemaResponse + }{ + "no-validation": { + req: ValidateSchemaRequest{ + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr1": tftypes.String, + "attr2": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attr1": tftypes.NewValue(tftypes.String, "attr1value"), + "attr2": tftypes.NewValue(tftypes.String, "attr2value"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "attr1": { + Type: types.StringType, + Required: true, + }, + "attr2": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateSchemaResponse{}, + }, + "warnings": { + req: ValidateSchemaRequest{ + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr1": tftypes.String, + "attr2": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attr1": tftypes.NewValue(tftypes.String, "attr1value"), + "attr2": tftypes.NewValue(tftypes.String, "attr2value"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "attr1": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testWarningAttributeValidator{}, + }, + }, + "attr2": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testWarningAttributeValidator{}, + }, + }, + }, + }, + }, + }, + resp: ValidateSchemaResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testWarningDiagnostic, + testWarningDiagnostic, + }, + }, + }, + "errors": { + req: ValidateSchemaRequest{ + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr1": tftypes.String, + "attr2": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attr1": tftypes.NewValue(tftypes.String, "attr1value"), + "attr2": tftypes.NewValue(tftypes.String, "attr2value"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "attr1": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + "attr2": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, + }, + }, + }, + resp: ValidateSchemaResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + testErrorDiagnostic, + testErrorDiagnostic, + }, + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + var got ValidateSchemaResponse + tc.req.Config.Schema.validate(context.Background(), tc.req, &got) + + if diff := cmp.Diff(got, tc.resp); diff != "" { + t.Errorf("Unexpected response (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/tfsdk/schema_validation.go b/tfsdk/schema_validation.go new file mode 100644 index 000000000..8e6f1276b --- /dev/null +++ b/tfsdk/schema_validation.go @@ -0,0 +1,23 @@ +package tfsdk + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateSchemaRequest repesents a request for validating a Schema. +type ValidateSchemaRequest struct { + // Config contains the entire configuration of the data source, provider, or resource. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config Config +} + +// ValidateSchemaResponse represents a response to a +// ValidateSchemaRequest. +type ValidateSchemaResponse struct { + // Diagnostics report errors or warnings related to validating the schema. + // An empty slice indicates success, with no warnings or errors generated. + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/tfsdk/serve.go b/tfsdk/serve.go index b81e9a27c..622b6175b 100644 --- a/tfsdk/serve.go +++ b/tfsdk/serve.go @@ -222,15 +222,85 @@ func (s *server) GetProviderSchema(ctx context.Context, _ *tfprotov6.GetProvider } func (s *server) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { - // uncomment when we implement this function - // ctx = s.registerContext(ctx) - - // We don't actually do anything as part of this. In theory, we could - // validate the configuration for the provider block? Need to check in - // again with the core team about the goal of this RPC. - return &tfprotov6.ValidateProviderConfigResponse{ + ctx = s.registerContext(ctx) + resp := &tfprotov6.ValidateProviderConfigResponse{ + // This RPC allows a modified configuration to be returned. This was + // previously used to allow a "required" provider attribute (as defined + // by a schema) to still be "optional" with a default value, typically + // through an environment variable. Other tooling based on the provider + // schema information could not determine this implementation detail. + // To ensure accuracy going forward, this implementation is opinionated + // towards accurate provider schema definitions and optional values + // can be filled in or return errors during ConfigureProvider(). PreparedConfig: req.Config, - }, nil + } + + schema, diags := s.p.GetSchema(ctx) + + if diags != nil { + resp.Diagnostics = append(resp.Diagnostics, diags...) + + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + } + + config, err := req.Config.Unmarshal(schema.TerraformType(ctx)) + + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error parsing config", + Detail: "The provider had a problem parsing the config. Report this to the provider developer:\n\n" + err.Error(), + }) + + return resp, nil + } + + vpcReq := ValidateProviderConfigRequest{ + Config: Config{ + Raw: config, + Schema: schema, + }, + } + + if provider, ok := s.p.(ProviderWithConfigValidators); ok { + for _, configValidator := range provider.ConfigValidators(ctx) { + vpcRes := &ValidateProviderConfigResponse{ + Diagnostics: resp.Diagnostics, + } + + configValidator.Validate(ctx, vpcReq, vpcRes) + + resp.Diagnostics = vpcRes.Diagnostics + } + } + + if provider, ok := s.p.(ProviderWithValidateConfig); ok { + vpcRes := &ValidateProviderConfigResponse{ + Diagnostics: resp.Diagnostics, + } + + provider.ValidateConfig(ctx, vpcReq, vpcRes) + + resp.Diagnostics = vpcRes.Diagnostics + } + + validateSchemaReq := ValidateSchemaRequest{ + Config: Config{ + Raw: config, + Schema: schema, + }, + } + validateSchemaResp := ValidateSchemaResponse{ + Diagnostics: resp.Diagnostics, + } + + schema.validate(ctx, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics = validateSchemaResp.Diagnostics + + return resp, nil } func (s *server) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { @@ -272,12 +342,93 @@ func (s *server) StopProvider(ctx context.Context, _ *tfprotov6.StopProviderRequ return &tfprotov6.StopProviderResponse{}, nil } -func (s *server) ValidateResourceConfig(ctx context.Context, _ *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { - // uncomment when we implement this function - //ctx = s.registerContext(ctx) +func (s *server) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + resp := &tfprotov6.ValidateResourceConfigResponse{} + + // Get the type of resource, so we can get its schema and create an + // instance + resourceType, diags := s.getResourceType(ctx, req.TypeName) + resp.Diagnostics = append(resp.Diagnostics, diags...) + + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + // Get the schema from the resource type, so we can embed it in the + // config + resourceSchema, diags := resourceType.GetSchema(ctx) + resp.Diagnostics = append(resp.Diagnostics, diags...) + + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + // Create the resource instance, so we can call its methods and handle + // the request + resource, diags := resourceType.NewResource(ctx, s.p) + resp.Diagnostics = append(resp.Diagnostics, diags...) + + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + config, err := req.Config.Unmarshal(resourceSchema.TerraformType(ctx)) + + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error parsing config", + Detail: "The provider had a problem parsing the config. Report this to the provider developer:\n\n" + err.Error(), + }) + + return resp, nil + } - // TODO: support validation - return &tfprotov6.ValidateResourceConfigResponse{}, nil + vrcReq := ValidateResourceConfigRequest{ + Config: Config{ + Raw: config, + Schema: resourceSchema, + }, + } + + if resource, ok := resource.(ResourceWithConfigValidators); ok { + for _, configValidator := range resource.ConfigValidators(ctx) { + vrcRes := &ValidateResourceConfigResponse{ + Diagnostics: resp.Diagnostics, + } + + configValidator.Validate(ctx, vrcReq, vrcRes) + + resp.Diagnostics = vrcRes.Diagnostics + } + } + + if resource, ok := resource.(ResourceWithValidateConfig); ok { + vrcRes := &ValidateResourceConfigResponse{ + Diagnostics: resp.Diagnostics, + } + + resource.ValidateConfig(ctx, vrcReq, vrcRes) + + resp.Diagnostics = vrcRes.Diagnostics + } + + validateSchemaReq := ValidateSchemaRequest{ + Config: Config{ + Raw: config, + Schema: resourceSchema, + }, + } + validateSchemaResp := ValidateSchemaResponse{ + Diagnostics: resp.Diagnostics, + } + + resourceSchema.validate(ctx, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics = validateSchemaResp.Diagnostics + + return resp, nil } func (s *server) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { @@ -823,12 +974,93 @@ func (s *server) ImportResourceState(ctx context.Context, _ *tfprotov6.ImportRes return &tfprotov6.ImportResourceStateResponse{}, nil } -func (s *server) ValidateDataResourceConfig(ctx context.Context, _ *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { - // uncomment when we implement this function - // ctx = s.registerContext(ctx) +func (s *server) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + resp := &tfprotov6.ValidateDataResourceConfigResponse{} + + // Get the type of data source, so we can get its schema and create an + // instance + dataSourceType, diags := s.getDataSourceType(ctx, req.TypeName) + resp.Diagnostics = append(resp.Diagnostics, diags...) + + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + // Get the schema from the data source type, so we can embed it in the + // config + dataSourceSchema, diags := dataSourceType.GetSchema(ctx) + resp.Diagnostics = append(resp.Diagnostics, diags...) - // TODO: support validation - return &tfprotov6.ValidateDataResourceConfigResponse{}, nil + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + // Create the data source instance, so we can call its methods and handle + // the request + dataSource, diags := dataSourceType.NewDataSource(ctx, s.p) + resp.Diagnostics = append(resp.Diagnostics, diags...) + + if diagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + config, err := req.Config.Unmarshal(dataSourceSchema.TerraformType(ctx)) + + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error parsing config", + Detail: "The provider had a problem parsing the config. Report this to the provider developer:\n\n" + err.Error(), + }) + + return resp, nil + } + + vrcReq := ValidateDataSourceConfigRequest{ + Config: Config{ + Raw: config, + Schema: dataSourceSchema, + }, + } + + if dataSource, ok := dataSource.(DataSourceWithConfigValidators); ok { + for _, configValidator := range dataSource.ConfigValidators(ctx) { + vrcRes := &ValidateDataSourceConfigResponse{ + Diagnostics: resp.Diagnostics, + } + + configValidator.Validate(ctx, vrcReq, vrcRes) + + resp.Diagnostics = vrcRes.Diagnostics + } + } + + if dataSource, ok := dataSource.(DataSourceWithValidateConfig); ok { + vrcRes := &ValidateDataSourceConfigResponse{ + Diagnostics: resp.Diagnostics, + } + + dataSource.ValidateConfig(ctx, vrcReq, vrcRes) + + resp.Diagnostics = vrcRes.Diagnostics + } + + validateSchemaReq := ValidateSchemaRequest{ + Config: Config{ + Raw: config, + Schema: dataSourceSchema, + }, + } + validateSchemaResp := ValidateSchemaResponse{ + Diagnostics: resp.Diagnostics, + } + + dataSourceSchema.validate(ctx, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics = validateSchemaResp.Diagnostics + + return resp, nil } func (s *server) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { diff --git a/tfsdk/serve_data_source_config_validators_test.go b/tfsdk/serve_data_source_config_validators_test.go new file mode 100644 index 000000000..0bf4017c2 --- /dev/null +++ b/tfsdk/serve_data_source_config_validators_test.go @@ -0,0 +1,92 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeDataSourceTypeConfigValidators struct{} + +func (dt testServeDataSourceTypeConfigValidators) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Optional: true, + }, + }, + }, nil +} + +func (dt testServeDataSourceTypeConfigValidators) NewDataSource(_ context.Context, p Provider) (DataSource, []*tfprotov6.Diagnostic) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeDataSourceConfigValidators{ + provider: provider, + }, nil +} + +var testServeDataSourceTypeConfigValidatorsSchema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Optional: true, + Type: tftypes.String, + }, + }, + }, +} + +var testServeDataSourceTypeConfigValidatorsType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, +} + +type testServeDataSourceConfigValidators struct { + provider *testServeProvider +} + +func (r testServeDataSourceConfigValidators) Read(ctx context.Context, req ReadDataSourceRequest, resp *ReadDataSourceResponse) { +} + +func (r testServeDataSourceConfigValidators) ConfigValidators(ctx context.Context) []DataSourceConfigValidator { + r.provider.validateDataSourceConfigCalledDataSourceType = "test_config_validators" + + return []DataSourceConfigValidator{ + newTestDataSourceConfigValidator(r.provider.validateDataSourceConfigImpl), + // Verify multiple validators + newTestDataSourceConfigValidator(r.provider.validateDataSourceConfigImpl), + } +} + +type testDataSourceConfigValidator struct { + DataSourceConfigValidator + + impl func(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +func (v testDataSourceConfigValidator) Description(ctx context.Context) string { + return "test data source config validator" +} +func (v testDataSourceConfigValidator) MarkdownDescription(ctx context.Context) string { + return "**test** data source config validator" +} +func (v testDataSourceConfigValidator) Validate(ctx context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) { + v.impl(ctx, req, resp) +} + +func newTestDataSourceConfigValidator(impl func(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse)) testDataSourceConfigValidator { + return testDataSourceConfigValidator{impl: impl} +} diff --git a/tfsdk/serve_data_source_validate_config_test.go b/tfsdk/serve_data_source_validate_config_test.go new file mode 100644 index 000000000..a6ca6bb61 --- /dev/null +++ b/tfsdk/serve_data_source_validate_config_test.go @@ -0,0 +1,67 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeDataSourceTypeValidateConfig struct{} + +func (dt testServeDataSourceTypeValidateConfig) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Optional: true, + }, + }, + }, nil +} + +func (dt testServeDataSourceTypeValidateConfig) NewDataSource(_ context.Context, p Provider) (DataSource, []*tfprotov6.Diagnostic) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeDataSourceValidateConfig{ + provider: provider, + }, nil +} + +var testServeDataSourceTypeValidateConfigSchema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Optional: true, + Type: tftypes.String, + }, + }, + }, +} + +var testServeDataSourceTypeValidateConfigType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, +} + +type testServeDataSourceValidateConfig struct { + provider *testServeProvider +} + +func (r testServeDataSourceValidateConfig) Read(ctx context.Context, req ReadDataSourceRequest, resp *ReadDataSourceResponse) { +} + +func (r testServeDataSourceValidateConfig) ValidateConfig(ctx context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) { + r.provider.validateDataSourceConfigCalledDataSourceType = "test_validate_config" + r.provider.validateDataSourceConfigImpl(ctx, req, resp) +} diff --git a/tfsdk/serve_provider_config_validators_test.go b/tfsdk/serve_provider_config_validators_test.go new file mode 100644 index 000000000..41abb4038 --- /dev/null +++ b/tfsdk/serve_provider_config_validators_test.go @@ -0,0 +1,58 @@ +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeProviderWithConfigValidators struct { + *testServeProvider +} + +func (t *testServeProviderWithConfigValidators) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Optional: true, + }, + }, + }, nil +} + +var testServeProviderWithConfigValidatorsType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, +} + +func (p testServeProviderWithConfigValidators) ConfigValidators(ctx context.Context) []ProviderConfigValidator { + return []ProviderConfigValidator{ + newTestProviderConfigValidator(p.validateProviderConfigImpl), + // Verify multiple validators + newTestProviderConfigValidator(p.validateProviderConfigImpl), + } +} + +type testProviderConfigValidator struct { + ProviderConfigValidator + + impl func(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} + +func (v testProviderConfigValidator) Description(ctx context.Context) string { + return "test provider config validator" +} +func (v testProviderConfigValidator) MarkdownDescription(ctx context.Context) string { + return "**test** provider config validator" +} +func (v testProviderConfigValidator) Validate(ctx context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) { + v.impl(ctx, req, resp) +} + +func newTestProviderConfigValidator(impl func(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse)) testProviderConfigValidator { + return testProviderConfigValidator{impl: impl} +} diff --git a/tfsdk/serve_provider_test.go b/tfsdk/serve_provider_test.go index f0278df61..2c56ad9de 100644 --- a/tfsdk/serve_provider_test.go +++ b/tfsdk/serve_provider_test.go @@ -10,11 +10,18 @@ import ( ) type testServeProvider struct { + // validate provider config request + validateProviderConfigImpl func(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) + // configure configuredVal tftypes.Value configuredSchema Schema configuredTFVersion string + // validate resource config request + validateResourceConfigCalledResourceType string + validateResourceConfigImpl func(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) + // read resource request readResourceCurrentStateValue tftypes.Value readResourceCurrentStateSchema Schema @@ -51,6 +58,10 @@ type testServeProvider struct { updateFunc func(context.Context, UpdateResourceRequest, *UpdateResourceResponse) deleteFunc func(context.Context, DeleteResourceRequest, *DeleteResourceResponse) + // validate data source config request + validateDataSourceConfigCalledDataSourceType string + validateDataSourceConfigImpl func(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) + // read data source request readDataSourceConfigValue tftypes.Value readDataSourceConfigSchema Schema @@ -418,15 +429,19 @@ var testServeProviderProviderType = tftypes.Object{ func (t *testServeProvider) GetResources(_ context.Context) (map[string]ResourceType, []*tfprotov6.Diagnostic) { return map[string]ResourceType{ - "test_one": testServeResourceTypeOne{}, - "test_two": testServeResourceTypeTwo{}, + "test_one": testServeResourceTypeOne{}, + "test_two": testServeResourceTypeTwo{}, + "test_config_validators": testServeResourceTypeConfigValidators{}, + "test_validate_config": testServeResourceTypeValidateConfig{}, }, nil } func (t *testServeProvider) GetDataSources(_ context.Context) (map[string]DataSourceType, []*tfprotov6.Diagnostic) { return map[string]DataSourceType{ - "test_one": testServeDataSourceTypeOne{}, - "test_two": testServeDataSourceTypeTwo{}, + "test_one": testServeDataSourceTypeOne{}, + "test_two": testServeDataSourceTypeTwo{}, + "test_config_validators": testServeDataSourceTypeConfigValidators{}, + "test_validate_config": testServeDataSourceTypeValidateConfig{}, }, nil } diff --git a/tfsdk/serve_provider_validate_config_test.go b/tfsdk/serve_provider_validate_config_test.go new file mode 100644 index 000000000..ea3cee779 --- /dev/null +++ b/tfsdk/serve_provider_validate_config_test.go @@ -0,0 +1,34 @@ +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeProviderWithValidateConfig struct { + *testServeProvider +} + +func (t *testServeProviderWithValidateConfig) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Optional: true, + }, + }, + }, nil +} + +var testServeProviderWithValidateConfigType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, +} + +func (p testServeProviderWithValidateConfig) ValidateConfig(ctx context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) { + p.validateProviderConfigImpl(ctx, req, resp) +} diff --git a/tfsdk/serve_resource_config_validators_test.go b/tfsdk/serve_resource_config_validators_test.go new file mode 100644 index 000000000..d162509c7 --- /dev/null +++ b/tfsdk/serve_resource_config_validators_test.go @@ -0,0 +1,98 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeResourceTypeConfigValidators struct{} + +func (dt testServeResourceTypeConfigValidators) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Optional: true, + }, + }, + }, nil +} + +func (dt testServeResourceTypeConfigValidators) NewResource(_ context.Context, p Provider) (Resource, []*tfprotov6.Diagnostic) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeResourceConfigValidators{ + provider: provider, + }, nil +} + +var testServeResourceTypeConfigValidatorsSchema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Optional: true, + Type: tftypes.String, + }, + }, + }, +} + +var testServeResourceTypeConfigValidatorsType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, +} + +type testServeResourceConfigValidators struct { + provider *testServeProvider +} + +func (r testServeResourceConfigValidators) Create(ctx context.Context, req CreateResourceRequest, resp *CreateResourceResponse) { +} +func (r testServeResourceConfigValidators) Read(ctx context.Context, req ReadResourceRequest, resp *ReadResourceResponse) { +} +func (r testServeResourceConfigValidators) Update(ctx context.Context, req UpdateResourceRequest, resp *UpdateResourceResponse) { +} +func (r testServeResourceConfigValidators) Delete(ctx context.Context, req DeleteResourceRequest, resp *DeleteResourceResponse) { +} + +func (r testServeResourceConfigValidators) ConfigValidators(ctx context.Context) []ResourceConfigValidator { + r.provider.validateResourceConfigCalledResourceType = "test_config_validators" + + return []ResourceConfigValidator{ + newTestResourceConfigValidator(r.provider.validateResourceConfigImpl), + // Verify multiple validators + newTestResourceConfigValidator(r.provider.validateResourceConfigImpl), + } +} + +type testResourceConfigValidator struct { + ResourceConfigValidator + + impl func(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} + +func (v testResourceConfigValidator) Description(ctx context.Context) string { + return "test resource config validator" +} +func (v testResourceConfigValidator) MarkdownDescription(ctx context.Context) string { + return "**test** resource config validator" +} +func (v testResourceConfigValidator) Validate(ctx context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) { + v.impl(ctx, req, resp) +} + +func newTestResourceConfigValidator(impl func(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse)) testResourceConfigValidator { + return testResourceConfigValidator{impl: impl} +} diff --git a/tfsdk/serve_resource_validate_config_test.go b/tfsdk/serve_resource_validate_config_test.go new file mode 100644 index 000000000..f3df85c55 --- /dev/null +++ b/tfsdk/serve_resource_validate_config_test.go @@ -0,0 +1,73 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeResourceTypeValidateConfig struct{} + +func (dt testServeResourceTypeValidateConfig) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Attributes: map[string]Attribute{ + "string": { + Type: types.StringType, + Optional: true, + }, + }, + }, nil +} + +func (dt testServeResourceTypeValidateConfig) NewResource(_ context.Context, p Provider) (Resource, []*tfprotov6.Diagnostic) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeResourceValidateConfig{ + provider: provider, + }, nil +} + +var testServeResourceTypeValidateConfigSchema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Optional: true, + Type: tftypes.String, + }, + }, + }, +} + +var testServeResourceTypeValidateConfigType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, +} + +type testServeResourceValidateConfig struct { + provider *testServeProvider +} + +func (r testServeResourceValidateConfig) Create(ctx context.Context, req CreateResourceRequest, resp *CreateResourceResponse) { +} +func (r testServeResourceValidateConfig) Read(ctx context.Context, req ReadResourceRequest, resp *ReadResourceResponse) { +} +func (r testServeResourceValidateConfig) Update(ctx context.Context, req UpdateResourceRequest, resp *UpdateResourceResponse) { +} +func (r testServeResourceValidateConfig) Delete(ctx context.Context, req DeleteResourceRequest, resp *DeleteResourceResponse) { +} + +func (r testServeResourceValidateConfig) ValidateConfig(ctx context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) { + r.provider.validateResourceConfigCalledResourceType = "test_validate_config" + r.provider.validateResourceConfigImpl(ctx, req, resp) +} diff --git a/tfsdk/serve_test.go b/tfsdk/serve_test.go index 8f9c1f845..836d90fc6 100644 --- a/tfsdk/serve_test.go +++ b/tfsdk/serve_test.go @@ -216,12 +216,16 @@ func TestServerGetProviderSchema(t *testing.T) { expected := &tfprotov6.GetProviderSchemaResponse{ Provider: testServeProviderProviderSchema, ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeResourceTypeOneSchema, - "test_two": testServeResourceTypeTwoSchema, + "test_one": testServeResourceTypeOneSchema, + "test_two": testServeResourceTypeTwoSchema, + "test_config_validators": testServeResourceTypeConfigValidatorsSchema, + "test_validate_config": testServeResourceTypeValidateConfigSchema, }, DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeDataSourceTypeOneSchema, - "test_two": testServeDataSourceTypeTwoSchema, + "test_one": testServeDataSourceTypeOneSchema, + "test_two": testServeDataSourceTypeTwoSchema, + "test_config_validators": testServeDataSourceTypeConfigValidatorsSchema, + "test_validate_config": testServeDataSourceTypeValidateConfigSchema, }, } if diff := cmp.Diff(expected, got); diff != "" { @@ -244,12 +248,16 @@ func TestServerGetProviderSchemaWithProviderMeta(t *testing.T) { expected := &tfprotov6.GetProviderSchemaResponse{ Provider: testServeProviderProviderSchema, ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeResourceTypeOneSchema, - "test_two": testServeResourceTypeTwoSchema, + "test_one": testServeResourceTypeOneSchema, + "test_two": testServeResourceTypeTwoSchema, + "test_config_validators": testServeResourceTypeConfigValidatorsSchema, + "test_validate_config": testServeResourceTypeValidateConfigSchema, }, DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeDataSourceTypeOneSchema, - "test_two": testServeDataSourceTypeTwoSchema, + "test_one": testServeDataSourceTypeOneSchema, + "test_two": testServeDataSourceTypeTwoSchema, + "test_config_validators": testServeDataSourceTypeConfigValidatorsSchema, + "test_validate_config": testServeDataSourceTypeValidateConfigSchema, }, ProviderMeta: &tfprotov6.Schema{ Version: 2, @@ -271,6 +279,341 @@ func TestServerGetProviderSchemaWithProviderMeta(t *testing.T) { } } +func TestServerValidateProviderConfig(t *testing.T) { + t.Parallel() + + type testCase struct { + // request input + config tftypes.Value + provider Provider + providerType tftypes.Type + + // response expectations + expectedDiags []*tfprotov6.Diagnostic + } + + tests := map[string]testCase{ + "no_validation": { + config: tftypes.NewValue(testServeProviderProviderType, map[string]tftypes.Value{ + "required": tftypes.NewValue(tftypes.String, "this is a required value"), + "optional": tftypes.NewValue(tftypes.String, nil), + "computed": tftypes.NewValue(tftypes.String, nil), + "optional_computed": tftypes.NewValue(tftypes.String, "they filled this one out"), + "sensitive": tftypes.NewValue(tftypes.String, "hunter42"), + "deprecated": tftypes.NewValue(tftypes.String, "oops"), + "string": tftypes.NewValue(tftypes.String, "a new string value"), + "number": tftypes.NewValue(tftypes.Number, 1234), + "bool": tftypes.NewValue(tftypes.Bool, true), + "list-string": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + "list-list-string": tftypes.NewValue(tftypes.List{ElementType: tftypes.List{ElementType: tftypes.String}}, []tftypes.Value{ + tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "rojo"), + tftypes.NewValue(tftypes.String, "azul"), + tftypes.NewValue(tftypes.String, "verde"), + }), + }), + "list-object": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "hello, world"), + "bar": tftypes.NewValue(tftypes.Bool, true), + "baz": tftypes.NewValue(tftypes.Number, 4567), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "goodnight, moon"), + "bar": tftypes.NewValue(tftypes.Bool, false), + "baz": tftypes.NewValue(tftypes.Number, 8675309), + }), + }), + "object": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + "quux": tftypes.List{ElementType: tftypes.String}, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "testing123"), + "bar": tftypes.NewValue(tftypes.Bool, true), + "baz": tftypes.NewValue(tftypes.Number, 123), + "quux": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + }), + "empty-object": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, map[string]tftypes.Value{}), + "single-nested-attributes": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "almost done"), + "bar": tftypes.NewValue(tftypes.Number, 12), + }), + "list-nested-attributes": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), + "map": tftypes.NewValue(tftypes.Map{AttributeType: tftypes.Number}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 123), + "bar": tftypes.NewValue(tftypes.Number, 456), + "baz": tftypes.NewValue(tftypes.Number, 789), + }), + "map-nested-attributes": tftypes.NewValue(tftypes.Map{AttributeType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "bar": tftypes.Number, + "foo": tftypes.String, + }}}, map[string]tftypes.Value{ + "hello": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "bar": tftypes.Number, + "foo": tftypes.String, + }}, map[string]tftypes.Value{ + "bar": tftypes.NewValue(tftypes.Number, 123456), + "foo": tftypes.NewValue(tftypes.String, "world"), + }), + "goodnight": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "bar": tftypes.Number, + "foo": tftypes.String, + }}, map[string]tftypes.Value{ + "bar": tftypes.NewValue(tftypes.Number, 56789), + "foo": tftypes.NewValue(tftypes.String, "moon"), + }), + }), + }), + provider: &testServeProvider{}, + providerType: testServeProviderProviderType, + }, + "config_validators_no_diags": { + config: tftypes.NewValue(testServeResourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + provider: &testServeProviderWithConfigValidators{ + &testServeProvider{ + validateProviderConfigImpl: func(_ context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) {}, + }, + }, + providerType: testServeProviderWithConfigValidatorsType, + }, + "config_validators_one_diag": { + config: tftypes.NewValue(testServeResourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + provider: &testServeProviderWithConfigValidators{ + &testServeProvider{ + validateProviderConfigImpl: func(_ context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }) + }, + }, + }, + providerType: testServeProviderWithConfigValidatorsType, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + // ConfigValidators includes multiple calls + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "config_validators_two_diags": { + config: tftypes.NewValue(testServeResourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + provider: &testServeProviderWithConfigValidators{ + &testServeProvider{ + validateProviderConfigImpl: func(_ context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }...) + }, + }, + }, + providerType: testServeProviderWithConfigValidatorsType, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + // ConfigValidators includes multiple calls + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "validate_config_no_diags": { + config: tftypes.NewValue(testServeResourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + provider: &testServeProviderWithValidateConfig{ + &testServeProvider{ + validateProviderConfigImpl: func(_ context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) {}, + }, + }, + providerType: testServeProviderWithValidateConfigType, + }, + "validate_config_one_diag": { + config: tftypes.NewValue(testServeResourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + provider: &testServeProviderWithValidateConfig{ + &testServeProvider{ + validateProviderConfigImpl: func(_ context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }) + }, + }, + }, + providerType: testServeProviderWithValidateConfigType, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "validate_config_two_diags": { + config: tftypes.NewValue(testServeResourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + provider: &testServeProviderWithValidateConfig{ + &testServeProvider{ + validateProviderConfigImpl: func(_ context.Context, req ValidateProviderConfigRequest, resp *ValidateProviderConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }...) + }, + }, + }, + providerType: testServeProviderWithValidateConfigType, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + } + + for name, tc := range tests { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + testServer := &server{ + p: tc.provider, + } + + dv, err := tfprotov6.NewDynamicValue(tc.providerType, tc.config) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + req := &tfprotov6.ValidateProviderConfigRequest{ + Config: &dv, + } + got, err := testServer.ValidateProviderConfig(context.Background(), req) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + if diff := cmp.Diff(got.Diagnostics, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diff in diagnostics (+wanted, -got): %s", diff) + } + }) + } +} + func TestServerConfigureProvider(t *testing.T) { t.Parallel() @@ -480,43 +823,264 @@ func TestServerConfigureProvider(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - s := new(testServeProvider) + s := new(testServeProvider) + testServer := &server{ + p: s, + } + dv, err := tfprotov6.NewDynamicValue(testServeProviderProviderType, tc.config) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + providerSchema, diags := s.GetSchema(context.Background()) + if len(diags) > 0 { + t.Errorf("Unexpected diags: %+v", diags) + return + } + got, err := testServer.ConfigureProvider(context.Background(), &tfprotov6.ConfigureProviderRequest{ + TerraformVersion: tc.tfVersion, + Config: &dv, + }) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + if s.configuredTFVersion != tc.tfVersion { + t.Errorf("Expected Terraform version to be %q, got %q", tc.tfVersion, s.configuredTFVersion) + } + if diff := cmp.Diff(got.Diagnostics, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diff in diagnostics (+wanted, -got): %s", diff) + } + if diff := cmp.Diff(s.configuredVal, tc.config); diff != "" { + t.Errorf("Unexpected diff in config (+wanted, -got): %s", diff) + return + } + if diff := cmp.Diff(s.configuredSchema, providerSchema); diff != "" { + t.Errorf("Unexpected diff in schema (+wanted, -got): %s", diff) + return + } + }) + } +} + +func TestServerValidateResourceConfig(t *testing.T) { + t.Parallel() + + type testCase struct { + // request input + config tftypes.Value + resource string + resourceType tftypes.Type + + impl func(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) + + // response expectations + expectedDiags []*tfprotov6.Diagnostic + } + + tests := map[string]testCase{ + "no_validation": { + config: tftypes.NewValue(testServeResourceTypeOneType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, ""), + "favorite_colors": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + "created_timestamp": tftypes.NewValue(tftypes.String, ""), + }), + resource: "test_one", + resourceType: testServeResourceTypeOneType, + }, + "config_validators_no_diags": { + config: tftypes.NewValue(testServeResourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + resource: "test_config_validators", + resourceType: testServeResourceTypeConfigValidatorsType, + + impl: func(_ context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) {}, + }, + "config_validators_one_diag": { + config: tftypes.NewValue(testServeResourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + resource: "test_config_validators", + resourceType: testServeResourceTypeConfigValidatorsType, + + impl: func(_ context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + // ConfigValidators includes multiple calls + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "config_validators_two_diags": { + config: tftypes.NewValue(testServeResourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + resource: "test_config_validators", + resourceType: testServeResourceTypeConfigValidatorsType, + + impl: func(_ context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }...) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + // ConfigValidators includes multiple calls + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "validate_config_no_diags": { + config: tftypes.NewValue(testServeResourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + resource: "test_validate_config", + resourceType: testServeResourceTypeValidateConfigType, + + impl: func(_ context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) {}, + }, + "validate_config_one_diag": { + config: tftypes.NewValue(testServeResourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + resource: "test_validate_config", + resourceType: testServeResourceTypeValidateConfigType, + + impl: func(_ context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "validate_config_two_diags": { + config: tftypes.NewValue(testServeResourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + resource: "test_validate_config", + resourceType: testServeResourceTypeValidateConfigType, + + impl: func(_ context.Context, req ValidateResourceConfigRequest, resp *ValidateResourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }...) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + } + + for name, tc := range tests { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + s := &testServeProvider{ + validateResourceConfigImpl: tc.impl, + } testServer := &server{ p: s, } - dv, err := tfprotov6.NewDynamicValue(testServeProviderProviderType, tc.config) + + dv, err := tfprotov6.NewDynamicValue(tc.resourceType, tc.config) if err != nil { t.Errorf("Unexpected error: %s", err) return } - - providerSchema, diags := s.GetSchema(context.Background()) - if len(diags) > 0 { - t.Errorf("Unexpected diags: %+v", diags) - return + req := &tfprotov6.ValidateResourceConfigRequest{ + TypeName: tc.resource, + Config: &dv, } - got, err := testServer.ConfigureProvider(context.Background(), &tfprotov6.ConfigureProviderRequest{ - TerraformVersion: tc.tfVersion, - Config: &dv, - }) + got, err := testServer.ValidateResourceConfig(context.Background(), req) if err != nil { t.Errorf("Unexpected error: %s", err) return } - if s.configuredTFVersion != tc.tfVersion { - t.Errorf("Expected Terraform version to be %q, got %q", tc.tfVersion, s.configuredTFVersion) + if s.validateResourceConfigCalledResourceType != tc.resource && !(tc.resource == "test_one" && s.validateResourceConfigCalledResourceType == "") { + t.Errorf("Called wrong resource. Expected to call %q, actually called %q", tc.resource, s.readDataSourceCalledDataSourceType) + return } if diff := cmp.Diff(got.Diagnostics, tc.expectedDiags); diff != "" { t.Errorf("Unexpected diff in diagnostics (+wanted, -got): %s", diff) } - if diff := cmp.Diff(s.configuredVal, tc.config); diff != "" { - t.Errorf("Unexpected diff in config (+wanted, -got): %s", diff) - return - } - if diff := cmp.Diff(s.configuredSchema, providerSchema); diff != "" { - t.Errorf("Unexpected diff in schema (+wanted, -got): %s", diff) - return - } }) } } @@ -2545,6 +3109,227 @@ func TestServerApplyResourceChange(t *testing.T) { } } +func TestServerValidateDataResourceConfig(t *testing.T) { + t.Parallel() + + type testCase struct { + // request input + config tftypes.Value + dataSource string + dataSourceType tftypes.Type + + impl func(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) + + // response expectations + expectedDiags []*tfprotov6.Diagnostic + } + + tests := map[string]testCase{ + "no_validation": { + config: tftypes.NewValue(testServeDataSourceTypeOneType, map[string]tftypes.Value{ + "current_date": tftypes.NewValue(tftypes.String, nil), + "current_time": tftypes.NewValue(tftypes.String, nil), + "is_dst": tftypes.NewValue(tftypes.Bool, nil), + }), + dataSource: "test_one", + dataSourceType: testServeDataSourceTypeOneType, + }, + "config_validators_no_diags": { + config: tftypes.NewValue(testServeDataSourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + dataSource: "test_config_validators", + dataSourceType: testServeDataSourceTypeConfigValidatorsType, + + impl: func(_ context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) {}, + }, + "config_validators_one_diag": { + config: tftypes.NewValue(testServeDataSourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + dataSource: "test_config_validators", + dataSourceType: testServeDataSourceTypeConfigValidatorsType, + + impl: func(_ context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + // ConfigValidators includes multiple calls + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "config_validators_two_diags": { + config: tftypes.NewValue(testServeDataSourceTypeConfigValidatorsType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + dataSource: "test_config_validators", + dataSourceType: testServeDataSourceTypeConfigValidatorsType, + + impl: func(_ context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }...) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + // ConfigValidators includes multiple calls + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "validate_config_no_diags": { + config: tftypes.NewValue(testServeDataSourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + dataSource: "test_validate_config", + dataSourceType: testServeDataSourceTypeValidateConfigType, + + impl: func(_ context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) {}, + }, + "validate_config_one_diag": { + config: tftypes.NewValue(testServeDataSourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + dataSource: "test_validate_config", + dataSourceType: testServeDataSourceTypeValidateConfigType, + + impl: func(_ context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + "validate_config_two_diags": { + config: tftypes.NewValue(testServeDataSourceTypeValidateConfigType, map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, nil), + }), + dataSource: "test_validate_config", + dataSourceType: testServeDataSourceTypeValidateConfigType, + + impl: func(_ context.Context, req ValidateDataSourceConfigRequest, resp *ValidateDataSourceConfigResponse) { + resp.Diagnostics = append(resp.Diagnostics, []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }...) + }, + + expectedDiags: []*tfprotov6.Diagnostic{ + { + Summary: "This is a warning", + Severity: tfprotov6.DiagnosticSeverityWarning, + Detail: "This is your final warning", + Attribute: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + }, + { + Summary: "This is an error", + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "Oops.", + }, + }, + }, + } + + for name, tc := range tests { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + s := &testServeProvider{ + validateDataSourceConfigImpl: tc.impl, + } + testServer := &server{ + p: s, + } + + dv, err := tfprotov6.NewDynamicValue(tc.dataSourceType, tc.config) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + req := &tfprotov6.ValidateDataResourceConfigRequest{ + TypeName: tc.dataSource, + Config: &dv, + } + got, err := testServer.ValidateDataResourceConfig(context.Background(), req) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + if s.validateDataSourceConfigCalledDataSourceType != tc.dataSource && !(tc.dataSource == "test_one" && s.validateDataSourceConfigCalledDataSourceType == "") { + t.Errorf("Called wrong data source. Expected to call %q, actually called %q", tc.dataSource, s.readDataSourceCalledDataSourceType) + return + } + if diff := cmp.Diff(got.Diagnostics, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diff in diagnostics (+wanted, -got): %s", diff) + } + }) + } +} + func TestServerReadDataSource(t *testing.T) { t.Parallel()