From 77e70417f9230860a38e77a6f57b9524882f5751 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 9 Jul 2021 14:03:26 -0400 Subject: [PATCH 01/35] docs/design: Initial Validation design document Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/17 Covering background, prior implementations, and goals. Further updates to this document will outline proposals and ultimately recommendations. --- docs/design/validation.md | 940 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 940 insertions(+) create mode 100644 docs/design/validation.md diff --git a/docs/design/validation.md b/docs/design/validation.md new file mode 100644 index 000000000..42ec42df0 --- /dev/null +++ b/docs/design/validation.md @@ -0,0 +1,940 @@ +# Validation + +Practitioners implementing Terraform configurations desire feedback surrounding the syntax, types, and acceptable values. This feedback, typically referred to as validation, is perferably given as early as possible before a configuration is applied. Terraform supports a plugin architecture, which extends the configuration and validation surface area based on the implementation details of those plugins. This framework provides validation hooks for plugins. This design document will outline background information on the problem space, prior framework choices, and proposals for this framework. + +## Background + +Terraform CLI, this framework, and a Terraform Provider each have differing responsibilities for validation. Depending on the configuration and operation being performed, full information for validation may not yet be visible. Before diving into the intricacies around plugin validation and this framework's design considerations, a general overview of Terraform's configuration and validation mechanisms is provided. Additional information about Terraform concepts not described in detail in this document can be found in the [Terraform Documentation](https://www.terraform.io/docs/). + +### Terraform Configuration + +The [Terraform configuration language](https://www.terraform.io/docs/language/) is declarative and an implementation of [HashiCorp Configuration Language](https://github.com/hashicorp/hcl) (HCL). HCL provides all the primitives and tokenization required to convert textual configuration files into meaningful concepts and constructs for Terraform. The Terraform CLI is responsible for reading and parsing configurations, performing syntax validation (e.g. feedback around unparseable configurations), and returning user interface output for all validation. + +An example of basic configuration syntax validation performed by Terraform CLI: + +```console +$ cat main.tf +this is invalid +$ terraform validate +╷ +│ Error: Unsupported block type +│ +│ on main.tf line 1: +│ 1: this is invalid +│ +│ Blocks of type "this" are not expected here. +╵ +╷ +│ Error: Invalid block definition +│ +│ on main.tf line 1: +│ 1: this is invalid +│ +│ A block definition must have block content delimited by "{" and +│ "}", starting on the same line as the block header. +``` + +The [Terraform configuration language defines its own type system](https://www.terraform.io/docs/language/expressions/types.html) which is translated to and from the type system implemented by plugins through the [Terraform Plugin Protocol](#terraform-plugin-protocol) which is described later. This framework is designed to transparently handle those conversions as much as possible, however it is important to note that there are potentially differences in terminology and implementation between the two. + +Many values in a Terraform configuration can be referenced in other locations, which can be used to order operations within Terraform: + +```terraform +resource "example_foo" "example" { + some_attribute = "this is a known value" +} + +resource "example_bar" "example" { + known = example_foo.example.some_attribute # Known value and expected to be "this is a known value" + unknown = example_foo.example.other_attribute # Likely unknown value +} +``` + +In these situations, the value of the `other_attribute` attribute from the `example_foo.example` resource is not present in the configuration, so the value _may_ (see next section) not be known until `example_foo.example` has been applied. These values are typically referred to as unknown values. This distinction is important in Terraform and validation, since this case might need to be explicitly handled by the framework or plugins. + +### Terraform Plan + +To provide detailed information about actions that Terraform intends to perform, Terraform CLI will generate a plan. For the purposes of validation, the plan is an extension of the available configuration information. Providers have an opportunity to modify the plan before it is finalized, which is where unknown values can potentially be filled in (e.g. with a provider defined default or if the value can be derived from other plan information). + +As an example of the human readable output of a plan: + +```terraform +$ cat main.tf +resource "random_pet" "example" { + length = 2 +} + +$ terraform plan + +Terraform used the selected providers to generate the following execution plan. Resource actions are +indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # random_pet.example will be created + + resource "random_pet" "example" { + + id = (known after apply) + + length = 2 + + separator = "-" + } + +Plan: 1 to add, 0 to change, 0 to destroy. +``` + +This plan is surfaced to providers in a machine readable manner through the [Terraform Plugin Protocol](#terraform-plugin-protocol) which is described later. + +### Terraform Providers + +Terraform Providers are a form of Terraform plugins, which are gRPC (formerly `net/rpc`) server processes that are lifecycle managed by Terraform CLI. Providers implement [managed resources](https://www.terraform.io/docs/language/resources/) and [data sources](https://www.terraform.io/docs/language/data-sources/). Often a managed resource is just called a "resource" although in the underlying implementation details a "resource" may refer to either for legacy reasons as seen later in the previous provider framework. + +From a configuration standpoint, Terraform implements the concepts of `provider`, `resource`, and `data` (source) configurations, while providers implement the details inside those configurations in what is called "schema" information. The schema defines attribute naming, types, and behaviors for consumption by the framework and Terraform CLI. + +To visualize the difference in a configuration: + +```terraform +# Provider configuration +provider "example" { # Defined by the configuration language + # Defined by the provider schema + input = "" # a required or optional string attribute named input +} + +# Resource configuration +resource "example_thing" "example" { # Defined by the configuration language + # Defined by the resource schema + input = 123 # a required or optional number attribute named input +} + +# Data source configuration +data "example_thing" "example" { # Defined by the configuration language + # Defined by the data source schema + input = true # a required or optional boolean attribute named input +} +``` + +Terraform supports the following validation for provider implementations: + +- Provider configurations +- Resource configurations and plans +- Data Source configurations and plans + +Within these, there are two types of validation: + +- Single attribute value validation (e.g. string length) +- Multiple attribute validation (e.g. attributes or attribute values that conflict with each other) + +The next sections will outline some of the underlying details relevant to implementation proposals in this framework. + +### Terraform Plugin Protocol + +The specification between Terraform CLI and plugins, such as Terraform Providers, is currently implemented via [Protocol Buffers](https://developers.google.com/protocol-buffers). Below highlights some of the service `rpc` (called by Terraform CLI) and `message` types that are intergral for validation support and applying/destroying a given configuration. + +#### `ApplyResourceChange` RPC + +Called during the `terraform apply` and `terraform destroy` commands. + +```protobuf +service Provider { + // ... + rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); +} + +message ApplyResourceChange { + message Request { + string type_name = 1; + DynamicValue prior_state = 2; + DynamicValue planned_state = 3; + DynamicValue config = 4; + bytes planned_private = 5; + DynamicValue provider_meta = 6; + } + message Response { + DynamicValue new_state = 1; + bytes private = 2; + repeated Diagnostic diagnostics = 3; + } +} +``` + +#### `PlanResourceChange` RPC + +Called during the `terraform apply`, `terraform destroy`, and `terraform plan` commands. + +```protobuf +service Provider { + // ... + rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); +} + +message PlanResourceChange { + message Request { + string type_name = 1; + DynamicValue prior_state = 2; + DynamicValue proposed_new_state = 3; + DynamicValue config = 4; + bytes prior_private = 5; + DynamicValue provider_meta = 6; + } + + message Response { + DynamicValue planned_state = 1; + repeated AttributePath requires_replace = 2; + bytes planned_private = 3; + repeated Diagnostic diagnostics = 4; + } +} +``` + +#### `ValidateDataSourceConfig` RPC + +Called during the `terraform apply`, `terraform destroy`, `terraform plan`, `terraform refresh`, and `terraform validate` commands if data sources are present. + +```protobuf +service Provider { + // ... + rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response); +} + +message ValidateDataResourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} +``` + +#### `ValidateProviderConfig` RPC + +Called during the `terraform apply`, `terraform destroy`, `terraform plan`, `terraform refresh`, and `terraform validate` commands if providers are present. + +```protobuf +service Provider { + // ... + rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response); +} + +message ValidateProviderConfig { + message Request { + DynamicValue config = 1; + } + message Response { + repeated Diagnostic diagnostics = 2; + } +} +``` + +#### `ValidateResourceTypeConfig` RPC + +Called during the `terraform apply`, `terraform destroy`, `terraform plan`, `terraform refresh`, and `terraform validate` commands if managed resources are present. + +```protobuf +service Provider { + // ... + rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response); +} + +message ValidateResourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} +``` + +#### `Diagnostics` Message + +Diagnostics in the protocol allow providers to return warnings and errors. + +```protobuf +message Diagnostic { + enum Severity { + INVALID = 0; + ERROR = 1; + WARNING = 2; + } + Severity severity = 1; + string summary = 2; + string detail = 3; + AttributePath attribute = 4; +} +``` + +### terraform-plugin-go + +The [`terraform-plugin-go` library](https://pkg.go.dev/hashicorp/terraform-plugin-go) is a low-level implementation of the [Terraform Plugin Protocol](#terraform-plugin-protocol) in Go and underpins this framework. This includes packages such as `tfprotov6` and `tftypes`. These are mentioned for completeness as some of these types are not yet abstracted in this framework and may be shown in implementation proposals. + +### terraform-plugin-framework + +Most of the Go types and functionality from `terraform-plugin-go` will be abstracted by this framework before reaching provider developers. The details represented here are not finalized as this framework is still being designed, however these current details are presented here for additional context in the later proposals. + +Generic `tftypes` values are abstracted into an `attr.Value` Go interface type with concrete Go types such as `types.String`: + +```go +// Value defines an interface for describing data associated with an attribute. +// Values allow provider developers to specify data in a convenient format, and +// have it transparently be converted to formats Terraform understands. +type Value interface { + // ToTerraformValue returns the data contained in the Value as + // a Go type that tftypes.NewValue will accept. + ToTerraformValue(context.Context) (interface{}, error) + + // Equal must return true if the Value is considered semantically equal + // to the Value passed as an argument. + Equal(Value) bool +} + +// ... separately ... + +var _ attr.Value = String{} + +// String represents a UTF-8 string value. +type String struct { + // Unknown will be true if the value is not yet known. + Unknown bool + + // Null will be true if the value was not set, or was explicitly set to + // null. + Null bool + + // Value contains the set value, as long as Unknown and Null are both + // false. + Value string +} +``` + +Resources (and similarly, but separately, data sources) are currently implmented in their own `Resource` and `ResourceType` Go interface types. Providers are responsible for implementing the concrete Go types. + +```go +// A ResourceType is a type of resource. For each type of resource this provider +// supports, it should define a type implementing ResourceType and return an +// instance of it in the map returned by Provider.GeResources. +type ResourceType interface { + // GetSchema returns the schema for this resource. + GetSchema(context.Context) (schema.Schema, []*tfprotov6.Diagnostic) + + // NewResource instantiates a new Resource of this ResourceType. + NewResource(context.Context, Provider) (Resource, []*tfprotov6.Diagnostic) +} + +// Resource represents a resource instance. This is the core interface that all +// resources must implement. +type Resource interface { + // Create is called when the provider must create a new resource. Config + // and planned state values should be read from the + // CreateResourceRequest and new state values set on the + // CreateResourceResponse. + Create(context.Context, CreateResourceRequest, *CreateResourceResponse) + + // Read is called when the provider must read resource values in order + // to update state. Planned state values should be read from the + // ReadResourceRequest and new state values set on the + // ReadResourceResponse. + Read(context.Context, ReadResourceRequest, *ReadResourceResponse) + + // Update is called to update the state of the resource. Config, planned + // state, and prior state values should be read from the + // UpdateResourceRequest and new state values set on the + // UpdateResourceResponse. + Update(context.Context, UpdateResourceRequest, *UpdateResourceResponse) + + // Delete is called when the provider must delete the resource. Config + // values may be read from the DeleteResourceRequest. + Delete(context.Context, DeleteResourceRequest, *DeleteResourceResponse) +} +``` + +Similar to the previous framework, schema attributes are currently implemented in their own `Attribute` Go structure type: + +```go +// Attribute defines the constraints and behaviors of a single field in a +// schema. Attributes are the fields that show up in Terraform state files and +// can be used in configuration files. +type Attribute struct { + // Type indicates what kind of attribute this is. You'll most likely + // want to use one of the types in the types package. + // + // If Type is set, Attributes cannot be. + Type attr.Type + + // Attributes can have their own, nested attributes. This nested map of + // attributes behaves exactly like the map of attributes on the Schema + // type. + // + // If Attributes is set, Type cannot be. + Attributes NestedAttributes + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose not to enter + // a value for this attribute or not. Optional and Required cannot both + // be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive bool + + // DeprecationMessage defines a message to display to practitioners + // using this attribute, warning them that it is deprecated and + // instructing them on what upgrade steps to take. + DeprecationMessage string +} +``` + +Although later designs surrounding the ability to allow providers to define custom schema types may change this particular Go typing detail. + +## Prior Implementations + +### terraform-plugin-sdk + +The previous framework for provider implementations, Terraform Plugin SDK, can be found in the `terraform-plugin-sdk` repository. That framework has existed since the very early days of Terraform, where it was previously contained in a combined CLI and provider codebase, to support the code and testing aspects of provider development. + +To implement managed resources and data sources, the previous framework was largely based around Go structure types and declarative definitions of intended behaviors. These were defined in the `helper/schema` package, in particular, the `Schema` and `Resource` types. + +#### `helper/schema.Schema` + +This type is the main entrypoint for declaring attribute information within a resource or data source. For example, + +```go +map[string]*schema.Schema{ + "attribute_name": { + Type: schema.TypeString, + Required: true, + }, +} +``` + +It supported single attribute value validation via the `ValidateFunc` or `ValidateDiagFunc` fields and multiple attribute validation via a collection of different fields (`AtLeastOneOf`, `ConflictsWith`, `ExactlyOneOf`, `RequiredWith`) which could be combined as necessary. For list, set, and map types, two additional fields (`MaxItems` and `MinItems`) provided validation for the number of elements. + +The multiple attribute validation support in the attribute schema is purely existance based, meaning it could not be conditional based on the attribute value. Conditional multiple attribute validation based on values was later added via the resource level `CustomizeDiff`, which will be described later on. + +These fields also required a full attribute path in "flatmap" syntax, which had limitations for declaring them against nested attributes. For example: + +```go +map[string]*schema.Schema{ + "root_attribute": { + Type: schema.TypeString, + Optional: true, + }, + "single_block": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list_attribute_one": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"single_block.0.list_attribute_two"}, // only valid due to MaxItems: 1 + }, + "list_attribute_two": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"single_block.0.list_attribute_one"}, // only valid due to MaxItems: 1 + }, + }, + }, + }, + "set_of_blocks": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "set_attribute_one": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{/* No flatmap address syntax for set_attribute_two */} + }, + "set_attribute_two": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{/* No flatmap address syntax for set_attribute_one */} + }, + }, + }, + }, +} +``` + +##### `AtLeastOneOf` + +This field enabled the schema to validate that at least one of the attribute addresses (in "flatmap" syntax) was present in a configuration. For example, + +```go +map[string]*schema.Schema{ + "attribute_one": { + Type: schema.TypeString, + Optional: true, + AtLeastOneOf: []string{"attribute_one", "attribute_two"}, + }, + "attribute_two": { + Type: schema.TypeString, + Optional: true, + AtLeastOneOf: []string{"attribute_one", "attribute_two"}, + }, +} +``` + +Gave the following results: + +```terraform +# Failed validation (error returned) +resource "example_thing" "example" {} + +# Passed validation +resource "example_thing" "example" { + attribute_one = "some_value" +} + +# Passed validation +resource "example_thing" "example" { + attribute_two = "some_value" +} + +# Passed validation +resource "example_thing" "example" { + attribute_one = "some_value" + attribute_two = "some_value" +} +``` + +##### `ConflictsWith` + +This field enabled the schema to validate that multiple of the attribute addresses (in "flatmap" syntax) were present in a configuration. For example, + +```go +map[string]*schema.Schema{ + "attribute_one": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"attribute_two"}, + }, + "attribute_two": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"attribute_one"}, + }, +} +``` + +Gave the following results: + +```terraform +# Passed validation +resource "example_thing" "example" {} + +# Passed validation +resource "example_thing" "example" { + attribute_one = "some_value" +} + +# Passed validation +resource "example_thing" "example" { + attribute_two = "some_value" +} + +# Failed validation (error returned) +resource "example_thing" "example" { + attribute_one = "some_value" + attribute_two = "some_value" +} +``` + +##### `ExactlyOneOf` + +This field enabled the schema to validate that one (and only one) of the attribute addresses (in "flatmap" syntax) must be present in a configuration. For example, + +```go +map[string]*schema.Schema{ + "attribute_one": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"attribute_one", "attribute_two"}, + }, + "attribute_two": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"attribute_one", "attribute_two"}, + }, +} +``` + +Gave the following results: + +```terraform +# Failed validation (error returned) +resource "example_thing" "example" {} + +# Passed validation +resource "example_thing" "example" { + attribute_one = "some_value" +} + +# Passed validation +resource "example_thing" "example" { + attribute_two = "some_value" +} + +# Failed validation (error returned) +resource "example_thing" "example" { + attribute_one = "some_value" + attribute_two = "some_value" +} +``` + +##### `MaxItems` + +This field enabled the schema to validate the maximum number of elements in a list, set, or map type. For example, + +```go +map[string]*schema.Schema{ + "single_block": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ /* ... nested attributes ... */ }, + }, + }, +} +``` + +Gave the following results: + +```terraform +# Passed validation +resource "example_thing" "example" {} + +# Passed validation +resource "example_thing" "example" { + single_block { + # ... nested attributes ... + } +} + +# Failed validation (error returned) +resource "example_thing" "example" { + single_block { + # ... nested attributes ... + } + + single_block { + # ... nested attributes ... + } +} +``` + +##### `MinItems` + +This field enabled the schema to validate the minimum number of elements in a list, set, or map type. For example, + +```go +map[string]*schema.Schema{ + "multiple_block": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ /* ... nested attributes ... */ }, + }, + }, +} +``` + +Gave the following results: + +```terraform +# Passed validation +resource "example_thing" "example" {} + +# Failed validation (error returned) +resource "example_thing" "example" { + multiple_block { + # ... nested attributes ... + } +} + +# Passed validation +resource "example_thing" "example" { + multiple_block { + # ... nested attributes ... + } + + multiple_block { + # ... nested attributes ... + } +} +``` + +##### `RequiredWith` + +This field enabled the schema to validate that any of the attribute addresses (in "flatmap" syntax) were implied as present in a configuration. For example, + +```go +map[string]*schema.Schema{ + "attribute_one": { + Type: schema.TypeString, + Optional: true, + }, + "attribute_two": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"attribute_one"}, + }, +} +``` + +Gave the following results: + +```terraform +# Passed validation +resource "example_thing" "example" {} + +# Failed validation (error returned) +resource "example_thing" "example" { + attribute_one = "some_value" +} + +# Passed validation +resource "example_thing" "example" { + attribute_two = "some_value" +} + +# Passed validation +resource "example_thing" "example" { + attribute_one = "some_value" + attribute_two = "some_value" +} +``` + +##### `ValidateFunc` / `ValidateDiagFunc` + +These fields provided single attribute value validation. `ValidateDiagFunc` was a more recent version of `ValidateFunc`, returning `Diagnostics` instead of warning string and error slices. + +For example, + +```go +// +map[string]*schema.Schema{ + "attribute_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(rawValue interface{}, attributePath string) (warnings []string, errors []error) { + value, ok := rawValue.(string) + + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be string", attributePath)) + return + } + + if value == "" { + errors = append(errors, fmt.Errorf("expected %s to not be empty", attributePath)) + } + + return + }, + }, +} +``` + +Gave the following results: + +```terraform +# Failed validation (error returned) +resource "example_thing" "example" { + attribute_name = "" +} + +# Passed validation +resource "example_thing" "example" { + attribute_name = "some_value" +} +``` + +These validation functions are expected to perform value type conversion to match the schema and the concepts of null or unknown values are not surfaced due to limitations in the previous framework type system. + +Rather than require provider developers to recreate relatively common value validations, a separate `helper/validation` package provides a wide variety of value validation functions and is described below. + +###### `helper/validation` Package + +This package has common validation functions which can be directly implemented within a `helper/schema.Schema#ValidateFunc`, for example: + +```go +map[string]*schema.Schema{ + "attribute_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, +} +``` + +The surface area of this package, as seen in its [Go documentation](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation), is quite large. This documentation will only summarize some of the capabilities of those functions, to highlight the breadth and depth this framework should also support. Whether this framework should also implement these functions or if they should be offered in other packages/modules is a separate decision point. + +- Generic String + - Contains Character/Substring + - Starts/Ends With + - Length Between/Maximum/Minimum + - One of a Collection (Enumeration) + - Regular Expression +- Generic Float/Integer + - Between/Maximum/Minimum + - Multiple Of (Modulo) + - One of a Collection (Enumeration) +- Encoding + - base64 +- Format + - JSON + - YAML +- Networking + - CIDR + - IPv4 Address + - IPv6 Address + - MAC Address + - Port Number +- Time/Date + - Day of week name + - Month name + - RFC3339 +- URI + - Scheme + +In addition to the above, there are two generic validation helper functions `Any()` and `All()`. These can be used to logically `OR` or `AND` multiple validation functions together: + +```go +map[string]*schema.Schema{ + "attribute_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 256), + validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z]+$`), "must contain only alphanumeric characters"), + ), + }, +} +``` + +#### `helper/schema.Resource#CustomizeDiff` + +As noted above, the multiple attribute validation was limited in the utility it could provide. Terraform CLI and the previous framework were enhanced to support modifying the plan or return an error before it was executed, allowing providers to introduce custom logic around resource recreation and a generic form of validation. This was implemented in the `CustomizeDiff` field of the `Resource` type as a function that had the plan information and provider instance available. + +For example: + +```go +&schema.Resource{ + // ... + CustomizeDiff: func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if value := diff.Get("attribute_one").(string); value == "special condition" { + if _, ok := diff.GetOk("attribute_two"); !ok { + return fmt.Errorf("'attribute_two' must be set when 'attribute_one' is %q", value) + } + } + + return nil + }, +} +``` + +In general, `CustomizeDiff` is not broadly utilized across the ecosystem due to the complexity of properly implementing and testing the functionality. + +##### `helper/customdiff` Package + +Similar to how the `helper/validation` package of common functionality was created for `ValidateFunc`, a [`helper/customizediff`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff) package was created for common `CustomizeDiff` functionality. + +In terms of validation, this provided helpers such as: + +```go +&schema.Resource{ + // ... + CustomizeDiff: customdiff.IfValue( + "attribute_one", + func(ctx context.Context, rawValue, meta interface{}) bool { + value, ok := rawValue.(string) + + // potentially difficult to diagnose type issue + if !ok { + return false + } + + return value == "special condition" + }, + customdiff.ValidateValue( + "attribute_two", + func(ctx context.Context, rawValue, meta interface{}) error { + value, ok := rawValue.(string) + + if !ok { + return fmt.Errorf("incorrect type conversion for attribute_two") + } + + if value != "" { + return fmt.Errorf("cannot provide attribute_two value when attribute_one is \"special condition\"") + } + + return nil + }, + ), + ), +} +``` + +These likely would have been simplified into further helpers should there have been more `CustomizeDiff` usage. + +## Goals + +This framework design should strive to accomplish the following with validation support. + +Allow provider developers access to all current types of provider validation: + +- Provider configurations +- Resource configurations and plans +- Data Source configurations and plans + +Including where possible: + +- Single attribute value validation (e.g. string length) +- Multiple attribute validation (e.g. attributes or attribute values that conflict with each other) + +In terms of implementation, the following core concepts: + +- Low level primitives (e.g other portions of the framework, external packages, and provider developers can implement higher level functionality) +- Reusability between single attribute and multiple attribute validation functionality (e.g. attribute value functions) +- Hooks for documentation (e.g. for future tooling such as provider documentation generators to self-document attributes) + +Finally, these other considerations: + +- Providing the appropriate amount of contextual information for debugging purposes +- Providing the appropriate amount of contextual information for practitioner facing output +- Ease of extending validation (e.g. handling type conversion and/or unknown values in the framework) +- Ease of testing validation (e.g. unit testing) +- Ease and succinctness of common validation scenarios (e.g. verbosity in provider code) From 2136a5e510522118fe8e49638b88dc3e2fb748f0 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 9 Jul 2021 15:12:15 -0400 Subject: [PATCH 02/35] docs/design: Minor clarifications and fixes for background sections of Validation design document --- docs/design/validation.md | 106 ++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 42ec42df0..4b6a1edc3 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -225,7 +225,7 @@ message ValidateProviderConfig { } ``` -#### `ValidateResourceTypeConfig` RPC +#### `ValidateResourceConfig` RPC Called during the `terraform apply`, `terraform destroy`, `terraform plan`, `terraform refresh`, and `terraform validate` commands if managed resources are present. @@ -272,42 +272,59 @@ The [`terraform-plugin-go` library](https://pkg.go.dev/hashicorp/terraform-plugi Most of the Go types and functionality from `terraform-plugin-go` will be abstracted by this framework before reaching provider developers. The details represented here are not finalized as this framework is still being designed, however these current details are presented here for additional context in the later proposals. -Generic `tftypes` values are abstracted into an `attr.Value` Go interface type with concrete Go types such as `types.String`: +Providers are currently implemented in the `Provider` Go interface type. Provider implementations are responsible for implementing the concrete Go type. ```go -// Value defines an interface for describing data associated with an attribute. -// Values allow provider developers to specify data in a convenient format, and -// have it transparently be converted to formats Terraform understands. -type Value interface { - // ToTerraformValue returns the data contained in the Value as - // a Go type that tftypes.NewValue will accept. - ToTerraformValue(context.Context) (interface{}, error) +// Provider is the core interface that all Terraform providers must implement. +type Provider interface { + // GetSchema returns the schema for this provider's configuration. If + // this provider has no configuration, return nil. + GetSchema(context.Context) (schema.Schema, []*tfprotov6.Diagnostic) - // Equal must return true if the Value is considered semantically equal - // to the Value passed as an argument. - Equal(Value) bool -} + // Configure is called at the beginning of the provider lifecycle, when + // Terraform sends to the provider the values the user specified in the + // provider configuration block. These are supplied in the + // ConfigureProviderRequest argument. + // Values from provider configuration are often used to initialise an + // API client, which should be stored on the struct implementing the + // Provider interface. + Configure(context.Context, ConfigureProviderRequest, *ConfigureProviderResponse) -// ... separately ... + // GetResources returns a map of the resource types this provider + // supports. + GetResources(context.Context) (map[string]ResourceType, []*tfprotov6.Diagnostic) -var _ attr.Value = String{} + // GetDataSources returns a map of the data source types this provider + // supports. + GetDataSources(context.Context) (map[string]DataSourceType, []*tfprotov6.Diagnostic) +} +``` -// String represents a UTF-8 string value. -type String struct { - // Unknown will be true if the value is not yet known. - Unknown bool +Data Sources are currently implemented in their own `DataSource` and `DataSourceType` Go interface types. Providers are responsible for implementing the concrete Go types. - // Null will be true if the value was not set, or was explicitly set to - // null. - Null bool +```go +// A DataSourceType is a type of data source. For each type of data source this +// provider supports, it should define a type implementing DataSourceType and +// return an instance of it in the map returned by Provider.GetDataSources. +type DataSourceType interface { + // GetSchema returns the schema for this data source. + GetSchema(context.Context) (schema.Schema, []*tfprotov6.Diagnostic) - // Value contains the set value, as long as Unknown and Null are both - // false. - Value string + // NewDataSource instantiates a new DataSource of this DataSourceType. + NewDataSource(context.Context, Provider) (DataSource, []*tfprotov6.Diagnostic) +} + +// DataSource implements a data source instance. +type DataSource interface { + // Read is called when the provider must read data source values in + // order to update state. Config values should be read from the + // ReadDataSourceRequest and new state values set on the + // ReadDataSourceResponse. + Read(context.Context, ReadDataSourceRequest, *ReadDataSourceResponse) } ``` -Resources (and similarly, but separately, data sources) are currently implmented in their own `Resource` and `ResourceType` Go interface types. Providers are responsible for implementing the concrete Go types. +Managed resources are currently implemented in their own `Resource` and `ResourceType` Go interface types. Providers are responsible for implementing the concrete Go types. ```go // A ResourceType is a type of resource. For each type of resource this provider @@ -413,6 +430,43 @@ type Attribute struct { Although later designs surrounding the ability to allow providers to define custom schema types may change this particular Go typing detail. +Values of `Attribute` in this framework are abstracted from the generic `tftypes` values into an `attr.Value` Go interface type: + +```go +// Value defines an interface for describing data associated with an attribute. +// Values allow provider developers to specify data in a convenient format, and +// have it transparently be converted to formats Terraform understands. +type Value interface { + // ToTerraformValue returns the data contained in the Value as + // a Go type that tftypes.NewValue will accept. + ToTerraformValue(context.Context) (interface{}, error) + + // Equal must return true if the Value is considered semantically equal + // to the Value passed as an argument. + Equal(Value) bool +} +``` + +This framework then implements concrete Go types such as `types.String`: + +```go +var _ attr.Value = String{} + +// String represents a UTF-8 string value. +type String struct { + // Unknown will be true if the value is not yet known. + Unknown bool + + // Null will be true if the value was not set, or was explicitly set to + // null. + Null bool + + // Value contains the set value, as long as Unknown and Null are both + // false. + Value string +} +``` + ## Prior Implementations ### terraform-plugin-sdk From 8aca1d64f7a55bec5ba667dcb1565047ad0134b0 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 12 Jul 2021 10:59:14 -0400 Subject: [PATCH 03/35] docs/design: Initial validation proposal sections for single attribute value validation --- docs/design/validation.md | 272 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4b6a1edc3..3d4d9594d 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -992,3 +992,275 @@ Finally, these other considerations: - Ease of extending validation (e.g. handling type conversion and/or unknown values in the framework) - Ease of testing validation (e.g. unit testing) - Ease and succinctness of common validation scenarios (e.g. verbosity in provider code) + +## Proposals + +### Single Attribute Value Validation + +This validation would be applicable to the `schema.Attribute` types declared within the `GetSchema()` of `DataSourceType`, `Provider`, and `ResourceType` implementations. For most of these proposals, the framework would walk through all attribute paths during the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` calls, executing the declared validation in each attribute if present. + +#### Declaring Value Validation for Attributes + +This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. + +##### Single Function Field on `schema.Attribute` + +Similar to the previous framework, a new field can be added to the `schema.Attribute` type. For example: + +```go +schema.Attribute{ + // ... + ValueValidation: T, +} +``` + +Implementators would be responsible for ensuring that single function covered all necessary validation. The framework could provide wrapper functions similar to the previous `All()` and `Any()` to allow simpler validations built from multiple functions. For example: + +```go +schema.Attribute{ + // ... + ValueValidation: All( + T, + T, + ), +} +``` + +As seen with the previous framework in practice however, it was very common to implement the `All()` wrapper function. New provider developers would be responsible for understanding that multiple validations are possible in the single function field and knowing that custom validation functions may not be necessary to write if using the wrapper functions. + +This proposal colocates the value validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. + +##### List of Functions Field on `schema.Attribute` + +A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: + +```go +schema.Attribute{ + // ... + ValueValidations: []T{ + T, + T, + }, +} +``` + +In this case, the framework would perform the validation similar to the previous framework `All()` wrapper function. The logical `AND` type of value validation is overwhelmingly more common in practice, which will simplify provider implementations. This still allows for an `Any()` based wrapper (logical `OR`) to be inserted if necessary. + +Colocating the value validation behaviors in the schema definition, means it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. This proposal will feel familiar to existing provider developers. New provider developers will immediately know that multiple validations are supported. + +##### New Attribute With Value Validation Type(s) + +The `schema.Attribute` type could be converted to a Go interface type and split into capabilities, similar to other interface types in the framework. For example: + +```go +type Attribute interface { + Type(context.Context) attr.Type + // ... +} + +type AttributeWithValueValidations struct { + Attribute + ValueValidations []T +} + +// or more interfaces + +type AttributeWithValueValidations interface { + Attribute + ValueValidations(/* ... */) []T +} +``` + +This type of proposal, in isolation, feels extraneous given the current attribute implementation. The framework does not appear to benefit from this splitting and it seems desirable that all attributes should be able to optionally enable value validation. Future considerations to allow declaring custom attribute types, outside of validation handling, are more likely to drive this type of potential change. + +##### Resource Level + +This proposal would introduce no changes to `schema.Attribute`. Instead, this would require value validation declarations at the `DataSource` and `Resource` level similar to other proposed attribute validations. The implementation details of this validation depends on those later proposals, however a rough sketch of this would be: + +```go +func (t *customResourceType) AttributeValidations(/* ... */) []T1 { + return []T1{ + T1(*tftypes.AttributePath, T2), // or ...T2 + } +} +``` + +This proposal makes value validation behaviors occur at a distance, meaning it is harder for provider developers to correlate the validation logic to the name/path and type information. It would also be very verbose for even moderately sized schemas with thorough value validation. The only real potential benefit to this type of value validation is that the framework implementation is very straightforward, to just go through this single list of validations instead of walking all attributes. + +It could be possible to implement another proposal in this space, while also supporting this one, however this could introduce unnecessary complexity into the implementation. + +#### Defining Attribute Value Validation Functions + +This section includes examples with incoming types as `tftypes.AttributePath` and the `attr.Value` interface type with an output type of `error`. These implementation details are discussed later and only shown for simpler illustrative purposes here. + +##### `AttributeValueValidationFunc` Type + +A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: + +```go +type AttributeValueValidationFunc func(context.Context, path *tftypes.AttributePath, value attr.Value) error +``` + +While the simplest implementation, this proposal does not allow for documentation hooks. It would strongly encourage all implementations to handle value type conversions since using a stronger type would risk panics that the framework cannot prevent and the compiler cannot check. + +##### `attr.ValueValidator` Generic Interface + +A new Go interface type could be created that defines an extensible value validation function type. For example: + +```go +type ValueValidator interface { + Describe(context.Context) string + MarkdownDescribe(context.Context) string + Validate(context.Context, path *tftypes.AttributePath, value attr.Value) error +} +``` + +With an example implementation: + +```go +type stringLengthBetweenValidator struct { + ValueValidator + + maximum int + minimum int +} + +func (v stringLengthBetweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) +} + +func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) +} + +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } + + if value.Unknown { + return fmt.Errorf("%s with unknown value", path) + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) + } + + return nil +} + +func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { + return stringLengthBetweenValidator{ + maximum: maximum, + minimum: minimum, + } +} +``` + +While this helps solve the documentation issue, e.g. with the following example slice type alias and receiver method: + +```go +// ValueValidators implements iteration functions across ValueValidator +type ValueValidators []ValueValidator + +// Descriptions returns all ValueValidator Description +func (vs ValueValidators) Descriptions(ctx context.Context) []string { + result := make([]string, 0, len(vs)) + + for _, v := range vs { + result = append(result, v.Description(ctx)) + } + return result +} +``` + +It still has value type issues similar to the generic `AttributeValueValidationFunc` proposal. + +##### `attr.ValueValidator` Typed Interface + +Multiple new Go interface types could be created that define extensible value validation functions with strong typing. For example: + +```go +// ValueValidator describes common validation functionality +type ValueValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string +} + +// StringValueValidator describes String value validation +type StringValueValidator interface { + ValueValidator + Validate(context.Context, *tftypes.AttributePath, types.String) error +} +``` + +Then, this framework can handle the appropriate type conversions and error handling: + +```go +// Validate performs all validation functions. +// +// Each type performs conversion or returns a conversion error +// prior to executing the typed validation function. +func (vs ValueValidators) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + for _, validator := range vs { + switch typedValidator := validator.(type) { + case StringValueValidator: + value, ok := rawValue.(types.String) + + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } + + if err := typedValidator.Validate(ctx, path, value); err != nil { + return err + } + default: + return fmt.Errorf("unknown validator type: %T", validator) + } + } + + return nil +} +``` + +Leaving the implementations to only be concerned with the typed value: + +```go +type stringLengthBetweenValidator struct { + StringValueValidator + + maximum int + minimum int +} + +func (v stringLengthBetweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) +} + +func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) +} + +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) error { + if value.Unknown { + return fmt.Errorf("%s with unknown value", path) + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) + } + + return nil +} + +func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { + return stringLengthBetweenValidator{ + maximum: maximum, + minimum: minimum, + } +} +``` + +This proposal allows each validation function to be succinctly defined with the expected value type. It may be possible to get the validation function implementations even closer to the true value logic if unknown values are also handled automatically by this framework, however that decision can be made further along in the design process. From 74811a3f3c4e4bd2bfc7f00ae3d2d9ed2c1ea1d4 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 12 Jul 2021 11:04:52 -0400 Subject: [PATCH 04/35] docs/design: Add quick note to attr.Value Typed Interfaces section about generic interface --- docs/design/validation.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/design/validation.md b/docs/design/validation.md index 3d4d9594d..69d7ec842 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1264,3 +1264,18 @@ func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator ``` This proposal allows each validation function to be succinctly defined with the expected value type. It may be possible to get the validation function implementations even closer to the true value logic if unknown values are also handled automatically by this framework, however that decision can be made further along in the design process. + +Even with this type of implementation, it is theoretically possible to create a "generic" type handler for escaping the strongly typed logic if necessary: + +```go +// GenericValueValidator describes value validation without a strong type. +// +// While it is generally preferred to use the typed validation interfaces, +// such as StringValueValidator, this interface allows custom implementations +// where the others may not be suitable. The Validate function is responsible +// for protecting against attr.Value type assertion panics. +type GenericValueValidator interface { + ValueValidator + Validate(context.Context, *tftypes.AttributePath, attr.Value) error +} +``` From 800f3178a4d75168e8c34534501a38885234eca8 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 12 Jul 2021 12:12:39 -0400 Subject: [PATCH 05/35] docs/design: Additional goal information for validation --- docs/design/validation.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 69d7ec842..4cce214ec 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -987,11 +987,12 @@ In terms of implementation, the following core concepts: Finally, these other considerations: -- Providing the appropriate amount of contextual information for debugging purposes -- Providing the appropriate amount of contextual information for practitioner facing output +- Providing the appropriate amount of contextual information for debugging purposes (e.g. logging) +- Providing the appropriate amount of contextual information for practitioner facing output (e.g. paths and values involved with validation decisions) - Ease of extending validation (e.g. handling type conversion and/or unknown values in the framework) - Ease of testing validation (e.g. unit testing) - Ease and succinctness of common validation scenarios (e.g. verbosity in provider code) +- Allowing potential future enhancements of validation behavioral decisions based on configuration (e.g. converting validation errors to warnings or logs) ## Proposals From 97d9567fe7a3355406524114062e069127a4f04f Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 12 Jul 2021 12:19:37 -0400 Subject: [PATCH 06/35] docs/design: Typo fixes in validation Co-authored-by: Paddy --- docs/design/validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4cce214ec..15d1ba68f 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1,6 +1,6 @@ # Validation -Practitioners implementing Terraform configurations desire feedback surrounding the syntax, types, and acceptable values. This feedback, typically referred to as validation, is perferably given as early as possible before a configuration is applied. Terraform supports a plugin architecture, which extends the configuration and validation surface area based on the implementation details of those plugins. This framework provides validation hooks for plugins. This design document will outline background information on the problem space, prior framework choices, and proposals for this framework. +Practitioners implementing Terraform configurations desire feedback surrounding the syntax, types, and acceptable values. This feedback, typically referred to as validation, is preferably given as early as possible before a configuration is applied. Terraform supports a plugin architecture, which extends the configuration and validation surface area based on the implementation details of those plugins. This framework provides validation hooks for plugins. This design document will outline background information on the problem space, prior framework choices, and proposals for this framework. ## Background @@ -490,7 +490,7 @@ map[string]*schema.Schema{ It supported single attribute value validation via the `ValidateFunc` or `ValidateDiagFunc` fields and multiple attribute validation via a collection of different fields (`AtLeastOneOf`, `ConflictsWith`, `ExactlyOneOf`, `RequiredWith`) which could be combined as necessary. For list, set, and map types, two additional fields (`MaxItems` and `MinItems`) provided validation for the number of elements. -The multiple attribute validation support in the attribute schema is purely existance based, meaning it could not be conditional based on the attribute value. Conditional multiple attribute validation based on values was later added via the resource level `CustomizeDiff`, which will be described later on. +The multiple attribute validation support in the attribute schema is purely existence based, meaning it could not be conditional based on the attribute value. Conditional multiple attribute validation based on values was later added via the resource level `CustomizeDiff`, which will be described later on. These fields also required a full attribute path in "flatmap" syntax, which had limitations for declaring them against nested attributes. For example: From 5b3588a5b695ee94a0ac3622e7db5fcecf98cd8f Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 12 Jul 2021 13:32:43 -0400 Subject: [PATCH 07/35] docs/design: Add proposals around attribute value validation function parameters --- docs/design/validation.md | 123 +++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 28 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 15d1ba68f..522912ce3 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1102,9 +1102,9 @@ A new Go type could be created that defines the signature of a value validation type AttributeValueValidationFunc func(context.Context, path *tftypes.AttributePath, value attr.Value) error ``` -While the simplest implementation, this proposal does not allow for documentation hooks. It would strongly encourage all implementations to handle value type conversions since using a stronger type would risk panics that the framework cannot prevent and the compiler cannot check. +While the simplest implementation, this proposal does not allow for documentation hooks. -##### `attr.ValueValidator` Generic Interface +##### `attr.ValueValidator` Interface A new Go interface type could be created that defines an extensible value validation function type. For example: @@ -1160,7 +1160,7 @@ func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator } ``` -While this helps solve the documentation issue, e.g. with the following example slice type alias and receiver method: +This helps solve the documentation issue with the following example slice type alias and receiver method: ```go // ValueValidators implements iteration functions across ValueValidator @@ -1177,11 +1177,30 @@ func (vs ValueValidators) Descriptions(ctx context.Context) []string { } ``` -It still has value type issues similar to the generic `AttributeValueValidationFunc` proposal. +#### Attribute Value Validation Function Value Parameter -##### `attr.ValueValidator` Typed Interface +Regardless the choice of concrete or interface types for the value validation functions, the parameters and returns for the implementations will play a crucial role on the extensibility and development experience. -Multiple new Go interface types could be created that define extensible value validation functions with strong typing. For example: +##### `attr.Value` Type + +The simplest implementation in the framework that could occur in all function types or interfaces is directly supplying an `attr.Value` and requiring implementations to handle all type conversion: + +```go +func (v someValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } + + // ... rest of logic ... +``` + +This proposal would strongly encourage all implementations to handle value type conversions since using a stronger type in the function signature would risk panics that the framework cannot prevent and the compiler cannot check. Any error handling here could become inconsistent across implementations. This type conversion logic feels like an unnecessary burden on implementors and could reduce the developer experience as this logic would always need to be repeated with little to no actual utility. + +##### `types.T` Type + +If using an `attr.ValueValidator` interface approach, multiple new Go interface types could be created that define extensible value validation functions with strong typing. For example: ```go // ValueValidator describes common validation functionality @@ -1229,21 +1248,6 @@ func (vs ValueValidators) Validate(ctx context.Context, path *tftypes.AttributeP Leaving the implementations to only be concerned with the typed value: ```go -type stringLengthBetweenValidator struct { - StringValueValidator - - maximum int - minimum int -} - -func (v stringLengthBetweenValidator) Description(_ context.Context) string { - return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) -} - -func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) -} - func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) error { if value.Unknown { return fmt.Errorf("%s with unknown value", path) @@ -1255,13 +1259,6 @@ func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftype return nil } - -func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { - return stringLengthBetweenValidator{ - maximum: maximum, - minimum: minimum, - } -} ``` This proposal allows each validation function to be succinctly defined with the expected value type. It may be possible to get the validation function implementations even closer to the true value logic if unknown values are also handled automatically by this framework, however that decision can be made further along in the design process. @@ -1280,3 +1277,73 @@ type GenericValueValidator interface { Validate(context.Context, *tftypes.AttributePath, attr.Value) error } ``` + +Offering the largest amount of flexibility for implementors to choose the level of desired abstraction, while not hindering more advanced implementations. + +#### Attribute Value Validation Function Path Parameter + +Another consideration with attribute value validation functions is whether the implementation should be responsible for adding context around the attribute path under validation and how that information (if provided) is surfaced to the function body. + +##### No Attribute Path Parameter + +Validation function implementations could potentially not have access to the attribute path under validation, instead relying on surrounding logic to handle wrapping errors or logging to include the path. For example: + +```go +tflog.Debug(ctx, "validating attribute path (%s) attribute value (%s): %s", attributePath.String(), value, validator.Description()) + +err := validator.Validate(ctx, value) + +if err != nil { + return fmt.Errorf("%s: %w", attributePath.String(), err) +} +``` + +This could be a double edged sword for extensibility. Implementators do not need to worry about handling the attribute path in error messages that are returned to practitioners or manually adding logging around it. This does however prevent the ability to provide that additional context to the validation logic, if for example the logic warrants making decisions based on the given path or additional logging that includes the full path. In practice with validation functions in the previous framework, path based decisions are rare at best, and this framework could be opinionated against that particular pattern. + +##### Adding Attribute Path to Context + +This framework could inject additional validation information into the `context.Context` being passed through to the validation functions. For example: + +```go +const ValidationAttributePathKey = "validation_attribute_path" + +validationCtx := context.WithValue(ctx, ValidationAttributePathKey, attributePath) +validator.Validate(ctx, value) +``` + +With implementations referencing this data: + +```go +func (v someValidator) Validate(ctx context.Context, rawValue attr.Value) error { + // ... + rawAttributePath := ctx.Value(ValidationAttributePathKey) + + attributePath, ok := rawAttributePath.(*tftypes.AttributePath) + + if !ok { + return fmt.Errorf("unexpected %s context value type: %T", ValidationAttributePathKey, rawAttributePath) + } + // ... +``` + +This experience seems subpar for developers though as they must know about the special context value(s) available and how to reference them appropriately, especially to avoid a type assertion panic. In this case, it seems more appropriately to pass the parameter directly, if necessary. + +##### `string` Type + +The attribute path could be passed to validation functions as its string representation. For example: + +```go +validator.Validate(ctx, attributePath.String(), value) +``` + +This would allow implementors to ignore the details of what the attribute path is or how to represent it appropriately. However, this seems unnecessarily limiting should the path information need to be used in the logic. In this case, calling a Go conventional `String()` receiver method on the actual attribute path type does not feel like a development burden for implementors as necessary. + +##### `*tftypes.AttributePath` Type + +The attribute path could be passed to validation functions directly using `*tftypes.AttributePath` or its abstraction in this framework. For example: + +```go +validator.Validate(ctx, attributePath, value) +``` + +This provides the ultimate flexibility for implementors, making the path information fully available in logic, logging, etc. This framework's design could also borrow ideas from the [No Attribute Path Parameter](#no-attribute-path-parameter) section and automatically handle logging and wrapping where appropriate, leaving it completely optional for implementators to handle the path information. From 2d6375af76f9abc22e5569f7559c876a7fb16a5b Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 12 Jul 2021 14:37:06 -0400 Subject: [PATCH 08/35] docs/design: Initial validation section on function returns --- docs/design/validation.md | 213 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/docs/design/validation.md b/docs/design/validation.md index 522912ce3..b0c02bc15 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1347,3 +1347,216 @@ validator.Validate(ctx, attributePath, value) ``` This provides the ultimate flexibility for implementors, making the path information fully available in logic, logging, etc. This framework's design could also borrow ideas from the [No Attribute Path Parameter](#no-attribute-path-parameter) section and automatically handle logging and wrapping where appropriate, leaving it completely optional for implementators to handle the path information. + +#### Attribute Value Validation Function Returns + +Depending on the validation function design, there could be important details about the validation process that need to be surfaced to callers. This section walks through different proposals on how information can be returned to callers. + +##### Attribute Value Validation Function `bool` Return + +Validation functions could implement return information via a `bool` type. For example: + +```go +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) bool { + value, ok := rawValue.(types.String) + + if !ok { + return false + } + + if value.Unknown { + return false + } + + return len(value.Value) > v.minimum && len(value.Value) < v.maximum +} +``` + +This proposal encodes no information in the response from these functions beyond a simple boolean "validation passed" versus "validation failed" value. Information such as whether validation failed due to type conversion problems or validation could not be performed due to an unknown value is hidden. Giving the ability for functions to surface details about unsuccessful validation back to callers is likely required broader utility in this framework and extensions to it. + +In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level, summary, or details associated with that diagnostic. + +##### Attribute Value Validation Function `error` Return + +Validation functions could implement return information via an untyped `error`. For example: + +```go +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } + + if value.Unknown { + return fmt.Errorf("%s with unknown value", path) + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) + } + + return nil +} +``` + +In this scenario, callers will know that validation did not pass, but not necessarily why. This proposal is only marginally better than the `bool` return value, as some manual error message context can be provided about the problem that caused the failure. However short of perfectly consistent error messaging which is not feasible to enforce in all implementors, callers will still not reasonably be able to perform actions based on the differing reasons for errors. + +In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level or summary associated with that diagnostic. The details would likely include the error messaging. + +##### Attribute Value Validation Function Typed Error Return + +This framework could provide typed errors for validation functions. For example: + +```go +type ValueValidatorInvalidTypeError struct { + Path *tftypes.AttributePath + Value attr.Value +} + +// Error implements the error interface +func (e ValueValidatorInvalidTypeError) Error() string { + // ... +} + +type ValueValidatorInvalidValueError struct { + Description string + Path *tftypes.AttributePath + Value attr.Value +} + +// Error implements the error interface +func (e ValueValidatorInvalidValueError) Error() string { + // ... +} + +type ValueValidatorUnknownValueError struct { + Path *tftypes.AttributePath +} + +// Error implements the error interface +func (e ValueValidatorUnknownValueError) Error() string { + // ... +} +``` + +With implementators able to return these such as: + +```go +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return ValueValidatorInvalidTypeError{ + Path: path, + Value: rawValue, + } + } + + if value.Unknown { + return ValueValidatorUnknownValueError{ + Path: path, + } + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return ValueValidatorInvalidValueError{ + Description: v.Description(ctx), + Path: path, + Value: value, + } + } + + return nil +} +``` + +This framework could also go further and require using one of these error types: + +```go +type ValueValidatorError interface {} + +// ... + +type ValueValidatorInvalidTypeError struct { + ValueValidatorError + + Path *tftypes.AttributePath + Value attr.Value +} + +// ... + +type ValueValidator interface { + // ... + Validate(context.Context, *tftypes.AttributePath, attr.Value) ValueValidatorError +} +``` + +Meaning that extensibility is guaranteed to follow certain compile time rules. + +In either the `error` or `ValueValidatorError` interface type scenarios, this allows callers to react to the responses by checking for underlying error types. For example, it is possible to implement a generic `Not()` (logical `NOT`) validation function that catches invalid values but passes through other errors: + +```go +func (v notValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + var invalidValueError ValueValidatorInvalidValueError + + err := v.validator.Validate(ctx, path, rawValue) + + if err == nil { + return ValueValidatorInvalidValueError{ + Description: v.Description(ctx), + Path: path, + Value: rawValue, + } + } + + if errors.As(err, &invalidValueError) { + return nil + } + + return err +} +``` + +In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level or summary associated with that diagnostic. The details would likely include the error messaging based on the error type implementations, although if it was warranted for extensibility, there could also be a "generic" `ValueValidatorError` type (or when there is an unrecognized `error` type) that this framework would pass over except transferring the messaging through to the diagnostic. Additional warning-only types could also be provided to allow further diagnostic customization. + +##### Attribute Value Validation Function Diagnostic Return + +Validation functions could directly return a `*tfprotov6.Diagnostic` or abstracted type from this framework. For example: + +```go +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) *tfprotov6.Diagnostic { + value, ok := rawValue.(types.String) + + if !ok { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Incorrect validation type", + Details: fmt.Sprintf("%s with incorrect type: %T", path, rawValue), + } + } + + if value.Unknown { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unknown validation value", + Details: fmt.Sprintf("received unknown value at path: %s", path), + } + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Value validation failed", + Details: fmt.Sprintf("%s with value %q %s", path, value.Value, v.Description(ctx)) + } + } + + return nil +} +``` + +In this scenario, it the implementor's responsibility to generate the appropriate diagnostic back, but they have full control of the output. It could be difficult for the framework to enforce implementation rules around these responses or potentially allow configuration overrides for them without creating more abstractions on top of this type or additional helper functions. Differing diagnostic implementations could introduce confusion for practitioners. + +In general, this proposal feels very similar to either the generic `error` type or typed error proposals above (depending on the implmentation details) with minimal utility over them beyond complete output customization. From 938ca981da3b1fe1ee0ed95e7336c5a095cd021f Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 13 Jul 2021 12:42:13 -0400 Subject: [PATCH 09/35] docs/design: Initial Multiple Attribute Validation sections --- docs/design/validation.md | 233 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 221 insertions(+), 12 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index b0c02bc15..b83fe971e 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1004,7 +1004,7 @@ This validation would be applicable to the `schema.Attribute` types declared wit This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. -##### Single Function Field on `schema.Attribute` +##### `ValueValidation` Field on `schema.Attribute` Similar to the previous framework, a new field can be added to the `schema.Attribute` type. For example: @@ -1031,7 +1031,7 @@ As seen with the previous framework in practice however, it was very common to i This proposal colocates the value validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. -##### List of Functions Field on `schema.Attribute` +##### `ValueValidations` Field on `schema.Attribute` A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: @@ -1074,17 +1074,9 @@ type AttributeWithValueValidations interface { This type of proposal, in isolation, feels extraneous given the current attribute implementation. The framework does not appear to benefit from this splitting and it seems desirable that all attributes should be able to optionally enable value validation. Future considerations to allow declaring custom attribute types, outside of validation handling, are more likely to drive this type of potential change. -##### Resource Level +##### Resource Level Attribute Value Validation Handling -This proposal would introduce no changes to `schema.Attribute`. Instead, this would require value validation declarations at the `DataSource` and `Resource` level similar to other proposed attribute validations. The implementation details of this validation depends on those later proposals, however a rough sketch of this would be: - -```go -func (t *customResourceType) AttributeValidations(/* ... */) []T1 { - return []T1{ - T1(*tftypes.AttributePath, T2), // or ...T2 - } -} -``` +This proposal would introduce no changes to `schema.Attribute`. Instead, this would require value validation declarations at the `DataSource` and `Resource` level similar to other proposed attribute validations in the [Declaring Multiple Attribute Validation for Resources](#declaring-multiple-attribute-validation-for-resources) section. This proposal makes value validation behaviors occur at a distance, meaning it is harder for provider developers to correlate the validation logic to the name/path and type information. It would also be very verbose for even moderately sized schemas with thorough value validation. The only real potential benefit to this type of value validation is that the framework implementation is very straightforward, to just go through this single list of validations instead of walking all attributes. @@ -1560,3 +1552,220 @@ func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftype In this scenario, it the implementor's responsibility to generate the appropriate diagnostic back, but they have full control of the output. It could be difficult for the framework to enforce implementation rules around these responses or potentially allow configuration overrides for them without creating more abstractions on top of this type or additional helper functions. Differing diagnostic implementations could introduce confusion for practitioners. In general, this proposal feels very similar to either the generic `error` type or typed error proposals above (depending on the implmentation details) with minimal utility over them beyond complete output customization. + +### Multiple Attribute Validation + +This framework should also provide the ability to handle validation situations across multiple attributes as noted in the goals. Some of the proposals from the [Single Attribute Value Validation](#single-attribute-value-validation) section are applicable for these proposals as well, so they are largely omitted here for brevity. Examples showing `attr.Value`, `*tftypes.AttributePath`, and bare `error` types are for illustrative purposes, whose final forms would be determined by those proposals. + +#### Declaring Multiple Attribute Validation for Attributes + +The previous framework implemented behaviors, such as `ConflictsWith`, as an individual field per behavior within each attribute. This section of proposals targets this specific functionality. One major caveat to these proposals is that they should not be considered exclusive to attribute value validations as it may be desirable to provide some consistency between the two implementations to improve developer experience. + +This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. + +##### Individual Behavior Fields on `schema.Attribute` + +Similar to the previous framework, individual fields for each attribute validation could be added to the `schema.Attribute` type which accepts multiple attribute paths. For example: + +```go +schema.Attribute{ + // ... + ConflictsWith: []tftypes.AttributePath, +} +``` + +A potential downside is that these behaviors cannot support the notion of conditional logic without changes to the implementations, since they can only be existence based if passed an attribute path. Allowing value validations in the declarations (on either side), could allieviate this issue. For example: + +```go +schema.Attribute{ + // ... + ConflictsWith: []func(AttributeValueValidator, tftypes.AttributePath, AttributeValueValidator), +} +``` + +Regardless of the potential value handling, this proposal would feel familiar for existing provider developers and be relatively trivial for them to implement. One noticable downside to this approach however is that there can be any number of related, but disjointed attribute behaviors. The previous framework supported four of these already and there is logical room for addtional behaviors, making updates to the `schema.Attribute` type a limiting factor in this validation space. This proposal also differs from value validation proposals, which are focused around a single field. + +##### `PathValidation` Field on `schema.Attribute` + +A new field for attribute validation can be added to the `schema.Attribute` type. For example: + +```go +schema.Attribute{ + // ... + PathValidation: T, +} +``` + +Implementators would be responsible for ensuring that single function covered all necessary validation. The framework could provide wrapper functions similar to the previous `All()` and `Any()` of `ValidateFunc` to allow simpler validations built from multiple functions. For example: + +```go +schema.Attribute{ + // ... + PathValidation: All( + T, + T, + ), +} +``` + +As seen with the previous framework in practice however, it was very common to implement the `All()` wrapper function. New provider developers would be responsible for understanding that multiple validations are possible in the single function field and knowing that custom validation functions may not be necessary to write if using the wrapper functions. + +This proposal colocates the attribute validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. + +##### `PathValidations` Field on `schema.Attribute` + +A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: + +```go +schema.Attribute{ + // ... + PathValidations: []T{ + T, + T, + }, +} +``` + +In this case, the framework would perform the validation similar to the previous framework `All()` wrapper function for `ValidateFunc`. The logical `AND` type of value validation is overwhelmingly more common in practice, which will simplify provider implementations. This still allows for an `Any()` based wrapper (logical `OR`) to be inserted if necessary. + +Colocating the attribute validation behaviors in the schema definition, means it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. This proposal will feel familiar to existing provider developers. New provider developers will immediately know that multiple validations are supported. + +##### Combined `Validations` Field on `schema.Attribute` + +A new field that accepts the union of [`ValueValidations` field on `schema.Attribute`](#valuevalidations-field-on-schemaattribute) and [`PathValidations` field on `schema.Attribute`](#pathvalidations-field-on-schemaattribute) can be added to the `schema.Attribute` type. For example: + +```go +schema.Attribute( + // ... + Validations: []I( + T1, + T2, + ) +) +``` + +Since value validation functions would inherently be implemented different than path validation functions and they are conceptually similar but different in certain ways, this could be complex to implement or understand correctly. When trying to handle documentation output for example, this framework or callers would need to distinguish between the two validation types to ensure the intended validation meanings are correct. + +##### Resource Level Attribute Path Validation Handling + +Rather than adjusting the `schema.Attribute` type for this type of validation, it could be forced to the resource (or data source) level. The [Declaring Multiple Attribute Validation for Resources](#declaring-multiple-attribute-validation-for-resources) proposals presented later are revelant for this section. To prevent proposal duplication, please see that section for more details and associated tradeoffs. + +#### Declaring Multiple Attribute Validation for Resources + +In the previous framework, the `CustomizeDiff` functionality enabled resource (or data source) level validation as a logical catch-all. These proposals cover the next iteration of that type of functionality. + +##### `PlanModifications` for Resources + +The [Plan Modifications design documentation](./plan-modifications.md) outlines proposals which broadly replace the previous framework's `CustomizeDiff` functionality. See that documentation for considerations and recommendations there. In this proposal for validation, new functions for validation would be provided within that framework, rather than introducing separate handling. + +Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. + +##### `AttributeValidations` for Resources + +This introduces a new extension interface type for `ResourceType` and `DataSourceType`. For example: + +```go +type DataSourceTypeWithAttributeValidations interface { + ResourceType + AttributeValidations(context.Context) AttributeValidators +} + +type ResourceTypeWithAttributeValidations interface { + ResourceType + AttributeValidations(context.Context) AttributeValidators +} +``` + +Where `AttributeValidators` is a slice of types to be discussed later. + +As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: + +```go +func (t *customResourceType) AttributeValidations(ctx context.Context) AttributeValidators { + return AttributeValidators{ + ConflictingAttributes(*tftypes.AttributePath, *tftypes.Attribute), + ConflictingAttributesWithValues(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath, ValueValidator), + PrerequisiteAttribute(*tftypes.AttributePath, *tftypes.AttributePath), + PrerequisiteAttributeWithValue(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath), + } +} +``` + +This setup would allow for the framework to provide flexible resource level validation with a low amount of friction for provider developers. Helper functions would be extensible and make the behaviors clear. + +#### Defining Attribute Validation Functions + +This section includes examples with parameter types as `tftypes.AttributePath` and the `attr.Value` interface type with an return type of `error`. These implementation details are shown for simpler illustrative purposes here, but will likely depend on the outcome from the [Single Attribute Value Validation](#single-attribute-value-validation) proposals. + +##### `AttributeValidationFunc` Type + +A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: + +```go +type AttributeValidationFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +``` + +This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. + +##### `AttributeValidator` Interface + +A new Go interface type could be created that defines an extensible attribute validation function type. For example: + +```go +type AttributeValidator interface { + Describe(context.Context) string + MarkdownDescribe(context.Context) string + Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +} +``` + +With an example implementation: + +```go +type conflictingAttributesValidator struct { + AttributeValidator + + path1 *tftypes.AttributePath + path2 *tftypes.AttributePath +} + +func (v conflictingAttributesValidator) Description(_ context.Context) string { + return fmt.Sprintf("%s and %s cannot both be configured", v.path1.String(), v.path2.String()) +} + +func (v conflictingAttributesValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("`%s` and `%s` cannot both be configured", v.path1.String(), v.path2.String()) +} + +func (v conflictingAttributesValidator) Validate(ctx context.Context, _ *tftypes.AttributePath, _ attr.Value, _ *tftypes.AttributePath, _ attr.Value) error { + if /* v.path1 configured */ && /* v.path2 configured */ { + return fmt.Errorf("%s", v.Description(ctx)) + } + + return nil +} + +func ConflictingAttributes(path1 *tftypes.AttributePath, path2 *tftypes.AttributePath) conflictingAttributesValidator { + return conflictingAttributesValidator{ + path1: path1, + path2: path1, + } +} +``` + +This helps solve the documentation issue with the following example slice type alias and receiver method: + +```go +// AttributeValidators implements iteration functions across AttributeValidator +type AttributeValidators []AttributeValidator + +// Descriptions returns all AttributeValidator Description +func (vs AttributeValidators) Descriptions(ctx context.Context) []string { + result := make([]string, 0, len(vs)) + + for _, v := range vs { + result = append(result, v.Description(ctx)) + } + return result +} +``` From 1fc4424ae6e24a98aa92cb8a8365ea5598ef1768 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 14 Jul 2021 08:52:47 -0400 Subject: [PATCH 10/35] docs/design: Add provider versions of validation function definitions --- docs/design/validation.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/design/validation.md b/docs/design/validation.md index b83fe971e..4c2527fe3 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1094,6 +1094,12 @@ A new Go type could be created that defines the signature of a value validation type AttributeValueValidationFunc func(context.Context, path *tftypes.AttributePath, value attr.Value) error ``` +To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: + +```go +type AttributeValueValidationFunc func(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value attr.Value) error +``` + While the simplest implementation, this proposal does not allow for documentation hooks. ##### `attr.ValueValidator` Interface @@ -1169,6 +1175,15 @@ func (vs ValueValidators) Descriptions(ctx context.Context) []string { } ``` +To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: + +```go +type ValueValidatorWithProvider interface { + ValueValidator + ValidateWithProvider(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value attr.Value) error +} +``` + #### Attribute Value Validation Function Value Parameter Regardless the choice of concrete or interface types for the value validation functions, the parameters and returns for the implementations will play a crucial role on the extensibility and development experience. @@ -1272,6 +1287,15 @@ type GenericValueValidator interface { Offering the largest amount of flexibility for implementors to choose the level of desired abstraction, while not hindering more advanced implementations. +To support passing through the provider instance, separate interface types could be introduced that include a function call with the `tfsdk.Provider` interface type: + +```go +type StringValueValidatorWithProvider interface { + ValueValidator + ValidateWithProvider(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value types.String) error +} +``` + #### Attribute Value Validation Function Path Parameter Another consideration with attribute value validation functions is whether the implementation should be responsible for adding context around the attribute path under validation and how that information (if provided) is surfaced to the function body. @@ -1705,6 +1729,12 @@ A new Go type could be created that defines the signature of a value validation type AttributeValidationFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error ``` +To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: + +```go +type AttributeValidationFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +``` + This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. ##### `AttributeValidator` Interface @@ -1769,3 +1799,12 @@ func (vs AttributeValidators) Descriptions(ctx context.Context) []string { return result } ``` + +To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: + +```go +type AttributeValidatorWithProvider interface { + AttributeValidator + ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +} +``` From 595a7c3fe3c9e9da9eafa23201a3b652de540116 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 14 Jul 2021 10:25:45 -0400 Subject: [PATCH 11/35] docs/design: Initial validation recommendations and minor fixes --- docs/design/validation.md | 248 +++++++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 5 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4c2527fe3..5ed52c13b 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1108,8 +1108,8 @@ A new Go interface type could be created that defines an extensible value valida ```go type ValueValidator interface { - Describe(context.Context) string - MarkdownDescribe(context.Context) string + Description(context.Context) string + MarkdownDescription(context.Context) string Validate(context.Context, path *tftypes.AttributePath, value attr.Value) error } ``` @@ -1690,7 +1690,7 @@ This introduces a new extension interface type for `ResourceType` and `DataSourc ```go type DataSourceTypeWithAttributeValidations interface { - ResourceType + DataSourceType AttributeValidations(context.Context) AttributeValidators } @@ -1743,8 +1743,8 @@ A new Go interface type could be created that defines an extensible attribute va ```go type AttributeValidator interface { - Describe(context.Context) string - MarkdownDescribe(context.Context) string + Description(context.Context) string + MarkdownDescription(context.Context) string Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error } ``` @@ -1808,3 +1808,241 @@ type AttributeValidatorWithProvider interface { ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error } ``` + +## Recommendations + +This section will summarize the proposals into specific recommendations for each topic. Code examples are provided in following sections to illustrate the concepts. The final section provides some future considerations for the framework and terraform-plugin-go. + +### Overview + +Defining all validation functionality via interface types will offer the framework the most flexibility for future enhancements while ensuring consistent implementations. Furthermore for attribute value validation functions, providing strongly typed interfaces for common value types will reduce implementor burden and ensure consistent invalid type error messaging, rather than potential panic scenarios. + +Attribute value validations should be implemented as a slice of the interface type on `schema.Attribute`. Multiple attribute validation, such as declaring conflicting attributes on attributes themselves, should be implemented as a separate slice of that differing interface type on `schema.Attribute`. This would be in addition to supporting that functionality with resource level multiple attribute validation. + +Attribute value validations should be required to accept the attribute path in its native type as a parameter. This will allow a flexible implementation for provider developers that may desire advanced logic based on the path. + +Validation functions should be required to return framework defined error types. These errors will either result in the framework returning consistent error diagnostics or callers (such as wrapper validation functions) otherwise handling these results in a predictable manner. Error types that equate to consistent warning diagnostics can also be provided, if desired. + +Resource level multiple attribute validation functions should be implemented separately from plan modifications to separate concerns. For example: + +### Attribute Level Example Implementation + +Example framework code: + +```go +// Well defined error types +type ValueValidatorError interface {} + +type ValueValidatorInvalidTypeError interface { + ValueValidatorError +} + +type ValueValidatorUnknownValueError interface { + ValueValidatorError +} + +type ValueValidatorUnsuccessfulValidationError interface { + ValueValidatorError +} + +// ValueValidator is an interface type for implementing common validation functionality. +type ValueValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string +} + +// ValueValidators is a type alias for a slice of ValueValidator. +type ValueValidators []ValueValidator + +// Descriptions returns all ValueValidator Description +func (vs ValueValidators) Descriptions(ctx context.Context) []string { + // ... +} + +// MarkdownDescriptions returns all ValueValidator MarkdownDescription +func (vs ValueValidators) MarkdownDescriptions(ctx context.Context) []string { + // ... +} + +// Validates performs all ValueValidator Validate or ValidateWithProvider +func (vs ValueValidators) Validates(ctx context.Context) diag.Diagnostics { + // ... +} + +// GenericValueValidator describes value validation without a strong type. +// +// While it is generally preferred to use the typed validation interfaces, +// such as StringValueValidator, this interface allows custom implementations +// where the others may not be suitable. The Validate function is responsible +// for protecting against attr.Value type assertion panics. +type GenericValueValidator interface { + ValueValidator + Validate(context.Context, *tftypes.AttributePath, attr.Value) error +} + +// StringValueValidator is an interface type for implementing String value validation. +type StringValueValidator interface { + ValueValidator + Validate(context.Context, *tftypes.AttributePath, types.String) ValueValidatorError +} + +// StringValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. +type StringValueValidatorWithProvider interface { + StringValueValidator + ValidateWithProvider(context.Context, tfsdk.Provider, *tftypes.AttributePath, types.String) ValueValidatorError +} + +type Attribute struct { + // ... + PathValidations AttributeValidators // described below + ValueValidations ValueValidators +} +``` + +Example validation function code: + +```go +type stringLengthBetweenValidator struct { + StringValueValidator + + maximum int + minimum int +} + +func (v stringLengthBetweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) +} + +func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) +} + +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) ValueValidatorError { + if value.Unknown { + return ValueValidatorUnknownValueError{ + Path: path, + } + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return ValueValidatorUnsuccessfulValidationError{ + Description: v.Description(ctx), + Path: path, + Value: value, + } + } + + return nil +} + +func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { + return stringLengthBetweenValidator{ + maximum: maximum, + minimum: minimum, + } +} +``` + +Example provider code: + +```go +schema.Attribute{ + Type: types.StringType, + Required: true, + PathValidations: AttributeValidators{ + ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), + }, + ValueValidations: ValueValidators{ + StringLengthBetween(1, 256), + }, +} +``` + +### Resource Level Example Implementation + +Example framework code: + +```go +// Well defined error types +type AttributeValidatorError interface {} + +type AttributeValidatorInvalidTypeError interface { + AttributeValidatorError +} + +type AttributeValidatorUnknownValueError interface { + AttributeValidatorError +} + +type AttributeValidatorUnsuccessfulValidationError interface { + AttributeValidatorError +} + +// AttributeValidator is an interface type for declaring multiple attribute validations. +type AttributeValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) AttributeValidatorError +} + +// AttributeValidators is a type alias for a slice of AttributeValidator. +type AttributeValidators []AttributeValidator + +// Descriptions returns all AttributeValidator Description +func (vs AttributeValidators) Descriptions(ctx context.Context) []string { + // ... +} + +// MarkdownDescriptions returns all AttributeValidator MarkdownDescription +func (vs AttributeValidators) MarkdownDescriptions(ctx context.Context) []string { + // ... +} + +// Validates performs all AttributeValidator Validate or ValidateWithProvider +func (vs AttributeValidators) Validates(ctx context.Context) diag.Diagnostics { + // ... +} + +// AttributeValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. +type AttributeValidatorWithProvider interface { + AttributeValidator + ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) AttributeValidatorError +} + +// DataSourceTypeWithAttributeValidations is an interface type that extends DataSourceType to include attribute validations. +type DataSourceTypeWithAttributeValidations interface { + DataSourceType + AttributeValidations(context.Context) AttributeValidators +} + +// ResourceTypeWithAttributeValidations is an interface type that extends ResourceType to include attribute validations. +type ResourceTypeWithAttributeValidations interface { + ResourceType + AttributeValidations(context.Context) AttributeValidators +} +``` + +Example provider code: + +```go +func (t *customResourceType) AttributeValidations(ctx context.Context) AttributeValidators { + return AttributeValidators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), + } +} +``` + +### Future Considerations + +It is recommended that the framework provide an abstracted `*tftypes.AttributePath` rather than depend on that type directly, but this can converted after an initial implementation. This is purely for decoupling the two projects, similar to other abstracted types already created in the framework. + +To better support provider-based validation functionality in the future, it is also recommended that the `Provider` interface type also add a new `Configured(context.Context) bool` function or another methodology for easily checking the configuration state of a provider instance. Adding a setter function could also allow the framework to manage the provider configuration state automatically. This would simplify validations that require provider instances since it will likely be required that implementations need to check on this status as part of the validation logic. + +It is recommended that the framework or the upstream terraform-plugin-go module provide functionality to declare relative attribute paths, such as "this" and "parent" methods to better enable nested attribute declarations. This will enable provider developers to create attribute paths such as: + +```go +NewAttributePath(CurrentPath().Parent().AttributeName("other_attr")) +``` From 779f7a8ea89a8daaad987d72c5cb1ae17f9daadf Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 15 Jul 2021 15:00:51 -0400 Subject: [PATCH 12/35] docs/design: Update validation types/functions to Validators --- docs/design/validation.md | 80 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 5ed52c13b..31a62ddbe 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1004,14 +1004,14 @@ This validation would be applicable to the `schema.Attribute` types declared wit This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. -##### `ValueValidation` Field on `schema.Attribute` +##### `ValueValidator` Field on `schema.Attribute` Similar to the previous framework, a new field can be added to the `schema.Attribute` type. For example: ```go schema.Attribute{ // ... - ValueValidation: T, + ValueValidator: T, } ``` @@ -1020,7 +1020,7 @@ Implementators would be responsible for ensuring that single function covered al ```go schema.Attribute{ // ... - ValueValidation: All( + ValueValidator: All( T, T, ), @@ -1031,14 +1031,14 @@ As seen with the previous framework in practice however, it was very common to i This proposal colocates the value validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. -##### `ValueValidations` Field on `schema.Attribute` +##### `ValueValidators` Field on `schema.Attribute` A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: ```go schema.Attribute{ // ... - ValueValidations: []T{ + ValueValidators: []T{ T, T, }, @@ -1059,16 +1059,16 @@ type Attribute interface { // ... } -type AttributeWithValueValidations struct { +type AttributeWithValueValidators struct { Attribute - ValueValidations []T + ValueValidators []T } // or more interfaces -type AttributeWithValueValidations interface { +type AttributeWithValueValidators interface { Attribute - ValueValidations(/* ... */) []T + ValueValidators(/* ... */) []T } ``` @@ -1086,18 +1086,18 @@ It could be possible to implement another proposal in this space, while also sup This section includes examples with incoming types as `tftypes.AttributePath` and the `attr.Value` interface type with an output type of `error`. These implementation details are discussed later and only shown for simpler illustrative purposes here. -##### `AttributeValueValidationFunc` Type +##### `AttributeValueValidatorFunc` Type A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: ```go -type AttributeValueValidationFunc func(context.Context, path *tftypes.AttributePath, value attr.Value) error +type AttributeValueValidatorFunc func(context.Context, path *tftypes.AttributePath, value attr.Value) error ``` To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: ```go -type AttributeValueValidationFunc func(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value attr.Value) error +type AttributeValueValidatorFunc func(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value attr.Value) error ``` While the simplest implementation, this proposal does not allow for documentation hooks. @@ -1609,14 +1609,14 @@ schema.Attribute{ Regardless of the potential value handling, this proposal would feel familiar for existing provider developers and be relatively trivial for them to implement. One noticable downside to this approach however is that there can be any number of related, but disjointed attribute behaviors. The previous framework supported four of these already and there is logical room for addtional behaviors, making updates to the `schema.Attribute` type a limiting factor in this validation space. This proposal also differs from value validation proposals, which are focused around a single field. -##### `PathValidation` Field on `schema.Attribute` +##### `PathValidator` Field on `schema.Attribute` A new field for attribute validation can be added to the `schema.Attribute` type. For example: ```go schema.Attribute{ // ... - PathValidation: T, + PathValidator: T, } ``` @@ -1625,7 +1625,7 @@ Implementators would be responsible for ensuring that single function covered al ```go schema.Attribute{ // ... - PathValidation: All( + PathValidator: All( T, T, ), @@ -1636,14 +1636,14 @@ As seen with the previous framework in practice however, it was very common to i This proposal colocates the attribute validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. -##### `PathValidations` Field on `schema.Attribute` +##### `PathValidators` Field on `schema.Attribute` A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: ```go schema.Attribute{ // ... - PathValidations: []T{ + PathValidators: []T{ T, T, }, @@ -1654,14 +1654,14 @@ In this case, the framework would perform the validation similar to the previous Colocating the attribute validation behaviors in the schema definition, means it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. This proposal will feel familiar to existing provider developers. New provider developers will immediately know that multiple validations are supported. -##### Combined `Validations` Field on `schema.Attribute` +##### Combined `Validators` Field on `schema.Attribute` -A new field that accepts the union of [`ValueValidations` field on `schema.Attribute`](#valuevalidations-field-on-schemaattribute) and [`PathValidations` field on `schema.Attribute`](#pathvalidations-field-on-schemaattribute) can be added to the `schema.Attribute` type. For example: +A new field that accepts the union of [`ValueValidators` field on `schema.Attribute`](#valuevalidators-field-on-schemaattribute) and [`PathValidators` field on `schema.Attribute`](#pathvalidators-field-on-schemaattribute) can be added to the `schema.Attribute` type. For example: ```go schema.Attribute( // ... - Validations: []I( + Validators: []I( T1, T2, ) @@ -1684,19 +1684,19 @@ The [Plan Modifications design documentation](./plan-modifications.md) outlines Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. -##### `AttributeValidations` for Resources +##### `AttributeValidators` for Resources This introduces a new extension interface type for `ResourceType` and `DataSourceType`. For example: ```go -type DataSourceTypeWithAttributeValidations interface { +type DataSourceTypeWithAttributeValidators interface { DataSourceType - AttributeValidations(context.Context) AttributeValidators + AttributeValidators(context.Context) AttributeValidators } -type ResourceTypeWithAttributeValidations interface { +type ResourceTypeWithAttributeValidators interface { ResourceType - AttributeValidations(context.Context) AttributeValidators + AttributeValidators(context.Context) AttributeValidators } ``` @@ -1705,7 +1705,7 @@ Where `AttributeValidators` is a slice of types to be discussed later. As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: ```go -func (t *customResourceType) AttributeValidations(ctx context.Context) AttributeValidators { +func (t *customResourceType) AttributeValidators(ctx context.Context) AttributeValidators { return AttributeValidators{ ConflictingAttributes(*tftypes.AttributePath, *tftypes.Attribute), ConflictingAttributesWithValues(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath, ValueValidator), @@ -1721,18 +1721,18 @@ This setup would allow for the framework to provide flexible resource level vali This section includes examples with parameter types as `tftypes.AttributePath` and the `attr.Value` interface type with an return type of `error`. These implementation details are shown for simpler illustrative purposes here, but will likely depend on the outcome from the [Single Attribute Value Validation](#single-attribute-value-validation) proposals. -##### `AttributeValidationFunc` Type +##### `AttributeValidatorsFunc` Type A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: ```go -type AttributeValidationFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +type AttributeValidatorsFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error ``` To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: ```go -type AttributeValidationFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +type AttributeValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error ``` This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. @@ -1894,8 +1894,8 @@ type StringValueValidatorWithProvider interface { type Attribute struct { // ... - PathValidations AttributeValidators // described below - ValueValidations ValueValidators + PathValidators AttributeValidators // described below + ValueValidators ValueValidators } ``` @@ -1949,10 +1949,10 @@ Example provider code: schema.Attribute{ Type: types.StringType, Required: true, - PathValidations: AttributeValidators{ + PathValidators: AttributeValidators{ ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), }, - ValueValidations: ValueValidators{ + ValueValidators: ValueValidators{ StringLengthBetween(1, 256), }, } @@ -2009,23 +2009,23 @@ type AttributeValidatorWithProvider interface { ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) AttributeValidatorError } -// DataSourceTypeWithAttributeValidations is an interface type that extends DataSourceType to include attribute validations. -type DataSourceTypeWithAttributeValidations interface { +// DataSourceTypeWithAttributeValidators is an interface type that extends DataSourceType to include attribute validations. +type DataSourceTypeWithAttributeValidators interface { DataSourceType - AttributeValidations(context.Context) AttributeValidators + AttributeValidators(context.Context) AttributeValidators } -// ResourceTypeWithAttributeValidations is an interface type that extends ResourceType to include attribute validations. -type ResourceTypeWithAttributeValidations interface { +// ResourceTypeWithAttributeValidators is an interface type that extends ResourceType to include attribute validations. +type ResourceTypeWithAttributeValidators interface { ResourceType - AttributeValidations(context.Context) AttributeValidators + AttributeValidators(context.Context) AttributeValidators } ``` Example provider code: ```go -func (t *customResourceType) AttributeValidations(ctx context.Context) AttributeValidators { +func (t *customResourceType) AttributeValidators(ctx context.Context) AttributeValidators { return AttributeValidators{ ConflictingAttributes( tftypes.NewAttributePath().AttributeName("first_attribute"), From be40423bbd992e667de26a69e9e97b726a2abc8a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 15 Jul 2021 16:17:23 -0400 Subject: [PATCH 13/35] docs/design: Remove confusing and incorrect note on attr.Value handling --- docs/design/validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 31a62ddbe..4e2b9739d 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1203,7 +1203,7 @@ func (v someValidator) Validate(ctx context.Context, path *tftypes.AttributePath // ... rest of logic ... ``` -This proposal would strongly encourage all implementations to handle value type conversions since using a stronger type in the function signature would risk panics that the framework cannot prevent and the compiler cannot check. Any error handling here could become inconsistent across implementations. This type conversion logic feels like an unnecessary burden on implementors and could reduce the developer experience as this logic would always need to be repeated with little to no actual utility. +Using this interface type would be required to support validation for custom value types. ##### `types.T` Type From 37a249a0826aa23d585350cafc5580a94948d888 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 15 Jul 2021 16:32:52 -0400 Subject: [PATCH 14/35] docs/design: Denote that diagnostics are the norm and that validation should not buck the trend without good reason Also fixes up some of the example diagnostics code. --- docs/design/validation.md | 91 +++++++++++++++------------------------ 1 file changed, 34 insertions(+), 57 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4e2b9739d..4307080e6 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1542,40 +1542,43 @@ In this scenario, it is this framework's responsibility to generate the appropri Validation functions could directly return a `*tfprotov6.Diagnostic` or abstracted type from this framework. For example: ```go -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) *tfprotov6.Diagnostic { +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) (diags tfprotov6.Diagnostics) { value, ok := rawValue.(types.String) if !ok { - return &tfprotov6.Diagnostic{ + diags = append(diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Incorrect validation type", Details: fmt.Sprintf("%s with incorrect type: %T", path, rawValue), - } + }) + return } if value.Unknown { - return &tfprotov6.Diagnostic{ + diags = append(diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unknown validation value", Details: fmt.Sprintf("received unknown value at path: %s", path), - } + }) + return } if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - return &tfprotov6.Diagnostic{ + diags = append(diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Value validation failed", Details: fmt.Sprintf("%s with value %q %s", path, value.Value, v.Description(ctx)) - } + }) + return } - return nil + return } ``` In this scenario, it the implementor's responsibility to generate the appropriate diagnostic back, but they have full control of the output. It could be difficult for the framework to enforce implementation rules around these responses or potentially allow configuration overrides for them without creating more abstractions on top of this type or additional helper functions. Differing diagnostic implementations could introduce confusion for practitioners. -In general, this proposal feels very similar to either the generic `error` type or typed error proposals above (depending on the implmentation details) with minimal utility over them beyond complete output customization. +In general, this proposal feels very similar to either the generic `error` type or typed error proposals above (depending on the implmentation details) with minimal utility over them beyond complete output customization. However, the rest of the framework is designed around diagnostics so this would introduce a different implementation. To remain consistent with other framework design while still pushing for consistency, helpers could be introduced to nudge developers towards standardized summary information, if desired. ### Multiple Attribute Validation @@ -1821,7 +1824,7 @@ Attribute value validations should be implemented as a slice of the interface ty Attribute value validations should be required to accept the attribute path in its native type as a parameter. This will allow a flexible implementation for provider developers that may desire advanced logic based on the path. -Validation functions should be required to return framework defined error types. These errors will either result in the framework returning consistent error diagnostics or callers (such as wrapper validation functions) otherwise handling these results in a predictable manner. Error types that equate to consistent warning diagnostics can also be provided, if desired. +Validation functions should be required to return diagnostics similar in design to other functionality in the framework. Helper functions could help return these results in a consistent manner. Resource level multiple attribute validation functions should be implemented separately from plan modifications to separate concerns. For example: @@ -1830,21 +1833,6 @@ Resource level multiple attribute validation functions should be implemented sep Example framework code: ```go -// Well defined error types -type ValueValidatorError interface {} - -type ValueValidatorInvalidTypeError interface { - ValueValidatorError -} - -type ValueValidatorUnknownValueError interface { - ValueValidatorError -} - -type ValueValidatorUnsuccessfulValidationError interface { - ValueValidatorError -} - // ValueValidator is an interface type for implementing common validation functionality. type ValueValidator interface { Description(context.Context) string @@ -1865,7 +1853,7 @@ func (vs ValueValidators) MarkdownDescriptions(ctx context.Context) []string { } // Validates performs all ValueValidator Validate or ValidateWithProvider -func (vs ValueValidators) Validates(ctx context.Context) diag.Diagnostics { +func (vs ValueValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { // ... } @@ -1877,19 +1865,19 @@ func (vs ValueValidators) Validates(ctx context.Context) diag.Diagnostics { // for protecting against attr.Value type assertion panics. type GenericValueValidator interface { ValueValidator - Validate(context.Context, *tftypes.AttributePath, attr.Value) error + Validate(context.Context, *tftypes.AttributePath, attr.Value) tfprotov6.Diagnostics } // StringValueValidator is an interface type for implementing String value validation. type StringValueValidator interface { ValueValidator - Validate(context.Context, *tftypes.AttributePath, types.String) ValueValidatorError + Validate(context.Context, *tftypes.AttributePath, types.String) tfprotov6.Diagnostics } // StringValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. type StringValueValidatorWithProvider interface { StringValueValidator - ValidateWithProvider(context.Context, tfsdk.Provider, *tftypes.AttributePath, types.String) ValueValidatorError + ValidateWithProvider(context.Context, tfsdk.Provider, *tftypes.AttributePath, types.String) tfprotov6.Diagnostics } type Attribute struct { @@ -1917,22 +1905,26 @@ func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) str return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) } -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) ValueValidatorError { +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) (diags tfprotov6.Diagnostics) { if value.Unknown { - return ValueValidatorUnknownValueError{ - Path: path, - } + diags = append(diags, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unknown validation value", + Details: fmt.Sprintf("received unknown value at path: %s", path), + }) + return } if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - return ValueValidatorUnsuccessfulValidationError{ - Description: v.Description(ctx), - Path: path, - Value: value, - } + diags = append(diags, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Value validation failed", + Details: fmt.Sprintf("%s with value %q %s", path, value.Value, v.Description(ctx)) + }) + return } - return nil + return } func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { @@ -1963,26 +1955,11 @@ schema.Attribute{ Example framework code: ```go -// Well defined error types -type AttributeValidatorError interface {} - -type AttributeValidatorInvalidTypeError interface { - AttributeValidatorError -} - -type AttributeValidatorUnknownValueError interface { - AttributeValidatorError -} - -type AttributeValidatorUnsuccessfulValidationError interface { - AttributeValidatorError -} - // AttributeValidator is an interface type for declaring multiple attribute validations. type AttributeValidator interface { Description(context.Context) string MarkdownDescription(context.Context) string - Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) AttributeValidatorError + Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } // AttributeValidators is a type alias for a slice of AttributeValidator. @@ -1999,14 +1976,14 @@ func (vs AttributeValidators) MarkdownDescriptions(ctx context.Context) []string } // Validates performs all AttributeValidator Validate or ValidateWithProvider -func (vs AttributeValidators) Validates(ctx context.Context) diag.Diagnostics { +func (vs AttributeValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { // ... } // AttributeValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. type AttributeValidatorWithProvider interface { AttributeValidator - ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) AttributeValidatorError + ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } // DataSourceTypeWithAttributeValidators is an interface type that extends DataSourceType to include attribute validations. From 3225b33c67e4a394f215c1292f499d225e61c1e2 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 10:22:29 -0400 Subject: [PATCH 15/35] docs/design: Add high level request/response types to match RPC The flow of the design document will need further updates to better highlight the proposed layering and abstraction concepts. --- docs/design/validation.md | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4307080e6..2935ab089 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -996,6 +996,95 @@ Finally, these other considerations: ## Proposals +### Provider Validation + +At a high level, request and response types will be provided to match the RPC call and for consistency with the rest of the framework: + +```go +type ValidateProviderConfigRequest struct { + Config tfsdk.Config +} + +type ValidateProviderConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} +``` + +An additional interface type will extend to the existing `Provider` type so provider developers can provide customized multiple attribute validation across all attributes: + +```go +type ProviderWithAttributeValidators interface { + Provider + AttributeValidators(context.Context) AttributeValidators +} +``` + +Where `AttributeValidators` is a slice of types to be discussed later that uses or directly implements the request and response types. + +As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: + +```go +func (p *customProvider) AttributeValidators(ctx context.Context) AttributeValidators { + return AttributeValidators{ + CustomAttributeValidator(*tftypes.AttributePath, *tftypes.Attribute), + } +} +``` + +The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. + +### Data Source and Resource Validation + +At a high level, request and response types will be provided to match the RPC calls and with consistency with the rest of the framework: + +```go +type ValidateDataSourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} + +type ValidateDataSourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type ValidateResourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} + +type ValidateResourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} +``` + +An additional interface types will extend to the existing `DataSourceType` and `ResourceType` types so provider developers can provide customized multiple attribute validation across all attributes: + +```go +type DataSourceTypeWithAttributeValidators interface { + DataSourceType + AttributeValidators(context.Context) AttributeValidators +} + +type ResourceTypeWithAttributeValidators interface { + ResourceType + AttributeValidators(context.Context) AttributeValidators +} +``` + +Where `AttributeValidators` is a slice of types to be discussed later that uses or directly implements the request and response types. + +As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: + +```go +func (rt *customResourceType) AttributeValidators(ctx context.Context) AttributeValidators { + return AttributeValidators{ + CustomAttributeValidator(*tftypes.AttributePath, *tftypes.Attribute), + } +} +``` + +The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. + ### Single Attribute Value Validation This validation would be applicable to the `schema.Attribute` types declared within the `GetSchema()` of `DataSourceType`, `Provider`, and `ResourceType` implementations. For most of these proposals, the framework would walk through all attribute paths during the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` calls, executing the declared validation in each attribute if present. @@ -1828,6 +1917,23 @@ Validation functions should be required to return diagnostics similar in design Resource level multiple attribute validation functions should be implemented separately from plan modifications to separate concerns. For example: +### Provider Level Example Implementation + +```go +type ValidateProviderConfigRequest struct { + Config tfsdk.Config +} + +type ValidateProviderConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type ProviderWithAttributeValidators interface { + Provider + AttributeValidators(context.Context) AttributeValidators +} +``` + ### Attribute Level Example Implementation Example framework code: From c552fe439014c7f48590923c96923b0f6a982652 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 10:25:54 -0400 Subject: [PATCH 16/35] docs/design: Rename AttributeValidators to Validators In the future, validators may not only be for attributes. --- docs/design/validation.md | 86 +++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 2935ab089..50e42f3ef 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1013,19 +1013,19 @@ type ValidateProviderConfigResponse struct { An additional interface type will extend to the existing `Provider` type so provider developers can provide customized multiple attribute validation across all attributes: ```go -type ProviderWithAttributeValidators interface { +type ProviderWithValidators interface { Provider - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } ``` -Where `AttributeValidators` is a slice of types to be discussed later that uses or directly implements the request and response types. +Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: ```go -func (p *customProvider) AttributeValidators(ctx context.Context) AttributeValidators { - return AttributeValidators{ +func (p *customProvider) Validators(ctx context.Context) Validators { + return Validators{ CustomAttributeValidator(*tftypes.AttributePath, *tftypes.Attribute), } } @@ -1060,24 +1060,24 @@ type ValidateResourceConfigResponse struct { An additional interface types will extend to the existing `DataSourceType` and `ResourceType` types so provider developers can provide customized multiple attribute validation across all attributes: ```go -type DataSourceTypeWithAttributeValidators interface { +type DataSourceTypeWithValidators interface { DataSourceType - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } -type ResourceTypeWithAttributeValidators interface { +type ResourceTypeWithValidators interface { ResourceType - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } ``` -Where `AttributeValidators` is a slice of types to be discussed later that uses or directly implements the request and response types. +Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: ```go -func (rt *customResourceType) AttributeValidators(ctx context.Context) AttributeValidators { - return AttributeValidators{ +func (rt *customResourceType) Validators(ctx context.Context) Validators { + return Validators{ CustomAttributeValidator(*tftypes.AttributePath, *tftypes.Attribute), } } @@ -1776,29 +1776,29 @@ The [Plan Modifications design documentation](./plan-modifications.md) outlines Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. -##### `AttributeValidators` for Resources +##### `Validators` for Resources This introduces a new extension interface type for `ResourceType` and `DataSourceType`. For example: ```go -type DataSourceTypeWithAttributeValidators interface { +type DataSourceTypeWithValidators interface { DataSourceType - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } -type ResourceTypeWithAttributeValidators interface { +type ResourceTypeWithValidators interface { ResourceType - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } ``` -Where `AttributeValidators` is a slice of types to be discussed later. +Where `Validators` is a slice of types to be discussed later. As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: ```go -func (t *customResourceType) AttributeValidators(ctx context.Context) AttributeValidators { - return AttributeValidators{ +func (t *customResourceType) Validators(ctx context.Context) Validators { + return Validators{ ConflictingAttributes(*tftypes.AttributePath, *tftypes.Attribute), ConflictingAttributesWithValues(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath, ValueValidator), PrerequisiteAttribute(*tftypes.AttributePath, *tftypes.AttributePath), @@ -1813,18 +1813,18 @@ This setup would allow for the framework to provide flexible resource level vali This section includes examples with parameter types as `tftypes.AttributePath` and the `attr.Value` interface type with an return type of `error`. These implementation details are shown for simpler illustrative purposes here, but will likely depend on the outcome from the [Single Attribute Value Validation](#single-attribute-value-validation) proposals. -##### `AttributeValidatorsFunc` Type +##### `ValidatorsFunc` Type A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: ```go -type AttributeValidatorsFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +type ValidatorsFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error ``` To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: ```go -type AttributeValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +type ValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error ``` This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. @@ -1878,11 +1878,11 @@ func ConflictingAttributes(path1 *tftypes.AttributePath, path2 *tftypes.Attribut This helps solve the documentation issue with the following example slice type alias and receiver method: ```go -// AttributeValidators implements iteration functions across AttributeValidator -type AttributeValidators []AttributeValidator +// Validators implements iteration functions across AttributeValidator +type Validators []AttributeValidator // Descriptions returns all AttributeValidator Description -func (vs AttributeValidators) Descriptions(ctx context.Context) []string { +func (vs Validators) Descriptions(ctx context.Context) []string { result := make([]string, 0, len(vs)) for _, v := range vs { @@ -1928,9 +1928,9 @@ type ValidateProviderConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } -type ProviderWithAttributeValidators interface { +type ProviderWithValidators interface { Provider - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } ``` @@ -1988,7 +1988,7 @@ type StringValueValidatorWithProvider interface { type Attribute struct { // ... - PathValidators AttributeValidators // described below + PathValidators Validators // described below ValueValidators ValueValidators } ``` @@ -2047,7 +2047,7 @@ Example provider code: schema.Attribute{ Type: types.StringType, Required: true, - PathValidators: AttributeValidators{ + PathValidators: Validators{ ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), }, ValueValidators: ValueValidators{ @@ -2068,21 +2068,21 @@ type AttributeValidator interface { Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } -// AttributeValidators is a type alias for a slice of AttributeValidator. -type AttributeValidators []AttributeValidator +// Validators is a type alias for a slice of AttributeValidator. +type Validators []AttributeValidator // Descriptions returns all AttributeValidator Description -func (vs AttributeValidators) Descriptions(ctx context.Context) []string { +func (vs Validators) Descriptions(ctx context.Context) []string { // ... } // MarkdownDescriptions returns all AttributeValidator MarkdownDescription -func (vs AttributeValidators) MarkdownDescriptions(ctx context.Context) []string { +func (vs Validators) MarkdownDescriptions(ctx context.Context) []string { // ... } // Validates performs all AttributeValidator Validate or ValidateWithProvider -func (vs AttributeValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { +func (vs Validators) Validates(ctx context.Context) tfprotov6.Diagnostics { // ... } @@ -2092,24 +2092,24 @@ type AttributeValidatorWithProvider interface { ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } -// DataSourceTypeWithAttributeValidators is an interface type that extends DataSourceType to include attribute validations. -type DataSourceTypeWithAttributeValidators interface { +// DataSourceTypeWithValidators is an interface type that extends DataSourceType to include attribute validations. +type DataSourceTypeWithValidators interface { DataSourceType - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } -// ResourceTypeWithAttributeValidators is an interface type that extends ResourceType to include attribute validations. -type ResourceTypeWithAttributeValidators interface { +// ResourceTypeWithValidators is an interface type that extends ResourceType to include attribute validations. +type ResourceTypeWithValidators interface { ResourceType - AttributeValidators(context.Context) AttributeValidators + Validators(context.Context) Validators } ``` Example provider code: ```go -func (t *customResourceType) AttributeValidators(ctx context.Context) AttributeValidators { - return AttributeValidators{ +func (t *customResourceType) Validators(ctx context.Context) Validators { + return Validators{ ConflictingAttributes( tftypes.NewAttributePath().AttributeName("first_attribute"), tftypes.NewAttributePath().AttributeName("second_attribute"), From 98d0700916e90fc9db644f10a9a207a57ab4b266 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 10:27:55 -0400 Subject: [PATCH 17/35] docs/design: Clarify that Resource and Data Source configurations are really the only thing that can be validated currently Terraform currently re-calls ValidateDataSourceConfig and ValidateResourceConfig during plan, not a separate RPC and not a separate logical behavior of the previous framework. --- docs/design/validation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 50e42f3ef..6466f6f5c 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -114,8 +114,8 @@ data "example_thing" "example" { # Defined by the configuration language Terraform supports the following validation for provider implementations: - Provider configurations -- Resource configurations and plans -- Data Source configurations and plans +- Resource configurations (and configurations during plans) +- Data Source configurations (and configurations during plans) Within these, there are two types of validation: @@ -971,8 +971,8 @@ This framework design should strive to accomplish the following with validation Allow provider developers access to all current types of provider validation: - Provider configurations -- Resource configurations and plans -- Data Source configurations and plans +- Resource configurations (and configurations during plans) +- Data Source configurations (and configurations during plans) Including where possible: From 89d77d16473a8d826b9ff69af3a8b8bc1a447713 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 10:31:59 -0400 Subject: [PATCH 18/35] docs/design: Also catch AttributeValidator -> Validator --- docs/design/validation.md | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 6466f6f5c..4f855accc 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1026,7 +1026,7 @@ As an example sketch, provider developers could introduce a function that fulfil ```go func (p *customProvider) Validators(ctx context.Context) Validators { return Validators{ - CustomAttributeValidator(*tftypes.AttributePath, *tftypes.Attribute), + CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), } } ``` @@ -1078,7 +1078,7 @@ As an example sketch, provider developers could introduce a function that fulfil ```go func (rt *customResourceType) Validators(ctx context.Context) Validators { return Validators{ - CustomAttributeValidator(*tftypes.AttributePath, *tftypes.Attribute), + CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), } } ``` @@ -1829,12 +1829,12 @@ type ValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftype This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. -##### `AttributeValidator` Interface +##### `Validator` Interface A new Go interface type could be created that defines an extensible attribute validation function type. For example: ```go -type AttributeValidator interface { +type Validator interface { Description(context.Context) string MarkdownDescription(context.Context) string Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error @@ -1845,7 +1845,7 @@ With an example implementation: ```go type conflictingAttributesValidator struct { - AttributeValidator + Validator path1 *tftypes.AttributePath path2 *tftypes.AttributePath @@ -1878,10 +1878,10 @@ func ConflictingAttributes(path1 *tftypes.AttributePath, path2 *tftypes.Attribut This helps solve the documentation issue with the following example slice type alias and receiver method: ```go -// Validators implements iteration functions across AttributeValidator -type Validators []AttributeValidator +// Validators implements iteration functions across Validator +type Validators []Validator -// Descriptions returns all AttributeValidator Description +// Descriptions returns all Validator Description func (vs Validators) Descriptions(ctx context.Context) []string { result := make([]string, 0, len(vs)) @@ -1895,8 +1895,8 @@ func (vs Validators) Descriptions(ctx context.Context) []string { To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: ```go -type AttributeValidatorWithProvider interface { - AttributeValidator +type ValidatorWithProvider interface { + Validator ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error } ``` @@ -2061,34 +2061,34 @@ schema.Attribute{ Example framework code: ```go -// AttributeValidator is an interface type for declaring multiple attribute validations. -type AttributeValidator interface { +// Validator is an interface type for declaring multiple attribute validations. +type Validator interface { Description(context.Context) string MarkdownDescription(context.Context) string Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } -// Validators is a type alias for a slice of AttributeValidator. -type Validators []AttributeValidator +// Validators is a type alias for a slice of Validator. +type Validators []Validator -// Descriptions returns all AttributeValidator Description +// Descriptions returns all Validator Description func (vs Validators) Descriptions(ctx context.Context) []string { // ... } -// MarkdownDescriptions returns all AttributeValidator MarkdownDescription +// MarkdownDescriptions returns all Validator MarkdownDescription func (vs Validators) MarkdownDescriptions(ctx context.Context) []string { // ... } -// Validates performs all AttributeValidator Validate or ValidateWithProvider +// Validates performs all Validator Validate or ValidateWithProvider func (vs Validators) Validates(ctx context.Context) tfprotov6.Diagnostics { // ... } -// AttributeValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. -type AttributeValidatorWithProvider interface { - AttributeValidator +// ValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. +type ValidatorWithProvider interface { + Validator ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } From acd388e8de61c73cc0bdb4b636211112ee9bc234 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 10:36:39 -0400 Subject: [PATCH 19/35] docs/design: Shift recommendations to flow better --- docs/design/validation.md | 128 ++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4f855accc..4b2af7783 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1917,6 +1917,41 @@ Validation functions should be required to return diagnostics similar in design Resource level multiple attribute validation functions should be implemented separately from plan modifications to separate concerns. For example: +### Validator Example Implementation + +```go +// Validator is an interface type for declaring low level validation functions. +type Validator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics +} + +// Validators is a type alias for a slice of Validator. +type Validators []Validator + +// Descriptions returns all Validator Description +func (vs Validators) Descriptions(ctx context.Context) []string { + // ... +} + +// MarkdownDescriptions returns all Validator MarkdownDescription +func (vs Validators) MarkdownDescriptions(ctx context.Context) []string { + // ... +} + +// Validates performs all Validator Validate or ValidateWithProvider +func (vs Validators) Validates(ctx context.Context) tfprotov6.Diagnostics { + // ... +} + +// ValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. +type ValidatorWithProvider interface { + Validator + ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics +} +``` + ### Provider Level Example Implementation ```go @@ -1934,6 +1969,37 @@ type ProviderWithValidators interface { } ``` +### Resource Level Example Implementation + +Example framework code: + +```go +// DataSourceTypeWithValidators is an interface type that extends DataSourceType to include attribute validations. +type DataSourceTypeWithValidators interface { + DataSourceType + Validators(context.Context) Validators +} + +// ResourceTypeWithValidators is an interface type that extends ResourceType to include attribute validations. +type ResourceTypeWithValidators interface { + ResourceType + Validators(context.Context) Validators +} +``` + +Example provider code: + +```go +func (t *customResourceType) Validators(ctx context.Context) Validators { + return Validators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), + } +} +``` + ### Attribute Level Example Implementation Example framework code: @@ -2056,68 +2122,6 @@ schema.Attribute{ } ``` -### Resource Level Example Implementation - -Example framework code: - -```go -// Validator is an interface type for declaring multiple attribute validations. -type Validator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics -} - -// Validators is a type alias for a slice of Validator. -type Validators []Validator - -// Descriptions returns all Validator Description -func (vs Validators) Descriptions(ctx context.Context) []string { - // ... -} - -// MarkdownDescriptions returns all Validator MarkdownDescription -func (vs Validators) MarkdownDescriptions(ctx context.Context) []string { - // ... -} - -// Validates performs all Validator Validate or ValidateWithProvider -func (vs Validators) Validates(ctx context.Context) tfprotov6.Diagnostics { - // ... -} - -// ValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. -type ValidatorWithProvider interface { - Validator - ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics -} - -// DataSourceTypeWithValidators is an interface type that extends DataSourceType to include attribute validations. -type DataSourceTypeWithValidators interface { - DataSourceType - Validators(context.Context) Validators -} - -// ResourceTypeWithValidators is an interface type that extends ResourceType to include attribute validations. -type ResourceTypeWithValidators interface { - ResourceType - Validators(context.Context) Validators -} -``` - -Example provider code: - -```go -func (t *customResourceType) Validators(ctx context.Context) Validators { - return Validators{ - ConflictingAttributes( - tftypes.NewAttributePath().AttributeName("first_attribute"), - tftypes.NewAttributePath().AttributeName("second_attribute"), - ), - } -} -``` - ### Future Considerations It is recommended that the framework provide an abstracted `*tftypes.AttributePath` rather than depend on that type directly, but this can converted after an initial implementation. This is purely for decoupling the two projects, similar to other abstracted types already created in the framework. From 73fcfb7b5a91d01e0ccf5609965c6118e482ac20 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 10:39:47 -0400 Subject: [PATCH 20/35] docs/design: Ensure resource level example implementation includes new request/response types and split data source vs resource --- docs/design/validation.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 4b2af7783..105ef1f60 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1952,8 +1952,31 @@ type ValidatorWithProvider interface { } ``` +### Data Source Example Implementation + +Example framework code: + +```go +type ValidateDataSourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} + +type ValidateDataSourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +// DataSourceTypeWithValidators is an interface type that extends DataSourceType to include attribute validations. +type DataSourceTypeWithValidators interface { + DataSourceType + Validators(context.Context) Validators +} +``` + ### Provider Level Example Implementation +Example framework code: + ```go type ValidateProviderConfigRequest struct { Config tfsdk.Config @@ -1974,10 +1997,13 @@ type ProviderWithValidators interface { Example framework code: ```go -// DataSourceTypeWithValidators is an interface type that extends DataSourceType to include attribute validations. -type DataSourceTypeWithValidators interface { - DataSourceType - Validators(context.Context) Validators +type ValidateResourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} + +type ValidateResourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic } // ResourceTypeWithValidators is an interface type that extends ResourceType to include attribute validations. From 97cd823b897842fd373944e524e5c0a5d1dca5cc Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 11:24:53 -0400 Subject: [PATCH 21/35] docs/design: Request and response pattern proposal and updated recommendation --- docs/design/validation.md | 379 ++++++++++++++++++++++++++------------ 1 file changed, 259 insertions(+), 120 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 105ef1f60..df3c60609 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1057,16 +1057,16 @@ type ValidateResourceConfigResponse struct { } ``` -An additional interface types will extend to the existing `DataSourceType` and `ResourceType` types so provider developers can provide customized multiple attribute validation across all attributes: +An additional interface types will extend to the existing `DataSource` and `Resource` types so provider developers can provide customized multiple attribute validation across all attributes: ```go -type DataSourceTypeWithValidators interface { - DataSourceType +type DataSourceWithValidators interface { + DataSource Validators(context.Context) Validators } -type ResourceTypeWithValidators interface { - ResourceType +type ResourceWithValidators interface { + Resource Validators(context.Context) Validators } ``` @@ -1076,7 +1076,7 @@ Where `Validators` is a slice of types to be discussed later that uses or direct As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: ```go -func (rt *customResourceType) Validators(ctx context.Context) Validators { +func (r *customResource) Validators(ctx context.Context) Validators { return Validators{ CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), } @@ -1085,6 +1085,142 @@ func (rt *customResourceType) Validators(ctx context.Context) Validators { The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. +### Defining Low Level Validation Functions + +#### `ValidatorsFunc` Type + +A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: + +```go +type ValidatorsFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +``` + +To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: + +```go +type ValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +``` + +This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. + +#### Single `Validator` Interface + +A new Go interface type could be created that defines an extensible attribute validation function type. For example: + +```go +type Validator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error +} +``` + +With an example implementation: + +```go +type conflictingAttributesValidator struct { + Validator + + path1 *tftypes.AttributePath + path2 *tftypes.AttributePath +} + +func (v conflictingAttributesValidator) Description(_ context.Context) string { + return fmt.Sprintf("%s and %s cannot both be configured", v.path1.String(), v.path2.String()) +} + +func (v conflictingAttributesValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("`%s` and `%s` cannot both be configured", v.path1.String(), v.path2.String()) +} + +func (v conflictingAttributesValidator) Validate(ctx context.Context, _ *tftypes.AttributePath, _ attr.Value, _ *tftypes.AttributePath, _ attr.Value) error { + if /* v.path1 configured */ && /* v.path2 configured */ { + return fmt.Errorf("%s", v.Description(ctx)) + } + + return nil +} + +func ConflictingAttributes(path1 *tftypes.AttributePath, path2 *tftypes.AttributePath) conflictingAttributesValidator { + return conflictingAttributesValidator{ + path1: path1, + path2: path1, + } +} +``` + +This helps solve the documentation issue with the following example slice type alias and receiver method: + +```go +// Validators implements iteration functions across Validator +type Validators []Validator + +// Descriptions returns all Validator Description +func (vs Validators) Descriptions(ctx context.Context) []string { + result := make([]string, 0, len(vs)) + + for _, v := range vs { + result = append(result, v.Description(ctx)) + } + return result +} +``` + +To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: + +```go +type ValidatorWithProvider interface { + Validator + ValidateWithProvider(context.Context, tfsdk.Provider, tftypes.AttributePath, attr.Value, tftypes.AttributePath, attr.Value) error +} +``` + +However, this does not align well with the request and response model pattern used throughout the rest of the framework. Interface compatibility breaks if parameters or returns require adjustment for future changes. This single interface could not also handle any potential nuance between provider, data source, and resource validation. + +#### Request and Response Pattern + +The framework could implement the request and response pattern for validation, typed to each RPC. For example: + +```go +type Validator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string +} + +type DataSourceConfigValidator interface { + Validator + ValidateDataSourceConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +type ProviderConfigValidator interface { + Validator + ValidateProviderConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} + +type ResourceConfigValidator interface { + Validator + ValidateResourceConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} +``` + +Provider instances for data sources or resources could be supported by providing further interface types: + +```go +// DataSourceConfigValidator is an interface type for declaring configuration validation that requires a provider instance. +type DataSourceConfigValidatorWithProvider interface { + DataSourceConfigValidator + DataSourceConfigValidatorWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +// ResourceConfigValidator is an interface type for declaring configuration validation that requires a provider instance. +type ResourceConfigValidatorWithProvider interface { + ResourceConfigValidator + ResourceConfigValidatorWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} +``` + +This would provide the lowest level and most customizable option to enable the framework and provider developers to abstract functionality on top. It also ensures compability can be maintained should parameters or returns necessitate changes, while also satisifying the ability for documentation hooks. + ### Single Attribute Value Validation This validation would be applicable to the `schema.Attribute` types declared within the `GetSchema()` of `DataSourceType`, `Provider`, and `ResourceType` implementations. For most of these proposals, the framework would walk through all attribute paths during the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` calls, executing the declared validation in each attribute if present. @@ -1809,98 +1945,6 @@ func (t *customResourceType) Validators(ctx context.Context) Validators { This setup would allow for the framework to provide flexible resource level validation with a low amount of friction for provider developers. Helper functions would be extensible and make the behaviors clear. -#### Defining Attribute Validation Functions - -This section includes examples with parameter types as `tftypes.AttributePath` and the `attr.Value` interface type with an return type of `error`. These implementation details are shown for simpler illustrative purposes here, but will likely depend on the outcome from the [Single Attribute Value Validation](#single-attribute-value-validation) proposals. - -##### `ValidatorsFunc` Type - -A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: - -```go -type ValidatorsFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error -``` - -To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: - -```go -type ValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error -``` - -This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. - -##### `Validator` Interface - -A new Go interface type could be created that defines an extensible attribute validation function type. For example: - -```go -type Validator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error -} -``` - -With an example implementation: - -```go -type conflictingAttributesValidator struct { - Validator - - path1 *tftypes.AttributePath - path2 *tftypes.AttributePath -} - -func (v conflictingAttributesValidator) Description(_ context.Context) string { - return fmt.Sprintf("%s and %s cannot both be configured", v.path1.String(), v.path2.String()) -} - -func (v conflictingAttributesValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("`%s` and `%s` cannot both be configured", v.path1.String(), v.path2.String()) -} - -func (v conflictingAttributesValidator) Validate(ctx context.Context, _ *tftypes.AttributePath, _ attr.Value, _ *tftypes.AttributePath, _ attr.Value) error { - if /* v.path1 configured */ && /* v.path2 configured */ { - return fmt.Errorf("%s", v.Description(ctx)) - } - - return nil -} - -func ConflictingAttributes(path1 *tftypes.AttributePath, path2 *tftypes.AttributePath) conflictingAttributesValidator { - return conflictingAttributesValidator{ - path1: path1, - path2: path1, - } -} -``` - -This helps solve the documentation issue with the following example slice type alias and receiver method: - -```go -// Validators implements iteration functions across Validator -type Validators []Validator - -// Descriptions returns all Validator Description -func (vs Validators) Descriptions(ctx context.Context) []string { - result := make([]string, 0, len(vs)) - - for _, v := range vs { - result = append(result, v.Description(ctx)) - } - return result -} -``` - -To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: - -```go -type ValidatorWithProvider interface { - Validator - ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error -} -``` - ## Recommendations This section will summarize the proposals into specific recommendations for each topic. Code examples are provided in following sections to illustrate the concepts. The final section provides some future considerations for the framework and terraform-plugin-go. @@ -1924,7 +1968,6 @@ Resource level multiple attribute validation functions should be implemented sep type Validator interface { Description(context.Context) string MarkdownDescription(context.Context) string - Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics } // Validators is a type alias for a slice of Validator. @@ -1939,17 +1982,6 @@ func (vs Validators) Descriptions(ctx context.Context) []string { func (vs Validators) MarkdownDescriptions(ctx context.Context) []string { // ... } - -// Validates performs all Validator Validate or ValidateWithProvider -func (vs Validators) Validates(ctx context.Context) tfprotov6.Diagnostics { - // ... -} - -// ValidatorWithProvider is an interface type for declaring multiple attribute validation that requires a provider instance. -type ValidatorWithProvider interface { - Validator - ValidateWithProvider(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) tfprotov6.Diagnostics -} ``` ### Data Source Example Implementation @@ -1966,10 +1998,52 @@ type ValidateDataSourceConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } -// DataSourceTypeWithValidators is an interface type that extends DataSourceType to include attribute validations. -type DataSourceTypeWithValidators interface { +type DataSourceConfigValidator interface { + Validator + ValidateDataSourceConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +// DataSourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. +type DataSourceConfigValidatorWithProvider interface { + DataSourceConfigValidator + ValidateDataSourceConfigWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +// DataSourceConfigValidators is a type alias for a slice of DataSourceConfigValidator. +type DataSourceConfigValidators []Validator + +// Descriptions returns all DataSourceConfigValidator Description +func (vs DataSourceConfigValidators) Descriptions(ctx context.Context) []string { + // ... +} + +// MarkdownDescriptions returns all DataSourceConfigValidator MarkdownDescription +func (vs DataSourceConfigValidators) MarkdownDescriptions(ctx context.Context) []string { + // ... +} + +// Validates performs all DataSourceConfigValidator ValidateDataSourceConfig or ValidateDataSourceConfigWithProvider +func (vs DataSourceConfigValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { + // ... +} + +// DataSourceWithConfigValidators is an interface type that extends DataSource to include validations. +type DataSourceWithConfigValidators interface { DataSourceType - Validators(context.Context) Validators + ConfigValidators(context.Context) DataSourceConfigValidators +} +``` + +Example provider code: + +```go +func (t *customDataSourceType) ConfigValidators(ctx context.Context) DataSourceConfigValidators { + return DataSourceConfigValidators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), + } } ``` @@ -1986,9 +2060,45 @@ type ValidateProviderConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } -type ProviderWithValidators interface { +type ProviderConfigValidator interface { + Validator + ValidateProviderConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} + +// ProviderConfigValidators is a type alias for a slice of ProviderConfigValidator. +type ProviderConfigValidators []Validator + +// Descriptions returns all Validator Description +func (vs ProviderConfigValidators) Descriptions(ctx context.Context) []string { + // ... +} + +// MarkdownDescriptions returns all Validator MarkdownDescription +func (vs ProviderConfigValidators) MarkdownDescriptions(ctx context.Context) []string { + // ... +} + +// Validates performs all Validator ValidateProviderConfig or ValidateProviderConfigWithProvider +func (vs ProviderConfigValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { + // ... +} + +type ProviderWithConfigValidators interface { Provider - Validators(context.Context) Validators + ConfigValidators(context.Context) ProviderConfigValidators +} +``` + +Example provider code: + +```go +func (p *customProvider) ConfigValidators(ctx context.Context) ProviderConfigValidators { + return ProviderConfigValidators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), + } } ``` @@ -2006,18 +2116,47 @@ type ValidateResourceConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } -// ResourceTypeWithValidators is an interface type that extends ResourceType to include attribute validations. -type ResourceTypeWithValidators interface { +type ResourceConfigValidator interface { + Validator + ValidateResourceConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} + +// ResourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. +type ResourceConfigValidatorWithProvider interface { + ResourceConfigValidator + ResourceConfigValidatorWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} + +// ResourceConfigValidators is a type alias for a slice of ResourceConfigValidator. +type ResourceConfigValidators []Validator + +// Descriptions returns all ResourceConfigValidator Description +func (vs ResourceConfigValidators) Descriptions(ctx context.Context) []string { + // ... +} + +// MarkdownDescriptions returns all ResourceConfigValidator MarkdownDescription +func (vs ResourceConfigValidators) MarkdownDescriptions(ctx context.Context) []string { + // ... +} + +// Validates performs all ResourceConfigValidator ValidateResourceConfig or ValidateResourceConfigWithProvider +func (vs ResourceConfigValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { + // ... +} + +// ResourceWithConfigValidators is an interface type that extends ResourceType to include validations. +type ResourceWithConfigValidators interface { ResourceType - Validators(context.Context) Validators + ConfigValidators(context.Context) ResourceConfigValidators } ``` Example provider code: ```go -func (t *customResourceType) Validators(ctx context.Context) Validators { - return Validators{ +func (t *customResourceType) ConfigValidators(ctx context.Context) ResourceConfigValidators { + return ResourceConfigValidators{ ConflictingAttributes( tftypes.NewAttributePath().AttributeName("first_attribute"), tftypes.NewAttributePath().AttributeName("second_attribute"), From 50d28915d008160fe4c36bb80d05253b866c3486 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 16 Jul 2021 11:30:24 -0400 Subject: [PATCH 22/35] docs/design: Ensure ConfigValidators is on DataSource and Resource not DataSourceType and ResourceType --- docs/design/validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index df3c60609..57f7e1499 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -2037,7 +2037,7 @@ type DataSourceWithConfigValidators interface { Example provider code: ```go -func (t *customDataSourceType) ConfigValidators(ctx context.Context) DataSourceConfigValidators { +func (d *customDataSource) ConfigValidators(ctx context.Context) DataSourceConfigValidators { return DataSourceConfigValidators{ ConflictingAttributes( tftypes.NewAttributePath().AttributeName("first_attribute"), @@ -2155,7 +2155,7 @@ type ResourceWithConfigValidators interface { Example provider code: ```go -func (t *customResourceType) ConfigValidators(ctx context.Context) ResourceConfigValidators { +func (r *customResource) ConfigValidators(ctx context.Context) ResourceConfigValidators { return ResourceConfigValidators{ ConflictingAttributes( tftypes.NewAttributePath().AttributeName("first_attribute"), From 69db57c9829a1a9ef8e3130d61b3248e4e5fe480 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 09:01:32 -0400 Subject: [PATCH 23/35] docs/design: Add value validation request/response pattern --- docs/design/validation.md | 124 +++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 57f7e1499..33f4b7f8f 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1177,7 +1177,7 @@ type ValidatorWithProvider interface { However, this does not align well with the request and response model pattern used throughout the rest of the framework. Interface compatibility breaks if parameters or returns require adjustment for future changes. This single interface could not also handle any potential nuance between provider, data source, and resource validation. -#### Request and Response Pattern +#### Validator Request and Response Pattern The framework could implement the request and response pattern for validation, typed to each RPC. For example: @@ -1327,7 +1327,7 @@ type AttributeValueValidatorFunc func(context.Context, provider tfsdk.Provider, While the simplest implementation, this proposal does not allow for documentation hooks. -##### `attr.ValueValidator` Interface +##### Single `attr.ValueValidator` Interface A new Go interface type could be created that defines an extensible value validation function type. For example: @@ -1409,6 +1409,42 @@ type ValueValidatorWithProvider interface { } ``` +However, this does not align well with the request and response model pattern used throughout the rest of the framework. Interface compatibility breaks if parameters or returns require adjustment for future changes. This single interface could not also handle any potential nuance between provider, data source, and resource validation. + +#### Value Validator Request and Response Pattern + +The framework could implement the request and response pattern for value validation. For example: + +```go +type ValidateValueRequest struct { + AttributePath tftypes.AttributePath + Config attr.Value +} + +type ValidateValueResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type ValueValidator interface { + Validator + ValidateValue(context.Context, ValidateValueRequest, *ValidateValueResponse) +} +``` + +Provider instances for data sources or resources could be supported by providing further interface types: + +```go +// ValueValidator is an interface type for declaring configuration value validation that requires a provider instance. +type ValueValidatorWithProvider interface { + Validator + ValueValidatorWithProvider(context.Context, tfsdk.Provider, ValidateValueRequest, *ValidateValueResponse) +} +``` + +This would provide the lowest level and most customizable option to enable the framework and provider developers to abstract functionality on top. It also ensures compability can be maintained should parameters or returns necessitate changes, while also satisifying the ability for documentation hooks. + +One caveat to this single type is that it would not capture any nuance between data sources, providers, and resources should their RPCs provide differing functionality in the future. + #### Attribute Value Validation Function Value Parameter Regardless the choice of concrete or interface types for the value validation functions, the parameters and returns for the implementations will play a crucial role on the extensibility and development experience. @@ -2066,7 +2102,7 @@ type ProviderConfigValidator interface { } // ProviderConfigValidators is a type alias for a slice of ProviderConfigValidator. -type ProviderConfigValidators []Validator +type ProviderConfigValidators []ProviderConfigValidator // Descriptions returns all Validator Description func (vs ProviderConfigValidators) Descriptions(ctx context.Context) []string { @@ -2128,7 +2164,7 @@ type ResourceConfigValidatorWithProvider interface { } // ResourceConfigValidators is a type alias for a slice of ResourceConfigValidator. -type ResourceConfigValidators []Validator +type ResourceConfigValidators []ResourceConfigValidator // Descriptions returns all ResourceConfigValidator Description func (vs ResourceConfigValidators) Descriptions(ctx context.Context) []string { @@ -2170,57 +2206,51 @@ func (r *customResource) ConfigValidators(ctx context.Context) ResourceConfigVal Example framework code: ```go -// ValueValidator is an interface type for implementing common validation functionality. -type ValueValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string -} - -// ValueValidators is a type alias for a slice of ValueValidator. -type ValueValidators []ValueValidator - -// Descriptions returns all ValueValidator Description -func (vs ValueValidators) Descriptions(ctx context.Context) []string { +type Attribute struct { // ... + Validators Validators } -// MarkdownDescriptions returns all ValueValidator MarkdownDescription -func (vs ValueValidators) MarkdownDescriptions(ctx context.Context) []string { - // ... +type ValidateValueRequest struct { + AttributePath tftypes.AttributePath + Config attr.Value } -// Validates performs all ValueValidator Validate or ValidateWithProvider -func (vs ValueValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { - // ... +type ValidateValueResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -// GenericValueValidator describes value validation without a strong type. +// ValueValidator describes value validation without a strong type. // // While it is generally preferred to use the typed validation interfaces, // such as StringValueValidator, this interface allows custom implementations -// where the others may not be suitable. The Validate function is responsible -// for protecting against attr.Value type assertion panics. -type GenericValueValidator interface { - ValueValidator - Validate(context.Context, *tftypes.AttributePath, attr.Value) tfprotov6.Diagnostics +// where the others may not be suitable. +type ValueValidator interface { + Validator + ValidateValue(context.Context, ValidateValueRequest, *ValidateValueResponse) +} + +// ValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. +type ValueValidatorWithProvider interface { + Validator + ValidateValueWithProvider(context.Context, tfsdk.Provider, ValidateValueRequest, *ValidateValueResponse) +} + +type ValidateStringValueRequest struct { + AttributePath tftypes.AttributePath + Config types.String } // StringValueValidator is an interface type for implementing String value validation. type StringValueValidator interface { - ValueValidator - Validate(context.Context, *tftypes.AttributePath, types.String) tfprotov6.Diagnostics + Validator + ValidateValue(context.Context, ValidateStringValueRequest, *ValidateValueResponse) } // StringValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. type StringValueValidatorWithProvider interface { StringValueValidator - ValidateWithProvider(context.Context, tfsdk.Provider, *tftypes.AttributePath, types.String) tfprotov6.Diagnostics -} - -type Attribute struct { - // ... - PathValidators Validators // described below - ValueValidators ValueValidators + ValidateValueWithProvider(context.Context, tfsdk.Provider, ValidateStringValueRequest, *ValidateStringValueResponse) } ``` @@ -2242,21 +2272,21 @@ func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) str return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) } -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) (diags tfprotov6.Diagnostics) { - if value.Unknown { - diags = append(diags, &tfprotov6.Diagnostic{ +func (v stringLengthBetweenValidator) ValidateValue(ctx context.Context, req ValidateStringValueRequest, resp *ValidateValueResponse) { + if req.Config.Unknown { + resp.diags = append(resp.diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unknown validation value", - Details: fmt.Sprintf("received unknown value at path: %s", path), + Details: fmt.Sprintf("received unknown value at path: %s", req.Config.AttributePath), }) return } - if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - diags = append(diags, &tfprotov6.Diagnostic{ + if len(req.Config.Value) < v.minimum || len(req.Config.Value) > v.maximum { + resp.diags = append(resp.diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Value validation failed", - Details: fmt.Sprintf("%s with value %q %s", path, value.Value, v.Description(ctx)) + Details: fmt.Sprintf("%s with value %q %s", req.Config.AttributePath, req.Config.Value, v.Description(ctx)) }) return } @@ -2276,12 +2306,10 @@ Example provider code: ```go schema.Attribute{ - Type: types.StringType, - Required: true, - PathValidators: Validators{ + Type: types.StringType, + Required: true, + Validators: Validators{ ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), - }, - ValueValidators: ValueValidators{ StringLengthBetween(1, 256), }, } From 55ec609e92f7de4302792996249b806c62084503 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 09:04:18 -0400 Subject: [PATCH 24/35] docs/design: resp.Diagnostics not resp.diags --- docs/design/validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 33f4b7f8f..a09ac9fd5 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -2274,7 +2274,7 @@ func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) str func (v stringLengthBetweenValidator) ValidateValue(ctx context.Context, req ValidateStringValueRequest, resp *ValidateValueResponse) { if req.Config.Unknown { - resp.diags = append(resp.diags, &tfprotov6.Diagnostic{ + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unknown validation value", Details: fmt.Sprintf("received unknown value at path: %s", req.Config.AttributePath), @@ -2283,7 +2283,7 @@ func (v stringLengthBetweenValidator) ValidateValue(ctx context.Context, req Val } if len(req.Config.Value) < v.minimum || len(req.Config.Value) > v.maximum { - resp.diags = append(resp.diags, &tfprotov6.Diagnostic{ + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Value validation failed", Details: fmt.Sprintf("%s with value %q %s", req.Config.AttributePath, req.Config.Value, v.Description(ctx)) From 33c80ec847f46755ba65fc7323b74b07c9b7f446 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 09:16:47 -0400 Subject: [PATCH 25/35] docs/design: Reintroduce ValueValidator as a top level interface so helpers can pare down types if necessary. --- docs/design/validation.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index a09ac9fd5..49fce5f8e 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -2220,19 +2220,24 @@ type ValidateValueResponse struct { Diagnostics []*tfprotov6.Diagnostic } -// ValueValidator describes value validation without a strong type. +// ValueValidator is an interface for all value validation functionality. +type ValueValidator interface { + Validator +} + +// GenericValueValidator describes value validation without a strong type. // // While it is generally preferred to use the typed validation interfaces, // such as StringValueValidator, this interface allows custom implementations // where the others may not be suitable. -type ValueValidator interface { - Validator +type GenericValueValidator interface { + ValueValidator ValidateValue(context.Context, ValidateValueRequest, *ValidateValueResponse) } -// ValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. -type ValueValidatorWithProvider interface { - Validator +// GenericValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. +type GenericValueValidatorWithProvider interface { + ValueValidator ValidateValueWithProvider(context.Context, tfsdk.Provider, ValidateValueRequest, *ValidateValueResponse) } @@ -2243,7 +2248,7 @@ type ValidateStringValueRequest struct { // StringValueValidator is an interface type for implementing String value validation. type StringValueValidator interface { - Validator + ValueValidator ValidateValue(context.Context, ValidateStringValueRequest, *ValidateValueResponse) } From 0cafd66ef0b469300a495b86cb7ca826c9f205ca Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 09:51:04 -0400 Subject: [PATCH 26/35] docs/design: Additional note for validation functions during plan modifications would introduce special framework logic --- docs/design/validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 49fce5f8e..fe1d61340 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1946,7 +1946,7 @@ In the previous framework, the `CustomizeDiff` functionality enabled resource (o The [Plan Modifications design documentation](./plan-modifications.md) outlines proposals which broadly replace the previous framework's `CustomizeDiff` functionality. See that documentation for considerations and recommendations there. In this proposal for validation, new functions for validation would be provided within that framework, rather than introducing separate handling. -Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. +Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. Another wrinkle is that plan modifications are only intended to run during `terraform plan` (`PlanResourceChanges` RPC) and `terraform apply` (`ApplyResourceChanges` RPC), so the framework would be introducing its own additional logic to extract and perform any validation functions during the `terraform validate` (`ValidateDataSourceConfig`/`ValidateProviderConfig`/`ValidateResourceConfig` RPCs). ##### `Validators` for Resources From 7737c9f960ec15330b680323c164f34d41548946 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 13:02:02 -0400 Subject: [PATCH 27/35] docs/design: Simplify and clarify validation interfaces by removing Validator, remove slice alias types, punt stronger value typing until later --- docs/design/validation.md | 248 +++++++++++++------------------------- 1 file changed, 82 insertions(+), 166 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index fe1d61340..f4f8f6962 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1182,24 +1182,22 @@ However, this does not align well with the request and response model pattern us The framework could implement the request and response pattern for validation, typed to each RPC. For example: ```go -type Validator interface { +type DataSourceConfigValidator interface { Description(context.Context) string MarkdownDescription(context.Context) string -} - -type DataSourceConfigValidator interface { - Validator - ValidateDataSourceConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) + Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) } type ProviderConfigValidator interface { - Validator - ValidateProviderConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) } type ResourceConfigValidator interface { - Validator - ValidateResourceConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } ``` @@ -1208,14 +1206,16 @@ Provider instances for data sources or resources could be supported by providing ```go // DataSourceConfigValidator is an interface type for declaring configuration validation that requires a provider instance. type DataSourceConfigValidatorWithProvider interface { - DataSourceConfigValidator - DataSourceConfigValidatorWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) + Description(context.Context) string + MarkdownDescription(context.Context) string + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) } // ResourceConfigValidator is an interface type for declaring configuration validation that requires a provider instance. type ResourceConfigValidatorWithProvider interface { - ResourceConfigValidator - ResourceConfigValidatorWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) + Description(context.Context) string + MarkdownDescription(context.Context) string + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } ``` @@ -1411,33 +1411,40 @@ type ValueValidatorWithProvider interface { However, this does not align well with the request and response model pattern used throughout the rest of the framework. Interface compatibility breaks if parameters or returns require adjustment for future changes. This single interface could not also handle any potential nuance between provider, data source, and resource validation. -#### Value Validator Request and Response Pattern +#### Attribute Validator Request and Response Pattern -The framework could implement the request and response pattern for value validation. For example: +The framework could implement the request and response pattern for attribute validation. For example: ```go -type ValidateValueRequest struct { +type ValidateAttributeRequest struct { + // AttributePath contains the path of the attribute. AttributePath tftypes.AttributePath - Config attr.Value + + // AttributeConfig contains the value of the attribute. + AttributeConfig attr.Value + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config } -type ValidateValueResponse struct { +type ValidateAttributeResponse struct { Diagnostics []*tfprotov6.Diagnostic } -type ValueValidator interface { - Validator - ValidateValue(context.Context, ValidateValueRequest, *ValidateValueResponse) +type AttributeValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) } ``` Provider instances for data sources or resources could be supported by providing further interface types: ```go -// ValueValidator is an interface type for declaring configuration value validation that requires a provider instance. -type ValueValidatorWithProvider interface { - Validator - ValueValidatorWithProvider(context.Context, tfsdk.Provider, ValidateValueRequest, *ValidateValueResponse) +// AttributeValidator is an interface type for declaring attribute validation that requires a provider instance. +type AttributeValidatorWithProvider interface { + AttributeValidator + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateAttributeRequest, *ValidateAttributeResponse) } ``` @@ -1987,38 +1994,9 @@ This section will summarize the proposals into specific recommendations for each ### Overview -Defining all validation functionality via interface types will offer the framework the most flexibility for future enhancements while ensuring consistent implementations. Furthermore for attribute value validation functions, providing strongly typed interfaces for common value types will reduce implementor burden and ensure consistent invalid type error messaging, rather than potential panic scenarios. - -Attribute value validations should be implemented as a slice of the interface type on `schema.Attribute`. Multiple attribute validation, such as declaring conflicting attributes on attributes themselves, should be implemented as a separate slice of that differing interface type on `schema.Attribute`. This would be in addition to supporting that functionality with resource level multiple attribute validation. - -Attribute value validations should be required to accept the attribute path in its native type as a parameter. This will allow a flexible implementation for provider developers that may desire advanced logic based on the path. - -Validation functions should be required to return diagnostics similar in design to other functionality in the framework. Helper functions could help return these results in a consistent manner. - -Resource level multiple attribute validation functions should be implemented separately from plan modifications to separate concerns. For example: - -### Validator Example Implementation +Defining all validation functionality via interface types will offer the framework the most flexibility for future enhancements while ensuring consistent implementations. The request and response pattern should be used to enable backwards (in the case of field deprecations) and forwards compatibility. -```go -// Validator is an interface type for declaring low level validation functions. -type Validator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string -} - -// Validators is a type alias for a slice of Validator. -type Validators []Validator - -// Descriptions returns all Validator Description -func (vs Validators) Descriptions(ctx context.Context) []string { - // ... -} - -// MarkdownDescriptions returns all Validator MarkdownDescription -func (vs Validators) MarkdownDescriptions(ctx context.Context) []string { - // ... -} -``` +All validation should be implemented separately from plan modifications as they address differing concerns and operations within the Terraform. Attribute validations should be implemented as a slice of the interface type on `schema.Attribute` while Data Source, Provider, and Resource level validation should be implemented as new extension interface types. Further helper functions and designs can reduce implementation details. ### Data Source Example Implementation @@ -2035,38 +2013,21 @@ type ValidateDataSourceConfigResponse struct { } type DataSourceConfigValidator interface { - Validator - ValidateDataSourceConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) } // DataSourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. type DataSourceConfigValidatorWithProvider interface { DataSourceConfigValidator - ValidateDataSourceConfigWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) -} - -// DataSourceConfigValidators is a type alias for a slice of DataSourceConfigValidator. -type DataSourceConfigValidators []Validator - -// Descriptions returns all DataSourceConfigValidator Description -func (vs DataSourceConfigValidators) Descriptions(ctx context.Context) []string { - // ... -} - -// MarkdownDescriptions returns all DataSourceConfigValidator MarkdownDescription -func (vs DataSourceConfigValidators) MarkdownDescriptions(ctx context.Context) []string { - // ... -} - -// Validates performs all DataSourceConfigValidator ValidateDataSourceConfig or ValidateDataSourceConfigWithProvider -func (vs DataSourceConfigValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { - // ... + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) } // DataSourceWithConfigValidators is an interface type that extends DataSource to include validations. type DataSourceWithConfigValidators interface { DataSourceType - ConfigValidators(context.Context) DataSourceConfigValidators + ConfigValidators(context.Context) []DataSourceConfigValidator } ``` @@ -2097,31 +2058,14 @@ type ValidateProviderConfigResponse struct { } type ProviderConfigValidator interface { - Validator - ValidateProviderConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) -} - -// ProviderConfigValidators is a type alias for a slice of ProviderConfigValidator. -type ProviderConfigValidators []ProviderConfigValidator - -// Descriptions returns all Validator Description -func (vs ProviderConfigValidators) Descriptions(ctx context.Context) []string { - // ... -} - -// MarkdownDescriptions returns all Validator MarkdownDescription -func (vs ProviderConfigValidators) MarkdownDescriptions(ctx context.Context) []string { - // ... -} - -// Validates performs all Validator ValidateProviderConfig or ValidateProviderConfigWithProvider -func (vs ProviderConfigValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { - // ... + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) } type ProviderWithConfigValidators interface { Provider - ConfigValidators(context.Context) ProviderConfigValidators + ConfigValidators(context.Context) []ProviderConfigValidator } ``` @@ -2153,38 +2097,21 @@ type ValidateResourceConfigResponse struct { } type ResourceConfigValidator interface { - Validator - ValidateResourceConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } // ResourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. type ResourceConfigValidatorWithProvider interface { ResourceConfigValidator - ResourceConfigValidatorWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) -} - -// ResourceConfigValidators is a type alias for a slice of ResourceConfigValidator. -type ResourceConfigValidators []ResourceConfigValidator - -// Descriptions returns all ResourceConfigValidator Description -func (vs ResourceConfigValidators) Descriptions(ctx context.Context) []string { - // ... -} - -// MarkdownDescriptions returns all ResourceConfigValidator MarkdownDescription -func (vs ResourceConfigValidators) MarkdownDescriptions(ctx context.Context) []string { - // ... -} - -// Validates performs all ResourceConfigValidator ValidateResourceConfig or ValidateResourceConfigWithProvider -func (vs ResourceConfigValidators) Validates(ctx context.Context) tfprotov6.Diagnostics { - // ... + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } -// ResourceWithConfigValidators is an interface type that extends ResourceType to include validations. +// ResourceWithConfigValidators is an interface type that extends Resource to include validations. type ResourceWithConfigValidators interface { ResourceType - ConfigValidators(context.Context) ResourceConfigValidators + ConfigValidators(context.Context) []ResourceConfigValidator } ``` @@ -2206,56 +2133,32 @@ func (r *customResource) ConfigValidators(ctx context.Context) ResourceConfigVal Example framework code: ```go -type Attribute struct { - // ... - Validators Validators -} - -type ValidateValueRequest struct { +type ValidateAttributeRequest struct { + // AttributePath contains the path of the attribute. AttributePath tftypes.AttributePath - Config attr.Value -} - -type ValidateValueResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} -// ValueValidator is an interface for all value validation functionality. -type ValueValidator interface { - Validator -} - -// GenericValueValidator describes value validation without a strong type. -// -// While it is generally preferred to use the typed validation interfaces, -// such as StringValueValidator, this interface allows custom implementations -// where the others may not be suitable. -type GenericValueValidator interface { - ValueValidator - ValidateValue(context.Context, ValidateValueRequest, *ValidateValueResponse) -} + // AttributeConfig contains the value of the attribute. + AttributeConfig attr.Value -// GenericValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. -type GenericValueValidatorWithProvider interface { - ValueValidator - ValidateValueWithProvider(context.Context, tfsdk.Provider, ValidateValueRequest, *ValidateValueResponse) + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config } -type ValidateStringValueRequest struct { - AttributePath tftypes.AttributePath - Config types.String +type ValidateAttributeResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -// StringValueValidator is an interface type for implementing String value validation. -type StringValueValidator interface { - ValueValidator - ValidateValue(context.Context, ValidateStringValueRequest, *ValidateValueResponse) +// AttributeValidator describes attribute validation functionality. +type AttributeValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) } -// StringValueValidatorWithProvider is an interface type for implementing String value validation with a provider instance. -type StringValueValidatorWithProvider interface { - StringValueValidator - ValidateValueWithProvider(context.Context, tfsdk.Provider, ValidateStringValueRequest, *ValidateStringValueResponse) +// Existing schema.Attribute struct type +type Attribute struct { + // ... + Validators []AttributeValidator } ``` @@ -2277,7 +2180,18 @@ func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) str return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) } -func (v stringLengthBetweenValidator) ValidateValue(ctx context.Context, req ValidateStringValueRequest, resp *ValidateValueResponse) { +func (v stringLengthBetweenValidator) Validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + value, ok := req.AttributeConfig.(types.String) // see also attr.ValueAs() proposal + + if !ok { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid value type", + Details: fmt.Sprintf("received incorrect value type (%T) at path: %s", req.AttributeConfig, req.Config.AttributePath), + }) + return + } + if req.Config.Unknown { resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, @@ -2313,7 +2227,7 @@ Example provider code: schema.Attribute{ Type: types.StringType, Required: true, - Validators: Validators{ + Validators: []AttributeValidator{ ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), StringLengthBetween(1, 256), }, @@ -2331,3 +2245,5 @@ It is recommended that the framework or the upstream terraform-plugin-go module ```go NewAttributePath(CurrentPath().Parent().AttributeName("other_attr")) ``` + +Strongly typed attribute validation can be introduced to simplify implementations for common value types, such as `types.String`. Future designs can discuss the potential designs and tradeoffs. From e9165b74f7a7dcddf981e55b6f4e70c27ad04983 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 13:24:26 -0400 Subject: [PATCH 28/35] docs/design: Example code comments and imperative interfaces --- docs/design/validation.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index f4f8f6962..ba0fcb152 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -2003,15 +2003,18 @@ All validation should be implemented separately from plan modifications as they Example framework code: ```go +// ValidateDataSourceConfigRequest contains request information from the ValidateDataSourceConfig RPC. type ValidateDataSourceConfigRequest struct { Config tfsdk.Config TypeName string } +// ValidateDataSourceConfigResponse contains request information for the ValidateDataSourceConfig RPC. type ValidateDataSourceConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } +// DataSourceConfigValidator describes a reusable Data Source configuration validation function. type DataSourceConfigValidator interface { Description(context.Context) string MarkdownDescription(context.Context) string @@ -2024,11 +2027,17 @@ type DataSourceConfigValidatorWithProvider interface { ValidateWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) } -// DataSourceWithConfigValidators is an interface type that extends DataSource to include validations. +// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. type DataSourceWithConfigValidators interface { - DataSourceType + DataSource ConfigValidators(context.Context) []DataSourceConfigValidator } + +// DataSourceWithValidateConfig is an interface type that extends DataSource to include imperative validation. +type DataSourceWithValidateConfig interface { + DataSource + ValidateConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} ``` Example provider code: @@ -2049,24 +2058,34 @@ func (d *customDataSource) ConfigValidators(ctx context.Context) DataSourceConfi Example framework code: ```go +// ValidateProviderConfigRequest contains request information from the ValidateProviderConfig RPC. type ValidateProviderConfigRequest struct { Config tfsdk.Config } +// ValidateProviderConfigResponse contains request information for the ValidateProviderConfig RPC. type ValidateProviderConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } +// ProviderConfigValidator describes a reusable Provider configuration validation function. type ProviderConfigValidator interface { Description(context.Context) string MarkdownDescription(context.Context) string Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) } +// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. type ProviderWithConfigValidators interface { Provider ConfigValidators(context.Context) []ProviderConfigValidator } + +// ProviderWithValidateConfig is an interface type that extends Provider to include imperative validation. +type ProviderWithValidateConfig interface { + Provider + ValidateConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} ``` Example provider code: @@ -2087,15 +2106,18 @@ func (p *customProvider) ConfigValidators(ctx context.Context) ProviderConfigVal Example framework code: ```go +// ValidateResourceConfigRequest contains request information from the ValidateResourceConfig RPC. type ValidateResourceConfigRequest struct { Config tfsdk.Config TypeName string } +// ValidateResourceConfigResponse contains request information for the ValidateResourceConfig RPC. type ValidateResourceConfigResponse struct { Diagnostics []*tfprotov6.Diagnostic } +// ResourceConfigValidator describes a reusable Resource configuration validation function. type ResourceConfigValidator interface { Description(context.Context) string MarkdownDescription(context.Context) string @@ -2108,11 +2130,17 @@ type ResourceConfigValidatorWithProvider interface { ValidateWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } -// ResourceWithConfigValidators is an interface type that extends Resource to include validations. +// ResourceWithConfigValidators is an interface type that extends Resource to include declarative validations. type ResourceWithConfigValidators interface { - ResourceType + Resource ConfigValidators(context.Context) []ResourceConfigValidator } + +// ResourceWithValidateConfig is an interface type that extends Resource to include imperative validations. +type ResourceWithValidateConfig interface { + Resource + ValidateConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} ``` Example provider code: From a6e374e819f79697131808210f3cc6fd9d0a411e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 15:28:31 -0400 Subject: [PATCH 29/35] docs/design: Rework proposal sections and create appendix for clarity --- docs/design/validation.md | 1345 ++++++++++++++++++------------------- 1 file changed, 669 insertions(+), 676 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index ba0fcb152..5d54e62cb 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -996,238 +996,227 @@ Finally, these other considerations: ## Proposals -### Provider Validation +### Typed Parameters Versus Request and Response Types -At a high level, request and response types will be provided to match the RPC call and for consistency with the rest of the framework: +#### Typed Parameters -```go -type ValidateProviderConfigRequest struct { - Config tfsdk.Config -} - -type ValidateProviderConfigResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} -``` - -An additional interface type will extend to the existing `Provider` type so provider developers can provide customized multiple attribute validation across all attributes: +The framework could implement bespoke error, path, and value types in function parameters and returns. For example: ```go -type ProviderWithValidators interface { - Provider - Validators(context.Context) Validators -} +func(context.Context, path *tftypes.AttributePath, value attr.Value) error ``` -Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. +While very explictly defining function signatures specific to the validation concepts as they are understood today (such as validation currently only being against configuration) to potentially make testing and implementation details easier, this presents future compatibility concerns. Any changes or additions would require breaking changes. Any semantic differences about the context of a call when it reaches the function cannot be captured. The rest of the framework has opted for a request and response type pattern to handle these concerns, where choosing typed parameters here does not seem to provide much benefit to have a separate implementation. -As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: +#### Request and Response Types + +The framework could implement the request and response pattern for validation, typed to each RPC. For example: ```go -func (p *customProvider) Validators(ctx context.Context) Validators { - return Validators{ - CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), - } -} +func(context.Context, ValidateRequest, *ValidateResponse) ``` -The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. - -### Data Source and Resource Validation +Ease of implementation and testing is slightly reduced because of the wrapper types, however compatibility is more guaranteed. The framework can signal smaller deprecations and implement underlying migrations if necessary. Each request and response can be tailed to the exact context and functionality available at the time. -At a high level, request and response types will be provided to match the RPC calls and with consistency with the rest of the framework: +Examples in the rest of the proposals will prefer this style where appropriate. -```go -type ValidateDataSourceConfigRequest struct { - Config tfsdk.Config - TypeName string -} +### Validation Function Types Versus Interfaces -type ValidateDataSourceConfigResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} +#### Validation Function Type -type ValidateResourceConfigRequest struct { - Config tfsdk.Config - TypeName string -} +New Go type(s) could be created that define the signature of a validation function, similar to the previous framework `SchemaValidateFunc`. For example: -type ValidateResourceConfigResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} +```go +type ValidationFunc func(context.Context, ValidationRequest, *ValidationResponse) ``` -An additional interface types will extend to the existing `DataSource` and `Resource` types so provider developers can provide customized multiple attribute validation across all attributes: +To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: ```go -type DataSourceWithValidators interface { - DataSource - Validators(context.Context) Validators -} - -type ResourceWithValidators interface { - Resource - Validators(context.Context) Validators -} +type ValidationFunc func(context.Context, provider tfsdk.Provider, ValidationRequest, *ValidationResponse) ``` -Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. +The main drawback of this approach is that it does not allow for documentation hooks. This also drifts from other design decisions of the framework without providing much benefit for the differing implementation. -As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: +#### Interfaces + +New Go interface type(s) could be created that require additional implementation details for validation functions. For example: ```go -func (r *customResource) Validators(ctx context.Context) Validators { - return Validators{ - CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), - } +type Validator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateRequest, *ValidateResponse) } ``` -The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. +This would provide the lowest level and most customizable option to enable the framework and provider developers to abstract functionality on top. It also ensures compability can be maintained should parameters or returns necessitate changes, while also satisifying the ability for documentation hooks. Many other pieces of the framework prefer this design. -### Defining Low Level Validation Functions +Examples in the rest of the proposals will prefer this style where appropriate. -#### `ValidatorsFunc` Type +### Data Source, Provider, and Resource Level Validation -A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: +#### Single Interface Versus Typed Interfaces -```go -type ValidatorsFunc func(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error -``` +##### Single Interface -To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: +The framework can introduce a single interface across `DataSource`, `Provider`, and `Resource` validation. For example: ```go -type ValidatorsFunc func(context.Context, provider tfsdk.Provider, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error -``` - -This proposal does not allow for documentation hooks. It could be confusing for implementors as they could be responsible for more complex validation logic or provider developers if many iterations of validation are implemented across many different functions since each would be unique. It might be possible to reduce this burden by passing in a `ValueValidator` as well. - -#### Single `Validator` Interface +type ValidateRequest struct { + Config tfsdk.Config +} -A new Go interface type could be created that defines an extensible attribute validation function type. For example: +type ValidateResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} -```go type Validator interface { Description(context.Context) string MarkdownDescription(context.Context) string - Validate(context.Context, path1 *tftypes.AttributePath, value1 attr.Value, path2 *tftypes.AttributePath, value2 attr.Value) error + Validate(context.Context, ValidateRequest, *ValidateResponse) } ``` -With an example implementation: +While simpler for implementations that are generic across `DataSource`, `Provider`, and `Resource` types, such as a function for declaring conflicting attribute paths in configurations, details associated with the underlying `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` RPC calls are lost. If future enhancements are type specific, request and response types may not be fully compatible introducing additional non-compiler rules that provider developers must follow. + +##### Typed Interfaces + +The framework can introduce interfaces to match the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` RPC calls. For example: ```go -type conflictingAttributesValidator struct { - Validator +type ValidateDataSourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} - path1 *tftypes.AttributePath - path2 *tftypes.AttributePath +type ValidateDataSourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -func (v conflictingAttributesValidator) Description(_ context.Context) string { - return fmt.Sprintf("%s and %s cannot both be configured", v.path1.String(), v.path2.String()) +type ValidateProviderConfigRequest struct { + Config tfsdk.Config } -func (v conflictingAttributesValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("`%s` and `%s` cannot both be configured", v.path1.String(), v.path2.String()) +type ValidateProviderConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -func (v conflictingAttributesValidator) Validate(ctx context.Context, _ *tftypes.AttributePath, _ attr.Value, _ *tftypes.AttributePath, _ attr.Value) error { - if /* v.path1 configured */ && /* v.path2 configured */ { - return fmt.Errorf("%s", v.Description(ctx)) - } +type ValidateResourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} - return nil +type ValidateResourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -func ConflictingAttributes(path1 *tftypes.AttributePath, path2 *tftypes.AttributePath) conflictingAttributesValidator { - return conflictingAttributesValidator{ - path1: path1, - path2: path1, - } +type DataSourceConfigValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + +type ProviderConfigValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} + +type ResourceConfigValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } ``` -This helps solve the documentation issue with the following example slice type alias and receiver method: +This will ensure that all features are compiler-checked for each validation request and response. + +#### Imperative Versus Declarative + +##### Imperative + +An additional interface type can extend the existing `Provider` type so provider developers can enable advanced validation imperatively: ```go -// Validators implements iteration functions across Validator -type Validators []Validator +type ProviderWithValidate interface { + Provider + Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} +``` -// Descriptions returns all Validator Description -func (vs Validators) Descriptions(ctx context.Context) []string { - result := make([]string, 0, len(vs)) +This would enable simpler inline validation function creation as other proposals could require additional interface methods to be fulfilled. Documentation hooks are not provided here, instead relying on provider developers to include that information inline. Reusability is possible, however the implementation details are more complicated for provider developers. - for _, v := range vs { - result = append(result, v.Description(ctx)) - } - return result +##### Declarative + +An additional interface type can extend the existing `Provider` type so provider developers can enable advanced validation declaratively: + +```go +type ProviderWithValidators interface { + Provider + Validators(context.Context) Validators } ``` -To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: +Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. + +As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: ```go -type ValidatorWithProvider interface { - Validator - ValidateWithProvider(context.Context, tfsdk.Provider, tftypes.AttributePath, attr.Value, tftypes.AttributePath, attr.Value) error +func (p *customProvider) Validators(ctx context.Context) Validators { + return Validators{ + CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), + } } ``` -However, this does not align well with the request and response model pattern used throughout the rest of the framework. Interface compatibility breaks if parameters or returns require adjustment for future changes. This single interface could not also handle any potential nuance between provider, data source, and resource validation. +This declarative pattern enables reusable functions and built-in documentation for future enhancements. It is also consistent with proposed attribute level validations. -#### Validator Request and Response Pattern +### Data Source and Resource Validation -The framework could implement the request and response pattern for validation, typed to each RPC. For example: +At a high level, request and response types will be provided to match the RPC calls and with consistency with the rest of the framework: ```go -type DataSourceConfigValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) -} -type ProviderConfigValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) -} -type ResourceConfigValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) -} + ``` -Provider instances for data sources or resources could be supported by providing further interface types: +An additional interface types will extend to the existing `DataSource` and `Resource` types so provider developers can provide customized multiple attribute validation across all attributes: ```go -// DataSourceConfigValidator is an interface type for declaring configuration validation that requires a provider instance. -type DataSourceConfigValidatorWithProvider interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - ValidateWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +type DataSourceWithValidators interface { + DataSource + Validators(context.Context) Validators } -// ResourceConfigValidator is an interface type for declaring configuration validation that requires a provider instance. -type ResourceConfigValidatorWithProvider interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - ValidateWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +type ResourceWithValidators interface { + Resource + Validators(context.Context) Validators } ``` -This would provide the lowest level and most customizable option to enable the framework and provider developers to abstract functionality on top. It also ensures compability can be maintained should parameters or returns necessitate changes, while also satisifying the ability for documentation hooks. +Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. + +As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: + +```go +func (r *customResource) Validators(ctx context.Context) Validators { + return Validators{ + CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), + } +} +``` + +The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. -### Single Attribute Value Validation +### Attribute Validation This validation would be applicable to the `schema.Attribute` types declared within the `GetSchema()` of `DataSourceType`, `Provider`, and `ResourceType` implementations. For most of these proposals, the framework would walk through all attribute paths during the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` calls, executing the declared validation in each attribute if present. #### Declaring Value Validation for Attributes -This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. +This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. ##### `ValueValidator` Field on `schema.Attribute` @@ -1297,15 +1286,13 @@ type AttributeWithValueValidators interface { } ``` -This type of proposal, in isolation, feels extraneous given the current attribute implementation. The framework does not appear to benefit from this splitting and it seems desirable that all attributes should be able to optionally enable value validation. Future considerations to allow declaring custom attribute types, outside of validation handling, are more likely to drive this type of potential change. - -##### Resource Level Attribute Value Validation Handling +This type of proposal, in isolation, feels extraneous given the current attribute implementation. The framework does not appear to benefit from this splitting and it seems desirable that all attributes should be able to enable value validation via optional data on the existing type. -This proposal would introduce no changes to `schema.Attribute`. Instead, this would require value validation declarations at the `DataSource` and `Resource` level similar to other proposed attribute validations in the [Declaring Multiple Attribute Validation for Resources](#declaring-multiple-attribute-validation-for-resources) section. +##### Resource Level Attribute Validation Handling -This proposal makes value validation behaviors occur at a distance, meaning it is harder for provider developers to correlate the validation logic to the name/path and type information. It would also be very verbose for even moderately sized schemas with thorough value validation. The only real potential benefit to this type of value validation is that the framework implementation is very straightforward, to just go through this single list of validations instead of walking all attributes. +This proposal would introduce no changes to `schema.Attribute`. Instead, this would require all attribute validation declarations at the `DataSource`, `Provider`, and `Resource` level. -It could be possible to implement another proposal in this space, while also supporting this one, however this could introduce unnecessary complexity into the implementation. +This proposal makes any value validation behaviors occur at a distance, meaning it is harder for provider developers to correlate the validation logic to the name/path and type information. It would also be very verbose for even moderately sized schemas with thorough value validation. The only real potential benefit to this framework implementation is that it is very straightforward from the framework perspective. The logic would execute the top level list of validations instead of walking all attributes to find other attributes. #### Defining Attribute Value Validation Functions @@ -1452,826 +1439,832 @@ This would provide the lowest level and most customizable option to enable the f One caveat to this single type is that it would not capture any nuance between data sources, providers, and resources should their RPCs provide differing functionality in the future. -#### Attribute Value Validation Function Value Parameter +### Multiple Attribute Validation -Regardless the choice of concrete or interface types for the value validation functions, the parameters and returns for the implementations will play a crucial role on the extensibility and development experience. +This framework should also provide the ability to handle validation situations across multiple attributes as noted in the goals. Some of the proposals from the [Single Attribute Value Validation](#single-attribute-value-validation) section are applicable for these proposals as well, so they are largely omitted here for brevity. Examples showing `attr.Value`, `*tftypes.AttributePath`, and bare `error` types are for illustrative purposes, whose final forms would be determined by those proposals. -##### `attr.Value` Type +#### Declaring Multiple Attribute Validation for Attributes -The simplest implementation in the framework that could occur in all function types or interfaces is directly supplying an `attr.Value` and requiring implementations to handle all type conversion: +The previous framework implemented behaviors, such as `ConflictsWith`, as an individual field per behavior within each attribute. This section of proposals targets this specific functionality. One major caveat to these proposals is that they should not be considered exclusive to attribute value validations as it may be desirable to provide some consistency between the two implementations to improve developer experience. + +This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. + +##### Individual Behavior Fields on `schema.Attribute` + +Similar to the previous framework, individual fields for each attribute validation could be added to the `schema.Attribute` type which accepts multiple attribute paths. For example: ```go -func (v someValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { - value, ok := rawValue.(types.String) - - if !ok { - return fmt.Errorf("%s with incorrect type: %T", path, rawValue) - } +schema.Attribute{ + // ... + ConflictsWith: []tftypes.AttributePath, +} +``` - // ... rest of logic ... +A potential downside is that these behaviors cannot support the notion of conditional logic without changes to the implementations, since they can only be existence based if passed an attribute path. Allowing value validations in the declarations (on either side), could allieviate this issue. For example: + +```go +schema.Attribute{ + // ... + ConflictsWith: []func(AttributeValueValidator, tftypes.AttributePath, AttributeValueValidator), +} ``` -Using this interface type would be required to support validation for custom value types. +Regardless of the potential value handling, this proposal would feel familiar for existing provider developers and be relatively trivial for them to implement. One noticable downside to this approach however is that there can be any number of related, but disjointed attribute behaviors. The previous framework supported four of these already and there is logical room for addtional behaviors, making updates to the `schema.Attribute` type a limiting factor in this validation space. This proposal also differs from value validation proposals, which are focused around a single field. -##### `types.T` Type +##### `PathValidator` Field on `schema.Attribute` -If using an `attr.ValueValidator` interface approach, multiple new Go interface types could be created that define extensible value validation functions with strong typing. For example: +A new field for attribute validation can be added to the `schema.Attribute` type. For example: ```go -// ValueValidator describes common validation functionality -type ValueValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string +schema.Attribute{ + // ... + PathValidator: T, } +``` -// StringValueValidator describes String value validation -type StringValueValidator interface { - ValueValidator - Validate(context.Context, *tftypes.AttributePath, types.String) error +Implementators would be responsible for ensuring that single function covered all necessary validation. The framework could provide wrapper functions similar to the previous `All()` and `Any()` of `ValidateFunc` to allow simpler validations built from multiple functions. For example: + +```go +schema.Attribute{ + // ... + PathValidator: All( + T, + T, + ), } ``` -Then, this framework can handle the appropriate type conversions and error handling: +As seen with the previous framework in practice however, it was very common to implement the `All()` wrapper function. New provider developers would be responsible for understanding that multiple validations are possible in the single function field and knowing that custom validation functions may not be necessary to write if using the wrapper functions. -```go -// Validate performs all validation functions. -// -// Each type performs conversion or returns a conversion error -// prior to executing the typed validation function. -func (vs ValueValidators) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { - for _, validator := range vs { - switch typedValidator := validator.(type) { - case StringValueValidator: - value, ok := rawValue.(types.String) - - if !ok { - return fmt.Errorf("%s with incorrect type: %T", path, rawValue) - } - - if err := typedValidator.Validate(ctx, path, value); err != nil { - return err - } - default: - return fmt.Errorf("unknown validator type: %T", validator) - } - } +This proposal colocates the attribute validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. - return nil -} -``` +##### `PathValidators` Field on `schema.Attribute` -Leaving the implementations to only be concerned with the typed value: +A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: ```go -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) error { - if value.Unknown { - return fmt.Errorf("%s with unknown value", path) - } - - if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) - } - - return nil +schema.Attribute{ + // ... + PathValidators: []T{ + T, + T, + }, } ``` -This proposal allows each validation function to be succinctly defined with the expected value type. It may be possible to get the validation function implementations even closer to the true value logic if unknown values are also handled automatically by this framework, however that decision can be made further along in the design process. - -Even with this type of implementation, it is theoretically possible to create a "generic" type handler for escaping the strongly typed logic if necessary: +In this case, the framework would perform the validation similar to the previous framework `All()` wrapper function for `ValidateFunc`. The logical `AND` type of value validation is overwhelmingly more common in practice, which will simplify provider implementations. This still allows for an `Any()` based wrapper (logical `OR`) to be inserted if necessary. -```go -// GenericValueValidator describes value validation without a strong type. -// -// While it is generally preferred to use the typed validation interfaces, -// such as StringValueValidator, this interface allows custom implementations -// where the others may not be suitable. The Validate function is responsible -// for protecting against attr.Value type assertion panics. -type GenericValueValidator interface { - ValueValidator - Validate(context.Context, *tftypes.AttributePath, attr.Value) error -} -``` +Colocating the attribute validation behaviors in the schema definition, means it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. This proposal will feel familiar to existing provider developers. New provider developers will immediately know that multiple validations are supported. -Offering the largest amount of flexibility for implementors to choose the level of desired abstraction, while not hindering more advanced implementations. +##### Combined `Validators` Field on `schema.Attribute` -To support passing through the provider instance, separate interface types could be introduced that include a function call with the `tfsdk.Provider` interface type: +A new field that accepts the union of [`ValueValidators` field on `schema.Attribute`](#valuevalidators-field-on-schemaattribute) and [`PathValidators` field on `schema.Attribute`](#pathvalidators-field-on-schemaattribute) can be added to the `schema.Attribute` type. For example: ```go -type StringValueValidatorWithProvider interface { - ValueValidator - ValidateWithProvider(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value types.String) error -} +schema.Attribute( + // ... + Validators: []I( + T1, + T2, + ) +) ``` -#### Attribute Value Validation Function Path Parameter +Since value validation functions would inherently be implemented different than path validation functions and they are conceptually similar but different in certain ways, this could be complex to implement or understand correctly. When trying to handle documentation output for example, this framework or callers would need to distinguish between the two validation types to ensure the intended validation meanings are correct. -Another consideration with attribute value validation functions is whether the implementation should be responsible for adding context around the attribute path under validation and how that information (if provided) is surfaced to the function body. +##### Resource Level Attribute Path Validation Handling -##### No Attribute Path Parameter +Rather than adjusting the `schema.Attribute` type for this type of validation, it could be forced to the resource (or data source) level. The [Declaring Multiple Attribute Validation for Resources](#declaring-multiple-attribute-validation-for-resources) proposals presented later are revelant for this section. To prevent proposal duplication, please see that section for more details and associated tradeoffs. -Validation function implementations could potentially not have access to the attribute path under validation, instead relying on surrounding logic to handle wrapping errors or logging to include the path. For example: +#### Declaring Multiple Attribute Validation for Resources -```go -tflog.Debug(ctx, "validating attribute path (%s) attribute value (%s): %s", attributePath.String(), value, validator.Description()) +In the previous framework, the `CustomizeDiff` functionality enabled resource (or data source) level validation as a logical catch-all. These proposals cover the next iteration of that type of functionality. -err := validator.Validate(ctx, value) +##### `PlanModifications` for Resources -if err != nil { - return fmt.Errorf("%s: %w", attributePath.String(), err) -} -``` +The [Plan Modifications design documentation](./plan-modifications.md) outlines proposals which broadly replace the previous framework's `CustomizeDiff` functionality. See that documentation for considerations and recommendations there. In this proposal for validation, new functions for validation would be provided within that framework, rather than introducing separate handling. -This could be a double edged sword for extensibility. Implementators do not need to worry about handling the attribute path in error messages that are returned to practitioners or manually adding logging around it. This does however prevent the ability to provide that additional context to the validation logic, if for example the logic warrants making decisions based on the given path or additional logging that includes the full path. In practice with validation functions in the previous framework, path based decisions are rare at best, and this framework could be opinionated against that particular pattern. +Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. Another wrinkle is that plan modifications are only intended to run during `terraform plan` (`PlanResourceChanges` RPC) and `terraform apply` (`ApplyResourceChanges` RPC), so the framework would be introducing its own additional logic to extract and perform any validation functions during the `terraform validate` (`ValidateDataSourceConfig`/`ValidateProviderConfig`/`ValidateResourceConfig` RPCs). -##### Adding Attribute Path to Context +##### `Validators` for Resources -This framework could inject additional validation information into the `context.Context` being passed through to the validation functions. For example: +This introduces a new extension interface type for `ResourceType` and `DataSourceType`. For example: ```go -const ValidationAttributePathKey = "validation_attribute_path" +type DataSourceTypeWithValidators interface { + DataSourceType + Validators(context.Context) Validators +} -validationCtx := context.WithValue(ctx, ValidationAttributePathKey, attributePath) -validator.Validate(ctx, value) +type ResourceTypeWithValidators interface { + ResourceType + Validators(context.Context) Validators +} ``` -With implementations referencing this data: - -```go -func (v someValidator) Validate(ctx context.Context, rawValue attr.Value) error { - // ... - rawAttributePath := ctx.Value(ValidationAttributePathKey) +Where `Validators` is a slice of types to be discussed later. - attributePath, ok := rawAttributePath.(*tftypes.AttributePath) +As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: - if !ok { - return fmt.Errorf("unexpected %s context value type: %T", ValidationAttributePathKey, rawAttributePath) +```go +func (t *customResourceType) Validators(ctx context.Context) Validators { + return Validators{ + ConflictingAttributes(*tftypes.AttributePath, *tftypes.Attribute), + ConflictingAttributesWithValues(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath, ValueValidator), + PrerequisiteAttribute(*tftypes.AttributePath, *tftypes.AttributePath), + PrerequisiteAttributeWithValue(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath), } - // ... +} ``` -This experience seems subpar for developers though as they must know about the special context value(s) available and how to reference them appropriately, especially to avoid a type assertion panic. In this case, it seems more appropriately to pass the parameter directly, if necessary. +This setup would allow for the framework to provide flexible resource level validation with a low amount of friction for provider developers. Helper functions would be extensible and make the behaviors clear. -##### `string` Type +## Recommendations -The attribute path could be passed to validation functions as its string representation. For example: +This section will summarize the proposals into specific recommendations for each topic. Code examples are provided in following sections to illustrate the concepts. The final section provides some future considerations for the framework and terraform-plugin-go. -```go -validator.Validate(ctx, attributePath.String(), value) -``` +### Overview -This would allow implementors to ignore the details of what the attribute path is or how to represent it appropriately. However, this seems unnecessarily limiting should the path information need to be used in the logic. In this case, calling a Go conventional `String()` receiver method on the actual attribute path type does not feel like a development burden for implementors as necessary. +Defining all validation functionality via interface types will offer the framework the most flexibility for future enhancements while ensuring consistent implementations. The request and response pattern should be used to enable backwards (in the case of field deprecations) and forwards compatibility. -##### `*tftypes.AttributePath` Type +All validation should be implemented separately from plan modifications as they address differing concerns and operations within the Terraform. Attribute validations should be implemented as a slice of the interface type on `schema.Attribute` while Data Source, Provider, and Resource level validation should be implemented as new extension interface types. Further helper functions and designs can reduce implementation details. -The attribute path could be passed to validation functions directly using `*tftypes.AttributePath` or its abstraction in this framework. For example: +### Data Source Example Implementation + +Example framework code: ```go -validator.Validate(ctx, attributePath, value) -``` +// ValidateDataSourceConfigRequest contains request information from the ValidateDataSourceConfig RPC. +type ValidateDataSourceConfigRequest struct { + Config tfsdk.Config + TypeName string +} -This provides the ultimate flexibility for implementors, making the path information fully available in logic, logging, etc. This framework's design could also borrow ideas from the [No Attribute Path Parameter](#no-attribute-path-parameter) section and automatically handle logging and wrapping where appropriate, leaving it completely optional for implementators to handle the path information. +// ValidateDataSourceConfigResponse contains request information for the ValidateDataSourceConfig RPC. +type ValidateDataSourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} -#### Attribute Value Validation Function Returns +// DataSourceConfigValidator describes a reusable Data Source configuration validation function. +type DataSourceConfigValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} -Depending on the validation function design, there could be important details about the validation process that need to be surfaced to callers. This section walks through different proposals on how information can be returned to callers. +// DataSourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. +type DataSourceConfigValidatorWithProvider interface { + DataSourceConfigValidator + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} -##### Attribute Value Validation Function `bool` Return +// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. +type DataSourceWithConfigValidators interface { + DataSource + ConfigValidators(context.Context) []DataSourceConfigValidator +} -Validation functions could implement return information via a `bool` type. For example: +// DataSourceWithValidateConfig is an interface type that extends DataSource to include imperative validation. +type DataSourceWithValidateConfig interface { + DataSource + ValidateConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} +``` -```go -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) bool { - value, ok := rawValue.(types.String) - - if !ok { - return false - } +Example provider code: - if value.Unknown { - return false +```go +func (d *customDataSource) ConfigValidators(ctx context.Context) DataSourceConfigValidators { + return DataSourceConfigValidators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), } - - return len(value.Value) > v.minimum && len(value.Value) < v.maximum } ``` -This proposal encodes no information in the response from these functions beyond a simple boolean "validation passed" versus "validation failed" value. Information such as whether validation failed due to type conversion problems or validation could not be performed due to an unknown value is hidden. Giving the ability for functions to surface details about unsuccessful validation back to callers is likely required broader utility in this framework and extensions to it. - -In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level, summary, or details associated with that diagnostic. - -##### Attribute Value Validation Function `error` Return +### Provider Level Example Implementation -Validation functions could implement return information via an untyped `error`. For example: +Example framework code: ```go -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { - value, ok := rawValue.(types.String) - - if !ok { - return fmt.Errorf("%s with incorrect type: %T", path, rawValue) - } +// ValidateProviderConfigRequest contains request information from the ValidateProviderConfig RPC. +type ValidateProviderConfigRequest struct { + Config tfsdk.Config +} - if value.Unknown { - return fmt.Errorf("%s with unknown value", path) - } +// ValidateProviderConfigResponse contains request information for the ValidateProviderConfig RPC. +type ValidateProviderConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} - if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) - } +// ProviderConfigValidator describes a reusable Provider configuration validation function. +type ProviderConfigValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) +} - return nil +// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. +type ProviderWithConfigValidators interface { + Provider + ConfigValidators(context.Context) []ProviderConfigValidator +} + +// ProviderWithValidateConfig is an interface type that extends Provider to include imperative validation. +type ProviderWithValidateConfig interface { + Provider + ValidateConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) } ``` -In this scenario, callers will know that validation did not pass, but not necessarily why. This proposal is only marginally better than the `bool` return value, as some manual error message context can be provided about the problem that caused the failure. However short of perfectly consistent error messaging which is not feasible to enforce in all implementors, callers will still not reasonably be able to perform actions based on the differing reasons for errors. +Example provider code: -In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level or summary associated with that diagnostic. The details would likely include the error messaging. +```go +func (p *customProvider) ConfigValidators(ctx context.Context) ProviderConfigValidators { + return ProviderConfigValidators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), + } +} +``` -##### Attribute Value Validation Function Typed Error Return +### Resource Level Example Implementation -This framework could provide typed errors for validation functions. For example: +Example framework code: ```go -type ValueValidatorInvalidTypeError struct { - Path *tftypes.AttributePath - Value attr.Value +// ValidateResourceConfigRequest contains request information from the ValidateResourceConfig RPC. +type ValidateResourceConfigRequest struct { + Config tfsdk.Config + TypeName string } -// Error implements the error interface -func (e ValueValidatorInvalidTypeError) Error() string { - // ... +// ValidateResourceConfigResponse contains request information for the ValidateResourceConfig RPC. +type ValidateResourceConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -type ValueValidatorInvalidValueError struct { - Description string - Path *tftypes.AttributePath - Value attr.Value +// ResourceConfigValidator describes a reusable Resource configuration validation function. +type ResourceConfigValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } -// Error implements the error interface -func (e ValueValidatorInvalidValueError) Error() string { - // ... +// ResourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. +type ResourceConfigValidatorWithProvider interface { + ResourceConfigValidator + ValidateWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } -type ValueValidatorUnknownValueError struct { - Path *tftypes.AttributePath +// ResourceWithConfigValidators is an interface type that extends Resource to include declarative validations. +type ResourceWithConfigValidators interface { + Resource + ConfigValidators(context.Context) []ResourceConfigValidator } -// Error implements the error interface -func (e ValueValidatorUnknownValueError) Error() string { - // ... +// ResourceWithValidateConfig is an interface type that extends Resource to include imperative validations. +type ResourceWithValidateConfig interface { + Resource + ValidateConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) } ``` -With implementators able to return these such as: +Example provider code: ```go -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { - value, ok := rawValue.(types.String) - - if !ok { - return ValueValidatorInvalidTypeError{ - Path: path, - Value: rawValue, - } - } - - if value.Unknown { - return ValueValidatorUnknownValueError{ - Path: path, - } - } - - if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - return ValueValidatorInvalidValueError{ - Description: v.Description(ctx), - Path: path, - Value: value, - } +func (r *customResource) ConfigValidators(ctx context.Context) ResourceConfigValidators { + return ResourceConfigValidators{ + ConflictingAttributes( + tftypes.NewAttributePath().AttributeName("first_attribute"), + tftypes.NewAttributePath().AttributeName("second_attribute"), + ), } - - return nil } ``` -This framework could also go further and require using one of these error types: +### Attribute Level Example Implementation + +Example framework code: ```go -type ValueValidatorError interface {} +type ValidateAttributeRequest struct { + // AttributePath contains the path of the attribute. + AttributePath tftypes.AttributePath -// ... + // AttributeConfig contains the value of the attribute. + AttributeConfig attr.Value -type ValueValidatorInvalidTypeError struct { - ValueValidatorError + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config +} - Path *tftypes.AttributePath - Value attr.Value +type ValidateAttributeResponse struct { + Diagnostics []*tfprotov6.Diagnostic } -// ... +// AttributeValidator describes attribute validation functionality. +type AttributeValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string + Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) +} -type ValueValidator interface { +// Existing schema.Attribute struct type +type Attribute struct { // ... - Validate(context.Context, *tftypes.AttributePath, attr.Value) ValueValidatorError + Validators []AttributeValidator } ``` -Meaning that extensibility is guaranteed to follow certain compile time rules. - -In either the `error` or `ValueValidatorError` interface type scenarios, this allows callers to react to the responses by checking for underlying error types. For example, it is possible to implement a generic `Not()` (logical `NOT`) validation function that catches invalid values but passes through other errors: +Example validation function code: ```go -func (v notValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { - var invalidValueError ValueValidatorInvalidValueError - - err := v.validator.Validate(ctx, path, rawValue) - - if err == nil { - return ValueValidatorInvalidValueError{ - Description: v.Description(ctx), - Path: path, - Value: rawValue, - } - } - - if errors.As(err, &invalidValueError) { - return nil - } +type stringLengthBetweenValidator struct { + StringValueValidator - return err + maximum int + minimum int } -``` -In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level or summary associated with that diagnostic. The details would likely include the error messaging based on the error type implementations, although if it was warranted for extensibility, there could also be a "generic" `ValueValidatorError` type (or when there is an unrecognized `error` type) that this framework would pass over except transferring the messaging through to the diagnostic. Additional warning-only types could also be provided to allow further diagnostic customization. - -##### Attribute Value Validation Function Diagnostic Return +func (v stringLengthBetweenValidator) Description(_ context.Context) string { + return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) +} -Validation functions could directly return a `*tfprotov6.Diagnostic` or abstracted type from this framework. For example: +func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) +} -```go -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) (diags tfprotov6.Diagnostics) { - value, ok := rawValue.(types.String) +func (v stringLengthBetweenValidator) Validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + value, ok := req.AttributeConfig.(types.String) // see also attr.ValueAs() proposal if !ok { - diags = append(diags, &tfprotov6.Diagnostic{ + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Incorrect validation type", - Details: fmt.Sprintf("%s with incorrect type: %T", path, rawValue), + Summary: "Invalid value type", + Details: fmt.Sprintf("received incorrect value type (%T) at path: %s", req.AttributeConfig, req.Config.AttributePath), }) return } - if value.Unknown { - diags = append(diags, &tfprotov6.Diagnostic{ + if req.Config.Unknown { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unknown validation value", - Details: fmt.Sprintf("received unknown value at path: %s", path), + Details: fmt.Sprintf("received unknown value at path: %s", req.Config.AttributePath), }) return } - if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - diags = append(diags, &tfprotov6.Diagnostic{ + if len(req.Config.Value) < v.minimum || len(req.Config.Value) > v.maximum { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Value validation failed", - Details: fmt.Sprintf("%s with value %q %s", path, value.Value, v.Description(ctx)) + Details: fmt.Sprintf("%s with value %q %s", req.Config.AttributePath, req.Config.Value, v.Description(ctx)) }) return } return } + +func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { + return stringLengthBetweenValidator{ + maximum: maximum, + minimum: minimum, + } +} ``` -In this scenario, it the implementor's responsibility to generate the appropriate diagnostic back, but they have full control of the output. It could be difficult for the framework to enforce implementation rules around these responses or potentially allow configuration overrides for them without creating more abstractions on top of this type or additional helper functions. Differing diagnostic implementations could introduce confusion for practitioners. +Example provider code: -In general, this proposal feels very similar to either the generic `error` type or typed error proposals above (depending on the implmentation details) with minimal utility over them beyond complete output customization. However, the rest of the framework is designed around diagnostics so this would introduce a different implementation. To remain consistent with other framework design while still pushing for consistency, helpers could be introduced to nudge developers towards standardized summary information, if desired. +```go +schema.Attribute{ + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), + StringLengthBetween(1, 256), + }, +} +``` -### Multiple Attribute Validation +### Future Considerations -This framework should also provide the ability to handle validation situations across multiple attributes as noted in the goals. Some of the proposals from the [Single Attribute Value Validation](#single-attribute-value-validation) section are applicable for these proposals as well, so they are largely omitted here for brevity. Examples showing `attr.Value`, `*tftypes.AttributePath`, and bare `error` types are for illustrative purposes, whose final forms would be determined by those proposals. +It is recommended that the framework provide an abstracted `*tftypes.AttributePath` rather than depend on that type directly, but this can converted after an initial implementation. This is purely for decoupling the two projects, similar to other abstracted types already created in the framework. + +To better support provider-based validation functionality in the future, it is also recommended that the `Provider` interface type also add a new `Configured(context.Context) bool` function or another methodology for easily checking the configuration state of a provider instance. Adding a setter function could also allow the framework to manage the provider configuration state automatically. This would simplify validations that require provider instances since it will likely be required that implementations need to check on this status as part of the validation logic. + +It is recommended that the framework or the upstream terraform-plugin-go module provide functionality to declare relative attribute paths, such as "this" and "parent" methods to better enable nested attribute declarations. This will enable provider developers to create attribute paths such as: + +```go +NewAttributePath(CurrentPath().Parent().AttributeName("other_attr")) +``` + +Strongly typed attribute validation can be introduced to simplify implementations for common value types, such as `types.String`. Future designs can discuss the potential designs and tradeoffs. + +## Appendix - Additional Design Considerations + +During this design process, varying implementations details were discussed, but including this level of detail would be distracting from the overall flow of this documentation. Rather than discard these choices, they are captured here for additional context in case they may be valuable. + +### Attribute Validation Input and Output Types + +#### Attribute Value Parameter + +Regardless the choice of concrete or interface types for the value validation functions, the parameters and returns for the implementations will play a crucial role on the extensibility and development experience. + +##### `attr.Value` Type + +The simplest implementation in the framework that could occur in all function types or interfaces is directly supplying an `attr.Value` and requiring implementations to handle all type conversion: -#### Declaring Multiple Attribute Validation for Attributes +```go +func (v someValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } -The previous framework implemented behaviors, such as `ConflictsWith`, as an individual field per behavior within each attribute. This section of proposals targets this specific functionality. One major caveat to these proposals is that they should not be considered exclusive to attribute value validations as it may be desirable to provide some consistency between the two implementations to improve developer experience. + // ... rest of logic ... +``` -This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. +Using this interface type would be required to support validation for custom value types. Type implementations could introduce helpers to automatically handle this type conversion for simplication. -##### Individual Behavior Fields on `schema.Attribute` +##### `types.T` Type -Similar to the previous framework, individual fields for each attribute validation could be added to the `schema.Attribute` type which accepts multiple attribute paths. For example: +If using an `attr.ValueValidator` interface approach, multiple new Go interface types could be created that define extensible value validation functions with strong typing. For example: ```go -schema.Attribute{ - // ... - ConflictsWith: []tftypes.AttributePath, +// ValueValidator describes common validation functionality +type ValueValidator interface { + Description(context.Context) string + MarkdownDescription(context.Context) string } -``` - -A potential downside is that these behaviors cannot support the notion of conditional logic without changes to the implementations, since they can only be existence based if passed an attribute path. Allowing value validations in the declarations (on either side), could allieviate this issue. For example: -```go -schema.Attribute{ - // ... - ConflictsWith: []func(AttributeValueValidator, tftypes.AttributePath, AttributeValueValidator), +// StringValueValidator describes String value validation +type StringValueValidator interface { + ValueValidator + Validate(context.Context, *tftypes.AttributePath, types.String) error } ``` -Regardless of the potential value handling, this proposal would feel familiar for existing provider developers and be relatively trivial for them to implement. One noticable downside to this approach however is that there can be any number of related, but disjointed attribute behaviors. The previous framework supported four of these already and there is logical room for addtional behaviors, making updates to the `schema.Attribute` type a limiting factor in this validation space. This proposal also differs from value validation proposals, which are focused around a single field. +Then, this framework can handle the appropriate type conversions and error handling: -##### `PathValidator` Field on `schema.Attribute` +```go +// Validate performs all validation functions. +// +// Each type performs conversion or returns a conversion error +// prior to executing the typed validation function. +func (vs ValueValidators) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + for _, validator := range vs { + switch typedValidator := validator.(type) { + case StringValueValidator: + value, ok := rawValue.(types.String) -A new field for attribute validation can be added to the `schema.Attribute` type. For example: + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } -```go -schema.Attribute{ - // ... - PathValidator: T, + if err := typedValidator.Validate(ctx, path, value); err != nil { + return err + } + default: + return fmt.Errorf("unknown validator type: %T", validator) + } + } + + return nil } ``` -Implementators would be responsible for ensuring that single function covered all necessary validation. The framework could provide wrapper functions similar to the previous `All()` and `Any()` of `ValidateFunc` to allow simpler validations built from multiple functions. For example: +Leaving the implementations to only be concerned with the typed value: ```go -schema.Attribute{ - // ... - PathValidator: All( - T, - T, - ), -} -``` +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, value types.String) error { + if value.Unknown { + return fmt.Errorf("%s with unknown value", path) + } -As seen with the previous framework in practice however, it was very common to implement the `All()` wrapper function. New provider developers would be responsible for understanding that multiple validations are possible in the single function field and knowing that custom validation functions may not be necessary to write if using the wrapper functions. + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) + } -This proposal colocates the attribute validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. + return nil +} +``` -##### `PathValidators` Field on `schema.Attribute` +This proposal allows each validation function to be succinctly defined with the expected value type. It may be possible to get the validation function implementations even closer to the true value logic if unknown values are also handled automatically by this framework, however that decision can be made further along in the design process. -A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: +Even with this type of implementation, it is theoretically possible to create a "generic" type handler for escaping the strongly typed logic if necessary: ```go -schema.Attribute{ - // ... - PathValidators: []T{ - T, - T, - }, +// GenericValueValidator describes value validation without a strong type. +// +// While it is generally preferred to use the typed validation interfaces, +// such as StringValueValidator, this interface allows custom implementations +// where the others may not be suitable. The Validate function is responsible +// for protecting against attr.Value type assertion panics. +type GenericValueValidator interface { + ValueValidator + Validate(context.Context, *tftypes.AttributePath, attr.Value) error } ``` -In this case, the framework would perform the validation similar to the previous framework `All()` wrapper function for `ValidateFunc`. The logical `AND` type of value validation is overwhelmingly more common in practice, which will simplify provider implementations. This still allows for an `Any()` based wrapper (logical `OR`) to be inserted if necessary. - -Colocating the attribute validation behaviors in the schema definition, means it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. This proposal will feel familiar to existing provider developers. New provider developers will immediately know that multiple validations are supported. - -##### Combined `Validators` Field on `schema.Attribute` +Offering the largest amount of flexibility for implementors to choose the level of desired abstraction, while not hindering more advanced implementations. -A new field that accepts the union of [`ValueValidators` field on `schema.Attribute`](#valuevalidators-field-on-schemaattribute) and [`PathValidators` field on `schema.Attribute`](#pathvalidators-field-on-schemaattribute) can be added to the `schema.Attribute` type. For example: +To support passing through the provider instance, separate interface types could be introduced that include a function call with the `tfsdk.Provider` interface type: ```go -schema.Attribute( - // ... - Validators: []I( - T1, - T2, - ) -) +type StringValueValidatorWithProvider interface { + ValueValidator + ValidateWithProvider(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value types.String) error +} ``` -Since value validation functions would inherently be implemented different than path validation functions and they are conceptually similar but different in certain ways, this could be complex to implement or understand correctly. When trying to handle documentation output for example, this framework or callers would need to distinguish between the two validation types to ensure the intended validation meanings are correct. +#### Attribute Path Parameter -##### Resource Level Attribute Path Validation Handling +Another consideration with attribute validation is whether the implementation should be responsible for adding context around the attribute path under validation and how that information (if provided) is surfaced to the function body. -Rather than adjusting the `schema.Attribute` type for this type of validation, it could be forced to the resource (or data source) level. The [Declaring Multiple Attribute Validation for Resources](#declaring-multiple-attribute-validation-for-resources) proposals presented later are revelant for this section. To prevent proposal duplication, please see that section for more details and associated tradeoffs. +##### No Attribute Path Parameter -#### Declaring Multiple Attribute Validation for Resources +Validation function implementations could potentially not have access to the attribute path under validation, instead relying on surrounding logic to handle wrapping errors or logging to include the path. For example: -In the previous framework, the `CustomizeDiff` functionality enabled resource (or data source) level validation as a logical catch-all. These proposals cover the next iteration of that type of functionality. +```go +tflog.Debug(ctx, "validating attribute path (%s) attribute value (%s): %s", attributePath.String(), value, validator.Description()) -##### `PlanModifications` for Resources +err := validator.Validate(ctx, value) -The [Plan Modifications design documentation](./plan-modifications.md) outlines proposals which broadly replace the previous framework's `CustomizeDiff` functionality. See that documentation for considerations and recommendations there. In this proposal for validation, new functions for validation would be provided within that framework, rather than introducing separate handling. +if err != nil { + return fmt.Errorf("%s: %w", attributePath.String(), err) +} +``` -Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. Another wrinkle is that plan modifications are only intended to run during `terraform plan` (`PlanResourceChanges` RPC) and `terraform apply` (`ApplyResourceChanges` RPC), so the framework would be introducing its own additional logic to extract and perform any validation functions during the `terraform validate` (`ValidateDataSourceConfig`/`ValidateProviderConfig`/`ValidateResourceConfig` RPCs). +This could be a double edged sword for extensibility. Implementators do not need to worry about handling the attribute path in error messages that are returned to practitioners or manually adding logging around it. This does however prevent the ability to provide that additional context to the validation logic, if for example the logic warrants making decisions based on the given path or additional logging that includes the full path. In practice with validation functions in the previous framework, path based decisions are rare at best, and this framework could be opinionated against that particular pattern. -##### `Validators` for Resources +##### Adding Attribute Path to Context -This introduces a new extension interface type for `ResourceType` and `DataSourceType`. For example: +This framework could inject additional validation information into the `context.Context` being passed through to the validation functions. For example: ```go -type DataSourceTypeWithValidators interface { - DataSourceType - Validators(context.Context) Validators -} +const ValidationAttributePathKey = "validation_attribute_path" -type ResourceTypeWithValidators interface { - ResourceType - Validators(context.Context) Validators -} +validationCtx := context.WithValue(ctx, ValidationAttributePathKey, attributePath) +validator.Validate(ctx, value) ``` -Where `Validators` is a slice of types to be discussed later. - -As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: +With implementations referencing this data: ```go -func (t *customResourceType) Validators(ctx context.Context) Validators { - return Validators{ - ConflictingAttributes(*tftypes.AttributePath, *tftypes.Attribute), - ConflictingAttributesWithValues(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath, ValueValidator), - PrerequisiteAttribute(*tftypes.AttributePath, *tftypes.AttributePath), - PrerequisiteAttributeWithValue(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath), +func (v someValidator) Validate(ctx context.Context, rawValue attr.Value) error { + // ... + rawAttributePath := ctx.Value(ValidationAttributePathKey) + + attributePath, ok := rawAttributePath.(*tftypes.AttributePath) + + if !ok { + return fmt.Errorf("unexpected %s context value type: %T", ValidationAttributePathKey, rawAttributePath) } -} + // ... ``` -This setup would allow for the framework to provide flexible resource level validation with a low amount of friction for provider developers. Helper functions would be extensible and make the behaviors clear. - -## Recommendations +This experience seems subpar for developers though as they must know about the special context value(s) available and how to reference them appropriately, especially to avoid a type assertion panic. In this case, it seems more appropriately to pass the parameter directly, if necessary. -This section will summarize the proposals into specific recommendations for each topic. Code examples are provided in following sections to illustrate the concepts. The final section provides some future considerations for the framework and terraform-plugin-go. +##### `string` Type -### Overview +The attribute path could be passed to validation functions as its string representation. For example: -Defining all validation functionality via interface types will offer the framework the most flexibility for future enhancements while ensuring consistent implementations. The request and response pattern should be used to enable backwards (in the case of field deprecations) and forwards compatibility. +```go +validator.Validate(ctx, attributePath.String(), value) +``` -All validation should be implemented separately from plan modifications as they address differing concerns and operations within the Terraform. Attribute validations should be implemented as a slice of the interface type on `schema.Attribute` while Data Source, Provider, and Resource level validation should be implemented as new extension interface types. Further helper functions and designs can reduce implementation details. +This would allow implementors to ignore the details of what the attribute path is or how to represent it appropriately. However, this seems unnecessarily limiting should the path information need to be used in the logic. In this case, calling a Go conventional `String()` receiver method on the actual attribute path type does not feel like a development burden for implementors as necessary. -### Data Source Example Implementation +##### `*tftypes.AttributePath` Type -Example framework code: +The attribute path could be passed to validation functions directly using `*tftypes.AttributePath` or its abstraction in this framework. For example: ```go -// ValidateDataSourceConfigRequest contains request information from the ValidateDataSourceConfig RPC. -type ValidateDataSourceConfigRequest struct { - Config tfsdk.Config - TypeName string -} - -// ValidateDataSourceConfigResponse contains request information for the ValidateDataSourceConfig RPC. -type ValidateDataSourceConfigResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} +validator.Validate(ctx, attributePath, value) +``` -// DataSourceConfigValidator describes a reusable Data Source configuration validation function. -type DataSourceConfigValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) -} +This provides the ultimate flexibility for implementors, making the path information fully available in logic, logging, etc. This framework's design could also borrow ideas from the [No Attribute Path Parameter](#no-attribute-path-parameter) section and automatically handle logging and wrapping where appropriate, leaving it completely optional for implementators to handle the path information. -// DataSourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. -type DataSourceConfigValidatorWithProvider interface { - DataSourceConfigValidator - ValidateWithProvider(context.Context, tfsdk.Provider, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) -} +#### Attribute Validation Return Value -// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. -type DataSourceWithConfigValidators interface { - DataSource - ConfigValidators(context.Context) []DataSourceConfigValidator -} +Depending on the validation function design, there could be important details about the validation process that need to be surfaced to callers. This section walks through different proposals on how information can be returned to callers. -// DataSourceWithValidateConfig is an interface type that extends DataSource to include imperative validation. -type DataSourceWithValidateConfig interface { - DataSource - ValidateConfig(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) -} -``` +##### Attribute Validation `bool` Return -Example provider code: +Validation functions could return information via a `bool` type. For example: ```go -func (d *customDataSource) ConfigValidators(ctx context.Context) DataSourceConfigValidators { - return DataSourceConfigValidators{ - ConflictingAttributes( - tftypes.NewAttributePath().AttributeName("first_attribute"), - tftypes.NewAttributePath().AttributeName("second_attribute"), - ), +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) bool { + value, ok := rawValue.(types.String) + + if !ok { + return false } -} -``` - -### Provider Level Example Implementation - -Example framework code: - -```go -// ValidateProviderConfigRequest contains request information from the ValidateProviderConfig RPC. -type ValidateProviderConfigRequest struct { - Config tfsdk.Config -} -// ValidateProviderConfigResponse contains request information for the ValidateProviderConfig RPC. -type ValidateProviderConfigResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} + if value.Unknown { + return false + } -// ProviderConfigValidator describes a reusable Provider configuration validation function. -type ProviderConfigValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) + return len(value.Value) > v.minimum && len(value.Value) < v.maximum } +``` -// DataSourceWithConfigValidators is an interface type that extends DataSource to include declarative validations. -type ProviderWithConfigValidators interface { - Provider - ConfigValidators(context.Context) []ProviderConfigValidator -} +This proposal encodes no information in the response from these functions beyond a simple boolean "validation passed" versus "validation failed" value. Information such as whether validation failed due to type conversion problems or validation could not be performed due to an unknown value is hidden. Giving the ability for functions to surface details about unsuccessful validation back to callers is likely required broader utility in this framework and extensions to it. -// ProviderWithValidateConfig is an interface type that extends Provider to include imperative validation. -type ProviderWithValidateConfig interface { - Provider - ValidateConfig(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) -} -``` +In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level, summary, or details associated with that diagnostic. -Example provider code: +##### Attribute Validation `error` Return + +Validation functions could implement return information via an untyped `error`. For example: ```go -func (p *customProvider) ConfigValidators(ctx context.Context) ProviderConfigValidators { - return ProviderConfigValidators{ - ConflictingAttributes( - tftypes.NewAttributePath().AttributeName("first_attribute"), - tftypes.NewAttributePath().AttributeName("second_attribute"), - ), +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return fmt.Errorf("%s with incorrect type: %T", path, rawValue) + } + + if value.Unknown { + return fmt.Errorf("%s with unknown value", path) + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) } + + return nil } ``` -### Resource Level Example Implementation +In this scenario, callers will know that validation did not pass, but not necessarily why. This proposal is only marginally better than the `bool` return value, as some manual error message context can be provided about the problem that caused the failure. However short of perfectly consistent error messaging which is not feasible to enforce in all implementors, callers will still not reasonably be able to perform actions based on the differing reasons for errors. -Example framework code: +In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level or summary associated with that diagnostic. The details would likely include the error messaging. + +##### Attribute Validation Typed Error Return + +This framework could provide typed errors for validation functions. For example: ```go -// ValidateResourceConfigRequest contains request information from the ValidateResourceConfig RPC. -type ValidateResourceConfigRequest struct { - Config tfsdk.Config - TypeName string +type ValueValidatorInvalidTypeError struct { + Path *tftypes.AttributePath + Value attr.Value } -// ValidateResourceConfigResponse contains request information for the ValidateResourceConfig RPC. -type ValidateResourceConfigResponse struct { - Diagnostics []*tfprotov6.Diagnostic +// Error implements the error interface +func (e ValueValidatorInvalidTypeError) Error() string { + // ... } -// ResourceConfigValidator describes a reusable Resource configuration validation function. -type ResourceConfigValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +type ValueValidatorInvalidValueError struct { + Description string + Path *tftypes.AttributePath + Value attr.Value } -// ResourceConfigValidatorWithProvider is an interface type for declaring configuration validation that requires a provider instance. -type ResourceConfigValidatorWithProvider interface { - ResourceConfigValidator - ValidateWithProvider(context.Context, tfsdk.Provider, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +// Error implements the error interface +func (e ValueValidatorInvalidValueError) Error() string { + // ... } -// ResourceWithConfigValidators is an interface type that extends Resource to include declarative validations. -type ResourceWithConfigValidators interface { - Resource - ConfigValidators(context.Context) []ResourceConfigValidator +type ValueValidatorUnknownValueError struct { + Path *tftypes.AttributePath } -// ResourceWithValidateConfig is an interface type that extends Resource to include imperative validations. -type ResourceWithValidateConfig interface { - Resource - ValidateConfig(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +// Error implements the error interface +func (e ValueValidatorUnknownValueError) Error() string { + // ... } ``` -Example provider code: +With implementators able to return these such as: ```go -func (r *customResource) ConfigValidators(ctx context.Context) ResourceConfigValidators { - return ResourceConfigValidators{ - ConflictingAttributes( - tftypes.NewAttributePath().AttributeName("first_attribute"), - tftypes.NewAttributePath().AttributeName("second_attribute"), - ), +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + value, ok := rawValue.(types.String) + + if !ok { + return ValueValidatorInvalidTypeError{ + Path: path, + Value: rawValue, + } + } + + if value.Unknown { + return ValueValidatorUnknownValueError{ + Path: path, + } + } + + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + return ValueValidatorInvalidValueError{ + Description: v.Description(ctx), + Path: path, + Value: value, + } } + + return nil } ``` -### Attribute Level Example Implementation - -Example framework code: +This framework could also go further and require using one of these error types: ```go -type ValidateAttributeRequest struct { - // AttributePath contains the path of the attribute. - AttributePath tftypes.AttributePath +type ValueValidatorError interface {} - // AttributeConfig contains the value of the attribute. - AttributeConfig attr.Value +// ... - // Config contains the entire configuration of the data source, provider, or resource. - Config tfsdk.Config -} +type ValueValidatorInvalidTypeError struct { + ValueValidatorError -type ValidateAttributeResponse struct { - Diagnostics []*tfprotov6.Diagnostic + Path *tftypes.AttributePath + Value attr.Value } -// AttributeValidator describes attribute validation functionality. -type AttributeValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) -} +// ... -// Existing schema.Attribute struct type -type Attribute struct { +type ValueValidator interface { // ... - Validators []AttributeValidator + Validate(context.Context, *tftypes.AttributePath, attr.Value) ValueValidatorError } ``` -Example validation function code: +Meaning that extensibility is guaranteed to follow certain compile time rules. + +In either the `error` or `ValueValidatorError` interface type scenarios, this allows callers to react to the responses by checking for underlying error types. For example, it is possible to implement a generic `Not()` (logical `NOT`) validation function that catches invalid values but passes through other errors: ```go -type stringLengthBetweenValidator struct { - StringValueValidator +func (v notValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { + var invalidValueError ValueValidatorInvalidValueError - maximum int - minimum int -} + err := v.validator.Validate(ctx, path, rawValue) -func (v stringLengthBetweenValidator) Description(_ context.Context) string { - return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) -} + if err == nil { + return ValueValidatorInvalidValueError{ + Description: v.Description(ctx), + Path: path, + Value: rawValue, + } + } -func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) + if errors.As(err, &invalidValueError) { + return nil + } + + return err } +``` -func (v stringLengthBetweenValidator) Validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { - value, ok := req.AttributeConfig.(types.String) // see also attr.ValueAs() proposal +In this scenario, it is this framework's responsibility to generate the appropriate diagnostic back. Implementors will not be able to influence the level or summary associated with that diagnostic. The details would likely include the error messaging based on the error type implementations, although if it was warranted for extensibility, there could also be a "generic" `ValueValidatorError` type (or when there is an unrecognized `error` type) that this framework would pass over except transferring the messaging through to the diagnostic. Additional warning-only types could also be provided to allow further diagnostic customization. + +##### Attribute Validation Diagnostic Return + +Validation functions could directly return a `*tfprotov6.Diagnostic` or abstracted type from this framework. For example: + +```go +func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) (diags tfprotov6.Diagnostics) { + value, ok := rawValue.(types.String) if !ok { - resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + diags = append(diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Invalid value type", - Details: fmt.Sprintf("received incorrect value type (%T) at path: %s", req.AttributeConfig, req.Config.AttributePath), + Summary: "Incorrect validation type", + Details: fmt.Sprintf("%s with incorrect type: %T", path, rawValue), }) return } - if req.Config.Unknown { - resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + if value.Unknown { + diags = append(diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unknown validation value", - Details: fmt.Sprintf("received unknown value at path: %s", req.Config.AttributePath), + Details: fmt.Sprintf("received unknown value at path: %s", path), }) return } - if len(req.Config.Value) < v.minimum || len(req.Config.Value) > v.maximum { - resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + if len(value.Value) < v.minimum || len(value.Value) > v.maximum { + diags = append(diags, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Value validation failed", - Details: fmt.Sprintf("%s with value %q %s", req.Config.AttributePath, req.Config.Value, v.Description(ctx)) + Details: fmt.Sprintf("%s with value %q %s", path, value.Value, v.Description(ctx)) }) return } return } - -func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { - return stringLengthBetweenValidator{ - maximum: maximum, - minimum: minimum, - } -} -``` - -Example provider code: - -```go -schema.Attribute{ - Type: types.StringType, - Required: true, - Validators: []AttributeValidator{ - ConflictsWithAttribute(tftypes.NewAttributePath().AttributeName("other_attribute")), - StringLengthBetween(1, 256), - }, -} ``` -### Future Considerations - -It is recommended that the framework provide an abstracted `*tftypes.AttributePath` rather than depend on that type directly, but this can converted after an initial implementation. This is purely for decoupling the two projects, similar to other abstracted types already created in the framework. - -To better support provider-based validation functionality in the future, it is also recommended that the `Provider` interface type also add a new `Configured(context.Context) bool` function or another methodology for easily checking the configuration state of a provider instance. Adding a setter function could also allow the framework to manage the provider configuration state automatically. This would simplify validations that require provider instances since it will likely be required that implementations need to check on this status as part of the validation logic. - -It is recommended that the framework or the upstream terraform-plugin-go module provide functionality to declare relative attribute paths, such as "this" and "parent" methods to better enable nested attribute declarations. This will enable provider developers to create attribute paths such as: - -```go -NewAttributePath(CurrentPath().Parent().AttributeName("other_attr")) -``` +In this scenario, it the implementor's responsibility to generate the appropriate diagnostic back, but they have full control of the output. It could be difficult for the framework to enforce implementation rules around these responses or potentially allow configuration overrides for them without creating more abstractions on top of this type or additional helper functions. Differing diagnostic implementations could introduce confusion for practitioners. -Strongly typed attribute validation can be introduced to simplify implementations for common value types, such as `types.String`. Future designs can discuss the potential designs and tradeoffs. +In general, this proposal feels very similar to either the generic `error` type or typed error proposals above (depending on the implmentation details) with minimal utility over them beyond complete output customization. However, the rest of the framework is designed around diagnostics so this would introduce a different implementation. To remain consistent with other framework design while still pushing for consistency, helpers could be introduced to nudge developers towards standardized summary information, if desired. From fa554f7d56b7e79268284e23664b8ed5c70ca315 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 16:13:50 -0400 Subject: [PATCH 30/35] docs/design: Additional proposal cleanup and clarity fixes --- docs/design/validation.md | 367 ++++---------------------------------- 1 file changed, 34 insertions(+), 333 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 5d54e62cb..acea4d408 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -996,6 +996,18 @@ Finally, these other considerations: ## Proposals +### Extending Plan Modifications Versus New Handling + +#### Extending Plan Modifications + +The [Plan Modifications design documentation](./plan-modifications.md) outlines proposals which broadly replace the previous framework's `CustomizeDiff` functionality. See that documentation for considerations and recommendations there. In this proposal for validation, new functions for validation would be provided within that framework, rather than introducing separate handling. + +Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. Another wrinkle is that plan modifications are only intended to run during `terraform plan` (`PlanResourceChanges` RPC) and `terraform apply` (`ApplyResourceChanges` RPC), so the framework would be introducing its own additional logic to extract and perform any validation functions during the `terraform validate` (`ValidateDataSourceConfig`/`ValidateProviderConfig`/`ValidateResourceConfig` RPCs). + +#### New Handling + +This framework can implement separate types and logic for validation. This aligns well with other design decisions in the framework and will enable it to provide targeted solutions that can capture context and functionality appropriately for various validation scenarios, which may not be appropriate if bundled with other functionality such as plan modifications. + ### Typed Parameters Versus Request and Response Types #### Typed Parameters @@ -1172,60 +1184,40 @@ func (p *customProvider) Validators(ctx context.Context) Validators { This declarative pattern enables reusable functions and built-in documentation for future enhancements. It is also consistent with proposed attribute level validations. -### Data Source and Resource Validation - -At a high level, request and response types will be provided to match the RPC calls and with consistency with the rest of the framework: - -```go - +### Attribute Validation +This validation would be applicable to the `schema.Attribute` types declared within the `GetSchema()` of `DataSourceType`, `Provider`, and `ResourceType` implementations. For most of these proposals, the framework would walk through all attribute paths during the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` calls, executing the declared validation in each attribute if present. -``` +#### Declaring Attribute Validation -An additional interface types will extend to the existing `DataSource` and `Resource` types so provider developers can provide customized multiple attribute validation across all attributes: +##### No Attribute Level Validation -```go -type DataSourceWithValidators interface { - DataSource - Validators(context.Context) Validators -} +This proposal would introduce no changes to `schema.Attribute`. Instead, this would require all attribute validation declarations at the `DataSource`, `Provider`, and `Resource` level. -type ResourceWithValidators interface { - Resource - Validators(context.Context) Validators -} -``` +This proposal makes any value validation behaviors occur at a distance, meaning it is harder for provider developers to correlate the validation logic to the name/path and type information. It would also be very verbose for even moderately sized schemas with thorough value validation. The only real potential benefit to this framework implementation is that it is very straightforward from the framework perspective. The logic would execute the top level list of validations instead of walking all attributes to find other attributes. -Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. +##### Individual Behavior Fields on `schema.Attribute` -As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: +Similar to the previous framework, individual fields for each attribute validation behavior could be added to the `schema.Attribute` type. For example: ```go -func (r *customResource) Validators(ctx context.Context) Validators { - return Validators{ - CustomValidator(*tftypes.AttributePath, *tftypes.Attribute), - } +schema.Attribute{ + // ... + ConflictsWith: /* ... */, + ValueValidation: /* ... */, } ``` -The request and response types will also be automatically handled by the framework to walk the provider schema and enable attribute-based value validation. This declarative pattern enables built-in documentation for future enhancements. - -### Attribute Validation - -This validation would be applicable to the `schema.Attribute` types declared within the `GetSchema()` of `DataSourceType`, `Provider`, and `ResourceType` implementations. For most of these proposals, the framework would walk through all attribute paths during the `ValidateDataSourceConfig`, `ValidateProviderConfig`, and `ValidateResourceConfig` calls, executing the declared validation in each attribute if present. - -#### Declaring Value Validation for Attributes +This proposal would feel familiar for existing provider developers and be relatively trivial for them to implement. One noticable downside to this approach however is that there can be any number of related, but disjointed attribute behaviors. The previous framework supported four behaviors in addition to value validation and there is logical room for addtional behaviors. Making updates to the `schema.Attribute` type becomes a limiting factor in this validation space. -This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. - -##### `ValueValidator` Field on `schema.Attribute` +##### `Validator` Field on `schema.Attribute` Similar to the previous framework, a new field can be added to the `schema.Attribute` type. For example: ```go schema.Attribute{ // ... - ValueValidator: T, + Validator: T, } ``` @@ -1234,7 +1226,7 @@ Implementators would be responsible for ensuring that single function covered al ```go schema.Attribute{ // ... - ValueValidator: All( + Validator: All( T, T, ), @@ -1245,14 +1237,14 @@ As seen with the previous framework in practice however, it was very common to i This proposal colocates the value validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. -##### `ValueValidators` Field on `schema.Attribute` +##### `Validators` Field on `schema.Attribute` A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: ```go schema.Attribute{ // ... - ValueValidators: []T{ + Validators: []T{ T, T, }, @@ -1273,312 +1265,21 @@ type Attribute interface { // ... } -type AttributeWithValueValidators struct { +type AttributeWithValidators struct { Attribute - ValueValidators []T + Validators []T } // or more interfaces -type AttributeWithValueValidators interface { +type AttributeWithValidators interface { Attribute - ValueValidators(/* ... */) []T + Validators(/* ... */) []T } ``` This type of proposal, in isolation, feels extraneous given the current attribute implementation. The framework does not appear to benefit from this splitting and it seems desirable that all attributes should be able to enable value validation via optional data on the existing type. -##### Resource Level Attribute Validation Handling - -This proposal would introduce no changes to `schema.Attribute`. Instead, this would require all attribute validation declarations at the `DataSource`, `Provider`, and `Resource` level. - -This proposal makes any value validation behaviors occur at a distance, meaning it is harder for provider developers to correlate the validation logic to the name/path and type information. It would also be very verbose for even moderately sized schemas with thorough value validation. The only real potential benefit to this framework implementation is that it is very straightforward from the framework perspective. The logic would execute the top level list of validations instead of walking all attributes to find other attributes. - -#### Defining Attribute Value Validation Functions - -This section includes examples with incoming types as `tftypes.AttributePath` and the `attr.Value` interface type with an output type of `error`. These implementation details are discussed later and only shown for simpler illustrative purposes here. - -##### `AttributeValueValidatorFunc` Type - -A new Go type could be created that defines the signature of a value validation function, similar to the previous framework `SchemaValidateFunc`. For example: - -```go -type AttributeValueValidatorFunc func(context.Context, path *tftypes.AttributePath, value attr.Value) error -``` - -To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: - -```go -type AttributeValueValidatorFunc func(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value attr.Value) error -``` - -While the simplest implementation, this proposal does not allow for documentation hooks. - -##### Single `attr.ValueValidator` Interface - -A new Go interface type could be created that defines an extensible value validation function type. For example: - -```go -type ValueValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, path *tftypes.AttributePath, value attr.Value) error -} -``` - -With an example implementation: - -```go -type stringLengthBetweenValidator struct { - ValueValidator - - maximum int - minimum int -} - -func (v stringLengthBetweenValidator) Description(_ context.Context) string { - return fmt.Sprintf("length must be between %d and %d", v.minimum, v.maximum) -} - -func (v stringLengthBetweenValidator) MarkdownDescription(_ context.Context) string { - return fmt.Sprintf("length must be between `%d` and `%d`", v.minimum, v.maximum) -} - -func (v stringLengthBetweenValidator) Validate(ctx context.Context, path *tftypes.AttributePath, rawValue attr.Value) error { - value, ok := rawValue.(types.String) - - if !ok { - return fmt.Errorf("%s with incorrect type: %T", path, rawValue) - } - - if value.Unknown { - return fmt.Errorf("%s with unknown value", path) - } - - if len(value.Value) < v.minimum || len(value.Value) > v.maximum { - return fmt.Errorf("%s with value %q %s", path, value.Value, v.Description(ctx)) - } - - return nil -} - -func StringLengthBetween(minimum int, maximum int) stringLengthBetweenValidator { - return stringLengthBetweenValidator{ - maximum: maximum, - minimum: minimum, - } -} -``` - -This helps solve the documentation issue with the following example slice type alias and receiver method: - -```go -// ValueValidators implements iteration functions across ValueValidator -type ValueValidators []ValueValidator - -// Descriptions returns all ValueValidator Description -func (vs ValueValidators) Descriptions(ctx context.Context) []string { - result := make([]string, 0, len(vs)) - - for _, v := range vs { - result = append(result, v.Description(ctx)) - } - return result -} -``` - -To support passing through the provider instance, a separate interface type could be introduced that includes a function call with the `tfsdk.Provider` interface type: - -```go -type ValueValidatorWithProvider interface { - ValueValidator - ValidateWithProvider(context.Context, provider tfsdk.Provider, path *tftypes.AttributePath, value attr.Value) error -} -``` - -However, this does not align well with the request and response model pattern used throughout the rest of the framework. Interface compatibility breaks if parameters or returns require adjustment for future changes. This single interface could not also handle any potential nuance between provider, data source, and resource validation. - -#### Attribute Validator Request and Response Pattern - -The framework could implement the request and response pattern for attribute validation. For example: - -```go -type ValidateAttributeRequest struct { - // AttributePath contains the path of the attribute. - AttributePath tftypes.AttributePath - - // AttributeConfig contains the value of the attribute. - AttributeConfig attr.Value - - // Config contains the entire configuration of the data source, provider, or resource. - Config tfsdk.Config -} - -type ValidateAttributeResponse struct { - Diagnostics []*tfprotov6.Diagnostic -} - -type AttributeValidator interface { - Description(context.Context) string - MarkdownDescription(context.Context) string - Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) -} -``` - -Provider instances for data sources or resources could be supported by providing further interface types: - -```go -// AttributeValidator is an interface type for declaring attribute validation that requires a provider instance. -type AttributeValidatorWithProvider interface { - AttributeValidator - ValidateWithProvider(context.Context, tfsdk.Provider, ValidateAttributeRequest, *ValidateAttributeResponse) -} -``` - -This would provide the lowest level and most customizable option to enable the framework and provider developers to abstract functionality on top. It also ensures compability can be maintained should parameters or returns necessitate changes, while also satisifying the ability for documentation hooks. - -One caveat to this single type is that it would not capture any nuance between data sources, providers, and resources should their RPCs provide differing functionality in the future. - -### Multiple Attribute Validation - -This framework should also provide the ability to handle validation situations across multiple attributes as noted in the goals. Some of the proposals from the [Single Attribute Value Validation](#single-attribute-value-validation) section are applicable for these proposals as well, so they are largely omitted here for brevity. Examples showing `attr.Value`, `*tftypes.AttributePath`, and bare `error` types are for illustrative purposes, whose final forms would be determined by those proposals. - -#### Declaring Multiple Attribute Validation for Attributes - -The previous framework implemented behaviors, such as `ConflictsWith`, as an individual field per behavior within each attribute. This section of proposals targets this specific functionality. One major caveat to these proposals is that they should not be considered exclusive to attribute value validations as it may be desirable to provide some consistency between the two implementations to improve developer experience. - -This section includes examples and details with `schema.Attribute` implemented as a Go structure type as it exists today. Future design considerations around creating specialized or custom attribute types may warrant switching this to an interface type with separate concrete types. - -##### Individual Behavior Fields on `schema.Attribute` - -Similar to the previous framework, individual fields for each attribute validation could be added to the `schema.Attribute` type which accepts multiple attribute paths. For example: - -```go -schema.Attribute{ - // ... - ConflictsWith: []tftypes.AttributePath, -} -``` - -A potential downside is that these behaviors cannot support the notion of conditional logic without changes to the implementations, since they can only be existence based if passed an attribute path. Allowing value validations in the declarations (on either side), could allieviate this issue. For example: - -```go -schema.Attribute{ - // ... - ConflictsWith: []func(AttributeValueValidator, tftypes.AttributePath, AttributeValueValidator), -} -``` - -Regardless of the potential value handling, this proposal would feel familiar for existing provider developers and be relatively trivial for them to implement. One noticable downside to this approach however is that there can be any number of related, but disjointed attribute behaviors. The previous framework supported four of these already and there is logical room for addtional behaviors, making updates to the `schema.Attribute` type a limiting factor in this validation space. This proposal also differs from value validation proposals, which are focused around a single field. - -##### `PathValidator` Field on `schema.Attribute` - -A new field for attribute validation can be added to the `schema.Attribute` type. For example: - -```go -schema.Attribute{ - // ... - PathValidator: T, -} -``` - -Implementators would be responsible for ensuring that single function covered all necessary validation. The framework could provide wrapper functions similar to the previous `All()` and `Any()` of `ValidateFunc` to allow simpler validations built from multiple functions. For example: - -```go -schema.Attribute{ - // ... - PathValidator: All( - T, - T, - ), -} -``` - -As seen with the previous framework in practice however, it was very common to implement the `All()` wrapper function. New provider developers would be responsible for understanding that multiple validations are possible in the single function field and knowing that custom validation functions may not be necessary to write if using the wrapper functions. - -This proposal colocates the attribute validation behaviors in the schema definition, meaning it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. - -##### `PathValidators` Field on `schema.Attribute` - -A new field that accepts a list of functions can be added to the `schema.Attribute` type. For example: - -```go -schema.Attribute{ - // ... - PathValidators: []T{ - T, - T, - }, -} -``` - -In this case, the framework would perform the validation similar to the previous framework `All()` wrapper function for `ValidateFunc`. The logical `AND` type of value validation is overwhelmingly more common in practice, which will simplify provider implementations. This still allows for an `Any()` based wrapper (logical `OR`) to be inserted if necessary. - -Colocating the attribute validation behaviors in the schema definition, means it is easier for provider developers to discover this type of validation and correlate the validation logic to the name and type information. This proposal will feel familiar to existing provider developers. New provider developers will immediately know that multiple validations are supported. - -##### Combined `Validators` Field on `schema.Attribute` - -A new field that accepts the union of [`ValueValidators` field on `schema.Attribute`](#valuevalidators-field-on-schemaattribute) and [`PathValidators` field on `schema.Attribute`](#pathvalidators-field-on-schemaattribute) can be added to the `schema.Attribute` type. For example: - -```go -schema.Attribute( - // ... - Validators: []I( - T1, - T2, - ) -) -``` - -Since value validation functions would inherently be implemented different than path validation functions and they are conceptually similar but different in certain ways, this could be complex to implement or understand correctly. When trying to handle documentation output for example, this framework or callers would need to distinguish between the two validation types to ensure the intended validation meanings are correct. - -##### Resource Level Attribute Path Validation Handling - -Rather than adjusting the `schema.Attribute` type for this type of validation, it could be forced to the resource (or data source) level. The [Declaring Multiple Attribute Validation for Resources](#declaring-multiple-attribute-validation-for-resources) proposals presented later are revelant for this section. To prevent proposal duplication, please see that section for more details and associated tradeoffs. - -#### Declaring Multiple Attribute Validation for Resources - -In the previous framework, the `CustomizeDiff` functionality enabled resource (or data source) level validation as a logical catch-all. These proposals cover the next iteration of that type of functionality. - -##### `PlanModifications` for Resources - -The [Plan Modifications design documentation](./plan-modifications.md) outlines proposals which broadly replace the previous framework's `CustomizeDiff` functionality. See that documentation for considerations and recommendations there. In this proposal for validation, new functions for validation would be provided within that framework, rather than introducing separate handling. - -Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. Another wrinkle is that plan modifications are only intended to run during `terraform plan` (`PlanResourceChanges` RPC) and `terraform apply` (`ApplyResourceChanges` RPC), so the framework would be introducing its own additional logic to extract and perform any validation functions during the `terraform validate` (`ValidateDataSourceConfig`/`ValidateProviderConfig`/`ValidateResourceConfig` RPCs). - -##### `Validators` for Resources - -This introduces a new extension interface type for `ResourceType` and `DataSourceType`. For example: - -```go -type DataSourceTypeWithValidators interface { - DataSourceType - Validators(context.Context) Validators -} - -type ResourceTypeWithValidators interface { - ResourceType - Validators(context.Context) Validators -} -``` - -Where `Validators` is a slice of types to be discussed later. - -As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: - -```go -func (t *customResourceType) Validators(ctx context.Context) Validators { - return Validators{ - ConflictingAttributes(*tftypes.AttributePath, *tftypes.Attribute), - ConflictingAttributesWithValues(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath, ValueValidator), - PrerequisiteAttribute(*tftypes.AttributePath, *tftypes.AttributePath), - PrerequisiteAttributeWithValue(*tftypes.AttributePath, ValueValidator, *tftypes.AttributePath), - } -} -``` - -This setup would allow for the framework to provide flexible resource level validation with a low amount of friction for provider developers. Helper functions would be extensible and make the behaviors clear. - ## Recommendations This section will summarize the proposals into specific recommendations for each topic. Code examples are provided in following sections to illustrate the concepts. The final section provides some future considerations for the framework and terraform-plugin-go. From 41fbf96d756d5bf8bbdf434071e0c0dd6e14b20d Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 16:25:02 -0400 Subject: [PATCH 31/35] docs/design: Add note that Terraform does not conceptually have differences between attribute value and multiple attribute validation --- docs/design/validation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/design/validation.md b/docs/design/validation.md index acea4d408..74e9e0393 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -122,6 +122,8 @@ Within these, there are two types of validation: - Single attribute value validation (e.g. string length) - Multiple attribute validation (e.g. attributes or attribute values that conflict with each other) +There is no difference between these types of validation to Terraform, as Terraform just works with errors and warnings being returned from providers, but the previous framework surfaced these different conceptually. + The next sections will outline some of the underlying details relevant to implementation proposals in this framework. ### Terraform Plugin Protocol From 47086a60ded91251f0f83d32fe834a84b2c64f8e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 16:27:58 -0400 Subject: [PATCH 32/35] docs/design: Remove incorrect schema.Attribute custom types sentence --- docs/design/validation.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 74e9e0393..b3460a13b 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -430,8 +430,6 @@ type Attribute struct { } ``` -Although later designs surrounding the ability to allow providers to define custom schema types may change this particular Go typing detail. - Values of `Attribute` in this framework are abstracted from the generic `tftypes` values into an `attr.Value` Go interface type: ```go From a168af3419b73ca659fd1206177913da646d5e51 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 16:29:13 -0400 Subject: [PATCH 33/35] Apply suggestions from code review Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> --- docs/design/validation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index b3460a13b..7e07a10b4 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -488,7 +488,7 @@ map[string]*schema.Schema{ } ``` -It supported single attribute value validation via the `ValidateFunc` or `ValidateDiagFunc` fields and multiple attribute validation via a collection of different fields (`AtLeastOneOf`, `ConflictsWith`, `ExactlyOneOf`, `RequiredWith`) which could be combined as necessary. For list, set, and map types, two additional fields (`MaxItems` and `MinItems`) provided validation for the number of elements. +It supported single attribute value validation via the `ValidateFunc` or `ValidateDiagFunc` fields and multiple attribute validation via a collection of different fields (`AtLeastOneOf`, `ConflictsWith`, `ExactlyOneOf`, `RequiredWith`) which could be combined as necessary. For list and set types, two additional fields (`MaxItems` and `MinItems`) provided validation for the number of elements. The multiple attribute validation support in the attribute schema is purely existence based, meaning it could not be conditional based on the attribute value. Conditional multiple attribute validation based on values was later added via the resource level `CustomizeDiff`, which will be described later on. @@ -710,7 +710,7 @@ resource "example_thing" "example" { ##### `MinItems` -This field enabled the schema to validate the minimum number of elements in a list, set, or map type. For example, +This field enabled the schema to validate the minimum number of elements in a list or set type. For example, ```go map[string]*schema.Schema{ From 9c0c726480c953afd3d337c9a07c03a4cccdc444 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 19 Jul 2021 17:19:01 -0400 Subject: [PATCH 34/35] docs/design: Clarify imperative versus declarative with data source, provider, and resource level validations --- docs/design/validation.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 7e07a10b4..87c155190 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -1148,29 +1148,47 @@ This will ensure that all features are compiler-checked for each validation requ ##### Imperative -An additional interface type can extend the existing `Provider` type so provider developers can enable advanced validation imperatively: +Additional interface types can extend the existing `DataSource`, `Provider`, and `Resource` types so provider developers can enable advanced validation imperatively: ```go +type DataSourceWithValidate interface { + DataSource + Validate(context.Context, ValidateDataSourceConfigRequest, *ValidateDataSourceConfigResponse) +} + type ProviderWithValidate interface { Provider Validate(context.Context, ValidateProviderConfigRequest, *ValidateProviderConfigResponse) } + +type ResourceWithValidate interface { + Resource + Validate(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) +} ``` This would enable simpler inline validation function creation as other proposals could require additional interface methods to be fulfilled. Documentation hooks are not provided here, instead relying on provider developers to include that information inline. Reusability is possible, however the implementation details are more complicated for provider developers. ##### Declarative -An additional interface type can extend the existing `Provider` type so provider developers can enable advanced validation declaratively: +Additional interface types can extend the existing `DataSource`, `Provider`, and `Resource` types so provider developers can enable advanced validation declaratively: ```go +type DataSourceWithValidators interface { + DataSource + Validators(context.Context) []T +} + type ProviderWithValidators interface { Provider - Validators(context.Context) Validators + Validators(context.Context) []T } -``` -Where `Validators` is a slice of types to be discussed later that uses or directly implements the request and response types. +type ResourceWithValidators interface { + Resource + Validators(context.Context) []T +} +``` As an example sketch, provider developers could introduce a function that fulfills the new interface with example helpers such as: From 190c3b0f39f02a5de8fabd8aac1b3b0a7aca449f Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 20 Jul 2021 07:44:13 -0400 Subject: [PATCH 35/35] docs/design: Validation verbiage and code recommendations from review --- docs/design/validation.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/design/validation.md b/docs/design/validation.md index 87c155190..1d9cc3f8b 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -122,7 +122,7 @@ Within these, there are two types of validation: - Single attribute value validation (e.g. string length) - Multiple attribute validation (e.g. attributes or attribute values that conflict with each other) -There is no difference between these types of validation to Terraform, as Terraform just works with errors and warnings being returned from providers, but the previous framework surfaced these different conceptually. +There is no difference between these types of validation to Terraform, as Terraform just works with errors and warnings being returned from providers, but the previous framework surfaced these concepts differently. The next sections will outline some of the underlying details relevant to implementation proposals in this framework. @@ -974,18 +974,18 @@ Allow provider developers access to all current types of provider validation: - Resource configurations (and configurations during plans) - Data Source configurations (and configurations during plans) -Including where possible: +Including support of these concepts where possible: - Single attribute value validation (e.g. string length) - Multiple attribute validation (e.g. attributes or attribute values that conflict with each other) -In terms of implementation, the following core concepts: +In terms of implementation, the following core concepts should be prioritized: -- Low level primitives (e.g other portions of the framework, external packages, and provider developers can implement higher level functionality) +- Composable building blocks (e.g other portions of the framework, external packages, and provider developers can implement higher level functionality) - Reusability between single attribute and multiple attribute validation functionality (e.g. attribute value functions) - Hooks for documentation (e.g. for future tooling such as provider documentation generators to self-document attributes) -Finally, these other considerations: +Finally, this design should have considerations for the following: - Providing the appropriate amount of contextual information for debugging purposes (e.g. logging) - Providing the appropriate amount of contextual information for practitioner facing output (e.g. paths and values involved with validation decisions) @@ -996,7 +996,7 @@ Finally, these other considerations: ## Proposals -### Extending Plan Modifications Versus New Handling +### Extending Plan Modifications Versus New Abstraction #### Extending Plan Modifications @@ -1004,7 +1004,7 @@ The [Plan Modifications design documentation](./plan-modifications.md) outlines Implementing against that design could prove complex for the framework as they are intended to serve differing purposes. It could also be confusing for provider developers in the same way that `CustomizeDiff` was confusing where differing logical rules applied to differing attribute value and operation scenarios. Another wrinkle is that plan modifications are only intended to run during `terraform plan` (`PlanResourceChanges` RPC) and `terraform apply` (`ApplyResourceChanges` RPC), so the framework would be introducing its own additional logic to extract and perform any validation functions during the `terraform validate` (`ValidateDataSourceConfig`/`ValidateProviderConfig`/`ValidateResourceConfig` RPCs). -#### New Handling +#### New Abstraction This framework can implement separate types and logic for validation. This aligns well with other design decisions in the framework and will enable it to provide targeted solutions that can capture context and functionality appropriately for various validation scenarios, which may not be appropriate if bundled with other functionality such as plan modifications. @@ -1042,13 +1042,13 @@ New Go type(s) could be created that define the signature of a validation functi type ValidationFunc func(context.Context, ValidationRequest, *ValidationResponse) ``` -To support passing through the provider instance to the function, the parameters would also need to include a `tfsdk.Provider` interface type: +To support passing through the provider instance to the function, the framework would either need to include a `tfsdk.Provider` field in the request type or as a separate parameter: ```go type ValidationFunc func(context.Context, provider tfsdk.Provider, ValidationRequest, *ValidationResponse) ``` -The main drawback of this approach is that it does not allow for documentation hooks. This also drifts from other design decisions of the framework without providing much benefit for the differing implementation. +For one-off implementations, the functionality can be written inline without creating an additional type. The main drawback of this approach is that it does not allow for documentation hooks. This also drifts from other design decisions of the framework without providing much benefit for the differing implementation. #### Interfaces @@ -1306,7 +1306,7 @@ This section will summarize the proposals into specific recommendations for each Defining all validation functionality via interface types will offer the framework the most flexibility for future enhancements while ensuring consistent implementations. The request and response pattern should be used to enable backwards (in the case of field deprecations) and forwards compatibility. -All validation should be implemented separately from plan modifications as they address differing concerns and operations within the Terraform. Attribute validations should be implemented as a slice of the interface type on `schema.Attribute` while Data Source, Provider, and Resource level validation should be implemented as new extension interface types. Further helper functions and designs can reduce implementation details. +All validation should be implemented separately from plan modifications as they address differing concerns and operations within Terraform. Attribute validations should be implemented as a slice of the interface type on `schema.Attribute` while Data Source, Provider, and Resource level validation should be implemented as new extension interface types. Further helper functions and designs can reduce implementation details. ### Data Source Example Implementation @@ -1504,7 +1504,7 @@ Example validation function code: ```go type stringLengthBetweenValidator struct { - StringValueValidator + AttributeValidator maximum int minimum int @@ -1574,11 +1574,11 @@ schema.Attribute{ ### Future Considerations -It is recommended that the framework provide an abstracted `*tftypes.AttributePath` rather than depend on that type directly, but this can converted after an initial implementation. This is purely for decoupling the two projects, similar to other abstracted types already created in the framework. +It is recommended to discuss whether the framework should provide an abstracted `*tftypes.AttributePath` rather than depend on that type directly. This can converted after an initial implementation and purely for decoupling the two projects, similar to other abstracted types already created in the framework. -To better support provider-based validation functionality in the future, it is also recommended that the `Provider` interface type also add a new `Configured(context.Context) bool` function or another methodology for easily checking the configuration state of a provider instance. Adding a setter function could also allow the framework to manage the provider configuration state automatically. This would simplify validations that require provider instances since it will likely be required that implementations need to check on this status as part of the validation logic. +To better support provider-based validation functionality in the future, it is also recommended to discuss whether the `Provider` interface type also add a new `Configured(context.Context) bool` function or another methodology for easily checking the configuration state of a provider instance. Adding a setter function could also allow the framework to manage the provider configuration state automatically. This would simplify validations that require provider instances since it will likely be required that implementations need to check on this status as part of the validation logic. -It is recommended that the framework or the upstream terraform-plugin-go module provide functionality to declare relative attribute paths, such as "this" and "parent" methods to better enable nested attribute declarations. This will enable provider developers to create attribute paths such as: +It is recommended to discuss whether the framework or the upstream terraform-plugin-go module provide functionality to declare relative attribute paths, such as "this" and "parent" methods to better enable nested attribute declarations. This will enable provider developers to create attribute paths such as: ```go NewAttributePath(CurrentPath().Parent().AttributeName("other_attr"))