From 639eda9555a1c121ba092426805256b689b4ea2a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 13:28:48 -0400 Subject: [PATCH 01/55] initial ephemeral resource interfaces --- ephemeral/close.go | 35 ++ ephemeral/config_validator.go | 28 ++ ephemeral/configure.go | 33 ++ ephemeral/doc.go | 23 + ephemeral/ephemeral_resource.go | 98 +++++ ephemeral/metadata.go | 23 + ephemeral/open.go | 53 +++ ephemeral/renew.go | 68 +++ ephemeral/schema.go | 26 ++ ephemeral/schema/attribute.go | 39 ++ ephemeral/schema/block.go | 30 ++ ephemeral/schema/doc.go | 8 + ephemeral/schema/schema.go | 187 +++++++++ ephemeral/validate_config.go | 32 ++ internal/fwschemadata/data_description.go | 6 + provider/provider.go | 16 + tfsdk/ephemeral_state.go | 94 +++++ tfsdk/ephemeral_state_test.go | 487 ++++++++++++++++++++++ 18 files changed, 1286 insertions(+) create mode 100644 ephemeral/close.go create mode 100644 ephemeral/config_validator.go create mode 100644 ephemeral/configure.go create mode 100644 ephemeral/doc.go create mode 100644 ephemeral/ephemeral_resource.go create mode 100644 ephemeral/metadata.go create mode 100644 ephemeral/open.go create mode 100644 ephemeral/renew.go create mode 100644 ephemeral/schema.go create mode 100644 ephemeral/schema/attribute.go create mode 100644 ephemeral/schema/block.go create mode 100644 ephemeral/schema/doc.go create mode 100644 ephemeral/schema/schema.go create mode 100644 ephemeral/validate_config.go create mode 100644 tfsdk/ephemeral_state.go create mode 100644 tfsdk/ephemeral_state_test.go diff --git a/ephemeral/close.go b/ephemeral/close.go new file mode 100644 index 000000000..1b76279b7 --- /dev/null +++ b/ephemeral/close.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// CloseRequest represents a request for the provider to close an ephemeral +// resource. An instance of this request struct is supplied as an argument to +// the ephemeral resource's Close function. +type CloseRequest struct { + // PriorState is the object representing the values of the ephemeral + // resource following the latest Open or Renew operation. + PriorState tfsdk.EphemeralState + + // Private is provider-defined ephemeral resource private state data + // which was previously provided by the latest Open or Renew operation. + // + // Use the GetKey method to read data. + Private *privatestate.ProviderData +} + +// CloseResponse represents a response to a CloseRequest. An +// instance of this response struct is supplied as an argument +// to the ephemeral resource's Close function, in which the provider +// should set values on the CloseResponse as appropriate. +type CloseResponse struct { + // Diagnostics report errors or warnings related to creating the + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/config_validator.go b/ephemeral/config_validator.go new file mode 100644 index 000000000..782718d8d --- /dev/null +++ b/ephemeral/config_validator.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import "context" + +// ConfigValidator describes reusable EphemeralResource configuration validation functionality. +type ConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to ephemeral resource plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to ephemeral resource Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // ValidateEphemeralResource performs the validation. + // + // This method name is separate from the datasource.ConfigValidator + // interface ValidateDataSource method name, provider.ConfigValidator + // interface ValidateProvider method name, and resource.ConfigValidator + // interface ValidateResource method name to allow generic validators. + ValidateEphemeralResource(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/ephemeral/configure.go b/ephemeral/configure.go new file mode 100644 index 000000000..37e374e05 --- /dev/null +++ b/ephemeral/configure.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// ConfigureRequest represents a request for the provider to configure an +// ephemeral resource, i.e., set provider-level data or clients. An instance of +// this request struct is supplied as an argument to the EphemeralResource type +// Configure method. +type ConfigureRequest struct { + // ProviderData is the data set in the + // [provider.ConfigureResponse.EphemeralResourceData] field. This data is + // provider-specifc and therefore can contain any necessary remote system + // clients, custom provider data, or anything else pertinent to the + // functionality of the EphemeralResource. + // + // This data is only set after the ConfigureProvider RPC has been called + // by Terraform. + ProviderData any +} + +// ConfigureResponse represents a response to a ConfigureRequest. An +// instance of this response struct is supplied as an argument to the +// EphemeralResource type Configure method. +type ConfigureResponse struct { + // Diagnostics report errors or warnings related to configuring of the + // Datasource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/doc.go b/ephemeral/doc.go new file mode 100644 index 000000000..4ac994cd8 --- /dev/null +++ b/ephemeral/doc.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package ephemeral contains all interfaces, request types, and response +// types for an ephemeral resource implementation. +// +// In Terraform, an ephemeral resource is a concept which enables provider +// developers to offer practitioners ephemeral values, which will not be stored +// in any artifact produced by Terraform (plan/state). Ephemeral resources can +// optionally implement renewal logic via the (EphemeralResource).Renew method +// and cleanup logic via the (EphemeralResource).Close method. +// +// Ephemeral resources are not saved into the Terraform plan or state and can +// only be referenced in other ephemeral values, such as provider configuration +// attributes. Ephemeral resources are defined by a type/name, such as "examplecloud_thing", +// a schema representing the structure and data types of configuration and lifecycle logic. +// +// The main starting point for implementations in this package is the +// EphemeralResource type which represents an instance of an ephemeral resource +// that has its own configuration and lifecycle logic. The [ephemeral.EphemeralResource] +// implementations are referenced by the [provider.ProviderWithEphemeralResources] type +// EphemeralResources method, which enables the ephemeral resource practitioner usage. +package ephemeral diff --git a/ephemeral/ephemeral_resource.go b/ephemeral/ephemeral_resource.go new file mode 100644 index 000000000..cfdc5b667 --- /dev/null +++ b/ephemeral/ephemeral_resource.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "context" +) + +// EphemeralResource represents an instance of an ephemeral resource type. This is the core +// interface that all ephemeral resources must implement. +// +// Ephemeral resources can optionally implement these additional concepts: +// +// - Configure: Include provider-level data or clients via EphemeralResourceWithConfigure +// - Validation: Schema-based or entire configuration via EphemeralResourceWithConfigValidators +// or EphemeralResourceWithValidateConfig. +// - Renewal: Refresh ephemeral values, such as a temporary access token, +// via EphemeralResourceWithRenew. Ephemeral resources can indicate to Terraform when +// a renewal must occur via the RenewAt response field of the Open/Renew methods. +// - Close: Allows providers to clean up the ephemeral resource +// via EphemeralResourceWithClose. +type EphemeralResource interface { + // Metadata should return the full name of the ephemeral resource, such as + // examplecloud_thing. + Metadata(context.Context, MetadataRequest, *MetadataResponse) + + // Schema should return the schema for this ephemeral resource. + Schema(context.Context, SchemaRequest, *SchemaResponse) + + // Open is called when the provider must generate a new ephemeral resource. Config values + // should be read from the OpenRequest and new response values set on the OpenResponse. + Open(context.Context, OpenRequest, *OpenResponse) +} + +// EphemeralResourceWithRenew is an interface type that extends EphemeralResource to +// include a method which the framework will call when Terraform detects that the +// provider-defined returned RenewAt time for an ephemeral resource has passed. This RenewAt +// response field can be set in the OpenResponse and RenewResponse. +type EphemeralResourceWithRenew interface { + EphemeralResource + + // Renew is called when the provider must refresh the ephemeral resource values based on + // the provided RenewAt time. This RenewAt response field can be set in the OpenResponse and RenewResponse. + Renew(context.Context, RenewRequest, *RenewResponse) +} + +// EphemeralResourceWithClose is an interface type that extends +// EphemeralResource to include a method which the framework will call when +// Terraform determines that the ephemeral values can be safely cleaned up. +type EphemeralResourceWithClose interface { + EphemeralResource + + // Close is called when the provider can clean up the ephemeral resource. + // Config values may be read from the CloseRequest. + Close(context.Context, CloseRequest, *CloseResponse) +} + +// EphemeralResourceWithConfigure is an interface type that extends EphemeralResource to +// include a method which the framework will automatically call so provider +// developers have the opportunity to setup any necessary provider-level data +// or clients in the EphemeralResource type. +type EphemeralResourceWithConfigure interface { + EphemeralResource + + // Configure enables provider-level data or clients to be set in the + // provider-defined EphemeralResource type. + Configure(context.Context, ConfigureRequest, *ConfigureResponse) +} + +// EphemeralResourceWithConfigValidators is an interface type that extends EphemeralResource to include declarative validations. +// +// Declaring validation using this methodology simplifies implmentation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type EphemeralResourceWithConfigValidators interface { + EphemeralResource + + // ConfigValidators returns a list of functions which will all be performed during validation. + ConfigValidators(context.Context) []ConfigValidator +} + +// EphemeralResourceWithValidateConfig is an interface type that extends EphemeralResource to include imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single ephemeral resource. Any documentation +// of this functionality must be manually added into schema descriptions. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type EphemeralResourceWithValidateConfig interface { + EphemeralResource + + // ValidateConfig performs the validation. + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/ephemeral/metadata.go b/ephemeral/metadata.go new file mode 100644 index 000000000..ed97522b8 --- /dev/null +++ b/ephemeral/metadata.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +// MetadataRequest represents a request for the EphemeralResource to return metadata, +// such as its type name. An instance of this request struct is supplied as +// an argument to the EphemeralResource type Metadata method. +type MetadataRequest struct { + // ProviderTypeName is the string returned from + // [provider.MetadataResponse.TypeName], if the Provider type implements + // the Metadata method. This string should prefix the EphemeralResource type name + // with an underscore in the response. + ProviderTypeName string +} + +// MetadataResponse represents a response to a MetadataRequest. An +// instance of this response struct is supplied as an argument to the +// EphemeralResource type Metadata method. +type MetadataResponse struct { + // TypeName should be the full ephemeral resource type, including the provider + // type prefix and an underscore. For example, examplecloud_thing. + TypeName string +} diff --git a/ephemeral/open.go b/ephemeral/open.go new file mode 100644 index 000000000..76db963ca --- /dev/null +++ b/ephemeral/open.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// OpenRequest represents a request for the provider to open an ephemeral +// resource. An instance of this request struct is supplied as an argument to +// the ephemeral resource's Open function. +type OpenRequest struct { + // Config is the configuration the user supplied for the ephemeral + // resource. + Config tfsdk.Config +} + +// OpenResponse represents a response to a OpenRequest. An +// instance of this response struct is supplied as an argument +// to the ephemeral resource's Open function, in which the provider +// should set values on the OpenResponse as appropriate. +type OpenResponse struct { + // State is the object representing the values of the ephemeral + // resource following the Open operation. This field is pre-populated + // from OpenRequest.Config and should be set during the resource's Open + // operation. + State tfsdk.EphemeralState + + // Private is the private state ephemeral resource data following the + // Open operation. This field is not pre-populated as there is no + // pre-existing private state data during the ephemeral resource's + // Open operation. + Private *privatestate.ProviderData + + // RenewAt is an optional date/time field that indicates to Terraform + // when this ephemeral resource must be renewed at. Terraform will call + // the (EphemeralResource).Renew method when the current date/time is on + // or after RenewAt during a Terraform operation. + // + // It is recommended to provide small leeway before an ephemeral resource + // expires, usually no more than a few minutes, to account for clock + // skew. + RenewAt time.Time + + // Diagnostics report errors or warnings related to creating the + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/renew.go b/ephemeral/renew.go new file mode 100644 index 000000000..9304e50fd --- /dev/null +++ b/ephemeral/renew.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// RenewRequest represents a request for the provider to renew an ephemeral +// resource. An instance of this request struct is supplied as an argument to +// the ephemeral resource's Renew function. +type RenewRequest struct { + // PriorState is the object representing the values of the ephemeral + // resource following the latest Open or Renew operation. + PriorState tfsdk.EphemeralState + + // TODO: Still being discussed, but we likely don't need config in the request, + // since PriorState should be guaranteed to contain the configuration values + state response values. + // + // Config is the configuration the user supplied for the ephemeral + // resource. + Config tfsdk.Config + + // Private is provider-defined ephemeral resource private state data + // which was previously provided by the latest Open or Renew operation. + // Any existing data is copied to RenewResponse.Private to prevent + // accidental private state data loss. + // + // Use the GetKey method to read data. Use the SetKey method on + // RenewResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// RenewResponse represents a response to a RenewRequest. An +// instance of this response struct is supplied as an argument +// to the ephemeral resource's Renew function, in which the provider +// should set values on the RenewResponse as appropriate. +type RenewResponse struct { + // State is the object representing the values of the ephemeral + // resource following the Renew operation. This field is pre-populated + // from RenewRequest.PriorState and should be set during the resource's + // Renew operation. + State tfsdk.EphemeralState + + // RenewAt is an optional date/time field that indicates to Terraform + // when this ephemeral resource must be renewed at. Terraform will call + // the (EphemeralResource).Renew method when the current date/time is on + // or after RenewAt during a Terraform operation. + // + // It is recommended to provide small leeway before an ephemeral resource + // expires, usually no more than a few minutes, to account for clock + // skew. + RenewAt time.Time + + // Private is the private state ephemeral resource data following the + // Renew operation. This field is pre-populated from RenewRequest.Private + // and can be modified during the ephemeral resource's Renew operation. + Private *privatestate.ProviderData + + // Diagnostics report errors or warnings related to creating the + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/schema.go b/ephemeral/schema.go new file mode 100644 index 000000000..bf674a87b --- /dev/null +++ b/ephemeral/schema.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" +) + +// SchemaRequest represents a request for the EphemeralResource to return its schema. +// An instance of this request struct is supplied as an argument to the +// EphemeralResource type Schema method. +type SchemaRequest struct{} + +// SchemaResponse represents a response to a SchemaRequest. An instance of this +// response struct is supplied as an argument to the EphemeralResource type Schema +// method. +type SchemaResponse struct { + // Schema is the schema of the ephemeral resource. + Schema schema.Schema + + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/schema/attribute.go b/ephemeral/schema/attribute.go new file mode 100644 index 000000000..8b6ebe60b --- /dev/null +++ b/ephemeral/schema/attribute.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Attribute defines a value field inside the Schema. Implementations in this +// package include: +// - BoolAttribute +// - DynamicAttribute +// - Float32Attribute +// - Float64Attribute +// - Int32Attribute +// - Int64Attribute +// - ListAttribute +// - MapAttribute +// - NumberAttribute +// - ObjectAttribute +// - SetAttribute +// - StringAttribute +// +// Additionally, the NestedAttribute interface extends Attribute with nested +// attributes. Only supported in protocol version 6. Implementations in this +// package include: +// - ListNestedAttribute +// - MapNestedAttribute +// - SetNestedAttribute +// - SingleNestedAttribute +// +// In practitioner configurations, an equals sign (=) is required to set +// the value. [Configuration Reference] +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Attribute interface { + fwschema.Attribute +} diff --git a/ephemeral/schema/block.go b/ephemeral/schema/block.go new file mode 100644 index 000000000..f741d8f8e --- /dev/null +++ b/ephemeral/schema/block.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Block defines a structural field inside a Schema. Implementations in this +// package include: +// - ListNestedBlock +// - SetNestedBlock +// - SingleNestedBlock +// +// In practitioner configurations, an equals sign (=) cannot be used to set the +// value. Blocks are instead repeated as necessary, or require the use of +// [Dynamic Block Expressions]. +// +// Prefer NestedAttribute over Block. Blocks should typically be used for +// configuration compatibility with previously existing schemas from an older +// Terraform Plugin SDK. Efforts should be made to convert from Block to +// NestedAttribute as a breaking change for practitioners. +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Block interface { + fwschema.Block +} diff --git a/ephemeral/schema/doc.go b/ephemeral/schema/doc.go new file mode 100644 index 000000000..2b20cfae0 --- /dev/null +++ b/ephemeral/schema/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package schema contains all available schema functionality for ephemeral resources. +// Ephemeral resource schemas define the structure and value types for configuration +// and state data. Schemas are implemented via the ephemeral.EphemeralResource type +// Schema method. +package schema diff --git a/ephemeral/schema/schema.go b/ephemeral/schema/schema.go new file mode 100644 index 000000000..3c92269fb --- /dev/null +++ b/ephemeral/schema/schema.go @@ -0,0 +1,187 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Schema must satify the fwschema.Schema interface. +var _ fwschema.Schema = Schema{} + +// Schema defines the structure and value types of ephemeral resource data. This type +// is used as the ephemeral.SchemaResponse type Schema field, which is +// implemented by the ephemeral.EphemeralResource type Schema method. +type Schema struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this ephemeral resource 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 ephemeral resource is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this ephemeral resource. The warning diagnostic + // summary is automatically set to "Ephemeral Resource Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Use examplecloud_other ephemeral resource instead. This ephemeral resource + // will be removed in the next major version of the provider." + // - "Remove this ephemeral resource as it no longer is valid and + // will be removed in the next major version of the provider." + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// schema. +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes returns the Attributes field value. +func (s Schema) GetAttributes() map[string]fwschema.Attribute { + return schemaAttributes(s.Attributes) +} + +// GetBlocks returns the Blocks field value. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(s.Blocks) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (s Schema) GetDeprecationMessage() string { + return s.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (s Schema) GetDescription() string { + return s.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (s Schema) GetMarkdownDescription() string { + return s.MarkdownDescription +} + +// GetVersion always returns 0 as ephemeral resource schemas cannot be versioned. +func (s Schema) GetVersion() int64 { + return 0 +} + +// Type returns the framework type of the schema. +func (s Schema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath returns the framework type at the given schema path. +func (s Schema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) +} + +// TypeAtTerraformPath returns the framework type at the given tftypes path. +func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} + +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +// +// Deprecated: Use the ValidateImplementation method instead. +func (s Schema) Validate() diag.Diagnostics { + return s.ValidateImplementation(context.Background()) +} + +// ValidateImplementation contains logic for validating the provider-defined +// implementation of the schema and underlying attributes and blocks to prevent +// unexpected errors or panics. This logic runs during the GetProviderSchema +// RPC, or via provider-defined unit testing, and should never include false +// positives. +func (s Schema) ValidateImplementation(ctx context.Context) diag.Diagnostics { + var diags diag.Diagnostics + + for attributeName, attribute := range s.GetAttributes() { + req := fwschema.ValidateImplementationRequest{ + Name: attributeName, + Path: path.Root(attributeName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateAttributeImplementation(ctx, attribute, req)...) + } + + for blockName, block := range s.GetBlocks() { + req := fwschema.ValidateImplementationRequest{ + Name: blockName, + Path: path.Root(blockName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateBlockImplementation(ctx, block, req)...) + } + + return diags +} + +// schemaAttributes is a ephemeral resource to fwschema type conversion function. +func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { + result := make(map[string]fwschema.Attribute, len(attributes)) + + for name, attribute := range attributes { + result[name] = attribute + } + + return result +} + +// schemaBlocks is a ephemeral resource to fwschema type conversion function. +func schemaBlocks(blocks map[string]Block) map[string]fwschema.Block { + result := make(map[string]fwschema.Block, len(blocks)) + + for name, block := range blocks { + result[name] = block + } + + return result +} diff --git a/ephemeral/validate_config.go b/ephemeral/validate_config.go new file mode 100644 index 000000000..ace26d48e --- /dev/null +++ b/ephemeral/validate_config.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateConfigRequest represents a request to validate the +// configuration of an ephemeral resource. An instance of this request struct is +// supplied as an argument to the EphemeralResource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigRequest struct { + // Config is the configuration the user supplied for the ephemeral resource. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config tfsdk.Config +} + +// ValidateConfigResponse represents a response to a +// ValidateConfigRequest. An instance of this response struct is +// supplied as an argument to the EphemeralResource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigResponse struct { + // Diagnostics report errors or warnings related to validating the ephemeral resource + // configuration. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics diag.Diagnostics +} diff --git a/internal/fwschemadata/data_description.go b/internal/fwschemadata/data_description.go index c002e9883..b72c86b1c 100644 --- a/internal/fwschemadata/data_description.go +++ b/internal/fwschemadata/data_description.go @@ -15,6 +15,10 @@ const ( // DataDescriptionState is used for Data that represents // a state-based value. DataDescriptionState DataDescription = "state" + + // DataDescriptionEphemeralState is used for Data that represents + // an ephemeral state-based value. + DataDescriptionEphemeralState DataDescription = "ephemeral-state" ) // DataDescription is a human friendly type for Data. Used in error @@ -40,6 +44,8 @@ func (d DataDescription) Title() string { return "Plan" case DataDescriptionState: return "State" + case DataDescriptionEphemeralState: + return "Ephemeral State" default: return "Data" } diff --git a/provider/provider.go b/provider/provider.go index ff0d18e81..61dfd39fe 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -85,6 +86,21 @@ type ProviderWithFunctions interface { Functions(context.Context) []func() function.Function } +// ProviderWithEphemeralResources is an interface type that extends Provider to +// include ephemeral resources for usage in practitioner configurations. +// +// Ephemeral resources are supported in Terraform version 1.10 and later. +type ProviderWithEphemeralResources interface { + Provider + + // EphemeralResources returns a slice of functions to instantiate each EphemeralResource + // implementation. + // + // The ephemeral resource type name is determined by the EphemeralResource implementing + // the Metadata method. All ephemeral resources must have unique names. + EphemeralResources(context.Context) []func() ephemeral.EphemeralResource +} + // ProviderWithMetaSchema is a provider with a provider meta schema, which // is configured by practitioners via the provider_meta configuration block // and the configuration data is included with certain data source and resource diff --git a/tfsdk/ephemeral_state.go b/tfsdk/ephemeral_state.go new file mode 100644 index 000000000..8053d1d2b --- /dev/null +++ b/tfsdk/ephemeral_state.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// EphemeralState represents the state for a Terraform ephemeral resource. +type EphemeralState struct { + Raw tftypes.Value + Schema fwschema.Schema +} + +// Get populates the struct passed as `target` with the entire ephemeral state. +func (s EphemeralState) Get(ctx context.Context, target interface{}) diag.Diagnostics { + return s.data().Get(ctx, target) +} + +// GetAttribute retrieves the attribute or block found at `path` and populates +// the `target` with the value. This method is intended for top level schema +// attributes or blocks. Use `types` package methods or custom types to step +// into collections. +// +// Attributes or elements under null or unknown collections return null +// values, however this behavior is not protected by compatibility promises. +func (s EphemeralState) GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics { + return s.data().GetAtPath(ctx, path, target) +} + +// PathMatches returns all matching path.Paths from the given path.Expression. +// +// If a parent path is null or unknown, which would prevent a full expression +// from matching, the parent path is returned rather than no match to prevent +// false positives. +func (s EphemeralState) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return s.data().PathMatches(ctx, pathExpr) +} + +// Set populates the entire ephemeral state using the supplied Go value. The value `val` +// should be a struct whose values have one of the attr.Value types. Each field +// must be tagged with the corresponding schema field. +func (s *EphemeralState) Set(ctx context.Context, val interface{}) diag.Diagnostics { + data := s.data() + diags := data.Set(ctx, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +// SetAttribute sets the attribute at `path` using the supplied Go value. +// +// The attribute path and value must be valid with the current schema. If the +// attribute path already has a value, it will be overwritten. If the attribute +// path does not have a value, it will be added, including any parent attribute +// paths as necessary. +// +// The value must not be an untyped nil. Use a typed nil or types package null +// value function instead. For example with a types.StringType attribute, +// use (*string)(nil) or types.StringNull(). +// +// Lists can only have the next element added according to the current length. +func (s *EphemeralState) SetAttribute(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics { + data := s.data() + diags := data.SetAtPath(ctx, path, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +func (s EphemeralState) data() *fwschemadata.Data { + return &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionEphemeralState, + Schema: s.Schema, + TerraformValue: s.Raw, + } +} diff --git a/tfsdk/ephemeral_state_test.go b/tfsdk/ephemeral_state_test.go new file mode 100644 index 000000000..08f7691f7 --- /dev/null +++ b/tfsdk/ephemeral_state_test.go @@ -0,0 +1,487 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk_test + +import ( + "context" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + intreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestEphemeralStateGet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ephemeralState tfsdk.EphemeralState + target any + expected any + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataGet for more exhaustive unit testing. + // These test cases are to ensure EphemeralState schema and data values are + // passed appropriately to the shared implementation. + "valid": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, "test"), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Optional: true, + Type: types.StringType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"string"` + }), + expected: &struct { + String types.String `tfsdk:"string"` + }{ + String: types.StringValue("test"), + }, + }, + "diagnostic": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "bool": tftypes.NewValue(tftypes.Bool, nil), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "bool": testschema.Attribute{ + Optional: true, + Type: types.BoolType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"bool"` + }), + expected: &struct { + String types.String `tfsdk:"bool"` + }{ + String: types.String{}, + }, + expectedDiags: diag.Diagnostics{ + diag.WithPath( + path.Root("bool"), + intreflect.DiagNewAttributeValueIntoWrongType{ + ValType: reflect.TypeOf(types.Bool{}), + TargetType: reflect.TypeOf(types.String{}), + SchemaType: types.BoolType, + }, + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.ephemeralState.Get(context.Background(), testCase.target) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(testCase.target, testCase.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestEphemeralStateGetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + ephemeralState tfsdk.EphemeralState + target interface{} + expected interface{} + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataGetAtPath for more exhaustive unit + // testing. These test cases are to ensure EphemeralState schema and data values + // are passed appropriately to the shared implementation. + "valid": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: types.StringType, + Required: true, + }, + }, + }, + }, + target: new(string), + expected: pointer("namevalue"), + }, + "diagnostics": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + }, + }, + }, + target: new(testtypes.String), + expected: &testtypes.String{InternalString: types.StringValue("namevalue"), CreatedBy: testtypes.StringTypeWithValidateWarning{}}, + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(path.Root("name"))}, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.ephemeralState.GetAttribute(context.Background(), path.Root("name"), tc.target) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.target, tc.expected, cmp.Transformer("testtypes", func(in *testtypes.String) testtypes.String { return *in }), cmp.Transformer("types", func(in *types.String) types.String { return *in })); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestEphemeralStatePathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ephemeralState tfsdk.EphemeralState + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataPathMatches for more exhaustive unit testing. + // These test cases are to ensure EphemeralState schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + ephemeralState: tfsdk.EphemeralState{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + ephemeralState: tfsdk.EphemeralState{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("not-test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Path Expression for Schema", + "The Terraform Provider unexpectedly provided a path expression that does not match the current schema. "+ + "This can happen if the path expression does not correctly follow the schema in structure or types. "+ + "Please report this to the provider developers.\n\n"+ + "Path Expression: not-test", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.ephemeralState.PathMatches(context.Background(), testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestEphemeralStateSet(t *testing.T) { + t.Parallel() + + type testCase struct { + ephemeralState tfsdk.EphemeralState + val interface{} + expected tftypes.Value + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataSet for more exhaustive unit testing. + // These test cases are to ensure EphemeralState schema and data values are + // passed appropriately to the shared implementation. + "valid": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "oldvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: types.StringType, + Required: true, + }, + }, + }, + }, + val: struct { + Name string `tfsdk:"name"` + }{ + Name: "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }, + "diagnostics": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.Value{}, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + }, + }, + }, + val: struct { + Name string `tfsdk:"name"` + }{ + Name: "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newvalue"), + }), + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(path.Root("name"))}, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.ephemeralState.Set(context.Background(), tc.val) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.ephemeralState.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestEphemeralStateSetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + ephemeralState tfsdk.EphemeralState + path path.Path + val interface{} + expected tftypes.Value + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataSetAtPath for more exhaustive unit + // testing. These test cases are to ensure EphemeralState schema and data values + // are passed appropriately to the shared implementation. + "valid": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + Required: true, + }, + "other": testschema.Attribute{ + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: path.Root("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "diagnostics": { + ephemeralState: tfsdk.EphemeralState{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "originalname"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + }, + }, + }, + path: path.Root("name"), + val: "newname", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newname"), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(path.Root("name")), + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.ephemeralState.SetAttribute(context.Background(), tc.path, tc.val) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + for _, diagnostic := range diags { + t.Log(diagnostic) + } + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.ephemeralState.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} From dfd0cdc8e5314badfa77b9530bfd997b6af155b3 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 13:50:48 -0400 Subject: [PATCH 02/55] add ephemeral resource configure data --- internal/fwserver/server.go | 5 +++++ internal/fwserver/server_configureprovider.go | 1 + .../fwserver/server_configureprovider_test.go | 18 ++++++++++++++++++ provider/configure.go | 5 +++++ .../docs/plugin/framework/providers/index.mdx | 4 ++-- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 5a0f90722..e9f8a55d1 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -33,6 +33,11 @@ type Server struct { // to [resource.ConfigureRequest.ProviderData]. ResourceConfigureData any + // EphemeralResourceConfigureData is the + // [provider.ConfigureResponse.EphemeralResourceData] field value which is passed + // to [ephemeral.ConfigureRequest.ProviderData]. + EphemeralResourceConfigureData any + // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index 2e04bc046..0b1807bce 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -37,4 +37,5 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR s.deferred = resp.Deferred s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData + s.EphemeralResourceConfigureData = resp.EphemeralResourceData } diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index f7e29568e..f8dd1e9e0 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -178,6 +178,20 @@ func TestServerConfigureProvider(t *testing.T) { }, }, }, + "response-ephemeralresourcedata": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.EphemeralResourceData = "test-provider-configure-value" + }, + }, + }, + request: &provider.ConfigureRequest{}, + expectedResponse: &provider.ConfigureResponse{ + EphemeralResourceData: "test-provider-configure-value", + }, + }, "response-invalid-deferral-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -235,6 +249,10 @@ func TestServerConfigureProvider(t *testing.T) { if diff := cmp.Diff(testCase.server.ResourceConfigureData, testCase.expectedResponse.ResourceData); diff != "" { t.Errorf("unexpected server.ResourceConfigureData difference: %s", diff) } + + if diff := cmp.Diff(testCase.server.EphemeralResourceConfigureData, testCase.expectedResponse.EphemeralResourceData); diff != "" { + t.Errorf("unexpected server.EphemeralResourceConfigureData difference: %s", diff) + } }) } } diff --git a/provider/configure.go b/provider/configure.go index 9b6678bf7..59e9ead44 100644 --- a/provider/configure.go +++ b/provider/configure.go @@ -62,6 +62,11 @@ type ConfigureResponse struct { // that implements the Configure method. ResourceData any + // EphemeralResourceData is provider-defined data, clients, etc. that is + // passed to [ephemeral.ConfigureRequest.ProviderData] for each + // EphemeralResource type that implements the Configure method. + EphemeralResourceData any + // Deferred indicates that Terraform should automatically defer // all resources and data sources for this provider. // diff --git a/website/docs/plugin/framework/providers/index.mdx b/website/docs/plugin/framework/providers/index.mdx index dc3352c33..1de1f106e 100644 --- a/website/docs/plugin/framework/providers/index.mdx +++ b/website/docs/plugin/framework/providers/index.mdx @@ -196,8 +196,8 @@ func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.Confi // Not returning early allows the logic to collect all errors. } - // Create data/clients and persist to resp.DataSourceData and - // resp.ResourceData as appropriate. + // Create data/clients and persist to resp.DataSourceData, resp.ResourceData, + // and resp.EphemeralResourceData as appropriate. } ``` From fe2b7dbbdad8ac4e45efee7e4a9eb1fd08003fcf Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 14:58:13 -0400 Subject: [PATCH 03/55] attribute implementations --- ephemeral/schema/bool_attribute.go | 187 +++++++ ephemeral/schema/bool_attribute_test.go | 425 ++++++++++++++++ ephemeral/schema/dynamic_attribute.go | 188 +++++++ ephemeral/schema/dynamic_attribute_test.go | 425 ++++++++++++++++ ephemeral/schema/float32_attribute.go | 191 +++++++ ephemeral/schema/float32_attribute_test.go | 426 ++++++++++++++++ ephemeral/schema/float64_attribute.go | 190 +++++++ ephemeral/schema/float64_attribute_test.go | 425 ++++++++++++++++ ephemeral/schema/int32_attribute.go | 191 +++++++ ephemeral/schema/int32_attribute_test.go | 425 ++++++++++++++++ ephemeral/schema/int64_attribute.go | 190 +++++++ ephemeral/schema/int64_attribute_test.go | 424 ++++++++++++++++ ephemeral/schema/list_attribute.go | 222 ++++++++ ephemeral/schema/list_attribute_test.go | 523 +++++++++++++++++++ ephemeral/schema/map_attribute.go | 225 +++++++++ ephemeral/schema/map_attribute_test.go | 523 +++++++++++++++++++ ephemeral/schema/number_attribute.go | 191 +++++++ ephemeral/schema/number_attribute_test.go | 425 ++++++++++++++++ ephemeral/schema/object_attribute.go | 224 +++++++++ ephemeral/schema/object_attribute_test.go | 556 +++++++++++++++++++++ ephemeral/schema/set_attribute.go | 220 ++++++++ ephemeral/schema/set_attribute_test.go | 523 +++++++++++++++++++ ephemeral/schema/string_attribute.go | 187 +++++++ ephemeral/schema/string_attribute_test.go | 425 ++++++++++++++++ 24 files changed, 7931 insertions(+) create mode 100644 ephemeral/schema/bool_attribute.go create mode 100644 ephemeral/schema/bool_attribute_test.go create mode 100644 ephemeral/schema/dynamic_attribute.go create mode 100644 ephemeral/schema/dynamic_attribute_test.go create mode 100644 ephemeral/schema/float32_attribute.go create mode 100644 ephemeral/schema/float32_attribute_test.go create mode 100644 ephemeral/schema/float64_attribute.go create mode 100644 ephemeral/schema/float64_attribute_test.go create mode 100644 ephemeral/schema/int32_attribute.go create mode 100644 ephemeral/schema/int32_attribute_test.go create mode 100644 ephemeral/schema/int64_attribute.go create mode 100644 ephemeral/schema/int64_attribute_test.go create mode 100644 ephemeral/schema/list_attribute.go create mode 100644 ephemeral/schema/list_attribute_test.go create mode 100644 ephemeral/schema/map_attribute.go create mode 100644 ephemeral/schema/map_attribute_test.go create mode 100644 ephemeral/schema/number_attribute.go create mode 100644 ephemeral/schema/number_attribute_test.go create mode 100644 ephemeral/schema/object_attribute.go create mode 100644 ephemeral/schema/object_attribute_test.go create mode 100644 ephemeral/schema/set_attribute.go create mode 100644 ephemeral/schema/set_attribute_test.go create mode 100644 ephemeral/schema/string_attribute.go create mode 100644 ephemeral/schema/string_attribute_test.go diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go new file mode 100644 index 000000000..b9f6e3820 --- /dev/null +++ b/ephemeral/schema/bool_attribute.go @@ -0,0 +1,187 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = BoolAttribute{} + _ fwxschema.AttributeWithBoolValidators = BoolAttribute{} +) + +// BoolAttribute represents a schema attribute that is a boolean. When +// retrieving the value for this attribute, use types.Bool as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a boolean or directly via the true/false keywords. +// +// example_attribute = true +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type BoolAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.BoolType. When retrieving data, the basetypes.BoolValuable + // associated with this custom type must be used in place of types.Bool. + CustomType basetypes.BoolTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Bool +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a BoolAttribute. +func (a BoolAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// BoolValidators returns the Validators field value. +func (a BoolAttribute) BoolValidators() []validator.Bool { + return a.Validators +} + +// Equal returns true if the given Attribute is a BoolAttribute +// and all fields are equal. +func (a BoolAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(BoolAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a BoolAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a BoolAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.BoolType +} + +// IsComputed returns the Computed field value. +func (a BoolAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a BoolAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a BoolAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a BoolAttribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go new file mode 100644 index 000000000..077ee1f1f --- /dev/null +++ b/ephemeral/schema/bool_attribute_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.BoolType"), + }, + "ElementKeyInt": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.BoolType"), + }, + "ElementKeyString": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.BoolType"), + }, + "ElementKeyValue": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.BoolType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeBoolValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected []validator.Bool + }{ + "no-validators": { + attribute: schema.BoolAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.BoolAttribute{ + Validators: []validator.Bool{}, + }, + expected: []validator.Bool{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.BoolValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.BoolAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: schema.BoolAttribute{}, + other: schema.BoolAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: schema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.BoolAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: schema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: schema.BoolAttribute{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.BoolAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.BoolAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "required": { + attribute: schema.BoolAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.BoolAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go new file mode 100644 index 000000000..6b1b6c83e --- /dev/null +++ b/ephemeral/schema/dynamic_attribute.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = DynamicAttribute{} + _ fwxschema.AttributeWithDynamicValidators = DynamicAttribute{} +) + +// DynamicAttribute represents a schema attribute that is a dynamic, rather +// than a single static type. Static types are always preferable over dynamic +// types in Terraform as practitioners will receive less helpful configuration +// assistance from validation error diagnostics and editor integrations. When +// retrieving the value for this attribute, use types.Dynamic as the value type +// unless the CustomType field is set. +// +// The concrete value type for a dynamic is determined at runtime in this order: +// 1. By Terraform, if defined in the configuration (if Required or Optional). +// 2. By the provider (if Computed). +// +// Once the concrete value type has been determined, it must remain consistent between +// plan and apply or Terraform will return an error. +type DynamicAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.DynamicType. When retrieving data, the basetypes.DynamicValuable + // associated with this custom type must be used in place of types.Dynamic. + CustomType basetypes.DynamicTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Dynamic +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a DynamicAttribute. +func (a DynamicAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a DynamicAttribute +// and all fields are equal. +func (a DynamicAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(DynamicAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a DynamicAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a DynamicAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a DynamicAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.DynamicType or the CustomType field value if defined. +func (a DynamicAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.DynamicType +} + +// IsComputed returns the Computed field value. +func (a DynamicAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a DynamicAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a DynamicAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a DynamicAttribute) IsSensitive() bool { + return a.Sensitive +} + +// DynamicValidators returns the Validators field value. +func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { + return a.Validators +} diff --git a/ephemeral/schema/dynamic_attribute_test.go b/ephemeral/schema/dynamic_attribute_test.go new file mode 100644 index 000000000..a718167a6 --- /dev/null +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.DynamicAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.DynamicType"), + }, + "ElementKeyInt": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.DynamicType"), + }, + "ElementKeyString": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.DynamicType"), + }, + "ElementKeyValue": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.DynamicType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.DynamicAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.DynamicAttribute{}, + other: testschema.AttributeWithDynamicValidators{}, + expected: false, + }, + "equal": { + attribute: schema.DynamicAttribute{}, + other: schema.DynamicAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-description": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "description": { + attribute: schema.DynamicAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.DynamicAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected attr.Type + }{ + "base": { + attribute: schema.DynamicAttribute{}, + expected: types.DynamicType, + }, + "custom-type": { + attribute: schema.DynamicAttribute{ + CustomType: testtypes.DynamicType{}, + }, + expected: testtypes.DynamicType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-computed": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.DynamicAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optional": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.DynamicAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-required": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "required": { + attribute: schema.DynamicAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.DynamicAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeDynamicValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected []validator.Dynamic + }{ + "no-validators": { + attribute: schema.DynamicAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.DynamicAttribute{ + Validators: []validator.Dynamic{}, + }, + expected: []validator.Dynamic{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.DynamicValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go new file mode 100644 index 000000000..8f3dbdc21 --- /dev/null +++ b/ephemeral/schema/float32_attribute.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed returns the Computed field value. +func (a Float32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Float32Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go new file mode 100644 index 000000000..e9e45d785 --- /dev/null +++ b/ephemeral/schema/float32_attribute_test.go @@ -0,0 +1,426 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []validator.Float32 + }{ + "no-validators": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float32Attribute{ + Validators: []validator.Float32{}, + }, + expected: []validator.Float32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: schema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float32Attribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go new file mode 100644 index 000000000..1313353ec --- /dev/null +++ b/ephemeral/schema/float64_attribute.go @@ -0,0 +1,190 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float64Attribute{} + _ fwxschema.AttributeWithFloat64Validators = Float64Attribute{} +) + +// Float64Attribute represents a schema attribute that is a 64-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float64 as the value type unless the CustomType field is set. +// +// Use Int64Attribute for 64-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float64Type. When retrieving data, the basetypes.Float64Valuable + // associated with this custom type must be used in place of types.Float64. + CustomType basetypes.Float64Typable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float64 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float64Attribute. +func (a Float64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float64Attribute +// and all fields are equal. +func (a Float64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float64Validators returns the Validators field value. +func (a Float64Attribute) Float64Validators() []validator.Float64 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float64Type or the CustomType field value if defined. +func (a Float64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float64Type +} + +// IsComputed returns the Computed field value. +func (a Float64Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Float64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Float64Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go new file mode 100644 index 000000000..4c3f2703a --- /dev/null +++ b/ephemeral/schema/float64_attribute_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float64Type"), + }, + "ElementKeyInt": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float64Type"), + }, + "ElementKeyString": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float64Type"), + }, + "ElementKeyValue": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float64Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeFloat64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected []validator.Float64 + }{ + "no-validators": { + attribute: schema.Float64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float64Attribute{ + Validators: []validator.Float64{}, + }, + expected: []validator.Float64{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float64Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float64Attribute{}, + other: schema.Float64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float64Attribute{}, + expected: types.Float64Type, + }, + "custom-type": { + attribute: schema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float64Attribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float64Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go new file mode 100644 index 000000000..89f852e8c --- /dev/null +++ b/ephemeral/schema/int32_attribute.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int32Attribute{} + _ fwxschema.AttributeWithInt32Validators = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Int32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// Int32Validators returns the Validators field value. +func (a Int32Attribute) Int32Validators() []validator.Int32 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Int32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Int32Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go new file mode 100644 index 000000000..ffc816c88 --- /dev/null +++ b/ephemeral/schema/int32_attribute_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int32Attribute{}, + other: schema.Int32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int32Attribute{}, + expected: types.Int32Type, + }, + // "custom-type": { + // attribute: schema.Int32Attribute{ + // CustomType: testtypes.Int32Type{}, + // }, + // expected: testtypes.Int32Type{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeInt32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected []validator.Int32 + }{ + "no-validators": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int32Attribute{ + Validators: []validator.Int32{}, + }, + expected: []validator.Int32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int32Attribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go new file mode 100644 index 000000000..ab9d5ca1b --- /dev/null +++ b/ephemeral/schema/int64_attribute.go @@ -0,0 +1,190 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int64Attribute{} + _ fwxschema.AttributeWithInt64Validators = Int64Attribute{} +) + +// Int64Attribute represents a schema attribute that is a 64-bit integer. +// When retrieving the value for this attribute, use types.Int64 as the value +// type unless the CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int64Type. When retrieving data, the basetypes.Int64Valuable + // associated with this custom type must be used in place of types.Int64. + CustomType basetypes.Int64Typable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Int64 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int64Attribute. +func (a Int64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int64Attribute +// and all fields are equal. +func (a Int64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int64Type or the CustomType field value if defined. +func (a Int64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int64Type +} + +// Int64Validators returns the Validators field value. +func (a Int64Attribute) Int64Validators() []validator.Int64 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int64Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Int64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Int64Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go new file mode 100644 index 000000000..a2635c76c --- /dev/null +++ b/ephemeral/schema/int64_attribute_test.go @@ -0,0 +1,424 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int64Type"), + }, + "ElementKeyInt": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int64Type"), + }, + "ElementKeyString": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int64Type"), + }, + "ElementKeyValue": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int64Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int64Attribute{}, + other: schema.Int64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int64Attribute{}, + expected: types.Int64Type, + }, + // "custom-type": { + // attribute: schema.Int64Attribute{ + // CustomType: testtypes.Int64Type{}, + // }, + // expected: testtypes.Int64Type{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeInt64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected []validator.Int64 + }{ + "no-validators": { + attribute: schema.Int64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int64Attribute{ + Validators: []validator.Int64{}, + }, + expected: []validator.Int64{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int64Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int64Attribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int64Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go new file mode 100644 index 000000000..9d502067f --- /dev/null +++ b/ephemeral/schema/list_attribute.go @@ -0,0 +1,222 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ListAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListAttribute{} + _ fwxschema.AttributeWithListValidators = ListAttribute{} +) + +// ListAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use ListNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a list or an element directly via square brace 0-based index syntax: +// +// # first known element +// .example_attribute[0] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ListType. When retrieving data, the basetypes.ListValuable + // associated with this custom type must be used in place of types.List. + CustomType basetypes.ListTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a list +// index or an error. +func (a ListAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ListAttribute +// and all fields are equal. +func (a ListAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ListAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ListType or the CustomType field value if defined. +func (a ListAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns the Computed field value. +func (a ListAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a ListAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a ListAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ListValidators returns the Validators field value. +func (a ListAttribute) ListValidators() []validator.List { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a ListAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go new file mode 100644 index 000000000..1b22bbbdc --- /dev/null +++ b/ephemeral/schema/list_attribute_test.go @@ -0,0 +1,523 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.ListAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.ListAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.ListAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.ListAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.ListAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.ListAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.ListAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.ListAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.ListAttribute{ + Computed: true, + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.ListAttribute{ + Computed: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.ListAttribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go new file mode 100644 index 000000000..516576a4e --- /dev/null +++ b/ephemeral/schema/map_attribute.go @@ -0,0 +1,225 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = MapAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapAttribute{} + _ fwxschema.AttributeWithMapValidators = MapAttribute{} +) + +// MapAttribute represents a schema attribute that is a map with a single +// element type. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use MapNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map or directly via curly brace syntax. +// +// # map of strings +// example_attribute = { +// key1 = "first", +// key2 = "second", +// } +// +// Terraform configurations reference this attribute using expressions that +// accept a map or an element directly via square brace string syntax: +// +// # key1 known element +// .example_attribute["key1"] +type MapAttribute struct { + // ElementType is the type for all elements of the map. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.MapType. When retrieving data, the basetypes.MapValuable + // associated with this custom type must be used in place of types.Map. + CustomType basetypes.MapTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Map +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a map +// index or an error. +func (a MapAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a MapAttribute +// and all fields are equal. +func (a MapAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(MapAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.MapType or the CustomType field value if defined. +func (a MapAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns the Computed field value. +func (a MapAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a MapAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a MapAttribute) IsSensitive() bool { + return a.Sensitive +} + +// MapValidators returns the Validators field value. +func (a MapAttribute) MapValidators() []validator.Map { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a MapAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go new file mode 100644 index 000000000..94219da83 --- /dev/null +++ b/ephemeral/schema/map_attribute_test.go @@ -0,0 +1,523 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapType"), + }, + "ElementKeyInt": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapType"), + }, + "ElementKeyString": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.MapAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.MapAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: types.MapType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.MapAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.MapAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.MapAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.MapAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.MapAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeMapValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.MapAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapAttribute{ + Computed: true, + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.MapAttribute{ + Computed: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.MapAttribute{ + Computed: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.MapAttribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go new file mode 100644 index 000000000..ffe4e0839 --- /dev/null +++ b/ephemeral/schema/number_attribute.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = NumberAttribute{} + _ fwxschema.AttributeWithNumberValidators = NumberAttribute{} +) + +// NumberAttribute represents a schema attribute that is a generic number with +// up to 512 bits of floating point or integer precision. When retrieving the +// value for this attribute, use types.Number as the value type unless the +// CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// Int64Attribute for 64-bit integer number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point or integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type NumberAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.NumberType. When retrieving data, the basetypes.NumberValuable + // associated with this custom type must be used in place of types.Number. + CustomType basetypes.NumberTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Number +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a NumberAttribute. +func (a NumberAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a NumberAttribute +// and all fields are equal. +func (a NumberAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(NumberAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a NumberAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.NumberType or the CustomType field value if defined. +func (a NumberAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.NumberType +} + +// IsComputed returns the Computed field value. +func (a NumberAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a NumberAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a NumberAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a NumberAttribute) IsSensitive() bool { + return a.Sensitive +} + +// NumberValidators returns the Validators field value. +func (a NumberAttribute) NumberValidators() []validator.Number { + return a.Validators +} diff --git a/ephemeral/schema/number_attribute_test.go b/ephemeral/schema/number_attribute_test.go new file mode 100644 index 000000000..7e326b90e --- /dev/null +++ b/ephemeral/schema/number_attribute_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.NumberType"), + }, + "ElementKeyInt": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.NumberType"), + }, + "ElementKeyString": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.NumberType"), + }, + "ElementKeyValue": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.NumberType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.NumberAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: schema.NumberAttribute{}, + other: schema.NumberAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: schema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.NumberAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: schema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: schema.NumberAttribute{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.NumberAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.NumberAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "required": { + attribute: schema.NumberAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.NumberAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeNumberValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected []validator.Number + }{ + "no-validators": { + attribute: schema.NumberAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.NumberAttribute{ + Validators: []validator.Number{}, + }, + expected: []validator.Number{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.NumberValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go new file mode 100644 index 000000000..eafa40c6e --- /dev/null +++ b/ephemeral/schema/object_attribute.go @@ -0,0 +1,224 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ObjectAttribute{} + _ fwschema.AttributeWithValidateImplementation = ObjectAttribute{} + _ fwxschema.AttributeWithObjectValidators = ObjectAttribute{} +) + +// ObjectAttribute represents a schema attribute that is an object with only +// type information for underlying attributes. When retrieving the value for +// this attribute, use types.Object as the value type unless the CustomType +// field is set. The AttributeTypes field must be set. +// +// Prefer SingleNestedAttribute over ObjectAttribute if the provider is +// using protocol version 6 and full attribute functionality is needed. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # object with one attribute +// example_attribute = { +// underlying_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute directly via period syntax: +// +// # underlying attribute +// .example_attribute.underlying_attribute +type ObjectAttribute struct { + // AttributeTypes is the mapping of underlying attribute names to attribute + // types. This field must be set. + // + // Attribute types that contain a collection with a nested dynamic type (i.e. types.List[types.Dynamic]) are not supported. + // If underlying dynamic collection values are required, replace this attribute definition with + // DynamicAttribute instead. + AttributeTypes map[string]attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into an +// attribute name or an error. +func (a ObjectAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ObjectAttribute +// and all fields are equal. +func (a ObjectAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ObjectAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ObjectAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ObjectAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ObjectAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ObjectType or the CustomType field value if defined. +func (a ObjectAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ObjectType{ + AttrTypes: a.AttributeTypes, + } +} + +// IsComputed returns the Computed field value. +func (a ObjectAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a ObjectAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ObjectAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a ObjectAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ObjectValidators returns the Validators field value. +func (a ObjectAttribute) ObjectValidators() []validator.Object { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a ObjectAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.AttributeTypes == nil && a.CustomType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingAttributeTypesDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go new file mode 100644 index 000000000..0d532fc2a --- /dev/null +++ b/ephemeral/schema/object_attribute_test.go @@ -0,0 +1,556 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("testattr"), + expected: types.StringType, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("undefined attribute name other in ObjectType"), + }, + "ElementKeyInt": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to ObjectType"), + }, + "ElementKeyString": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ObjectType"), + }, + "ElementKeyValue": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ObjectType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ObjectAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attribute-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.BoolType}}, + expected: false, + }, + "equal": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "description": { + attribute: schema.ObjectAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "markdown-description": { + attribute: schema.ObjectAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, + }, + // "custom-type": { + // attribute: schema.ObjectAttribute{ + // CustomType: testtypes.ObjectType{}, + // }, + // expected: testtypes.ObjectType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "computed": { + attribute: schema.ObjectAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "optional": { + attribute: schema.ObjectAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-required": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "required": { + attribute: schema.ObjectAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "sensitive": { + attribute: schema.ObjectAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: nil, + }, + "validators": { + attribute: schema.ObjectAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "attributetypes": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "attributetypes-dynamic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.DynamicType, + "test_list": types.ListType{ + ElemType: types.StringType, + }, + "test_obj": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.DynamicType, + }, + }, + }, + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "attributetypes-nested-collection-dynamic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.ListType{ + ElemType: types.DynamicType, + }, + }, + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "attributetypes-missing": { + attribute: schema.ObjectAttribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the AttributeTypes or CustomType field on an object Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + "customtype": { + attribute: schema.ObjectAttribute{ + Computed: true, + CustomType: testtypes.ObjectType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go new file mode 100644 index 000000000..261b02424 --- /dev/null +++ b/ephemeral/schema/set_attribute.go @@ -0,0 +1,220 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = SetAttribute{} + _ fwschema.AttributeWithValidateImplementation = SetAttribute{} + _ fwxschema.AttributeWithSetValidators = SetAttribute{} +) + +// SetAttribute represents a schema attribute that is a set with a single +// element type. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use SetNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set or directly via square brace syntax. +// +// # set of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a set. Sets cannot be indexed in Terraform, therefore an expression +// is required to access an explicit element. +type SetAttribute struct { + // ElementType is the type for all elements of the set. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.SetType. When retrieving data, the basetypes.SetValuable + // associated with this custom type must be used in place of types.Set. + CustomType basetypes.SetTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a set +// index or an error. +func (a SetAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a SetAttribute +// and all fields are equal. +func (a SetAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(SetAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SetAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SetAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.SetType or the CustomType field value if defined. +func (a SetAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns the Computed field value. +func (a SetAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a SetAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a SetAttribute) IsSensitive() bool { + return a.Sensitive +} + +// SetValidators returns the Validators field value. +func (a SetAttribute) SetValidators() []validator.Set { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a SetAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go new file mode 100644 index 000000000..0b6903829 --- /dev/null +++ b/ephemeral/schema/set_attribute_test.go @@ -0,0 +1,523 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetType"), + }, + "ElementKeyInt": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetType"), + }, + "ElementKeyString": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetType"), + }, + "ElementKeyValue": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: types.StringType, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.SetAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.SetAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: types.SetType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.SetAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.SetAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.SetAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.SetAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.SetAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.SetAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{}, + Optional: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.SetAttribute{ + Computed: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.SetAttribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go new file mode 100644 index 000000000..0c2dd9aba --- /dev/null +++ b/ephemeral/schema/string_attribute.go @@ -0,0 +1,187 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = StringAttribute{} + _ fwxschema.AttributeWithStringValidators = StringAttribute{} +) + +// StringAttribute represents a schema attribute that is a string. When +// retrieving the value for this attribute, use types.String as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a string or directly via double quote syntax. +// +// example_attribute = "value" +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type StringAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.StringType. When retrieving data, the basetypes.StringValuable + // associated with this custom type must be used in place of types.String. + CustomType basetypes.StringTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.String +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a StringAttribute. +func (a StringAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a StringAttribute +// and all fields are equal. +func (a StringAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(StringAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a StringAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a StringAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a StringAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.StringType +} + +// IsComputed returns the Computed field value. +func (a StringAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a StringAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a StringAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a StringAttribute) IsSensitive() bool { + return a.Sensitive +} + +// StringValidators returns the Validators field value. +func (a StringAttribute) StringValidators() []validator.String { + return a.Validators +} diff --git a/ephemeral/schema/string_attribute_test.go b/ephemeral/schema/string_attribute_test.go new file mode 100644 index 000000000..1c95add7d --- /dev/null +++ b/ephemeral/schema/string_attribute_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.StringType"), + }, + "ElementKeyInt": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.StringType"), + }, + "ElementKeyString": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.StringType"), + }, + "ElementKeyValue": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.StringType"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.StringAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: schema.StringAttribute{}, + other: schema.StringAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: schema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.StringAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: schema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: schema.StringAttribute{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.StringAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.StringAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-required": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "required": { + attribute: schema.StringAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.StringAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeStringValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []validator.String + }{ + "no-validators": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.StringAttribute{ + Validators: []validator.String{}, + }, + expected: []validator.String{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From a88242a8969922ab91cb00506dc05b38669c338e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 15:11:27 -0400 Subject: [PATCH 04/55] uncomment custom type tests --- ephemeral/schema/int32_attribute_test.go | 13 +++++++------ ephemeral/schema/int64_attribute_test.go | 13 +++++++------ ephemeral/schema/object_attribute_test.go | 12 ++++++------ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go index ffc816c88..bd6d4d36c 100644 --- a/ephemeral/schema/int32_attribute_test.go +++ b/ephemeral/schema/int32_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -231,12 +232,12 @@ func TestInt32AttributeGetType(t *testing.T) { attribute: schema.Int32Attribute{}, expected: types.Int32Type, }, - // "custom-type": { - // attribute: schema.Int32Attribute{ - // CustomType: testtypes.Int32Type{}, - // }, - // expected: testtypes.Int32Type{}, - // }, + "custom-type": { + attribute: schema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go index a2635c76c..e61165c5f 100644 --- a/ephemeral/schema/int64_attribute_test.go +++ b/ephemeral/schema/int64_attribute_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -230,12 +231,12 @@ func TestInt64AttributeGetType(t *testing.T) { attribute: schema.Int64Attribute{}, expected: types.Int64Type, }, - // "custom-type": { - // attribute: schema.Int64Attribute{ - // CustomType: testtypes.Int64Type{}, - // }, - // expected: testtypes.Int64Type{}, - // }, + "custom-type": { + attribute: schema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go index 0d532fc2a..429aba1e7 100644 --- a/ephemeral/schema/object_attribute_test.go +++ b/ephemeral/schema/object_attribute_test.go @@ -245,12 +245,12 @@ func TestObjectAttributeGetType(t *testing.T) { attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, }, - // "custom-type": { - // attribute: schema.ObjectAttribute{ - // CustomType: testtypes.ObjectType{}, - // }, - // expected: testtypes.ObjectType{}, - // }, + "custom-type": { + attribute: schema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } for name, testCase := range testCases { From 4e285b789741689e52f46201e5e37f6eba8e3ab0 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 15:26:43 -0400 Subject: [PATCH 05/55] added block implementations --- ephemeral/schema/list_nested_block.go | 205 +++++++ ephemeral/schema/list_nested_block_test.go | 570 +++++++++++++++++++ ephemeral/schema/nested_block_object.go | 94 +++ ephemeral/schema/nested_block_object_test.go | 366 ++++++++++++ ephemeral/schema/set_nested_block.go | 205 +++++++ ephemeral/schema/set_nested_block_test.go | 570 +++++++++++++++++++ ephemeral/schema/single_nested_block.go | 213 +++++++ ephemeral/schema/single_nested_block_test.go | 485 ++++++++++++++++ 8 files changed, 2708 insertions(+) create mode 100644 ephemeral/schema/list_nested_block.go create mode 100644 ephemeral/schema/list_nested_block_test.go create mode 100644 ephemeral/schema/nested_block_object.go create mode 100644 ephemeral/schema/nested_block_object_test.go create mode 100644 ephemeral/schema/set_nested_block.go create mode 100644 ephemeral/schema/set_nested_block_test.go create mode 100644 ephemeral/schema/single_nested_block.go create mode 100644 ephemeral/schema/single_nested_block_test.go diff --git a/ephemeral/schema/list_nested_block.go b/ephemeral/schema/list_nested_block.go new file mode 100644 index 000000000..4d098bc2d --- /dev/null +++ b/ephemeral/schema/list_nested_block.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = ListNestedBlock{} + _ fwschema.BlockWithValidateImplementation = ListNestedBlock{} + _ fwxschema.BlockWithListValidators = ListNestedBlock{} +) + +// ListNestedBlock represents a block that is a list of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer ListNestedAttribute over ListNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # list of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type ListNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this block definition with + // a DynamicAttribute. + NestedObject NestedBlockObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // basetypes.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType basetypes.ListTypable + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyInt, otherwise returns an error. +func (b ListNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is ListNestedBlock +// and all fields are equal. +func (b ListNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(ListNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b ListNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b ListNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b ListNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (b ListNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeList. +func (b ListNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeList +} + +// ListValidators returns the Validators field value. +func (b ListNestedBlock) ListValidators() []validator.List { + return b.Validators +} + +// Type returns ListType of ObjectType or CustomType. +func (b ListNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.ListType{ + ElemType: b.NestedObject.Type(), + } +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the block to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (b ListNestedBlock) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { + resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/list_nested_block_test.go b/ephemeral/schema/list_nested_block_test.go new file mode 100644 index 000000000..bb20c35de --- /dev/null +++ b/ephemeral/schema/list_nested_block_test.go @@ -0,0 +1,570 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedBlock"), + }, + "ElementKeyInt": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedBlock"), + }, + "ElementKeyValue": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedBlock"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.ListNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithListValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.ListNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.ListNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected []validator.List + }{ + "no-validators": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.ListNestedBlock{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected attr.Type + }{ + "base": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.ListNestedBlock{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a block that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" block definition with a DynamicAttribute.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.block.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/nested_block_object.go b/ephemeral/schema/nested_block_object.go new file mode 100644 index 000000000..2b560b606 --- /dev/null +++ b/ephemeral/schema/nested_block_object.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedBlockObjectWithValidators = NestedBlockObject{} + +// NestedBlockObject is the object containing the underlying attributes and +// blocks for a ListNestedBlock or SetNestedBlock. When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. +// +// This object enables customizing and simplifying details within its parent +// Block, therefore it cannot have Terraform schema fields such as Description, +// etc. +type NestedBlockObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObject is equivalent. +func (o NestedBlockObject) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObject); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObject) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(o.Blocks) +} + +// ObjectValidators returns the Validators field value. +func (o NestedBlockObject) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedBlockObject. +func (o NestedBlockObject) Type() basetypes.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedBlockObjectType(o) +} diff --git a/ephemeral/schema/nested_block_object_test.go b/ephemeral/schema/nested_block_object_test.go new file mode 100644 index 000000000..f484d10ca --- /dev/null +++ b/ephemeral/schema/nested_block_object_test.go @@ -0,0 +1,366 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on NestedBlockObject"), + }, + "ElementKeyInt": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedBlockObject"), + }, + "ElementKeyString": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedBlockObject"), + }, + "ElementKeyValue": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedBlockObject"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + other fwschema.NestedBlockObject + expected bool + }{ + "different-attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedBlockObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected map[string]fwschema.Block + }{ + "no-blocks": { + object: schema.NestedBlockObject{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedBlockObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedBlockObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected attr.Type + }{ + "base": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + "custom-type": { + object: schema.NestedBlockObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_nested_block.go b/ephemeral/schema/set_nested_block.go new file mode 100644 index 000000000..085163f37 --- /dev/null +++ b/ephemeral/schema/set_nested_block.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = SetNestedBlock{} + _ fwschema.BlockWithValidateImplementation = SetNestedBlock{} + _ fwxschema.BlockWithSetValidators = SetNestedBlock{} +) + +// SetNestedBlock represents a block that is a set of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer SetNestedAttribute over SetNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # set of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a set of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SetNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this block definition with + // a DynamicAttribute. + NestedObject NestedBlockObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // basetypes.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType basetypes.SetTypable + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyValue, otherwise returns an error. +func (b SetNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is SetNestedBlock +// and all fields are equal. +func (b SetNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SetNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SetNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SetNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SetNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (b SetNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeSet. +func (b SetNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSet +} + +// SetValidators returns the Validators field value. +func (b SetNestedBlock) SetValidators() []validator.Set { + return b.Validators +} + +// Type returns SetType of ObjectType or CustomType. +func (b SetNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.SetType{ + ElemType: b.NestedObject.Type(), + } +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the block to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (b SetNestedBlock) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { + resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/set_nested_block_test.go b/ephemeral/schema/set_nested_block_test.go new file mode 100644 index 000000000..0862026f4 --- /dev/null +++ b/ephemeral/schema/set_nested_block_test.go @@ -0,0 +1,570 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedBlock"), + }, + "ElementKeyString": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SetNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithSetValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.SetNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SetNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected []validator.Set + }{ + "no-validators": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SetNestedBlock{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SetNestedBlock{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a block that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" block definition with a DynamicAttribute.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.block.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/single_nested_block.go b/ephemeral/schema/single_nested_block.go new file mode 100644 index 000000000..926825a03 --- /dev/null +++ b/ephemeral/schema/single_nested_block.go @@ -0,0 +1,213 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = SingleNestedBlock{} + _ fwxschema.BlockWithObjectValidators = SingleNestedBlock{} +) + +// SingleNestedBlock represents a block that is a single object where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Object +// as the value type unless the CustomType field is set. +// +// Prefer SingleNestedAttribute over SingleNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block only once using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # single block +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_block.nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SingleNestedBlock struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (b SingleNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedBlock", step) + } + + if attribute, ok := b.Attributes[string(name)]; ok { + return attribute, nil + } + + if block, ok := b.Blocks[string(name)]; ok { + return block, nil + } + + return nil, fmt.Errorf("no attribute or block %q on SingleNestedBlock", name) +} + +// Equal returns true if the given Attribute is b SingleNestedBlock +// and all fields are equal. +func (b SingleNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SingleNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SingleNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SingleNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SingleNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns a generated NestedBlockObject from the +// Attributes, CustomType, and Validators field values. +func (b SingleNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + CustomType: b.CustomType, + Validators: b.Validators, + } +} + +// GetNestingMode always returns BlockNestingModeSingle. +func (b SingleNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSingle +} + +// ObjectValidators returns the Validators field value. +func (b SingleNestedBlock) ObjectValidators() []validator.Object { + return b.Validators +} + +// Type returns ObjectType or CustomType. +func (b SingleNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + attrTypes := make(map[string]attr.Type, len(b.Attributes)+len(b.Blocks)) + + for name, attribute := range b.Attributes { + attrTypes[name] = attribute.GetType() + } + + for name, block := range b.Blocks { + attrTypes[name] = block.Type() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/ephemeral/schema/single_nested_block_test.go b/ephemeral/schema/single_nested_block_test.go new file mode 100644 index 000000000..994f94894 --- /dev/null +++ b/ephemeral/schema/single_nested_block_test.go @@ -0,0 +1,485 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on SingleNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedBlock"), + }, + "ElementKeyString": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedBlock"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SingleNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.BlockWithObjectValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + other: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + block: schema.SingleNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SingleNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected []validator.Object + }{ + "no-validators": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SingleNestedBlock{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SingleNestedBlock{ + // CustomType: testtypes.SingleType{}, + // }, + // expected: testtypes.SingleType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 9b81285a8dfb1195baf1b99875bfda9e4c2f8e01 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 15:37:11 -0400 Subject: [PATCH 06/55] add nested attribute implementations --- ephemeral/schema/list_nested_attribute.go | 246 +++++++ .../schema/list_nested_attribute_test.go | 690 ++++++++++++++++++ ephemeral/schema/map_nested_attribute.go | 247 +++++++ ephemeral/schema/map_nested_attribute_test.go | 690 ++++++++++++++++++ ephemeral/schema/nested_attribute.go | 14 + ephemeral/schema/nested_attribute_object.go | 82 +++ .../schema/nested_attribute_object_test.go | 280 +++++++ ephemeral/schema/set_nested_attribute.go | 242 ++++++ ephemeral/schema/set_nested_attribute_test.go | 690 ++++++++++++++++++ ephemeral/schema/single_nested_attribute.go | 246 +++++++ .../schema/single_nested_attribute_test.go | 569 +++++++++++++++ 11 files changed, 3996 insertions(+) create mode 100644 ephemeral/schema/list_nested_attribute.go create mode 100644 ephemeral/schema/list_nested_attribute_test.go create mode 100644 ephemeral/schema/map_nested_attribute.go create mode 100644 ephemeral/schema/map_nested_attribute_test.go create mode 100644 ephemeral/schema/nested_attribute.go create mode 100644 ephemeral/schema/nested_attribute_object.go create mode 100644 ephemeral/schema/nested_attribute_object_test.go create mode 100644 ephemeral/schema/set_nested_attribute.go create mode 100644 ephemeral/schema/set_nested_attribute_test.go create mode 100644 ephemeral/schema/single_nested_attribute.go create mode 100644 ephemeral/schema/single_nested_attribute_test.go diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go new file mode 100644 index 000000000..b9b70d6fb --- /dev/null +++ b/ephemeral/schema/list_nested_attribute.go @@ -0,0 +1,246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = ListNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListNestedAttribute{} + _ fwxschema.AttributeWithListValidators = ListNestedAttribute{} +) + +// ListNestedAttribute represents an attribute that is a list of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ListAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list of objects or directly via square and curly brace syntax. +// +// # list of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_attribute[0] +// # first known object nested_attribute value +// .example_attribute[0].nested_attribute +type ListNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // basetypes.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType basetypes.ListTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyInt, otherwise returns an error. +func (a ListNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a ListNestedAttribute +// and all fields are equal. +func (a ListNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(ListNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ListNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a ListNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeList. +func (a ListNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeList +} + +// GetType returns ListType of ObjectType or CustomType. +func (a ListNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed returns the Computed field value. +func (a ListNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a ListNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a ListNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ListValidators returns the Validators field value. +func (a ListNestedAttribute) ListValidators() []validator.List { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go new file mode 100644 index 000000000..3d1ae651d --- /dev/null +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -0,0 +1,690 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedAttribute"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.ListNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.ListNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.ListNestedAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.ListNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.ListNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.ListNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.ListNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.ListNestedAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.ListNestedAttribute{ + Computed: true, + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go new file mode 100644 index 000000000..9729efdc3 --- /dev/null +++ b/ephemeral/schema/map_nested_attribute.go @@ -0,0 +1,247 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = MapNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapNestedAttribute{} + _ fwxschema.AttributeWithMapValidators = MapNestedAttribute{} +) + +// MapNestedAttribute represents an attribute that is a map of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use MapAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map of objects or directly via curly brace syntax. +// +// # map of objects +// example_attribute = { +// key = { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a map of objects or an element directly via square brace string +// syntax: +// +// # known object at key +// .example_attribute["key"] +// # known object nested_attribute value at key +// .example_attribute["key"].nested_attribute +type MapNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.MapType of types.ObjectType. When retrieving data, the + // basetypes.MapValuable associated with this custom type must be used in + // place of types.Map. + CustomType basetypes.MapTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Map +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyString, otherwise returns an error. +func (a MapNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyString) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to MapNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a MapNestedAttribute +// and all fields are equal. +func (a MapNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(MapNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeMap. +func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeMap +} + +// GetType returns MapType of ObjectType or CustomType. +func (a MapNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed returns the Computed field value. +func (a MapNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a MapNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a MapNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// MapValidators returns the Validators field value. +func (a MapNestedAttribute) MapValidators() []validator.Map { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go new file mode 100644 index 000000000..0e7986f89 --- /dev/null +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -0,0 +1,690 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapNestedAttribute"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.MapNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.MapNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.MapNestedAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.MapNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.MapNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.MapNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.MapNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeMapNestedValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.MapNestedAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapNestedAttribute{ + Computed: true, + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/nested_attribute.go b/ephemeral/schema/nested_attribute.go new file mode 100644 index 000000000..31d2ee158 --- /dev/null +++ b/ephemeral/schema/nested_attribute.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Nested attributes are only compatible with protocol version 6. +type NestedAttribute interface { + Attribute + fwschema.NestedAttribute +} diff --git a/ephemeral/schema/nested_attribute_object.go b/ephemeral/schema/nested_attribute_object.go new file mode 100644 index 000000000..3719a2398 --- /dev/null +++ b/ephemeral/schema/nested_attribute_object.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedAttributeObjectWithValidators = NestedAttributeObject{} + +// NestedAttributeObject is the object containing the underlying attributes +// for a ListNestedAttribute, MapNestedAttribute, SetNestedAttribute, or +// SingleNestedAttribute (automatically generated). When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. The Attributes field must be set. Nested attributes are only +// compatible with protocol version 6. +// +// This object enables customizing and simplifying details within its parent +// NestedAttribute, therefore it cannot have Terraform schema fields such as +// Required, Description, etc. +type NestedAttributeObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedAttributeObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedAttributeObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedAttributeObject is equivalent. +func (o NestedAttributeObject) Equal(other fwschema.NestedAttributeObject) bool { + if _, ok := other.(NestedAttributeObject); !ok { + return false + } + + return fwschema.NestedAttributeObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedAttributeObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// ObjectValidators returns the Validators field value. +func (o NestedAttributeObject) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedAttributeObject. +func (o NestedAttributeObject) Type() basetypes.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedAttributeObjectType(o) +} diff --git a/ephemeral/schema/nested_attribute_object_test.go b/ephemeral/schema/nested_attribute_object_test.go new file mode 100644 index 000000000..65c76544b --- /dev/null +++ b/ephemeral/schema/nested_attribute_object_test.go @@ -0,0 +1,280 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on NestedAttributeObject"), + }, + "ElementKeyInt": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedAttributeObject"), + }, + "ElementKeyString": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedAttributeObject"), + }, + "ElementKeyValue": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedAttributeObject"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + other fwschema.NestedAttributeObject + expected bool + }{ + "different-attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedAttributeObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedAttributeObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedAttributeObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + expected attr.Type + }{ + "base": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + "custom-type": { + object: schema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go new file mode 100644 index 000000000..1b17b8743 --- /dev/null +++ b/ephemeral/schema/set_nested_attribute.go @@ -0,0 +1,242 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SetNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = SetNestedAttribute{} + _ fwxschema.AttributeWithSetValidators = SetNestedAttribute{} +) + +// SetNestedAttribute represents an attribute that is a set of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use SetAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set of objects or directly via square and curly brace syntax. +// +// # set of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a set of objects. Sets cannot be indexed in Terraform, therefore +// an expression is required to access an explicit element. +type SetNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // basetypes.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType basetypes.SetTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyValue, otherwise returns an error. +func (a SetNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a SetNestedAttribute +// and all fields are equal. +func (a SetNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(SetNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SetNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SetNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeSet. +func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSet +} + +// GetType returns SetType of ObjectType or CustomType. +func (a SetNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed returns the Computed field value. +func (a SetNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a SetNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a SetNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// SetValidators returns the Validators field value. +func (a SetNestedAttribute) SetValidators() []validator.Set { + return a.Validators +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go new file mode 100644 index 000000000..1bd3daa65 --- /dev/null +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -0,0 +1,690 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.SetNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.SetNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.SetNestedAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.SetNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.SetNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.SetNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.SetNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SetNestedAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.SetNestedAttribute{ + Computed: true, + CustomType: testtypes.SetType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go new file mode 100644 index 000000000..811e76de4 --- /dev/null +++ b/ephemeral/schema/single_nested_attribute.go @@ -0,0 +1,246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SingleNestedAttribute{} + _ fwxschema.AttributeWithObjectValidators = SingleNestedAttribute{} +) + +// SingleNestedAttribute represents an attribute that is a single object where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Object +// as the value type unless the CustomType field is set. The Attributes field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ObjectAttribute if the underlying attributes do not require definition +// beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # single object +// example_attribute = { +// nested_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_attribute.nested_attribute +type SingleNestedAttribute struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // 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 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 + + // 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 + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (a SingleNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedAttribute", step) + } + + attribute, ok := a.Attributes[string(name)] + + if !ok { + return nil, fmt.Errorf("no attribute %q on SingleNestedAttribute", name) + } + + return attribute, nil +} + +// Equal returns true if the given Attribute is a SingleNestedAttribute +// and all fields are equal. +func (a SingleNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(SingleNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetAttributes returns the Attributes field value. +func (a SingleNestedAttribute) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(a.Attributes) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SingleNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SingleNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SingleNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns a generated NestedAttributeObject from the +// Attributes, CustomType, and Validators field values. +func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return NestedAttributeObject{ + Attributes: a.Attributes, + CustomType: a.CustomType, + Validators: a.Validators, + } +} + +// GetNestingMode always returns NestingModeSingle. +func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSingle +} + +// GetType returns ListType of ObjectType or CustomType. +func (a SingleNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + attrTypes := make(map[string]attr.Type, len(a.Attributes)) + + for name, attribute := range a.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} + +// IsComputed returns the Computed field value. +func (a SingleNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a SingleNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SingleNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a SingleNestedAttribute) IsSensitive() bool { + return a.Sensitive +} + +// ObjectValidators returns the Validators field value. +func (a SingleNestedAttribute) ObjectValidators() []validator.Object { + return a.Validators +} diff --git a/ephemeral/schema/single_nested_attribute_test.go b/ephemeral/schema/single_nested_attribute_test.go new file mode 100644 index 000000000..5b7209edb --- /dev/null +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -0,0 +1,569 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on SingleNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedAttribute"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SingleNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + attribute: schema.SingleNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.SingleNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + "custom-type": { + attribute: schema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.SingleNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "required": { + attribute: schema.SingleNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.SingleNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SingleNestedAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 99fb820e8d0e218f8b795eadf263ac5d1ad54f72 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 13 Aug 2024 15:45:39 -0400 Subject: [PATCH 07/55] add schema test --- ephemeral/schema/schema_test.go | 1357 +++++++++++++++++++++++++++++++ 1 file changed, 1357 insertions(+) create mode 100644 ephemeral/schema/schema_test.go diff --git a/ephemeral/schema/schema_test.go b/ephemeral/schema/schema_test.go new file mode 100644 index 000000000..64cc4806b --- /dev/null +++ b/ephemeral/schema/schema_test.go @@ -0,0 +1,1357 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: schema.Schema{}, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type schema.Schema", + ), + }, + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type schema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: test\n"+ + "Original Error: "+fwschema.ErrPathIsBlock.Error(), + ), + }, + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "WithElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtMapKey("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"test\"]\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "WithElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtSetValue(types.StringValue("test")), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringValue("test")), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value(\"test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.schema.AttributeAtPath(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "empty-nil": { + schema: schema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "nil": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: nil, + expectedErr: fwschema.ErrPathIsBlock.Error(), + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: nil, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: nil, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtTerraformPath(context.Background(), tc.path) + + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: schema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: schema.Schema{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected string + }{ + "no-deprecation-message": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + schema: schema.Schema{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected string + }{ + "no-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + schema: schema.Schema{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected string + }{ + "no-markdown-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + schema: schema.Schema{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected int64 + }{ + "0": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetVersion() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected attr.Type + }{ + "base": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_block_nested": schema.StringAttribute{}, + }, + }, + }, + "set_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_block_nested": schema.StringAttribute{}, + }, + }, + }, + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("list_block"), + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list_block_nested": types.StringType, + }, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.Schema{}, + path: path.Root("non-existent"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("non-existent"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: non-existent\n"+ + "Original Error: AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema", + ), + }, + }, + "ElementKeyInt": { + schema: schema.Schema{}, + path: path.Empty().AtListIndex(0), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + schema: schema.Schema{}, + path: path.Empty().AtMapKey("invalid"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("invalid"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"invalid\"]\n"+ + "Original Error: ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + schema: schema.Schema{}, + path: path.Empty().AtSetValue(types.StringNull()), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringNull()), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value()]\n"+ + "Original Error: ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.schema.TypeAtPath(context.Background(), testCase.path) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: schema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_block_nested": schema.StringAttribute{}, + }, + }, + }, + "set_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_block_nested": schema.StringAttribute{}, + }, + }, + }, + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("list_block"), + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list_block_nested": types.StringType, + }, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithAttributeName("non-existent"), + expectedError: fmt.Errorf("AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema"), + }, + "ElementKeyInt": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expectedError: fmt.Errorf("ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyString("invalid"), + expectedError: fmt.Errorf("ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + expectedError: fmt.Errorf("ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.TypeAtTerraformPath(context.Background(), testCase.path) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "validate-implementation-error": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "block-using-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"connection\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "attribute-and-blocks-using-reserved-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"connection\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "^": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "block-using-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "^": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"single_nested_attribute.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"single_nested_block.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$\" at schema path \"$\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"$.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"!\" at schema path \"$.^.!\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: true, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + "nested-attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"list_nested_attribute.test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + "nested-block-attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"list_nested_block.test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + "nested-nested-block-attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "list_nested_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"list_nested_block.list_nested_nested_block.test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.ValidateImplementation(context.Background()) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} From 745ce700e44dce6f694f699c18736573127b78ad Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 14 Aug 2024 08:37:34 -0400 Subject: [PATCH 08/55] remove todo --- ephemeral/renew.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/ephemeral/renew.go b/ephemeral/renew.go index 9304e50fd..5e69dfc29 100644 --- a/ephemeral/renew.go +++ b/ephemeral/renew.go @@ -18,9 +18,6 @@ type RenewRequest struct { // resource following the latest Open or Renew operation. PriorState tfsdk.EphemeralState - // TODO: Still being discussed, but we likely don't need config in the request, - // since PriorState should be guaranteed to contain the configuration values + state response values. - // // Config is the configuration the user supplied for the ephemeral // resource. Config tfsdk.Config From 76c9cadc970521b0f7f8dd73f740deb2fa52db6d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 27 Aug 2024 15:14:24 -0400 Subject: [PATCH 09/55] doc updates, renames, removals --- ephemeral/close.go | 6 +++--- ephemeral/doc.go | 2 +- ephemeral/ephemeral_resource.go | 19 +++++++++++++------ ephemeral/renew.go | 12 +++--------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/ephemeral/close.go b/ephemeral/close.go index 1b76279b7..1661bc82b 100644 --- a/ephemeral/close.go +++ b/ephemeral/close.go @@ -12,9 +12,9 @@ import ( // resource. An instance of this request struct is supplied as an argument to // the ephemeral resource's Close function. type CloseRequest struct { - // PriorState is the object representing the values of the ephemeral - // resource following the latest Open or Renew operation. - PriorState tfsdk.EphemeralState + // State is the object representing the values of the ephemeral + // resource following the Open operation. + State tfsdk.EphemeralState // Private is provider-defined ephemeral resource private state data // which was previously provided by the latest Open or Renew operation. diff --git a/ephemeral/doc.go b/ephemeral/doc.go index 4ac994cd8..02b0e6e3a 100644 --- a/ephemeral/doc.go +++ b/ephemeral/doc.go @@ -13,7 +13,7 @@ // Ephemeral resources are not saved into the Terraform plan or state and can // only be referenced in other ephemeral values, such as provider configuration // attributes. Ephemeral resources are defined by a type/name, such as "examplecloud_thing", -// a schema representing the structure and data types of configuration and lifecycle logic. +// a schema representing the structure and data types of configuration, and lifecycle logic. // // The main starting point for implementations in this package is the // EphemeralResource type which represents an instance of an ephemeral resource diff --git a/ephemeral/ephemeral_resource.go b/ephemeral/ephemeral_resource.go index cfdc5b667..5c6303543 100644 --- a/ephemeral/ephemeral_resource.go +++ b/ephemeral/ephemeral_resource.go @@ -12,13 +12,17 @@ import ( // Ephemeral resources can optionally implement these additional concepts: // // - Configure: Include provider-level data or clients via EphemeralResourceWithConfigure +// // - Validation: Schema-based or entire configuration via EphemeralResourceWithConfigValidators // or EphemeralResourceWithValidateConfig. -// - Renewal: Refresh ephemeral values, such as a temporary access token, -// via EphemeralResourceWithRenew. Ephemeral resources can indicate to Terraform when -// a renewal must occur via the RenewAt response field of the Open/Renew methods. -// - Close: Allows providers to clean up the ephemeral resource -// via EphemeralResourceWithClose. +// +// - Renew: Handle renewal of an expired remote object via EphemeralResourceWithRenew. +// Ephemeral resources can indicate to Terraform when a renewal must occur via the RenewAt +// response field of the Open/Renew methods. Renew cannot return new state data for the +// ephemeral resource instance, so this logic is only appropriate for remote objects like +// HashiCorp Vault leases, which can be renewed without changing their data. +// +// - Close: Allows providers to clean up the ephemeral resource via EphemeralResourceWithClose. type EphemeralResource interface { // Metadata should return the full name of the ephemeral resource, such as // examplecloud_thing. @@ -39,8 +43,11 @@ type EphemeralResource interface { type EphemeralResourceWithRenew interface { EphemeralResource - // Renew is called when the provider must refresh the ephemeral resource values based on + // Renew is called when the provider must renew the ephemeral resource based on // the provided RenewAt time. This RenewAt response field can be set in the OpenResponse and RenewResponse. + // + // Renew cannot return new state data for the ephemeral resource instance, so this logic is only appropriate + // for remote objects like HashiCorp Vault leases, which can be renewed without changing their data. Renew(context.Context, RenewRequest, *RenewResponse) } diff --git a/ephemeral/renew.go b/ephemeral/renew.go index 5e69dfc29..b208b0921 100644 --- a/ephemeral/renew.go +++ b/ephemeral/renew.go @@ -14,9 +14,9 @@ import ( // resource. An instance of this request struct is supplied as an argument to // the ephemeral resource's Renew function. type RenewRequest struct { - // PriorState is the object representing the values of the ephemeral - // resource following the latest Open or Renew operation. - PriorState tfsdk.EphemeralState + // State is the object representing the values of the ephemeral + // resource following the Open operation. + State tfsdk.EphemeralState // Config is the configuration the user supplied for the ephemeral // resource. @@ -37,12 +37,6 @@ type RenewRequest struct { // to the ephemeral resource's Renew function, in which the provider // should set values on the RenewResponse as appropriate. type RenewResponse struct { - // State is the object representing the values of the ephemeral - // resource following the Renew operation. This field is pre-populated - // from RenewRequest.PriorState and should be set during the resource's - // Renew operation. - State tfsdk.EphemeralState - // RenewAt is an optional date/time field that indicates to Terraform // when this ephemeral resource must be renewed at. Terraform will call // the (EphemeralResource).Renew method when the current date/time is on From 1f248564060d4463ade514232d5e1c0583df9a11 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 28 Aug 2024 17:49:56 -0400 Subject: [PATCH 10/55] initial protov5 + fwserver implementation (protov6 stubbed) --- go.mod | 20 +- go.sum | 32 +-- internal/fromproto5/closeephemeralresource.go | 58 +++++ internal/fromproto5/ephemeral_state.go | 53 +++++ internal/fromproto5/openephemeralresource.go | 51 +++++ internal/fromproto5/renewephemeralresource.go | 64 ++++++ .../validateephemeralresourceconfig.go | 31 +++ internal/fwserver/server.go | 24 +++ .../fwserver/server_closeephemeralresource.go | 88 ++++++++ .../fwserver/server_ephemeralresources.go | 198 ++++++++++++++++++ internal/fwserver/server_getmetadata.go | 14 ++ internal/fwserver/server_getproviderschema.go | 25 ++- .../fwserver/server_openephemeralresource.go | 93 ++++++++ .../fwserver/server_renewephemeralresource.go | 113 ++++++++++ .../server_validateephemeralresourceconfig.go | 109 ++++++++++ internal/logging/keys.go | 3 + .../server_closeephemeralresource.go | 50 +++++ .../server_openephemeralresource.go | 50 +++++ .../server_renewephemeralresource.go | 50 +++++ .../server_validateephemeralresourceconfig.go | 50 +++++ internal/proto6server/serve.go | 22 ++ internal/toproto5/closeephemeralresource.go | 25 +++ internal/toproto5/ephemeral_state.go | 28 +++ .../toproto5/ephemeralresourcemetadata.go | 19 ++ internal/toproto5/getmetadata.go | 15 +- internal/toproto5/getproviderschema.go | 25 ++- internal/toproto5/openephemeralresource.go | 37 ++++ internal/toproto5/renewephemeralresource.go | 31 +++ .../validateephemeralresourceconfig.go | 25 +++ 29 files changed, 1360 insertions(+), 43 deletions(-) create mode 100644 internal/fromproto5/closeephemeralresource.go create mode 100644 internal/fromproto5/ephemeral_state.go create mode 100644 internal/fromproto5/openephemeralresource.go create mode 100644 internal/fromproto5/renewephemeralresource.go create mode 100644 internal/fromproto5/validateephemeralresourceconfig.go create mode 100644 internal/fwserver/server_closeephemeralresource.go create mode 100644 internal/fwserver/server_ephemeralresources.go create mode 100644 internal/fwserver/server_openephemeralresource.go create mode 100644 internal/fwserver/server_renewephemeralresource.go create mode 100644 internal/fwserver/server_validateephemeralresourceconfig.go create mode 100644 internal/proto5server/server_closeephemeralresource.go create mode 100644 internal/proto5server/server_openephemeralresource.go create mode 100644 internal/proto5server/server_renewephemeralresource.go create mode 100644 internal/proto5server/server_validateephemeralresourceconfig.go create mode 100644 internal/toproto5/closeephemeralresource.go create mode 100644 internal/toproto5/ephemeral_state.go create mode 100644 internal/toproto5/ephemeralresourcemetadata.go create mode 100644 internal/toproto5/openephemeralresource.go create mode 100644 internal/toproto5/renewephemeralresource.go create mode 100644 internal/toproto5/validateephemeralresourceconfig.go diff --git a/go.mod b/go.mod index bbe8dfada..e27718172 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/hashicorp/terraform-plugin-framework -go 1.21 +go 1.22 -toolchain go1.21.6 +toolchain go1.22.0 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.23.0 + github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240827183355-145d7bc53a3f github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -14,7 +14,7 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-plugin v1.6.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect @@ -25,10 +25,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 352a8f86a..b618ef4c6 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= -github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= -github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240827183355-145d7bc53a3f h1:6E4kyvCnk1Vb0L/wEfvBZFF4vkkbvpsOOHoRkDu60I8= +github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240827183355-145d7bc53a3f/go.mod h1:ko0HcPe7AkwMukddKa13Qq5zyvi8V9KYxBZmqU8a9cE= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -46,23 +46,23 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/fromproto5/closeephemeralresource.go b/internal/fromproto5/closeephemeralresource.go new file mode 100644 index 000000000..d1bc83781 --- /dev/null +++ b/internal/fromproto5/closeephemeralresource.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// CloseEphemeralResourceRequest returns the *fwserver.CloseEphemeralResourceRequest +// equivalent of a *tfprotov5.CloseEphemeralResourceRequest. +func CloseEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.CloseEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.CloseEphemeralResourceRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.CloseEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + state, stateDiags := EphemeralState(ctx, proto5.State, ephemeralResourceSchema) + + diags.Append(stateDiags...) + + fw.State = state + + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto5/ephemeral_state.go b/internal/fromproto5/ephemeral_state.go new file mode 100644 index 000000000..ca374a9ca --- /dev/null +++ b/internal/fromproto5/ephemeral_state.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// EphemeralState returns the *tfsdk.EphemeralState for a *tfprotov5.DynamicValue and +// fwschema.Schema. +func EphemeralState(ctx context.Context, proto5DynamicValue *tfprotov5.DynamicValue, schema fwschema.Schema) (*tfsdk.EphemeralState, diag.Diagnostics) { + if proto5DynamicValue == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if schema == nil { + diags.AddError( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto5DynamicValue, schema, fwschemadata.DataDescriptionEphemeralState) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.EphemeralState{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto5/openephemeralresource.go b/internal/fromproto5/openephemeralresource.go new file mode 100644 index 000000000..60bb9b48c --- /dev/null +++ b/internal/fromproto5/openephemeralresource.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// OpenEphemeralResourceRequest returns the *fwserver.OpenEphemeralResourceRequest +// equivalent of a *tfprotov5.OpenEphemeralResourceRequest. +func OpenEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.OpenEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.OpenEphemeralResourceRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.OpenEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + config, configDiags := Config(ctx, proto5.Config, ephemeralResourceSchema) + + diags.Append(configDiags...) + + fw.Config = config + + return fw, diags +} diff --git a/internal/fromproto5/renewephemeralresource.go b/internal/fromproto5/renewephemeralresource.go new file mode 100644 index 000000000..e82bb3b58 --- /dev/null +++ b/internal/fromproto5/renewephemeralresource.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// RenewEphemeralResourceRequest returns the *fwserver.RenewEphemeralResourceRequest +// equivalent of a *tfprotov5.RenewEphemeralResourceRequest. +func RenewEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.RenewEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.RenewEphemeralResourceRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.RenewEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + config, configDiags := Config(ctx, proto5.Config, ephemeralResourceSchema) + + diags.Append(configDiags...) + + fw.Config = config + + state, stateDiags := EphemeralState(ctx, proto5.State, ephemeralResourceSchema) + + diags.Append(stateDiags...) + + fw.State = state + + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto5/validateephemeralresourceconfig.go b/internal/fromproto5/validateephemeralresourceconfig.go new file mode 100644 index 000000000..ec1acb4b8 --- /dev/null +++ b/internal/fromproto5/validateephemeralresourceconfig.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateEphemeralResourceConfigRequest returns the *fwserver.ValidateEphemeralResourceConfigRequest +// equivalent of a *tfprotov5.ValidateEphemeralResourceConfigRequest. +func ValidateEphemeralResourceConfigRequest(ctx context.Context, proto5 *tfprotov5.ValidateEphemeralResourceConfigRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.ValidateEphemeralResourceConfigRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + fw := &fwserver.ValidateEphemeralResourceConfigRequest{} + + config, diags := Config(ctx, proto5.Config, ephemeralResourceSchema) + + fw.Config = config + fw.EphemeralResource = ephemeralResource + + return fw, diags +} diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index e9f8a55d1..b6f2bc85e 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -61,6 +62,29 @@ type Server struct { // access from race conditions. dataSourceTypesMutex sync.Mutex + // ephemeralResourceSchemas is the cached EphemeralResource Schemas for RPCs that need to + // convert configuration data from the protocol. If not found, it will be + // fetched from the EphemeralResourceType.GetSchema() method. + ephemeralResourceSchemas map[string]fwschema.Schema + + // ephemeralResourceSchemasMutex is a mutex to protect concurrent ephemeralResourceSchemas + // access from race conditions. + ephemeralResourceSchemasMutex sync.RWMutex + + // ephemeralResourceFuncs is the cached EphemeralResource functions for RPCs that need to + // access ephemeral resources. If not found, it will be fetched from the + // Provider.EphemeralResources() method. + ephemeralResourceFuncs map[string]func() ephemeral.EphemeralResource + + // ephemeralResourceFuncsDiags is the cached Diagnostics obtained while populating + // ephemeralResourceFuncs. This is to ensure any warnings or errors are also + // returned appropriately when fetching ephemeralResourceFuncs. + ephemeralResourceFuncsDiags diag.Diagnostics + + // ephemeralResourceFuncsMutex is a mutex to protect concurrent ephemeralResourceFuncs + // access from race conditions. + ephemeralResourceFuncsMutex sync.Mutex + // deferred indicates an automatic provider deferral. When this is set, // the provider will automatically defer the PlanResourceChange, ReadResource, // ImportResourceState, and ReadDataSource RPCs. diff --git a/internal/fwserver/server_closeephemeralresource.go b/internal/fwserver/server_closeephemeralresource.go new file mode 100644 index 000000000..227d88de2 --- /dev/null +++ b/internal/fwserver/server_closeephemeralresource.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// CloseEphemeralResourceRequest is the framework server request for the +// CloseEphemeralResource RPC. +type CloseEphemeralResourceRequest struct { + State *tfsdk.EphemeralState + Private *privatestate.Data + EphemeralResourceSchema fwschema.Schema + EphemeralResource ephemeral.EphemeralResource +} + +// CloseEphemeralResourceResponse is the framework server response for the +// CloseEphemeralResource RPC. +type CloseEphemeralResourceResponse struct { + Diagnostics diag.Diagnostics +} + +// CloseEphemeralResource implements the framework server CloseEphemeralResource RPC. +func (s *Server) CloseEphemeralResource(ctx context.Context, req *CloseEphemeralResourceRequest, resp *CloseEphemeralResourceResponse) { + if req == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + resourceWithClose, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithClose) + if !ok { + // TODO: this diagnostic should be more worded towards a core or plugin-framework bug. + // Either something is bugged in core and called close incorrectly, or the framework populated + // the is_closable response field incorrectly. + resp.Diagnostics.AddError( + "Ephemeral Resource Close Not Implemented", + "This resource does not support close. Please contact the provider developer for additional information.", + ) + return + } + + privateProviderData := privatestate.EmptyProviderData(ctx) + if req.Private != nil && req.Private.Provider != nil { + privateProviderData = req.Private.Provider + } + + closeReq := ephemeral.CloseRequest{ + State: tfsdk.EphemeralState{ + Schema: req.EphemeralResourceSchema, + Raw: req.State.Raw.Copy(), + }, + Private: privateProviderData, + } + closeResp := ephemeral.CloseResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Close") + resourceWithClose.Close(ctx, closeReq, &closeResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Close") + + resp.Diagnostics = closeResp.Diagnostics +} diff --git a/internal/fwserver/server_ephemeralresources.go b/internal/fwserver/server_ephemeralresources.go new file mode 100644 index 000000000..15d54e118 --- /dev/null +++ b/internal/fwserver/server_ephemeralresources.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// EphemeralResource returns the EphemeralResource for a given type name. +func (s *Server) EphemeralResource(ctx context.Context, typeName string) (ephemeral.EphemeralResource, diag.Diagnostics) { + ephemeralResourceFuncs, diags := s.EphemeralResourceFuncs(ctx) + + ephemeralResourceFunc, ok := ephemeralResourceFuncs[typeName] + + if !ok { + diags.AddError( + "Ephemeral Resource Type Not Found", + fmt.Sprintf("No ephemeral resource type named %q was found in the provider.", typeName), + ) + + return nil, diags + } + + return ephemeralResourceFunc(), diags +} + +// EphemeralResourceFuncs returns a map of EphemeralResource functions. The results are cached +// on first use. +func (s *Server) EphemeralResourceFuncs(ctx context.Context) (map[string]func() ephemeral.EphemeralResource, diag.Diagnostics) { + logging.FrameworkTrace(ctx, "Checking EphemeralResourceFuncs lock") + s.ephemeralResourceFuncsMutex.Lock() + defer s.ephemeralResourceFuncsMutex.Unlock() + + if s.ephemeralResourceFuncs != nil { + return s.ephemeralResourceFuncs, s.ephemeralResourceFuncsDiags + } + + providerTypeName := s.ProviderTypeName(ctx) + s.ephemeralResourceFuncs = make(map[string]func() ephemeral.EphemeralResource) + + provider, ok := s.Provider.(provider.ProviderWithEphemeralResources) + + if !ok { + // Only ephemeral resource specific RPCs should return diagnostics about the + // provider not implementing ephemeral resources or missing ephemeral resources. + return s.ephemeralResourceFuncs, s.ephemeralResourceFuncsDiags + } + + logging.FrameworkTrace(ctx, "Calling provider defined Provider EphemeralResources") + ephemeralResourceFuncsSlice := provider.EphemeralResources(ctx) + logging.FrameworkTrace(ctx, "Called provider defined Provider EphemeralResources") + + for _, ephemeralResourceFunc := range ephemeralResourceFuncsSlice { + ephemeralResource := ephemeralResourceFunc() + + ephemeralResourceTypeNameReq := ephemeral.MetadataRequest{ + ProviderTypeName: providerTypeName, + } + ephemeralResourceTypeNameResp := ephemeral.MetadataResponse{} + + ephemeralResource.Metadata(ctx, ephemeralResourceTypeNameReq, &ephemeralResourceTypeNameResp) + + if ephemeralResourceTypeNameResp.TypeName == "" { + s.ephemeralResourceFuncsDiags.AddError( + "Ephemeral Resource Type Name Missing", + fmt.Sprintf("The %T EphemeralResource returned an empty string from the Metadata method. ", ephemeralResource)+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + logging.FrameworkTrace(ctx, "Found ephemeral resource type", map[string]interface{}{logging.KeyEphemeralResourceType: ephemeralResourceTypeNameResp.TypeName}) + + if _, ok := s.ephemeralResourceFuncs[ephemeralResourceTypeNameResp.TypeName]; ok { + s.ephemeralResourceFuncsDiags.AddError( + "Duplicate Ephemeral Resource Type Defined", + fmt.Sprintf("The %s ephemeral resource type name was returned for multiple ephemeral resources. ", ephemeralResourceTypeNameResp.TypeName)+ + "Ephemeral resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + s.ephemeralResourceFuncs[ephemeralResourceTypeNameResp.TypeName] = ephemeralResourceFunc + } + + return s.ephemeralResourceFuncs, s.ephemeralResourceFuncsDiags +} + +// EphemeralResourceMetadatas returns a slice of EphemeralResourceMetadata for the GetMetadata +// RPC. +func (s *Server) EphemeralResourceMetadatas(ctx context.Context) ([]EphemeralResourceMetadata, diag.Diagnostics) { + ephemeralResourceFuncs, diags := s.EphemeralResourceFuncs(ctx) + + ephemeralResourceMetadatas := make([]EphemeralResourceMetadata, 0, len(ephemeralResourceFuncs)) + + for typeName := range ephemeralResourceFuncs { + ephemeralResourceMetadatas = append(ephemeralResourceMetadatas, EphemeralResourceMetadata{ + TypeName: typeName, + }) + } + + return ephemeralResourceMetadatas, diags +} + +// EphemeralResourceSchema returns the EphemeralResource Schema for the given type name and +// caches the result for later EphemeralResource operations. +func (s *Server) EphemeralResourceSchema(ctx context.Context, typeName string) (fwschema.Schema, diag.Diagnostics) { + s.ephemeralResourceSchemasMutex.RLock() + ephemeralResourceSchema, ok := s.ephemeralResourceSchemas[typeName] + s.ephemeralResourceSchemasMutex.RUnlock() + + if ok { + return ephemeralResourceSchema, nil + } + + var diags diag.Diagnostics + + ephemeralResource, ephemeralResourceDiags := s.EphemeralResource(ctx, typeName) + + diags.Append(ephemeralResourceDiags...) + + if diags.HasError() { + return nil, diags + } + + schemaReq := ephemeral.SchemaRequest{} + schemaResp := ephemeral.SchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Schema method", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + ephemeralResource.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Schema method", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + + diags.Append(schemaResp.Diagnostics...) + + if diags.HasError() { + return schemaResp.Schema, diags + } + + s.ephemeralResourceSchemasMutex.Lock() + + if s.ephemeralResourceSchemas == nil { + s.ephemeralResourceSchemas = make(map[string]fwschema.Schema) + } + + s.ephemeralResourceSchemas[typeName] = schemaResp.Schema + + s.ephemeralResourceSchemasMutex.Unlock() + + return schemaResp.Schema, diags +} + +// EphemeralResourceSchemas returns a map of EphemeralResource Schemas for the +// GetProviderSchema RPC without caching since not all schemas are guaranteed to +// be necessary for later provider operations. The schema implementations are +// also validated. +func (s *Server) EphemeralResourceSchemas(ctx context.Context) (map[string]fwschema.Schema, diag.Diagnostics) { + ephemeralResourceSchemas := make(map[string]fwschema.Schema) + + ephemeralResourceFuncs, diags := s.EphemeralResourceFuncs(ctx) + + for typeName, ephemeralResourceFunc := range ephemeralResourceFuncs { + ephemeralResource := ephemeralResourceFunc() + + schemaReq := ephemeral.SchemaRequest{} + schemaResp := ephemeral.SchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Schema", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + ephemeralResource.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Schema", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + + diags.Append(schemaResp.Diagnostics...) + + if schemaResp.Diagnostics.HasError() { + continue + } + + validateDiags := schemaResp.Schema.ValidateImplementation(ctx) + + diags.Append(validateDiags...) + + if validateDiags.HasError() { + continue + } + + ephemeralResourceSchemas[typeName] = schemaResp.Schema + } + + return ephemeralResourceSchemas, diags +} diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index ebd0728a9..458694f2b 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -18,6 +18,7 @@ type GetMetadataRequest struct{} type GetMetadataResponse struct { DataSources []DataSourceMetadata Diagnostics diag.Diagnostics + EphemeralResources []EphemeralResourceMetadata Functions []FunctionMetadata Resources []ResourceMetadata ServerCapabilities *ServerCapabilities @@ -30,6 +31,13 @@ type DataSourceMetadata struct { TypeName string } +// EphemeralResourceMetadata is the framework server equivalent of the +// tfprotov5.EphemeralResourceMetadata and tfprotov6.EphemeralResourceMetadata types. +type EphemeralResourceMetadata struct { + // TypeName is the name of the ephemeral resource. + TypeName string +} + // FunctionMetadata is the framework server equivalent of the // tfprotov5.FunctionMetadata and tfprotov6.FunctionMetadata types. type FunctionMetadata struct { @@ -47,6 +55,7 @@ type ResourceMetadata struct { // GetMetadata implements the framework server GetMetadata RPC. func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp *GetMetadataResponse) { resp.DataSources = []DataSourceMetadata{} + resp.EphemeralResources = []EphemeralResourceMetadata{} resp.Functions = []FunctionMetadata{} resp.Resources = []ResourceMetadata{} resp.ServerCapabilities = s.ServerCapabilities() @@ -55,6 +64,10 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp resp.Diagnostics.Append(diags...) + ephemeralResourceMetadatas, diags := s.EphemeralResourceMetadatas(ctx) + + resp.Diagnostics.Append(diags...) + functionMetadatas, diags := s.FunctionMetadatas(ctx) resp.Diagnostics.Append(diags...) @@ -68,6 +81,7 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp } resp.DataSources = datasourceMetadatas + resp.EphemeralResources = ephemeralResourceMetadatas resp.Functions = functionMetadatas resp.Resources = resourceMetadatas } diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index afcca8352..b8061dd10 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -18,13 +18,14 @@ type GetProviderSchemaRequest struct{} // GetProviderSchemaResponse is the framework server response for the // GetProviderSchema RPC. type GetProviderSchemaResponse struct { - ServerCapabilities *ServerCapabilities - Provider fwschema.Schema - ProviderMeta fwschema.Schema - ResourceSchemas map[string]fwschema.Schema - DataSourceSchemas map[string]fwschema.Schema - FunctionDefinitions map[string]function.Definition - Diagnostics diag.Diagnostics + ServerCapabilities *ServerCapabilities + Provider fwschema.Schema + ProviderMeta fwschema.Schema + ResourceSchemas map[string]fwschema.Schema + DataSourceSchemas map[string]fwschema.Schema + EphemeralResourceSchemas map[string]fwschema.Schema + FunctionDefinitions map[string]function.Definition + Diagnostics diag.Diagnostics } // GetProviderSchema implements the framework server GetProviderSchema RPC. @@ -80,4 +81,14 @@ func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRe } resp.FunctionDefinitions = functions + + ephemeralResourceSchemas, diags := s.EphemeralResourceSchemas(ctx) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.EphemeralResourceSchemas = ephemeralResourceSchemas } diff --git a/internal/fwserver/server_openephemeralresource.go b/internal/fwserver/server_openephemeralresource.go new file mode 100644 index 000000000..8be1fbc3b --- /dev/null +++ b/internal/fwserver/server_openephemeralresource.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// OpenEphemeralResourceRequest is the framework server request for the +// OpenEphemeralResource RPC. +type OpenEphemeralResourceRequest struct { + Config *tfsdk.Config + EphemeralResourceSchema fwschema.Schema + EphemeralResource ephemeral.EphemeralResource +} + +// OpenEphemeralResourceResponse is the framework server response for the +// OpenEphemeralResource RPC. +type OpenEphemeralResourceResponse struct { + State *tfsdk.EphemeralState + Private *privatestate.Data + Diagnostics diag.Diagnostics + RenewAt time.Time + IsClosable bool +} + +// OpenEphemeralResource implements the framework server OpenEphemeralResource RPC. +func (s *Server) OpenEphemeralResource(ctx context.Context, req *OpenEphemeralResourceRequest, resp *OpenEphemeralResourceResponse) { + if req == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + openReq := ephemeral.OpenRequest{ + Config: tfsdk.Config{ + Schema: req.EphemeralResourceSchema, + }, + } + openResp := ephemeral.OpenResponse{ + State: tfsdk.EphemeralState{ + Schema: req.EphemeralResourceSchema, + }, + Private: privatestate.EmptyProviderData(ctx), + } + + if req.Config != nil { + openReq.Config = *req.Config + openResp.State.Raw = req.Config.Raw.Copy() + } + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Open") + req.EphemeralResource.Open(ctx, openReq, &openResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Open") + + resp.Diagnostics = openResp.Diagnostics + resp.State = &openResp.State + resp.RenewAt = openResp.RenewAt + + resp.Private = privatestate.EmptyData(ctx) + if openResp.Private != nil { + resp.Private.Provider = openResp.Private + } + + _, isClosable := req.EphemeralResource.(ephemeral.EphemeralResourceWithClose) + resp.IsClosable = isClosable +} diff --git a/internal/fwserver/server_renewephemeralresource.go b/internal/fwserver/server_renewephemeralresource.go new file mode 100644 index 000000000..80a7589d4 --- /dev/null +++ b/internal/fwserver/server_renewephemeralresource.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// RenewEphemeralResourceRequest is the framework server request for the +// RenewEphemeralResource RPC. +type RenewEphemeralResourceRequest struct { + Config *tfsdk.Config + State *tfsdk.EphemeralState + Private *privatestate.Data + EphemeralResourceSchema fwschema.Schema + EphemeralResource ephemeral.EphemeralResource +} + +// RenewEphemeralResourceResponse is the framework server response for the +// RenewEphemeralResource RPC. +type RenewEphemeralResourceResponse struct { + Private *privatestate.Data + Diagnostics diag.Diagnostics + RenewAt time.Time +} + +// RenewEphemeralResource implements the framework server RenewEphemeralResource RPC. +func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeralResourceRequest, resp *RenewEphemeralResourceResponse) { + if req == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + resourceWithRenew, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithRenew) + if !ok { + // TODO: this diagnostic should be more worded towards an invalid implementation or core bug. + // Either something is bugged in core and called renew incorrectly, or the provider populated + // the RenewAt response field without defining the renew function (invalid implementation). + resp.Diagnostics.AddError( + "Ephemeral Resource Renew Not Implemented", + "This resource does not support renew. Please contact the provider developer for additional information.", + ) + return + } + + // Ensure that resp.Private is never nil. + resp.Private = privatestate.EmptyData(ctx) + if req.Private != nil { + // Overwrite resp.Private with req.Private providing it is not nil. + resp.Private = req.Private + + // Ensure that resp.Private.Provider is never nil. + if resp.Private.Provider == nil { + resp.Private.Provider = privatestate.EmptyProviderData(ctx) + } + } + + renewReq := ephemeral.RenewRequest{ + Config: tfsdk.Config{ + Schema: req.EphemeralResourceSchema, + }, + State: tfsdk.EphemeralState{ + Schema: req.EphemeralResourceSchema, + Raw: req.State.Raw.Copy(), + }, + Private: resp.Private.Provider, + } + renewResp := ephemeral.RenewResponse{ + Private: renewReq.Private, + } + + if req.Config != nil { + renewReq.Config = *req.Config + } + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Renew") + resourceWithRenew.Renew(ctx, renewReq, &renewResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Renew") + + resp.Diagnostics = renewResp.Diagnostics + resp.RenewAt = renewResp.RenewAt + + if renewResp.Private != nil { + resp.Private.Provider = renewResp.Private + } +} diff --git a/internal/fwserver/server_validateephemeralresourceconfig.go b/internal/fwserver/server_validateephemeralresourceconfig.go new file mode 100644 index 000000000..a99a0dbfb --- /dev/null +++ b/internal/fwserver/server_validateephemeralresourceconfig.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateEphemeralResourceConfigRequest is the framework server request for the +// ValidateEphemeralResourceConfig RPC. +type ValidateEphemeralResourceConfigRequest struct { + Config *tfsdk.Config + EphemeralResource ephemeral.EphemeralResource +} + +// ValidateEphemeralResourceConfigResponse is the framework server response for the +// ValidateEphemeralResourceConfig RPC. +type ValidateEphemeralResourceConfigResponse struct { + Diagnostics diag.Diagnostics +} + +// ValidateEphemeralResourceConfig implements the framework server ValidateEphemeralResourceConfig RPC. +func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, req *ValidateEphemeralResourceConfigRequest, resp *ValidateEphemeralResourceConfigResponse) { + if req == nil || req.Config == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + vdscReq := ephemeral.ValidateConfigRequest{ + Config: *req.Config, + } + + if ephemeralResourceWithConfigValidators, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigValidators); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigValidators") + + for _, configValidator := range ephemeralResourceWithConfigValidators.ConfigValidators(ctx) { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + vdscResp := &ephemeral.ValidateConfigResponse{} + + logging.FrameworkTrace( + ctx, + "Calling provider defined EphemeralResourceConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + configValidator.ValidateEphemeralResource(ctx, vdscReq, vdscResp) + logging.FrameworkTrace( + ctx, + "Called provider defined EphemeralResourceConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + } + + if ephemeralResourceWithValidateConfig, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithValidateConfig); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithValidateConfig") + + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + vdscResp := &ephemeral.ValidateConfigResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource ValidateConfig") + ephemeralResourceWithValidateConfig.ValidateConfig(ctx, vdscReq, vdscResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource ValidateConfig") + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + + validateSchemaReq := ValidateSchemaRequest{ + Config: *req.Config, + } + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateSchemaResp := ValidateSchemaResponse{} + + SchemaValidate(ctx, req.Config.Schema, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics.Append(validateSchemaResp.Diagnostics...) +} diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 312a839ac..1443710c9 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -18,6 +18,9 @@ const ( // The type of data source being operated on, such as "archive_file" KeyDataSourceType = "tf_data_source_type" + // The type of ephemeral resource being operated on, such as "random_password" + KeyEphemeralResourceType = "tf_ephemeral_resource_type" + // The Deferred reason for an RPC response KeyDeferredReason = "tf_deferred_reason" diff --git a/internal/proto5server/server_closeephemeralresource.go b/internal/proto5server/server_closeephemeralresource.go new file mode 100644 index 000000000..1e07cef33 --- /dev/null +++ b/internal/proto5server/server_closeephemeralresource.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// CloseEphemeralResource satisfies the tfprotov5.ProviderServer interface. +func (s *Server) CloseEphemeralResource(ctx context.Context, proto5Req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.CloseEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.CloseEphemeralResourceRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.CloseEphemeralResource(ctx, fwReq, fwResp) + + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_openephemeralresource.go b/internal/proto5server/server_openephemeralresource.go new file mode 100644 index 000000000..b972bd4d8 --- /dev/null +++ b/internal/proto5server/server_openephemeralresource.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// OpenEphemeralResource satisfies the tfprotov5.ProviderServer interface. +func (s *Server) OpenEphemeralResource(ctx context.Context, proto5Req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.OpenEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.OpenEphemeralResourceRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.OpenEphemeralResource(ctx, fwReq, fwResp) + + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_renewephemeralresource.go b/internal/proto5server/server_renewephemeralresource.go new file mode 100644 index 000000000..76be1f019 --- /dev/null +++ b/internal/proto5server/server_renewephemeralresource.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// RenewEphemeralResource satisfies the tfprotov5.ProviderServer interface. +func (s *Server) RenewEphemeralResource(ctx context.Context, proto5Req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.RenewEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.RenewEphemeralResourceRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.RenewEphemeralResource(ctx, fwReq, fwResp) + + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_validateephemeralresourceconfig.go b/internal/proto5server/server_validateephemeralresourceconfig.go new file mode 100644 index 000000000..04018a01b --- /dev/null +++ b/internal/proto5server/server_validateephemeralresourceconfig.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateEphemeralResourceConfig satisfies the tfprotov5.ProviderServer interface. +func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, proto5Req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.ValidateEphemeralResourceConfigResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.ValidateEphemeralResourceConfigRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateEphemeralResourceConfig(ctx, fwReq, fwResp) + + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/serve.go b/internal/proto6server/serve.go index 26cf0c4e1..3a8170c59 100644 --- a/internal/proto6server/serve.go +++ b/internal/proto6server/serve.go @@ -21,6 +21,28 @@ type Server struct { contextCancelsMu sync.Mutex } +// TODO: Stub for now to satisfy compiler +// +// CloseEphemeralResource implements tfprotov6.ProviderServer. +func (s *Server) CloseEphemeralResource(context.Context, *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + panic("unimplemented") +} + +// OpenEphemeralResource implements tfprotov6.ProviderServer. +func (s *Server) OpenEphemeralResource(context.Context, *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + panic("unimplemented") +} + +// RenewEphemeralResource implements tfprotov6.ProviderServer. +func (s *Server) RenewEphemeralResource(context.Context, *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + panic("unimplemented") +} + +// ValidateEphemeralResourceConfig implements tfprotov6.ProviderServer. +func (s *Server) ValidateEphemeralResourceConfig(context.Context, *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + panic("unimplemented") +} + func (s *Server) registerContext(in context.Context) context.Context { ctx, cancel := context.WithCancel(in) s.contextCancelsMu.Lock() diff --git a/internal/toproto5/closeephemeralresource.go b/internal/toproto5/closeephemeralresource.go new file mode 100644 index 000000000..5bc9484db --- /dev/null +++ b/internal/toproto5/closeephemeralresource.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// CloseEphemeralResourceResponse returns the *tfprotov5.CloseEphemeralResourceResponse +// equivalent of a *fwserver.CloseEphemeralResourceResponse. +func CloseEphemeralResourceResponse(ctx context.Context, fw *fwserver.CloseEphemeralResourceResponse) *tfprotov5.CloseEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto5 +} diff --git a/internal/toproto5/ephemeral_state.go b/internal/toproto5/ephemeral_state.go new file mode 100644 index 000000000..9e91a58b3 --- /dev/null +++ b/internal/toproto5/ephemeral_state.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// EphemeralState returns the *tfprotov5.DynamicValue for a *tfsdk.EphemeralState. +func EphemeralState(ctx context.Context, fw *tfsdk.EphemeralState) (*tfprotov5.DynamicValue, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + data := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionEphemeralState, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + return DynamicValue(ctx, data) +} diff --git a/internal/toproto5/ephemeralresourcemetadata.go b/internal/toproto5/ephemeralresourcemetadata.go new file mode 100644 index 000000000..e301fa35b --- /dev/null +++ b/internal/toproto5/ephemeralresourcemetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// EphemeralResourceMetadata returns the tfprotov5.EphemeralResourceMetadata for a +// fwserver.EphemeralResourceMetadata. +func EphemeralResourceMetadata(ctx context.Context, fw fwserver.EphemeralResourceMetadata) tfprotov5.EphemeralResourceMetadata { + return tfprotov5.EphemeralResourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto5/getmetadata.go b/internal/toproto5/getmetadata.go index 9c1892d8a..4150c2473 100644 --- a/internal/toproto5/getmetadata.go +++ b/internal/toproto5/getmetadata.go @@ -17,25 +17,30 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) return nil } - protov6 := &tfprotov5.GetMetadataResponse{ + protov5 := &tfprotov5.GetMetadataResponse{ DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0, len(fw.EphemeralResources)), Functions: make([]tfprotov5.FunctionMetadata, 0, len(fw.Functions)), Resources: make([]tfprotov5.ResourceMetadata, 0, len(fw.Resources)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } for _, datasource := range fw.DataSources { - protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) + protov5.DataSources = append(protov5.DataSources, DataSourceMetadata(ctx, datasource)) + } + + for _, ephemeralResource := range fw.EphemeralResources { + protov5.EphemeralResources = append(protov5.EphemeralResources, EphemeralResourceMetadata(ctx, ephemeralResource)) } for _, function := range fw.Functions { - protov6.Functions = append(protov6.Functions, FunctionMetadata(ctx, function)) + protov5.Functions = append(protov5.Functions, FunctionMetadata(ctx, function)) } for _, resource := range fw.Resources { - protov6.Resources = append(protov6.Resources, ResourceMetadata(ctx, resource)) + protov5.Resources = append(protov5.Resources, ResourceMetadata(ctx, resource)) } - return protov6 + return protov5 } diff --git a/internal/toproto5/getproviderschema.go b/internal/toproto5/getproviderschema.go index 1fec486ae..28b8906ff 100644 --- a/internal/toproto5/getproviderschema.go +++ b/internal/toproto5/getproviderschema.go @@ -18,11 +18,12 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov5 := &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.DataSourceSchemas)), - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Functions: make(map[string]*tfprotov5.Function, len(fw.FunctionDefinitions)), - ResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.ResourceSchemas)), - ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), + DataSourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.DataSourceSchemas)), + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.EphemeralResourceSchemas)), + Functions: make(map[string]*tfprotov5.Function, len(fw.FunctionDefinitions)), + ResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.ResourceSchemas)), + ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } var err error @@ -83,5 +84,19 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } } + for ephemeralResourceType, ephemeralResourceSchema := range fw.EphemeralResourceSchemas { + protov5.EphemeralResourceSchemas[ephemeralResourceType], err = Schema(ctx, ephemeralResourceSchema) + + if err != nil { + protov5.Diagnostics = append(protov5.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"" + ephemeralResourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov5 + } + } + return protov5 } diff --git a/internal/toproto5/openephemeralresource.go b/internal/toproto5/openephemeralresource.go new file mode 100644 index 000000000..18bf5af63 --- /dev/null +++ b/internal/toproto5/openephemeralresource.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// OpenEphemeralResourceResponse returns the *tfprotov5.OpenEphemeralResourceResponse +// equivalent of a *fwserver.OpenEphemeralResourceResponse. +func OpenEphemeralResourceResponse(ctx context.Context, fw *fwserver.OpenEphemeralResourceResponse) *tfprotov5.OpenEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + IsClosable: fw.IsClosable, + } + + state, diags := EphemeralState(ctx, fw.State) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.State = state + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Private = newPrivate + + return proto5 +} diff --git a/internal/toproto5/renewephemeralresource.go b/internal/toproto5/renewephemeralresource.go new file mode 100644 index 000000000..5947c9dd1 --- /dev/null +++ b/internal/toproto5/renewephemeralresource.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// RenewEphemeralResourceResponse returns the *tfprotov5.RenewEphemeralResourceResponse +// equivalent of a *fwserver.RenewEphemeralResourceResponse. +func RenewEphemeralResourceResponse(ctx context.Context, fw *fwserver.RenewEphemeralResourceResponse) *tfprotov5.RenewEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + } + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Private = newPrivate + + return proto5 +} diff --git a/internal/toproto5/validateephemeralresourceconfig.go b/internal/toproto5/validateephemeralresourceconfig.go new file mode 100644 index 000000000..fcd19ac99 --- /dev/null +++ b/internal/toproto5/validateephemeralresourceconfig.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateEphemeralResourceConfigResponse returns the *tfprotov5.ValidateEphemeralResourceConfigResponse +// equivalent of a *fwserver.ValidateEphemeralResourceConfigResponse. +func ValidateEphemeralResourceConfigResponse(ctx context.Context, fw *fwserver.ValidateEphemeralResourceConfigResponse) *tfprotov5.ValidateEphemeralResourceConfigResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto5 +} From 07f743781b03fe1f5e166b01a017661604d5af6b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 29 Aug 2024 14:56:10 -0400 Subject: [PATCH 11/55] add fromproto5 tests --- .../fromproto5/closeephemeralresource_test.go | 147 +++++++++++++++ internal/fromproto5/ephemeral_state_test.go | 122 ++++++++++++ .../fromproto5/openephemeralresource_test.go | 122 ++++++++++++ .../fromproto5/renewephemeralresource_test.go | 175 ++++++++++++++++++ .../validateephemeralresourceconfig_test.go | 110 +++++++++++ internal/fwschemadata/data_description.go | 2 +- 6 files changed, 677 insertions(+), 1 deletion(-) create mode 100644 internal/fromproto5/closeephemeralresource_test.go create mode 100644 internal/fromproto5/ephemeral_state_test.go create mode 100644 internal/fromproto5/openephemeralresource_test.go create mode 100644 internal/fromproto5/renewephemeralresource_test.go create mode 100644 internal/fromproto5/validateephemeralresourceconfig_test.go diff --git a/internal/fromproto5/closeephemeralresource_test.go b/internal/fromproto5/closeephemeralresource_test.go new file mode 100644 index 000000000..821eadff2 --- /dev/null +++ b/internal/fromproto5/closeephemeralresource_test.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestCloseEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov5.CloseEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.CloseEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.CloseEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "private": { + input: &tfprotov5.CloseEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.CloseEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "state-missing-schema": { + input: &tfprotov5.CloseEphemeralResourceRequest{ + State: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "state": { + input: &tfprotov5.CloseEphemeralResourceRequest{ + State: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.CloseEphemeralResourceRequest{ + State: &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.CloseEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/ephemeral_state_test.go b/internal/fromproto5/ephemeral_state_test.go new file mode 100644 index 000000000..cd0fd4af0 --- /dev/null +++ b/internal/fromproto5/ephemeral_state_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestEphemeralState(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.DynamicValue + schema fwschema.Schema + expected *tfsdk.EphemeralState + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "missing-schema": { + input: &testProto5DynamicValue, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "invalid-schema": { + input: &testProto5DynamicValue, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue: AttributeName(\"test_attribute\"): couldn't decode bool: msgpack: invalid code=aa decoding bool", + ), + }, + }, + "valid": { + input: &testProto5DynamicValue, + schema: testFwSchema, + expected: &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.EphemeralState(context.Background(), testCase.input, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/openephemeralresource_test.go b/internal/fromproto5/openephemeralresource_test.go new file mode 100644 index 000000000..62cc1a31f --- /dev/null +++ b/internal/fromproto5/openephemeralresource_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestOpenEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.OpenEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.OpenEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.OpenEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.OpenEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/renewephemeralresource_test.go b/internal/fromproto5/renewephemeralresource_test.go new file mode 100644 index 000000000..e5bc70c14 --- /dev/null +++ b/internal/fromproto5/renewephemeralresource_test.go @@ -0,0 +1,175 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestRenewEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov5.RenewEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.RenewEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.RenewEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov5.RenewEphemeralResourceRequest{ + Config: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.RenewEphemeralResourceRequest{ + Config: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "private": { + input: &tfprotov5.RenewEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "state-missing-schema": { + input: &tfprotov5.RenewEphemeralResourceRequest{ + State: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "state": { + input: &tfprotov5.RenewEphemeralResourceRequest{ + State: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + State: &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.RenewEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/validateephemeralresourceconfig_test.go b/internal/fromproto5/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..bf4aec660 --- /dev/null +++ b/internal/fromproto5/validateephemeralresourceconfig_test.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateEphemeralResourceConfigRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.ValidateEphemeralResourceConfigRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + expected *fwserver.ValidateEphemeralResourceConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.ValidateEphemeralResourceConfigRequest{}, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testProto5DynamicValue, + }, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ValidateEphemeralResourceConfigRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fwschemadata/data_description.go b/internal/fwschemadata/data_description.go index b72c86b1c..5d6da22ba 100644 --- a/internal/fwschemadata/data_description.go +++ b/internal/fwschemadata/data_description.go @@ -18,7 +18,7 @@ const ( // DataDescriptionEphemeralState is used for Data that represents // an ephemeral state-based value. - DataDescriptionEphemeralState DataDescription = "ephemeral-state" + DataDescriptionEphemeralState DataDescription = "ephemeral state" ) // DataDescription is a human friendly type for Data. Used in error From 0985765c7e99e5b7b0333d0a9a214c8bb650d8a2 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 29 Aug 2024 17:13:09 -0400 Subject: [PATCH 12/55] add toproto5 tests --- .../toproto5/closeephemeralresource_test.go | 69 + internal/toproto5/ephemeral_state_test.go | 109 + .../ephemeralresourcemetadata_test.go | 46 + internal/toproto5/getmetadata_test.go | 46 +- internal/toproto5/getproviderschema_test.go | 1821 ++++++++++++++--- .../toproto5/openephemeralresource_test.go | 206 ++ .../toproto5/renewephemeralresource_test.go | 117 ++ .../validateephemeralresourceconfig_test.go | 69 + 8 files changed, 2152 insertions(+), 331 deletions(-) create mode 100644 internal/toproto5/closeephemeralresource_test.go create mode 100644 internal/toproto5/ephemeral_state_test.go create mode 100644 internal/toproto5/ephemeralresourcemetadata_test.go create mode 100644 internal/toproto5/openephemeralresource_test.go create mode 100644 internal/toproto5/renewephemeralresource_test.go create mode 100644 internal/toproto5/validateephemeralresourceconfig_test.go diff --git a/internal/toproto5/closeephemeralresource_test.go b/internal/toproto5/closeephemeralresource_test.go new file mode 100644 index 000000000..f10ab23b1 --- /dev/null +++ b/internal/toproto5/closeephemeralresource_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestCloseEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.CloseEphemeralResourceResponse + expected *tfprotov5.CloseEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.CloseEphemeralResourceResponse{}, + expected: &tfprotov5.CloseEphemeralResourceResponse{}, + }, + "diagnostics": { + input: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.CloseEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/ephemeral_state_test.go b/internal/toproto5/ephemeral_state_test.go new file mode 100644 index 000000000..9ee93185a --- /dev/null +++ b/internal/toproto5/ephemeral_state_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestEphemeralState(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testEphemeralState := &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testEphemeralStateInvalid := &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testCases := map[string]struct { + input *tfsdk.EphemeralState + expected *tfprotov5.DynamicValue + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testEphemeralStateInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state to the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + ), + }, + }, + "valid": { + input: testEphemeralState, + expected: &testProto5DynamicValue, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto5.EphemeralState(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/ephemeralresourcemetadata_test.go b/internal/toproto5/ephemeralresourcemetadata_test.go new file mode 100644 index 000000000..2e3e13731 --- /dev/null +++ b/internal/toproto5/ephemeralresourcemetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestEphemeralResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.EphemeralResourceMetadata + expected tfprotov5.EphemeralResourceMetadata + }{ + "TypeName": { + fw: fwserver.EphemeralResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov5.EphemeralResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.EphemeralResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/getmetadata_test.go b/internal/toproto5/getmetadata_test.go index 07001c939..f328d387e 100644 --- a/internal/toproto5/getmetadata_test.go +++ b/internal/toproto5/getmetadata_test.go @@ -45,6 +45,32 @@ func TestGetMetadataResponse(t *testing.T) { TypeName: "test_data_source_2", }, }, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + }, + }, + "ephemeralresources": { + input: &fwserver.GetMetadataResponse{ + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, }, @@ -71,8 +97,9 @@ func TestGetMetadataResponse(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, }, }, "functions": { @@ -87,7 +114,8 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{ { Name: "function1", @@ -111,8 +139,9 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{ { TypeName: "test_resource_1", @@ -131,9 +160,10 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 1683b0e7e..7a6cd4761 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -80,8 +81,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-computed": { @@ -110,8 +112,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-deprecated": { @@ -142,8 +145,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-optional": { @@ -172,8 +176,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-optional-computed": { @@ -204,8 +209,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-required": { @@ -234,8 +240,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-sensitive": { @@ -266,8 +273,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-bool": { @@ -296,8 +304,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-float32": { @@ -326,8 +335,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-float64": { @@ -356,8 +366,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-int32": { @@ -386,8 +397,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-int64": { @@ -416,16 +428,1077 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-list-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-list-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting data source schema", + Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-list-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-map-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting data source schema", + Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-map-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-number": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.NumberAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ObjectAttribute{ + Required: true, + AttributeTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting data source schema", + Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-single-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SingleNestedAttribute{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting data source schema", + Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-dynamic": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.DynamicPseudoType, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-block-list": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.ListNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_block", + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-block-set": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SetNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, + TypeName: "test_block", + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SingleNestedBlock{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-multiple-ephemeral-resources": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource_1": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + "test_ephemeral_resource_2": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource_1": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + "test_ephemeral_resource_2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-computed": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-deprecated": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + DeprecationMessage: "deprecated", + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Deprecated: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional-computed": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-required": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-sensitive": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Sensitive: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Sensitive: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-bool": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-list-list-string": { + "ephemeral-resource-attribute-type-list-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.ListType{ ElemType: types.StringType, @@ -436,8 +1509,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -457,15 +1531,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-list-nested-attributes": { + "ephemeral-resource-attribute-type-list-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -477,26 +1551,27 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": nil, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, }, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Error converting data source schema", - Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-list-object": { + "ephemeral-resource-attribute-type-list-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -509,8 +1584,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -532,12 +1608,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-list-string": { + "ephemeral-resource-attribute-type-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.StringType, }, @@ -546,8 +1622,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -565,15 +1642,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-map-nested-attributes": { + "ephemeral-resource-attribute-type-map-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -585,26 +1662,27 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": nil, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, }, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Error converting data source schema", - Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-map-string": { + "ephemeral-resource-attribute-type-map-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapAttribute{ Required: true, ElementType: types.StringType, }, @@ -613,8 +1691,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -632,12 +1711,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-number": { + "ephemeral-resource-attribute-type-number": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.NumberAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.NumberAttribute{ Required: true, }, }, @@ -645,8 +1724,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -662,12 +1742,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-object": { + "ephemeral-resource-attribute-type-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ObjectAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ObjectAttribute{ Required: true, AttributeTypes: map[string]attr.Type{ "test_object_attribute": types.StringType, @@ -678,8 +1758,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -699,15 +1780,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-nested-attributes": { + "ephemeral-resource-attribute-type-set-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -719,26 +1800,27 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": nil, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, }, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Error converting data source schema", - Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-object": { + "ephemeral-resource-attribute-type-set-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -751,8 +1833,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -774,12 +1857,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-set-string": { + "ephemeral-resource-attribute-type-set-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.SetType{ ElemType: types.StringType, @@ -790,8 +1873,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -811,12 +1895,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-string": { + "ephemeral-resource-attribute-type-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.StringType, }, @@ -825,8 +1909,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -844,14 +1929,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-single-nested-attributes": { + "ephemeral-resource-attribute-type-single-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SingleNestedAttribute{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SingleNestedAttribute{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -862,26 +1947,27 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": nil, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, }, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Error converting data source schema", - Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-string": { + "ephemeral-resource-attribute-type-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -889,8 +1975,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -906,12 +1993,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-dynamic": { + "ephemeral-resource-attribute-type-dynamic": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.DynamicAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.DynamicAttribute{ Required: true, }, }, @@ -919,8 +2006,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -936,15 +2024,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-block-list": { + "ephemeral-resource-block-list": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.ListNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.ListNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -955,8 +2043,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ { @@ -980,15 +2069,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-block-set": { + "ephemeral-resource-block-set": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SetNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SetNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -999,8 +2088,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ { @@ -1024,14 +2114,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-block-single": { + "ephemeral-resource-block-single": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SingleNestedBlock{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SingleNestedBlock{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -1041,8 +2131,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ { @@ -1078,7 +2169,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction1": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1106,7 +2198,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { DeprecationMessage: "test deprecation message", @@ -1129,7 +2222,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Description: "test description", @@ -1156,7 +2250,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{ @@ -1187,7 +2282,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1209,7 +2305,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1232,7 +2329,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1259,8 +2357,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1287,8 +2386,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1314,8 +2414,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1342,8 +2443,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1370,8 +2472,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1397,8 +2500,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1424,8 +2528,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1451,8 +2556,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1478,8 +2584,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1508,8 +2615,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1546,7 +2654,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1574,8 +2683,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1608,8 +2718,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1644,7 +2755,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1668,8 +2780,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1697,8 +2810,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1727,8 +2841,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1765,7 +2880,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1793,8 +2909,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1829,8 +2946,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1861,8 +2979,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1895,7 +3014,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1918,8 +3038,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1945,8 +3066,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1978,8 +3100,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -2019,8 +3142,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -2058,8 +3182,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -2093,8 +3218,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2120,8 +3246,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2147,8 +3274,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2174,8 +3302,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2201,8 +3330,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2231,8 +3361,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2269,7 +3400,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2297,7 +3429,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2331,8 +3464,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2367,7 +3501,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2391,8 +3526,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2420,8 +3556,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2450,8 +3587,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2488,7 +3626,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2516,8 +3655,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2552,8 +3692,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2584,8 +3725,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2618,7 +3760,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2641,8 +3784,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2677,8 +3821,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource_1": { Block: &tfprotov5.SchemaBlock{ @@ -2718,8 +3863,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2749,8 +3895,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2780,8 +3927,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2811,8 +3959,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2842,8 +3991,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2873,8 +4023,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2904,8 +4055,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2934,8 +4086,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2964,8 +4117,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2994,8 +4148,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3024,8 +4179,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3057,8 +4213,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3098,7 +4255,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3130,8 +4288,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3167,8 +4326,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3206,7 +4366,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3234,8 +4395,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3266,8 +4428,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3299,8 +4462,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3340,7 +4504,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3372,8 +4537,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3411,8 +4577,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3446,8 +4613,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3483,7 +4651,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3510,8 +4679,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3540,8 +4710,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3576,8 +4747,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3620,8 +4792,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3662,8 +4835,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3696,8 +4870,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{}, diff --git a/internal/toproto5/openephemeralresource_test.go b/internal/toproto5/openephemeralresource_test.go new file mode 100644 index 000000000..82d1b2fa0 --- /dev/null +++ b/internal/toproto5/openephemeralresource_test.go @@ -0,0 +1,206 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestOpenEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEphemeralState := &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + } + + testEphemeralStateInvalid := &tfsdk.EphemeralState{ + Raw: testProto5Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + } + + testCases := map[string]struct { + input *fwserver.OpenEphemeralResourceResponse + expected *tfprotov5.OpenEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.OpenEphemeralResourceResponse{}, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + IsClosable: false, + }, + }, + "diagnostics": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "diagnostics-invalid-state": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + State: testEphemeralStateInvalid, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Ephemeral State", + Detail: "An unexpected error was encountered when converting the ephemeral state to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, + "is-closable": { + input: &fwserver.OpenEphemeralResourceResponse{ + IsClosable: true, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + IsClosable: true, + }, + }, + "renew-at": { + input: &fwserver.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "state": { + input: &fwserver.OpenEphemeralResourceResponse{ + State: testEphemeralState, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + State: &testProto5DynamicValue, + }, + }, + "private-empty": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.OpenEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/renewephemeralresource_test.go b/internal/toproto5/renewephemeralresource_test.go new file mode 100644 index 000000000..975e297c8 --- /dev/null +++ b/internal/toproto5/renewephemeralresource_test.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" +) + +func TestRenewEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testCases := map[string]struct { + input *fwserver.RenewEphemeralResourceResponse + expected *tfprotov5.RenewEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.RenewEphemeralResourceResponse{}, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + }, + }, + "diagnostics": { + input: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "renew-at": { + input: &fwserver.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "private-empty": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.RenewEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/validateephemeralresourceconfig_test.go b/internal/toproto5/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..0a2a939f3 --- /dev/null +++ b/internal/toproto5/validateephemeralresourceconfig_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestValidateEphemeralResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateEphemeralResourceConfigResponse + expected *tfprotov5.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{}, + expected: &tfprotov5.ValidateEphemeralResourceConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ValidateEphemeralResourceConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 592fc33b5812a133e37535b5f9fd323f58ec4ac8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 30 Aug 2024 09:58:44 -0400 Subject: [PATCH 13/55] add proto5server tests --- .../fwserver/server_closeephemeralresource.go | 10 + .../fwserver/server_renewephemeralresource.go | 10 + .../proto5server/server_getmetadata_test.go | 160 +++++++++- .../server_getproviderschema_test.go | 260 ++++++++++++++- .../server_openephemeralresource_test.go | 302 ++++++++++++++++++ .../server_renewephemeralresource_test.go | 284 ++++++++++++++++ ...er_validateephemeralresourceconfig_test.go | 168 ++++++++++ .../testing/testprovider/ephemeralresource.go | 47 +++ .../ephemeralresourcewithclose.go | 30 ++ .../ephemeralresourcewithrenew.go | 30 ++ .../ephemeralresourcewithvalidateconfig.go | 30 ++ internal/testing/testprovider/provider.go | 20 +- 12 files changed, 1321 insertions(+), 30 deletions(-) create mode 100644 internal/proto5server/server_openephemeralresource_test.go create mode 100644 internal/proto5server/server_renewephemeralresource_test.go create mode 100644 internal/proto5server/server_validateephemeralresourceconfig_test.go create mode 100644 internal/testing/testprovider/ephemeralresource.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithclose.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithrenew.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go diff --git a/internal/fwserver/server_closeephemeralresource.go b/internal/fwserver/server_closeephemeralresource.go index 227d88de2..d8a70a8d0 100644 --- a/internal/fwserver/server_closeephemeralresource.go +++ b/internal/fwserver/server_closeephemeralresource.go @@ -66,6 +66,16 @@ func (s *Server) CloseEphemeralResource(ctx context.Context, req *CloseEphemeral return } + if req.State == nil { + resp.Diagnostics.AddError( + "Unexpected Close Request", + "An unexpected error was encountered when closing the ephemeral resource. The state was missing.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + return + } + privateProviderData := privatestate.EmptyProviderData(ctx) if req.Private != nil && req.Private.Provider != nil { privateProviderData = req.Private.Provider diff --git a/internal/fwserver/server_renewephemeralresource.go b/internal/fwserver/server_renewephemeralresource.go index 80a7589d4..8d27fe8d9 100644 --- a/internal/fwserver/server_renewephemeralresource.go +++ b/internal/fwserver/server_renewephemeralresource.go @@ -70,6 +70,16 @@ func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeral return } + if req.State == nil { + resp.Diagnostics.AddError( + "Unexpected Renew Request", + "An unexpected error was encountered when renewing the ephemeral resource. The state was missing.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + return + } + // Ensure that resp.Private is never nil. resp.Private = privatestate.EmptyData(ctx) if req.Private != nil { diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index decc62739..25d82f7e0 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -61,8 +62,9 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -96,7 +98,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -134,7 +137,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -151,6 +155,134 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, + "ephemeralresources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource1", + }, + { + TypeName: "test_ephemeral_resource2", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -178,7 +310,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{ { Name: "function1", @@ -221,7 +354,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -259,7 +393,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -303,8 +438,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{ { TypeName: "test_resource1", @@ -346,7 +482,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -384,7 +521,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 29b33024a..ef46a163f 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -11,6 +11,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -103,7 +105,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -159,7 +162,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -200,7 +204,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -220,6 +225,195 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "ephemeralschemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource1": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + "test_ephemeral_resource2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -257,7 +451,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "function1": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -319,7 +514,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -360,7 +556,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -398,8 +595,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -437,8 +635,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -505,8 +704,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -585,7 +785,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -626,7 +827,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -774,6 +976,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Checking FunctionTypes lock", "@module": "sdk.framework", }, + { + "@level": "trace", + "@message": "Checking EphemeralResourceFuncs lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Checking ProviderTypeName lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider EphemeralResources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider EphemeralResources", + "@module": "sdk.framework", + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/proto5server/server_openephemeralresource_test.go b/internal/proto5server/server_openephemeralresource_test.go new file mode 100644 index 000000000..a01c29a5a --- /dev/null +++ b/internal/proto5server/server_openephemeralresource_test.go @@ -0,0 +1,302 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.OpenEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov5.OpenEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + State: testEmptyDynamicValue, + }, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + State: testConfigDynamicValue, + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + State: testConfigDynamicValue, + }, + }, + "response-is-closable": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + // Implementing ephemeral.EphemeralResourceWithClose will set IsClosable to true + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, _ ephemeral.CloseRequest, _ *ephemeral.CloseResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + State: testEmptyDynamicValue, + IsClosable: true, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + State: testEmptyDynamicValue, + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "response-state": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-state-value") + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + State: testStateDynamicValue, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.OpenEphemeralResource(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_renewephemeralresource_test.go b/internal/proto5server/server_renewephemeralresource_test.go new file mode 100644 index 000000000..768146b5e --- /dev/null +++ b/internal/proto5server/server_renewephemeralresource_test.go @@ -0,0 +1,284 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.RenewEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov5.RenewEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + State: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value for test_required: %s", config.TestRequired.ValueString()) + } + + if !config.TestComputed.IsNull() { + resp.Diagnostics.AddError("unexpected req.Config value for test_computed: %s", config.TestComputed.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + Config: testConfigDynamicValue, + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{}, + }, + "request-state": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_required: %s", data.TestRequired.ValueString()) + } + + if data.TestComputed.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_computed: %s", data.TestComputed.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + Config: testConfigDynamicValue, + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + Config: testConfigDynamicValue, + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + State: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.RenewEphemeralResource(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_validateephemeralresourceconfig_test.go b/internal/proto5server/server_validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..6505bbf53 --- /dev/null +++ b/internal/proto5server/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,168 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov5.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.ValidateEphemeralResourceConfigRequest + expectedError error + expectedResponse *tfprotov5.ValidateEphemeralResourceConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateEphemeralResourceConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateEphemeralResourceConfig(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/ephemeralresource.go b/internal/testing/testprovider/ephemeralresource.go new file mode 100644 index 000000000..a6b0ea6ae --- /dev/null +++ b/internal/testing/testprovider/ephemeralresource.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResource{} + +// Declarative ephemeral.EphemeralResource for unit testing. +type EphemeralResource struct { + // EphemeralResource interface methods + MetadataMethod func(context.Context, ephemeral.MetadataRequest, *ephemeral.MetadataResponse) + SchemaMethod func(context.Context, ephemeral.SchemaRequest, *ephemeral.SchemaResponse) + OpenMethod func(context.Context, ephemeral.OpenRequest, *ephemeral.OpenResponse) +} + +// Metadata satisfies the ephemeral.EphemeralResource interface. +func (r *EphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + if r.MetadataMethod == nil { + return + } + + r.MetadataMethod(ctx, req, resp) +} + +// Schema satisfies the ephemeral.EphemeralResource interface. +func (r *EphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + if r.SchemaMethod == nil { + return + } + + r.SchemaMethod(ctx, req, resp) +} + +// Open satisfies the ephemeral.EphemeralResource interface. +func (r *EphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + if r.OpenMethod == nil { + return + } + + r.OpenMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithclose.go b/internal/testing/testprovider/ephemeralresourcewithclose.go new file mode 100644 index 000000000..545ac9aca --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithclose.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithClose{} +var _ ephemeral.EphemeralResourceWithClose = &EphemeralResourceWithClose{} + +// Declarative ephemeral.EphemeralResourceWithClose for unit testing. +type EphemeralResourceWithClose struct { + *EphemeralResource + + // EphemeralResourceWithClose interface methods + CloseMethod func(context.Context, ephemeral.CloseRequest, *ephemeral.CloseResponse) +} + +// Close satisfies the ephemeral.EphemeralResourceWithClose interface. +func (p *EphemeralResourceWithClose) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + if p.CloseMethod == nil { + return + } + + p.CloseMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithrenew.go b/internal/testing/testprovider/ephemeralresourcewithrenew.go new file mode 100644 index 000000000..174e80d76 --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithrenew.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithRenew{} +var _ ephemeral.EphemeralResourceWithRenew = &EphemeralResourceWithRenew{} + +// Declarative ephemeral.EphemeralResourceWithRenew for unit testing. +type EphemeralResourceWithRenew struct { + *EphemeralResource + + // EphemeralResourceWithRenew interface methods + RenewMethod func(context.Context, ephemeral.RenewRequest, *ephemeral.RenewResponse) +} + +// Renew satisfies the ephemeral.EphemeralResourceWithRenew interface. +func (p *EphemeralResourceWithRenew) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + if p.RenewMethod == nil { + return + } + + p.RenewMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go b/internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go new file mode 100644 index 000000000..3c6ac4ffb --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithValidateConfig{} +var _ ephemeral.EphemeralResourceWithValidateConfig = &EphemeralResourceWithValidateConfig{} + +// Declarative ephemeral.EphemeralResourceWithValidateConfig for unit testing. +type EphemeralResourceWithValidateConfig struct { + *EphemeralResource + + // EphemeralResourceWithValidateConfig interface methods + ValidateConfigMethod func(context.Context, ephemeral.ValidateConfigRequest, *ephemeral.ValidateConfigResponse) +} + +// ValidateConfig satisfies the ephemeral.EphemeralResourceWithValidateConfig interface. +func (p *EphemeralResourceWithValidateConfig) ValidateConfig(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + if p.ValidateConfigMethod == nil { + return + } + + p.ValidateConfigMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index eb9ef7138..0b2c536da 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -16,11 +17,12 @@ var _ provider.Provider = &Provider{} // Declarative provider.Provider for unit testing. type Provider struct { // Provider interface methods - MetadataMethod func(context.Context, provider.MetadataRequest, *provider.MetadataResponse) - ConfigureMethod func(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) - SchemaMethod func(context.Context, provider.SchemaRequest, *provider.SchemaResponse) - DataSourcesMethod func(context.Context) []func() datasource.DataSource - ResourcesMethod func(context.Context) []func() resource.Resource + MetadataMethod func(context.Context, provider.MetadataRequest, *provider.MetadataResponse) + ConfigureMethod func(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) + SchemaMethod func(context.Context, provider.SchemaRequest, *provider.SchemaResponse) + DataSourcesMethod func(context.Context) []func() datasource.DataSource + ResourcesMethod func(context.Context) []func() resource.Resource + EphemeralResourcesMethod func(context.Context) []func() ephemeral.EphemeralResource } // Configure satisfies the provider.Provider interface. @@ -67,3 +69,11 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return p.ResourcesMethod(ctx) } + +func (p *Provider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource { + if p == nil || p.EphemeralResourcesMethod == nil { + return nil + } + + return p.EphemeralResourcesMethod(ctx) +} From ec24f3323e45a11d3eea54c89a7bdb9ad7beb22f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 30 Aug 2024 11:01:38 -0400 Subject: [PATCH 14/55] implement protov6 --- internal/fromproto6/closeephemeralresource.go | 58 + .../fromproto6/closeephemeralresource_test.go | 147 ++ internal/fromproto6/ephemeral_state.go | 53 + internal/fromproto6/ephemeral_state_test.go | 122 ++ internal/fromproto6/openephemeralresource.go | 51 + .../fromproto6/openephemeralresource_test.go | 122 ++ internal/fromproto6/renewephemeralresource.go | 64 + .../fromproto6/renewephemeralresource_test.go | 175 ++ .../validateephemeralresourceconfig.go | 31 + .../validateephemeralresourceconfig_test.go | 110 + internal/proto6server/serve.go | 22 - .../server_closeephemeralresource.go | 50 + .../server_openephemeralresource.go | 50 + .../server_openephemeralresource_test.go | 302 +++ .../server_renewephemeralresource.go | 50 + .../server_renewephemeralresource_test.go | 284 +++ .../server_validateephemeralresourceconfig.go | 50 + ...er_validateephemeralresourceconfig_test.go | 168 ++ internal/toproto6/closeephemeralresource.go | 25 + .../toproto6/closeephemeralresource_test.go | 69 + internal/toproto6/ephemeral_state.go | 28 + internal/toproto6/ephemeral_state_test.go | 109 + .../toproto6/ephemeralresourcemetadata.go | 19 + .../ephemeralresourcemetadata_test.go | 46 + internal/toproto6/getmetadata.go | 5 + internal/toproto6/getmetadata_test.go | 46 +- internal/toproto6/getproviderschema.go | 25 +- internal/toproto6/getproviderschema_test.go | 1878 ++++++++++++++--- internal/toproto6/openephemeralresource.go | 37 + .../toproto6/openephemeralresource_test.go | 206 ++ internal/toproto6/renewephemeralresource.go | 31 + .../toproto6/renewephemeralresource_test.go | 117 + .../validateephemeralresourceconfig.go | 25 + .../validateephemeralresourceconfig_test.go | 69 + 34 files changed, 4280 insertions(+), 364 deletions(-) create mode 100644 internal/fromproto6/closeephemeralresource.go create mode 100644 internal/fromproto6/closeephemeralresource_test.go create mode 100644 internal/fromproto6/ephemeral_state.go create mode 100644 internal/fromproto6/ephemeral_state_test.go create mode 100644 internal/fromproto6/openephemeralresource.go create mode 100644 internal/fromproto6/openephemeralresource_test.go create mode 100644 internal/fromproto6/renewephemeralresource.go create mode 100644 internal/fromproto6/renewephemeralresource_test.go create mode 100644 internal/fromproto6/validateephemeralresourceconfig.go create mode 100644 internal/fromproto6/validateephemeralresourceconfig_test.go create mode 100644 internal/proto6server/server_closeephemeralresource.go create mode 100644 internal/proto6server/server_openephemeralresource.go create mode 100644 internal/proto6server/server_openephemeralresource_test.go create mode 100644 internal/proto6server/server_renewephemeralresource.go create mode 100644 internal/proto6server/server_renewephemeralresource_test.go create mode 100644 internal/proto6server/server_validateephemeralresourceconfig.go create mode 100644 internal/proto6server/server_validateephemeralresourceconfig_test.go create mode 100644 internal/toproto6/closeephemeralresource.go create mode 100644 internal/toproto6/closeephemeralresource_test.go create mode 100644 internal/toproto6/ephemeral_state.go create mode 100644 internal/toproto6/ephemeral_state_test.go create mode 100644 internal/toproto6/ephemeralresourcemetadata.go create mode 100644 internal/toproto6/ephemeralresourcemetadata_test.go create mode 100644 internal/toproto6/openephemeralresource.go create mode 100644 internal/toproto6/openephemeralresource_test.go create mode 100644 internal/toproto6/renewephemeralresource.go create mode 100644 internal/toproto6/renewephemeralresource_test.go create mode 100644 internal/toproto6/validateephemeralresourceconfig.go create mode 100644 internal/toproto6/validateephemeralresourceconfig_test.go diff --git a/internal/fromproto6/closeephemeralresource.go b/internal/fromproto6/closeephemeralresource.go new file mode 100644 index 000000000..7d5099a2c --- /dev/null +++ b/internal/fromproto6/closeephemeralresource.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// CloseEphemeralResourceRequest returns the *fwserver.CloseEphemeralResourceRequest +// equivalent of a *tfprotov6.CloseEphemeralResourceRequest. +func CloseEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.CloseEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.CloseEphemeralResourceRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.CloseEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + state, stateDiags := EphemeralState(ctx, proto6.State, ephemeralResourceSchema) + + diags.Append(stateDiags...) + + fw.State = state + + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto6/closeephemeralresource_test.go b/internal/fromproto6/closeephemeralresource_test.go new file mode 100644 index 000000000..65689ab4a --- /dev/null +++ b/internal/fromproto6/closeephemeralresource_test.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestCloseEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov6.CloseEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.CloseEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.CloseEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "private": { + input: &tfprotov6.CloseEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.CloseEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "state-missing-schema": { + input: &tfprotov6.CloseEphemeralResourceRequest{ + State: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "state": { + input: &tfprotov6.CloseEphemeralResourceRequest{ + State: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.CloseEphemeralResourceRequest{ + State: &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.CloseEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/ephemeral_state.go b/internal/fromproto6/ephemeral_state.go new file mode 100644 index 000000000..828b171e7 --- /dev/null +++ b/internal/fromproto6/ephemeral_state.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// EphemeralState returns the *tfsdk.EphemeralState for a *tfprotov6.DynamicValue and +// fwschema.Schema. +func EphemeralState(ctx context.Context, proto6DynamicValue *tfprotov6.DynamicValue, schema fwschema.Schema) (*tfsdk.EphemeralState, diag.Diagnostics) { + if proto6DynamicValue == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if schema == nil { + diags.AddError( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto6DynamicValue, schema, fwschemadata.DataDescriptionEphemeralState) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.EphemeralState{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto6/ephemeral_state_test.go b/internal/fromproto6/ephemeral_state_test.go new file mode 100644 index 000000000..6d8d1bb9b --- /dev/null +++ b/internal/fromproto6/ephemeral_state_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestEphemeralState(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.DynamicValue + schema fwschema.Schema + expected *tfsdk.EphemeralState + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "missing-schema": { + input: &testProto6DynamicValue, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "invalid-schema": { + input: &testProto6DynamicValue, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue: AttributeName(\"test_attribute\"): couldn't decode bool: msgpack: invalid code=aa decoding bool", + ), + }, + }, + "valid": { + input: &testProto6DynamicValue, + schema: testFwSchema, + expected: &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.EphemeralState(context.Background(), testCase.input, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/openephemeralresource.go b/internal/fromproto6/openephemeralresource.go new file mode 100644 index 000000000..ac9962b4e --- /dev/null +++ b/internal/fromproto6/openephemeralresource.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// OpenEphemeralResourceRequest returns the *fwserver.OpenEphemeralResourceRequest +// equivalent of a *tfprotov6.OpenEphemeralResourceRequest. +func OpenEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.OpenEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.OpenEphemeralResourceRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.OpenEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + config, configDiags := Config(ctx, proto6.Config, ephemeralResourceSchema) + + diags.Append(configDiags...) + + fw.Config = config + + return fw, diags +} diff --git a/internal/fromproto6/openephemeralresource_test.go b/internal/fromproto6/openephemeralresource_test.go new file mode 100644 index 000000000..d715029f7 --- /dev/null +++ b/internal/fromproto6/openephemeralresource_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestOpenEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.OpenEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.OpenEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.OpenEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.OpenEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/renewephemeralresource.go b/internal/fromproto6/renewephemeralresource.go new file mode 100644 index 000000000..ee6f13b71 --- /dev/null +++ b/internal/fromproto6/renewephemeralresource.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// RenewEphemeralResourceRequest returns the *fwserver.RenewEphemeralResourceRequest +// equivalent of a *tfprotov6.RenewEphemeralResourceRequest. +func RenewEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.RenewEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.RenewEphemeralResourceRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.RenewEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + config, configDiags := Config(ctx, proto6.Config, ephemeralResourceSchema) + + diags.Append(configDiags...) + + fw.Config = config + + state, stateDiags := EphemeralState(ctx, proto6.State, ephemeralResourceSchema) + + diags.Append(stateDiags...) + + fw.State = state + + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto6/renewephemeralresource_test.go b/internal/fromproto6/renewephemeralresource_test.go new file mode 100644 index 000000000..8453f3bff --- /dev/null +++ b/internal/fromproto6/renewephemeralresource_test.go @@ -0,0 +1,175 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestRenewEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov6.RenewEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.RenewEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.RenewEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov6.RenewEphemeralResourceRequest{ + Config: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.RenewEphemeralResourceRequest{ + Config: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "private": { + input: &tfprotov6.RenewEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "state-missing-schema": { + input: &tfprotov6.RenewEphemeralResourceRequest{ + State: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "state": { + input: &tfprotov6.RenewEphemeralResourceRequest{ + State: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + State: &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.RenewEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/validateephemeralresourceconfig.go b/internal/fromproto6/validateephemeralresourceconfig.go new file mode 100644 index 000000000..f913ede97 --- /dev/null +++ b/internal/fromproto6/validateephemeralresourceconfig.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateEphemeralResourceConfigRequest returns the *fwserver.ValidateEphemeralResourceConfigRequest +// equivalent of a *tfprotov6.ValidateEphemeralResourceConfigRequest. +func ValidateEphemeralResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.ValidateEphemeralResourceConfigRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.ValidateEphemeralResourceConfigRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + fw := &fwserver.ValidateEphemeralResourceConfigRequest{} + + config, diags := Config(ctx, proto6.Config, ephemeralResourceSchema) + + fw.Config = config + fw.EphemeralResource = ephemeralResource + + return fw, diags +} diff --git a/internal/fromproto6/validateephemeralresourceconfig_test.go b/internal/fromproto6/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..39d793335 --- /dev/null +++ b/internal/fromproto6/validateephemeralresourceconfig_test.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateEphemeralResourceConfigRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.ValidateEphemeralResourceConfigRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + expected *fwserver.ValidateEphemeralResourceConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.ValidateEphemeralResourceConfigRequest{}, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ValidateEphemeralResourceConfigRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/serve.go b/internal/proto6server/serve.go index 3a8170c59..26cf0c4e1 100644 --- a/internal/proto6server/serve.go +++ b/internal/proto6server/serve.go @@ -21,28 +21,6 @@ type Server struct { contextCancelsMu sync.Mutex } -// TODO: Stub for now to satisfy compiler -// -// CloseEphemeralResource implements tfprotov6.ProviderServer. -func (s *Server) CloseEphemeralResource(context.Context, *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { - panic("unimplemented") -} - -// OpenEphemeralResource implements tfprotov6.ProviderServer. -func (s *Server) OpenEphemeralResource(context.Context, *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { - panic("unimplemented") -} - -// RenewEphemeralResource implements tfprotov6.ProviderServer. -func (s *Server) RenewEphemeralResource(context.Context, *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { - panic("unimplemented") -} - -// ValidateEphemeralResourceConfig implements tfprotov6.ProviderServer. -func (s *Server) ValidateEphemeralResourceConfig(context.Context, *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { - panic("unimplemented") -} - func (s *Server) registerContext(in context.Context) context.Context { ctx, cancel := context.WithCancel(in) s.contextCancelsMu.Lock() diff --git a/internal/proto6server/server_closeephemeralresource.go b/internal/proto6server/server_closeephemeralresource.go new file mode 100644 index 000000000..430ff2eaa --- /dev/null +++ b/internal/proto6server/server_closeephemeralresource.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// CloseEphemeralResource satisfies the tfprotov6.ProviderServer interface. +func (s *Server) CloseEphemeralResource(ctx context.Context, proto6Req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.CloseEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.CloseEphemeralResourceRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.CloseEphemeralResource(ctx, fwReq, fwResp) + + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_openephemeralresource.go b/internal/proto6server/server_openephemeralresource.go new file mode 100644 index 000000000..5ec9b2dff --- /dev/null +++ b/internal/proto6server/server_openephemeralresource.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// OpenEphemeralResource satisfies the tfprotov6.ProviderServer interface. +func (s *Server) OpenEphemeralResource(ctx context.Context, proto6Req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.OpenEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.OpenEphemeralResourceRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.OpenEphemeralResource(ctx, fwReq, fwResp) + + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_openephemeralresource_test.go b/internal/proto6server/server_openephemeralresource_test.go new file mode 100644 index 000000000..7014c3d7f --- /dev/null +++ b/internal/proto6server/server_openephemeralresource_test.go @@ -0,0 +1,302 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.OpenEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov6.OpenEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + State: testEmptyDynamicValue, + }, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + State: testConfigDynamicValue, + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + State: testConfigDynamicValue, + }, + }, + "response-is-closable": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + // Implementing ephemeral.EphemeralResourceWithClose will set IsClosable to true + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, _ ephemeral.CloseRequest, _ *ephemeral.CloseResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + State: testEmptyDynamicValue, + IsClosable: true, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + State: testEmptyDynamicValue, + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + "response-state": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-state-value") + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + State: testStateDynamicValue, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.OpenEphemeralResource(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_renewephemeralresource.go b/internal/proto6server/server_renewephemeralresource.go new file mode 100644 index 000000000..e60657d4f --- /dev/null +++ b/internal/proto6server/server_renewephemeralresource.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// RenewEphemeralResource satisfies the tfprotov6.ProviderServer interface. +func (s *Server) RenewEphemeralResource(ctx context.Context, proto6Req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.RenewEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.RenewEphemeralResourceRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.RenewEphemeralResource(ctx, fwReq, fwResp) + + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_renewephemeralresource_test.go b/internal/proto6server/server_renewephemeralresource_test.go new file mode 100644 index 000000000..e14231538 --- /dev/null +++ b/internal/proto6server/server_renewephemeralresource_test.go @@ -0,0 +1,284 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.RenewEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov6.RenewEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + State: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value for test_required: %s", config.TestRequired.ValueString()) + } + + if !config.TestComputed.IsNull() { + resp.Diagnostics.AddError("unexpected req.Config value for test_computed: %s", config.TestComputed.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + Config: testConfigDynamicValue, + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{}, + }, + "request-state": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_required: %s", data.TestRequired.ValueString()) + } + + if data.TestComputed.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_computed: %s", data.TestComputed.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + Config: testConfigDynamicValue, + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + Config: testConfigDynamicValue, + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + State: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.RenewEphemeralResource(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_validateephemeralresourceconfig.go b/internal/proto6server/server_validateephemeralresourceconfig.go new file mode 100644 index 000000000..822860326 --- /dev/null +++ b/internal/proto6server/server_validateephemeralresourceconfig.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateEphemeralResourceConfig satisfies the tfprotov6.ProviderServer interface. +func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, proto6Req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.ValidateEphemeralResourceConfigResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.ValidateEphemeralResourceConfigRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateEphemeralResourceConfig(ctx, fwReq, fwResp) + + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_validateephemeralresourceconfig_test.go b/internal/proto6server/server_validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..9e1932143 --- /dev/null +++ b/internal/proto6server/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,168 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov6.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.ValidateEphemeralResourceConfigRequest + expectedError error + expectedResponse *tfprotov6.ValidateEphemeralResourceConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateEphemeralResourceConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateEphemeralResourceConfig(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/closeephemeralresource.go b/internal/toproto6/closeephemeralresource.go new file mode 100644 index 000000000..46810b9d7 --- /dev/null +++ b/internal/toproto6/closeephemeralresource.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// CloseEphemeralResourceResponse returns the *tfprotov6.CloseEphemeralResourceResponse +// equivalent of a *fwserver.CloseEphemeralResourceResponse. +func CloseEphemeralResourceResponse(ctx context.Context, fw *fwserver.CloseEphemeralResourceResponse) *tfprotov6.CloseEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto6 +} diff --git a/internal/toproto6/closeephemeralresource_test.go b/internal/toproto6/closeephemeralresource_test.go new file mode 100644 index 000000000..cf9830dd0 --- /dev/null +++ b/internal/toproto6/closeephemeralresource_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestCloseEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.CloseEphemeralResourceResponse + expected *tfprotov6.CloseEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.CloseEphemeralResourceResponse{}, + expected: &tfprotov6.CloseEphemeralResourceResponse{}, + }, + "diagnostics": { + input: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.CloseEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/ephemeral_state.go b/internal/toproto6/ephemeral_state.go new file mode 100644 index 000000000..39a49cbef --- /dev/null +++ b/internal/toproto6/ephemeral_state.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// EphemeralState returns the *tfprotov6.DynamicValue for a *tfsdk.EphemeralState. +func EphemeralState(ctx context.Context, fw *tfsdk.EphemeralState) (*tfprotov6.DynamicValue, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + data := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionEphemeralState, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + return DynamicValue(ctx, data) +} diff --git a/internal/toproto6/ephemeral_state_test.go b/internal/toproto6/ephemeral_state_test.go new file mode 100644 index 000000000..b87eab395 --- /dev/null +++ b/internal/toproto6/ephemeral_state_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestEphemeralState(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testEphemeralState := &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testEphemeralStateInvalid := &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testCases := map[string]struct { + input *tfsdk.EphemeralState + expected *tfprotov6.DynamicValue + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testEphemeralStateInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral State", + "An unexpected error was encountered when converting the ephemeral state to the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + ), + }, + }, + "valid": { + input: testEphemeralState, + expected: &testProto6DynamicValue, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto6.EphemeralState(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/ephemeralresourcemetadata.go b/internal/toproto6/ephemeralresourcemetadata.go new file mode 100644 index 000000000..56fab9951 --- /dev/null +++ b/internal/toproto6/ephemeralresourcemetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// EphemeralResourceMetadata returns the tfprotov6.EphemeralResourceMetadata for a +// fwserver.EphemeralResourceMetadata. +func EphemeralResourceMetadata(ctx context.Context, fw fwserver.EphemeralResourceMetadata) tfprotov6.EphemeralResourceMetadata { + return tfprotov6.EphemeralResourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto6/ephemeralresourcemetadata_test.go b/internal/toproto6/ephemeralresourcemetadata_test.go new file mode 100644 index 000000000..c62b90797 --- /dev/null +++ b/internal/toproto6/ephemeralresourcemetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestEphemeralResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.EphemeralResourceMetadata + expected tfprotov6.EphemeralResourceMetadata + }{ + "TypeName": { + fw: fwserver.EphemeralResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov6.EphemeralResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.EphemeralResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/getmetadata.go b/internal/toproto6/getmetadata.go index 0924f3c9f..314072392 100644 --- a/internal/toproto6/getmetadata.go +++ b/internal/toproto6/getmetadata.go @@ -20,6 +20,7 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) protov6 := &tfprotov6.GetMetadataResponse{ DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResources: make([]tfprotov6.EphemeralResourceMetadata, 0, len(fw.EphemeralResources)), Functions: make([]tfprotov6.FunctionMetadata, 0, len(fw.Functions)), Resources: make([]tfprotov6.ResourceMetadata, 0, len(fw.Resources)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), @@ -29,6 +30,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) } + for _, ephemeralResource := range fw.EphemeralResources { + protov6.EphemeralResources = append(protov6.EphemeralResources, EphemeralResourceMetadata(ctx, ephemeralResource)) + } + for _, function := range fw.Functions { protov6.Functions = append(protov6.Functions, FunctionMetadata(ctx, function)) } diff --git a/internal/toproto6/getmetadata_test.go b/internal/toproto6/getmetadata_test.go index 5c0590500..40a6b05e7 100644 --- a/internal/toproto6/getmetadata_test.go +++ b/internal/toproto6/getmetadata_test.go @@ -45,8 +45,9 @@ func TestGetMetadataResponse(t *testing.T) { TypeName: "test_data_source_2", }, }, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, }, }, "diagnostics": { @@ -71,6 +72,32 @@ func TestGetMetadataResponse(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + }, + }, + "ephemeralresources": { + input: &fwserver.GetMetadataResponse{ + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, }, @@ -87,7 +114,8 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{ { Name: "function1", @@ -111,8 +139,9 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{ { TypeName: "test_resource_1", @@ -131,9 +160,10 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, diff --git a/internal/toproto6/getproviderschema.go b/internal/toproto6/getproviderschema.go index ee221abbf..d88a5381e 100644 --- a/internal/toproto6/getproviderschema.go +++ b/internal/toproto6/getproviderschema.go @@ -18,11 +18,12 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov6 := &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.DataSourceSchemas)), - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Functions: make(map[string]*tfprotov6.Function, len(fw.FunctionDefinitions)), - ResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.ResourceSchemas)), - ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), + DataSourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.DataSourceSchemas)), + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.EphemeralResourceSchemas)), + Functions: make(map[string]*tfprotov6.Function, len(fw.FunctionDefinitions)), + ResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.ResourceSchemas)), + ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } var err error @@ -83,5 +84,19 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } } + for ephemeralResourceType, ephemeralResourceSchema := range fw.EphemeralResourceSchemas { + protov6.EphemeralResourceSchemas[ephemeralResourceType], err = Schema(ctx, ephemeralResourceSchema) + + if err != nil { + protov6.Diagnostics = append(protov6.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"" + ephemeralResourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov6 + } + } + return protov6 } diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index cba045d18..2df173187 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -80,8 +81,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-computed": { @@ -110,8 +112,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-deprecated": { @@ -142,8 +145,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-optional": { @@ -172,8 +176,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-optional-computed": { @@ -204,8 +209,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-required": { @@ -234,8 +240,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-sensitive": { @@ -266,8 +273,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-bool": { @@ -296,8 +304,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-float64": { @@ -326,8 +335,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-int32": { @@ -343,8 +353,1117 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-int64": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.Int64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-list-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-list-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeList, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-list-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-map-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeMap, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-map-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-number": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.NumberAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ObjectAttribute{ + Required: true, + AttributeTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSet, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-single-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SingleNestedAttribute{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-dynamic": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.DynamicPseudoType, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-block-list": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.ListNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_block", + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-block-set": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SetNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "test_block", + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SingleNestedBlock{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-multiple-ephemeral-resources": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource_1": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + "test_ephemeral_resource_2": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_1": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + "test_ephemeral_resource_2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-computed": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-deprecated": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + DeprecationMessage: "deprecated", + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Deprecated: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional-computed": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-required": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-sensitive": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Sensitive: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Sensitive: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-bool": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -360,12 +1479,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-int64": { + "ephemeral-resource-attribute-type-int64": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.Int64Attribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int64Attribute{ Required: true, }, }, @@ -373,8 +1492,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -390,12 +1510,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-list-string": { + "ephemeral-resource-attribute-type-list-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.ListType{ ElemType: types.StringType, @@ -406,8 +1526,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -427,15 +1548,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-nested-attributes": { + "ephemeral-resource-attribute-type-list-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -447,8 +1568,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -473,12 +1595,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-object": { + "ephemeral-resource-attribute-type-list-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -491,8 +1613,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -514,12 +1637,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-string": { + "ephemeral-resource-attribute-type-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.StringType, }, @@ -528,8 +1651,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -547,15 +1671,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-map-nested-attributes": { + "ephemeral-resource-attribute-type-map-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -567,8 +1691,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -593,12 +1718,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-map-string": { + "ephemeral-resource-attribute-type-map-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapAttribute{ Required: true, ElementType: types.StringType, }, @@ -607,8 +1732,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -626,12 +1752,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-number": { + "ephemeral-resource-attribute-type-number": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.NumberAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.NumberAttribute{ Required: true, }, }, @@ -639,8 +1765,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -656,12 +1783,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-object": { + "ephemeral-resource-attribute-type-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ObjectAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ObjectAttribute{ Required: true, AttributeTypes: map[string]attr.Type{ "test_object_attribute": types.StringType, @@ -672,8 +1799,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -693,15 +1821,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-nested-attributes": { + "ephemeral-resource-attribute-type-set-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -713,8 +1841,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -739,12 +1868,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-object": { + "ephemeral-resource-attribute-type-set-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -757,8 +1886,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -780,12 +1910,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-set-string": { + "ephemeral-resource-attribute-type-set-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.SetType{ ElemType: types.StringType, @@ -796,8 +1926,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -817,12 +1948,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-string": { + "ephemeral-resource-attribute-type-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.StringType, }, @@ -831,8 +1962,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -850,14 +1982,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-single-nested-attributes": { + "ephemeral-resource-attribute-type-single-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SingleNestedAttribute{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SingleNestedAttribute{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -868,8 +2000,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -894,12 +2027,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-string": { + "ephemeral-resource-attribute-type-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -907,8 +2040,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -924,12 +2058,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-dynamic": { + "ephemeral-resource-attribute-type-dynamic": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.DynamicAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.DynamicAttribute{ Required: true, }, }, @@ -937,8 +2071,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -954,15 +2089,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-block-list": { + "ephemeral-resource-block-list": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.ListNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.ListNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -973,8 +2108,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ { @@ -998,15 +2134,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-block-set": { + "ephemeral-resource-block-set": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SetNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SetNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -1017,8 +2153,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ { @@ -1042,14 +2179,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-block-single": { + "ephemeral-resource-block-single": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SingleNestedBlock{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SingleNestedBlock{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -1059,8 +2196,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ { @@ -1096,7 +2234,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction1": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1124,7 +2263,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { DeprecationMessage: "test deprecation message", @@ -1147,7 +2287,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Description: "test description", @@ -1174,7 +2315,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{ @@ -1205,7 +2347,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1227,7 +2370,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1250,7 +2394,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1277,8 +2422,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1305,8 +2451,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1332,8 +2479,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1360,8 +2508,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1388,8 +2537,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1415,8 +2565,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1442,8 +2593,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1469,8 +2621,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1499,8 +2652,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1537,8 +2691,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1578,8 +2733,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1612,8 +2768,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1648,8 +2805,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1685,8 +2843,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1714,8 +2873,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1744,8 +2904,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1782,8 +2943,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1823,8 +2985,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1859,8 +3022,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1891,8 +3055,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1925,8 +3090,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1961,8 +3127,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1988,8 +3155,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2021,8 +3189,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -2062,8 +3231,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -2101,8 +3271,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -2136,8 +3307,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2163,8 +3335,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2190,8 +3363,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2217,8 +3391,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2244,8 +3419,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2274,8 +3450,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2312,8 +3489,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2353,8 +3531,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2387,8 +3566,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2423,8 +3603,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2460,8 +3641,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2489,8 +3671,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2519,8 +3702,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2557,8 +3741,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2598,8 +3783,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2634,8 +3820,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2666,8 +3853,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2700,8 +3888,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2736,8 +3925,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2772,8 +3962,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource_1": { Block: &tfprotov6.SchemaBlock{ @@ -2813,8 +4004,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2844,8 +4036,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2875,8 +4068,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2906,8 +4100,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2937,8 +4132,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2968,8 +4164,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2999,8 +4196,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3029,8 +4227,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3059,8 +4258,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3089,8 +4289,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3122,8 +4323,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3163,8 +4365,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3207,8 +4410,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3244,8 +4448,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3283,8 +4488,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3323,8 +4529,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3355,8 +4562,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3388,8 +4596,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3429,8 +4638,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3473,8 +4683,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3512,8 +4723,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3547,8 +4759,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3584,8 +4797,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3623,8 +4837,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3653,8 +4868,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3689,8 +4905,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3733,8 +4950,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3775,8 +4993,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3809,8 +5028,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{}, diff --git a/internal/toproto6/openephemeralresource.go b/internal/toproto6/openephemeralresource.go new file mode 100644 index 000000000..4324d5a11 --- /dev/null +++ b/internal/toproto6/openephemeralresource.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// OpenEphemeralResourceResponse returns the *tfprotov6.OpenEphemeralResourceResponse +// equivalent of a *fwserver.OpenEphemeralResourceResponse. +func OpenEphemeralResourceResponse(ctx context.Context, fw *fwserver.OpenEphemeralResourceResponse) *tfprotov6.OpenEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + IsClosable: fw.IsClosable, + } + + state, diags := EphemeralState(ctx, fw.State) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.State = state + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Private = newPrivate + + return proto6 +} diff --git a/internal/toproto6/openephemeralresource_test.go b/internal/toproto6/openephemeralresource_test.go new file mode 100644 index 000000000..e15c2907d --- /dev/null +++ b/internal/toproto6/openephemeralresource_test.go @@ -0,0 +1,206 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestOpenEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEphemeralState := &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + } + + testEphemeralStateInvalid := &tfsdk.EphemeralState{ + Raw: testProto6Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + } + + testCases := map[string]struct { + input *fwserver.OpenEphemeralResourceResponse + expected *tfprotov6.OpenEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.OpenEphemeralResourceResponse{}, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + IsClosable: false, + }, + }, + "diagnostics": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "diagnostics-invalid-state": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + State: testEphemeralStateInvalid, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Ephemeral State", + Detail: "An unexpected error was encountered when converting the ephemeral state to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, + "is-closable": { + input: &fwserver.OpenEphemeralResourceResponse{ + IsClosable: true, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + IsClosable: true, + }, + }, + "renew-at": { + input: &fwserver.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + "state": { + input: &fwserver.OpenEphemeralResourceResponse{ + State: testEphemeralState, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + State: &testProto6DynamicValue, + }, + }, + "private-empty": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.OpenEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/renewephemeralresource.go b/internal/toproto6/renewephemeralresource.go new file mode 100644 index 000000000..4afcb9962 --- /dev/null +++ b/internal/toproto6/renewephemeralresource.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// RenewEphemeralResourceResponse returns the *tfprotov6.RenewEphemeralResourceResponse +// equivalent of a *fwserver.RenewEphemeralResourceResponse. +func RenewEphemeralResourceResponse(ctx context.Context, fw *fwserver.RenewEphemeralResourceResponse) *tfprotov6.RenewEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + } + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Private = newPrivate + + return proto6 +} diff --git a/internal/toproto6/renewephemeralresource_test.go b/internal/toproto6/renewephemeralresource_test.go new file mode 100644 index 000000000..54c3b4f64 --- /dev/null +++ b/internal/toproto6/renewephemeralresource_test.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" +) + +func TestRenewEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testCases := map[string]struct { + input *fwserver.RenewEphemeralResourceResponse + expected *tfprotov6.RenewEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.RenewEphemeralResourceResponse{}, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + }, + }, + "diagnostics": { + input: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "renew-at": { + input: &fwserver.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + "private-empty": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.RenewEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/validateephemeralresourceconfig.go b/internal/toproto6/validateephemeralresourceconfig.go new file mode 100644 index 000000000..5237b35dc --- /dev/null +++ b/internal/toproto6/validateephemeralresourceconfig.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateEphemeralResourceConfigResponse returns the *tfprotov6.ValidateEphemeralResourceConfigResponse +// equivalent of a *fwserver.ValidateEphemeralResourceConfigResponse. +func ValidateEphemeralResourceConfigResponse(ctx context.Context, fw *fwserver.ValidateEphemeralResourceConfigResponse) *tfprotov6.ValidateEphemeralResourceConfigResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto6 +} diff --git a/internal/toproto6/validateephemeralresourceconfig_test.go b/internal/toproto6/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..a088f2b88 --- /dev/null +++ b/internal/toproto6/validateephemeralresourceconfig_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestValidateEphemeralResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateEphemeralResourceConfigResponse + expected *tfprotov6.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{}, + expected: &tfprotov6.ValidateEphemeralResourceConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ValidateEphemeralResourceConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From bda9bbc9539c55adfaa16328a74d1aec182f9638 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 30 Aug 2024 16:33:55 -0400 Subject: [PATCH 15/55] schema + metadata tests --- .../proto5server/server_getmetadata_test.go | 3 + .../proto6server/server_getmetadata_test.go | 164 ++++++++++- .../server_getproviderschema_test.go | 258 +++++++++++++++++- 3 files changed, 401 insertions(+), 24 deletions(-) diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 25d82f7e0..cdc3ca520 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -557,6 +557,9 @@ func TestServerGetMetadata(t *testing.T) { sort.Slice(got.DataSources, func(i int, j int) bool { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.EphemeralResources, func(i int, j int) bool { + return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName + }) sort.Slice(got.Functions, func(i int, j int) bool { return got.Functions[i].Name < got.Functions[j].Name diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 35dc1a4c7..c4ab6d5e0 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -61,8 +62,9 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -106,8 +108,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -143,6 +146,135 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource1", + }, + { + TypeName: "test_ephemeral_resource2", + }, + }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ @@ -178,7 +310,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{ { Name: "function1", @@ -221,7 +354,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -259,7 +393,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -303,8 +438,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{ { TypeName: "test_resource1", @@ -346,7 +482,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -384,7 +521,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -420,6 +558,10 @@ func TestServerGetMetadata(t *testing.T) { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.EphemeralResources, func(i int, j int) bool { + return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName + }) + sort.Slice(got.Functions, func(i int, j int) bool { return got.Functions[i].Name < got.Functions[j].Name }) diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index fc5958f25..6420d513d 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -11,6 +11,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -103,7 +105,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -169,7 +172,8 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, - Functions: map[string]*tfprotov6.Function{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -209,6 +213,196 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource1": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + "test_ephemeral_resource2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, @@ -257,7 +451,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "function1": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -319,7 +514,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -360,7 +556,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -398,8 +595,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -437,8 +635,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -505,8 +704,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -585,7 +785,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -626,7 +827,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -774,6 +976,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Checking FunctionTypes lock", "@module": "sdk.framework", }, + { + "@level": "trace", + "@message": "Checking EphemeralResourceFuncs lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Checking ProviderTypeName lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider EphemeralResources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider EphemeralResources", + "@module": "sdk.framework", + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { From c4f94a9a0862ecc844e878462ec20438d140113d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 30 Aug 2024 16:50:10 -0400 Subject: [PATCH 16/55] add close proto5/6 tests --- .../server_closeephemeralresource_test.go | 194 ++++++++++++++++++ .../server_closeephemeralresource_test.go | 194 ++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 internal/proto5server/server_closeephemeralresource_test.go create mode 100644 internal/proto6server/server_closeephemeralresource_test.go diff --git a/internal/proto5server/server_closeephemeralresource_test.go b/internal/proto5server/server_closeephemeralresource_test.go new file mode 100644 index 000000000..e9d6263cf --- /dev/null +++ b/internal/proto5server/server_closeephemeralresource_test.go @@ -0,0 +1,194 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.CloseEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov5.CloseEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CloseEphemeralResourceRequest{ + State: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.CloseEphemeralResourceResponse{}, + }, + "request-state": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_required: %s", data.TestRequired.ValueString()) + } + + if data.TestComputed.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_computed: %s", data.TestComputed.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CloseEphemeralResourceRequest{ + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.CloseEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CloseEphemeralResourceRequest{ + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.CloseEphemeralResource(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_closeephemeralresource_test.go b/internal/proto6server/server_closeephemeralresource_test.go new file mode 100644 index 000000000..55a7fe58c --- /dev/null +++ b/internal/proto6server/server_closeephemeralresource_test.go @@ -0,0 +1,194 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.CloseEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov6.CloseEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CloseEphemeralResourceRequest{ + State: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.CloseEphemeralResourceResponse{}, + }, + "request-state": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_required: %s", data.TestRequired.ValueString()) + } + + if data.TestComputed.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("unexpected req.State value for test_computed: %s", data.TestComputed.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CloseEphemeralResourceRequest{ + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.CloseEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CloseEphemeralResourceRequest{ + State: testStateDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.CloseEphemeralResource(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} From bff71bb41551213c6a1e013991341635665996f1 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 3 Sep 2024 10:37:31 -0400 Subject: [PATCH 17/55] add fwserver tests for schema/metadata --- internal/fwserver/server_getmetadata_test.go | 204 ++++++++++- .../fwserver/server_getproviderschema_test.go | 331 +++++++++++++++++- 2 files changed, 500 insertions(+), 35 deletions(-) diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 09461f601..d58e0801a 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -32,9 +33,10 @@ func TestServerGetMetadata(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -75,8 +77,9 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -109,7 +112,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Duplicate Data Source Type Defined", @@ -145,7 +149,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Data Source Type Name Missing", @@ -188,6 +193,166 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "testprovidertype_data_source", }, }, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource1", + }, + { + TypeName: "test_ephemeral_resource2", + }, + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Ephemeral Resource Type Defined", + "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. "+ + "Ephemeral resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Type Name Missing", + "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "testprovidertype_ephemeral_resource", + }, + }, Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -222,7 +387,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{ { Name: "function1", @@ -264,7 +430,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Duplicate Function Name Defined", @@ -300,7 +467,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Function Name Missing", @@ -342,8 +510,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, - Functions: []fwserver.FunctionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "test_resource1", @@ -384,7 +553,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Duplicate Resource Type Defined", @@ -420,7 +590,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Resource Type Name Missing", @@ -458,8 +629,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, - Functions: []fwserver.FunctionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "testprovidertype_resource", diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index 3c975d11b..43a0c9a4e 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -36,10 +38,11 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -106,9 +109,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -312,6 +316,290 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource1": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + "test_ephemeral_resource2": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "$": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$\" at schema path \"$\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + }, + "ephemeralschema-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: nil, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Ephemeral Resource Type Defined", + "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. "+ + "Ephemeral resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: nil, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Type Name Missing", + "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "testprovidertype_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + }, FunctionDefinitions: map[string]function.Definition{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, @@ -357,7 +645,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{ "function1": { Return: function.StringReturn{}, @@ -535,8 +824,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, Provider: providerschema.Schema{ Attributes: map[string]providerschema.Attribute{ "test": providerschema.StringAttribute{ @@ -601,9 +891,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ProviderMeta: metaschema.Schema{ Attributes: map[string]metaschema.Attribute{ "test": metaschema.StringAttribute{ @@ -696,9 +987,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "test_resource1": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ @@ -908,9 +1200,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "testprovidertype_resource": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ From 9ff4f273c25119af41e357802547a0b42026879c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 3 Sep 2024 10:41:05 -0400 Subject: [PATCH 18/55] prevent random false positives --- internal/fwserver/server_getmetadata_test.go | 4 ++++ internal/proto5server/server_getmetadata_test.go | 1 + 2 files changed, 5 insertions(+) diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index d58e0801a..532765e1a 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -660,6 +660,10 @@ func TestServerGetMetadata(t *testing.T) { return response.DataSources[i].TypeName < response.DataSources[j].TypeName }) + sort.Slice(response.EphemeralResources, func(i int, j int) bool { + return response.EphemeralResources[i].TypeName < response.EphemeralResources[j].TypeName + }) + sort.Slice(response.Functions, func(i int, j int) bool { return response.Functions[i].Name < response.Functions[j].Name }) diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index cdc3ca520..d6c652d07 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -557,6 +557,7 @@ func TestServerGetMetadata(t *testing.T) { sort.Slice(got.DataSources, func(i int, j int) bool { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.EphemeralResources, func(i int, j int) bool { return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName }) From 56463dc0b6fce2e9d4909049fb6744002c499163 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 3 Sep 2024 10:55:53 -0400 Subject: [PATCH 19/55] validate fwserver tests --- ...er_validateephemeralresourceconfig_test.go | 308 ++++++++++++++++++ .../ephemeralresourceconfigvalidator.go | 47 +++ .../ephemeralresourcewithconfigvalidators.go | 30 ++ 3 files changed, 385 insertions(+) create mode 100644 internal/fwserver/server_validateephemeralresourceconfig_test.go create mode 100644 internal/testing/testprovider/ephemeralresourceconfigvalidator.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go diff --git a/internal/fwserver/server_validateephemeralresourceconfig_test.go b/internal/fwserver/server_validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..24cb08e70 --- /dev/null +++ b/internal/fwserver/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,308 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := tfsdk.Config{ + Raw: testValue, + Schema: testSchema, + } + + testSchemaAttributeValidator := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+req.ConfigValue.ValueString()) + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidator := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidator, + } + + testSchemaAttributeValidatorError := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeError(req.Path, "error summary", "error detail") + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorError := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorError, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.ValidateEphemeralResourceConfigRequest + expectedResponse *fwserver.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-AttributeValidator": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfigAttributeValidator, + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchemaAttributeValidator + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-AttributeValidator-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfigAttributeValidatorError, + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorError + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "error summary", + "error detail", + ), + }, + }, + }, + "request-config-EphemeralResourceWithConfigValidators": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithConfigValidators{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []ephemeral.ConfigValidator { + return []ephemeral.ConfigValidator{ + &testprovider.EphemeralResourceConfigValidator{ + ValidateEphemeralResourceMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-EphemeralResourceWithConfigValidators-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithConfigValidators{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []ephemeral.ConfigValidator { + return []ephemeral.ConfigValidator{ + &testprovider.EphemeralResourceConfigValidator{ + ValidateEphemeralResourceMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddError("error summary 1", "error detail 1") + }, + }, + &testprovider.EphemeralResourceConfigValidator{ + ValidateEphemeralResourceMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + // Intentionally set diagnostics instead of add/append. + // The framework should not overwrite existing diagnostics. + // Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/pull/94 + resp.Diagnostics = diag.Diagnostics{ + diag.NewErrorDiagnostic("error summary 2", "error detail 2"), + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "error summary 1", + "error detail 1", + ), + diag.NewErrorDiagnostic( + "error summary 2", + "error detail 2", + ), + }}, + }, + "request-config-EphemeralResourceWithValidateConfig": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-EphemeralResourceWithValidateConfig-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.ValidateEphemeralResourceConfigResponse{} + testCase.server.ValidateEphemeralResourceConfig(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/ephemeralresourceconfigvalidator.go b/internal/testing/testprovider/ephemeralresourceconfigvalidator.go new file mode 100644 index 000000000..afbe3c49a --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourceconfigvalidator.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.ConfigValidator = &EphemeralResourceConfigValidator{} + +// Declarative ephemeral.ConfigValidator for unit testing. +type EphemeralResourceConfigValidator struct { + // EphemeralResourceConfigValidator interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateEphemeralResourceMethod func(context.Context, ephemeral.ValidateConfigRequest, *ephemeral.ValidateConfigResponse) +} + +// Description satisfies the ephemeral.ConfigValidator interface. +func (v *EphemeralResourceConfigValidator) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the ephemeral.ConfigValidator interface. +func (v *EphemeralResourceConfigValidator) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the ephemeral.ConfigValidator interface. +func (v *EphemeralResourceConfigValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + if v.ValidateEphemeralResourceMethod == nil { + return + } + + v.ValidateEphemeralResourceMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go b/internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go new file mode 100644 index 000000000..824ceef78 --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigValidators{} +var _ ephemeral.EphemeralResourceWithConfigValidators = &EphemeralResourceWithConfigValidators{} + +// Declarative ephemeral.EphemeralResourceWithConfigValidators for unit testing. +type EphemeralResourceWithConfigValidators struct { + *EphemeralResource + + // EphemeralResourceWithConfigValidators interface methods + ConfigValidatorsMethod func(context.Context) []ephemeral.ConfigValidator +} + +// ConfigValidators satisfies the ephemeral.EphemeralResourceWithConfigValidators interface. +func (p *EphemeralResourceWithConfigValidators) ConfigValidators(ctx context.Context) []ephemeral.ConfigValidator { + if p.ConfigValidatorsMethod == nil { + return nil + } + + return p.ConfigValidatorsMethod(ctx) +} From c7d5ae2d479caeef288080613da68fe8afb5f9cf Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 3 Sep 2024 16:44:10 -0400 Subject: [PATCH 20/55] open/renew/close fwserver tests --- .../fwserver/server_closeephemeralresource.go | 2 +- .../server_closeephemeralresource_test.go | 286 ++++++++++++ .../server_openephemeralresource_test.go | 324 ++++++++++++++ .../fwserver/server_renewephemeralresource.go | 2 +- .../server_renewephemeralresource_test.go | 406 ++++++++++++++++++ .../ephemeralresourcewithconfigure.go | 30 ++ .../ephemeralresourcewithconfigureandclose.go | 43 ++ .../ephemeralresourcewithconfigureandrenew.go | 43 ++ 8 files changed, 1134 insertions(+), 2 deletions(-) create mode 100644 internal/fwserver/server_closeephemeralresource_test.go create mode 100644 internal/fwserver/server_openephemeralresource_test.go create mode 100644 internal/fwserver/server_renewephemeralresource_test.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigure.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go diff --git a/internal/fwserver/server_closeephemeralresource.go b/internal/fwserver/server_closeephemeralresource.go index d8a70a8d0..7fb3d492a 100644 --- a/internal/fwserver/server_closeephemeralresource.go +++ b/internal/fwserver/server_closeephemeralresource.go @@ -61,7 +61,7 @@ func (s *Server) CloseEphemeralResource(ctx context.Context, req *CloseEphemeral // the is_closable response field incorrectly. resp.Diagnostics.AddError( "Ephemeral Resource Close Not Implemented", - "This resource does not support close. Please contact the provider developer for additional information.", + "This ephemeral resource does not support close. Please contact the provider developer for additional information.", ) return } diff --git a/internal/fwserver/server_closeephemeralresource_test.go b/internal/fwserver/server_closeephemeralresource_test.go new file mode 100644 index 000000000..7862408ff --- /dev/null +++ b/internal/fwserver/server_closeephemeralresource_test.go @@ -0,0 +1,286 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testState := &tfsdk.EphemeralState{ + Raw: testStateValue, + Schema: testSchema, + } + + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.CloseEphemeralResourceRequest + expectedResponse *fwserver.CloseEphemeralResourceResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "request-state-missing": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {}, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Close Request", + "An unexpected error was encountered when closing the ephemeral resource. The state was missing.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + }, + "request-state": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) + } + + if data.TestComputed.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestComputed.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + Private: testPrivate, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + var expected []byte + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "ephemeralresource-no-close-implementation-diagnostic": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + State: testState, + EphemeralResourceSchema: testSchema, + // Doesn't implement Close interface + EphemeralResource: &testprovider.EphemeralResource{}, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Close Not Implemented", + "This ephemeral resource does not support close. Please contact the provider developer for additional information.", + ), + }, + }, + }, + "ephemeralresource-configure-data": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithConfigureAndClose{ + ConfigureMethod: func(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + // In practice, the Configure method would save the + // provider data to the EphemeralResource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.CloseEphemeralResourceResponse{} + testCase.server.CloseEphemeralResource(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_openephemeralresource_test.go b/internal/fwserver/server_openephemeralresource_test.go new file mode 100644 index 000000000..bbfa66cf9 --- /dev/null +++ b/internal/fwserver/server_openephemeralresource_test.go @@ -0,0 +1,324 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := &tfsdk.Config{ + Raw: testConfigValue, + Schema: testSchema, + } + + testStateUnchanged := &tfsdk.EphemeralState{ + Raw: testConfigValue, + Schema: testSchema, + } + + testState := &tfsdk.EphemeralState{ + Raw: testStateValue, + Schema: testSchema, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.OpenEphemeralResourceRequest + expectedResponse *fwserver.OpenEphemeralResourceResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testStateUnchanged, + Private: testEmptyPrivate, + }, + }, + "ephemeralresource-configure-data": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithConfigure{ + ConfigureMethod: func(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + // In practice, the Configure method would save the + // provider data to the EphemeralResource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testStateUnchanged, + Private: testEmptyPrivate, + }, + }, + "response-default-values": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {}, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testStateUnchanged, + Private: testEmptyPrivate, + RenewAt: *new(time.Time), + IsClosable: false, + }, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + State: testStateUnchanged, + Private: testEmptyPrivate, + }, + }, + "response-renew-at": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testStateUnchanged, + Private: testEmptyPrivate, + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "response-is-closable": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {}, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {}, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testStateUnchanged, + Private: testEmptyPrivate, + // Implements ephemeral.EphemeralResourceWithClose interface + IsClosable: true, + }, + }, + "response-state": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-state-value") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testState, + Private: testEmptyPrivate, + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + State: testStateUnchanged, + Private: testPrivateProvider, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.OpenEphemeralResourceResponse{} + testCase.server.OpenEphemeralResource(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_renewephemeralresource.go b/internal/fwserver/server_renewephemeralresource.go index 8d27fe8d9..208c973d0 100644 --- a/internal/fwserver/server_renewephemeralresource.go +++ b/internal/fwserver/server_renewephemeralresource.go @@ -65,7 +65,7 @@ func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeral // the RenewAt response field without defining the renew function (invalid implementation). resp.Diagnostics.AddError( "Ephemeral Resource Renew Not Implemented", - "This resource does not support renew. Please contact the provider developer for additional information.", + "This ephemeral resource does not support renew. Please contact the provider developer for additional information.", ) return } diff --git a/internal/fwserver/server_renewephemeralresource_test.go b/internal/fwserver/server_renewephemeralresource_test.go new file mode 100644 index 000000000..d7afbb995 --- /dev/null +++ b/internal/fwserver/server_renewephemeralresource_test.go @@ -0,0 +1,406 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := &tfsdk.Config{ + Raw: testConfigValue, + Schema: testSchema, + } + + testState := &tfsdk.EphemeralState{ + Raw: testStateValue, + Schema: testSchema, + } + + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.RenewEphemeralResourceRequest + expectedResponse *fwserver.RenewEphemeralResourceResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + }, + }, + "request-state-missing": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Renew Request", + "An unexpected error was encountered when renewing the ephemeral resource. The state was missing.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + }, + "request-state": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) + } + + if data.TestComputed.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestComputed.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + }, + }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + Private: testPrivate, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testPrivate, + }, + }, + "request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var expected []byte + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + }, + }, + "ephemeralresource-no-renew-implementation-diagnostic": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + // Doesn't implement Renew interface + EphemeralResource: &testprovider.EphemeralResource{}, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Renew Not Implemented", + "This ephemeral resource does not support renew. Please contact the provider developer for additional information.", + ), + }, + }, + }, + "ephemeralresource-configure-data": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithConfigureAndRenew{ + ConfigureMethod: func(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + // In practice, the Configure method would save the + // provider data to the EphemeralResource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + }, + }, + "response-default-values": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + RenewAt: *new(time.Time), + }, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + Private: testEmptyPrivate, + }, + }, + "response-renew-at": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + Config: testConfig, + State: testState, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testPrivateProvider, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.RenewEphemeralResourceResponse{} + testCase.server.RenewEphemeralResource(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigure.go b/internal/testing/testprovider/ephemeralresourcewithconfigure.go new file mode 100644 index 000000000..60ae3d430 --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigure.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigure{} +var _ ephemeral.EphemeralResourceWithConfigure = &EphemeralResourceWithConfigure{} + +// Declarative ephemeral.EphemeralResourceWithConfigure for unit testing. +type EphemeralResourceWithConfigure struct { + *EphemeralResource + + // EphemeralResourceWithConfigure interface methods + ConfigureMethod func(context.Context, ephemeral.ConfigureRequest, *ephemeral.ConfigureResponse) +} + +// Configure satisfies the ephemeral.EphemeralResourceWithConfigure interface. +func (d *EphemeralResourceWithConfigure) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if d.ConfigureMethod == nil { + return + } + + d.ConfigureMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go b/internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go new file mode 100644 index 000000000..a991fbdfc --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigureAndClose{} +var _ ephemeral.EphemeralResourceWithConfigure = &EphemeralResourceWithConfigureAndClose{} +var _ ephemeral.EphemeralResourceWithClose = &EphemeralResourceWithConfigureAndClose{} + +// Declarative ephemeral.EphemeralResourceWithConfigureAndClose for unit testing. +type EphemeralResourceWithConfigureAndClose struct { + *EphemeralResource + + // EphemeralResourceWithConfigure interface methods + ConfigureMethod func(context.Context, ephemeral.ConfigureRequest, *ephemeral.ConfigureResponse) + + // EphemeralResourceWithClose interface methods + CloseMethod func(context.Context, ephemeral.CloseRequest, *ephemeral.CloseResponse) +} + +// Configure satisfies the ephemeral.EphemeralResourceWithConfigure interface. +func (r *EphemeralResourceWithConfigureAndClose) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} + +// Close satisfies the ephemeral.EphemeralResourceWithClose interface. +func (r *EphemeralResourceWithConfigureAndClose) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + if r.CloseMethod == nil { + return + } + + r.CloseMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go b/internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go new file mode 100644 index 000000000..d9feeb16d --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigureAndRenew{} +var _ ephemeral.EphemeralResourceWithConfigure = &EphemeralResourceWithConfigureAndRenew{} +var _ ephemeral.EphemeralResourceWithRenew = &EphemeralResourceWithConfigureAndRenew{} + +// Declarative ephemeral.EphemeralResourceWithConfigureAndRenew for unit testing. +type EphemeralResourceWithConfigureAndRenew struct { + *EphemeralResource + + // EphemeralResourceWithConfigure interface methods + ConfigureMethod func(context.Context, ephemeral.ConfigureRequest, *ephemeral.ConfigureResponse) + + // EphemeralResourceWithRenew interface methods + RenewMethod func(context.Context, ephemeral.RenewRequest, *ephemeral.RenewResponse) +} + +// Configure satisfies the ephemeral.EphemeralResourceWithConfigure interface. +func (r *EphemeralResourceWithConfigureAndRenew) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} + +// Renew satisfies the ephemeral.EphemeralResourceWithRenew interface. +func (r *EphemeralResourceWithConfigureAndRenew) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + if r.RenewMethod == nil { + return + } + + r.RenewMethod(ctx, req, resp) +} From dce7c875a80c0ce5aeceb4785d97f7bb55f06b4e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 4 Sep 2024 10:55:58 -0400 Subject: [PATCH 21/55] update error message --- internal/fwserver/server_closeephemeralresource.go | 9 +++++---- internal/fwserver/server_closeephemeralresource_test.go | 4 +++- internal/fwserver/server_renewephemeralresource.go | 7 +++---- internal/fwserver/server_renewephemeralresource_test.go | 4 +++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/fwserver/server_closeephemeralresource.go b/internal/fwserver/server_closeephemeralresource.go index 7fb3d492a..76ba5a2cc 100644 --- a/internal/fwserver/server_closeephemeralresource.go +++ b/internal/fwserver/server_closeephemeralresource.go @@ -56,12 +56,13 @@ func (s *Server) CloseEphemeralResource(ctx context.Context, req *CloseEphemeral resourceWithClose, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithClose) if !ok { - // TODO: this diagnostic should be more worded towards a core or plugin-framework bug. - // Either something is bugged in core and called close incorrectly, or the framework populated - // the is_closable response field incorrectly. + // The framework automatically sets the indicator to Terraform core that enables calling close using + // this interface, so if we get this diagnostic then there is a bug in either Terraform core or framework. resp.Diagnostics.AddError( "Ephemeral Resource Close Not Implemented", - "This ephemeral resource does not support close. Please contact the provider developer for additional information.", + "An unexpected error was encountered when closing the ephemeral resource. Terraform sent a close request for an "+ + "ephemeral resource that has not implemented close logic.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", ) return } diff --git a/internal/fwserver/server_closeephemeralresource_test.go b/internal/fwserver/server_closeephemeralresource_test.go index 7862408ff..c4e6cbce6 100644 --- a/internal/fwserver/server_closeephemeralresource_test.go +++ b/internal/fwserver/server_closeephemeralresource_test.go @@ -193,7 +193,9 @@ func TestServerCloseEphemeralResource(t *testing.T) { Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Ephemeral Resource Close Not Implemented", - "This ephemeral resource does not support close. Please contact the provider developer for additional information.", + "An unexpected error was encountered when closing the ephemeral resource. Terraform sent a close request for an "+ + "ephemeral resource that has not implemented close logic.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", ), }, }, diff --git a/internal/fwserver/server_renewephemeralresource.go b/internal/fwserver/server_renewephemeralresource.go index 208c973d0..2cbe96646 100644 --- a/internal/fwserver/server_renewephemeralresource.go +++ b/internal/fwserver/server_renewephemeralresource.go @@ -60,12 +60,11 @@ func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeral resourceWithRenew, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithRenew) if !ok { - // TODO: this diagnostic should be more worded towards an invalid implementation or core bug. - // Either something is bugged in core and called renew incorrectly, or the provider populated - // the RenewAt response field without defining the renew function (invalid implementation). resp.Diagnostics.AddError( "Ephemeral Resource Renew Not Implemented", - "This ephemeral resource does not support renew. Please contact the provider developer for additional information.", + "An unexpected error was encountered when renewing the ephemeral resource. Terraform sent a renewal request for an "+ + "ephemeral resource that has not implemented renewal logic.\n\n"+ + "Please report this to the provider developer.", ) return } diff --git a/internal/fwserver/server_renewephemeralresource_test.go b/internal/fwserver/server_renewephemeralresource_test.go index d7afbb995..567baff09 100644 --- a/internal/fwserver/server_renewephemeralresource_test.go +++ b/internal/fwserver/server_renewephemeralresource_test.go @@ -252,7 +252,9 @@ func TestServerRenewEphemeralResource(t *testing.T) { Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Ephemeral Resource Renew Not Implemented", - "This ephemeral resource does not support renew. Please contact the provider developer for additional information.", + "An unexpected error was encountered when renewing the ephemeral resource. Terraform sent a renewal request for an "+ + "ephemeral resource that has not implemented renewal logic.\n\n"+ + "Please report this to the provider developer.", ), }, }, From b84bb1ddcd3abaf7de40da910211398cee580502 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 4 Sep 2024 17:20:30 -0400 Subject: [PATCH 22/55] update plugin go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e27718172..718705816 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.0 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240827183355-145d7bc53a3f + github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240904211550-7ea64adab752 github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index b618ef4c6..3d99403d3 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOs github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240827183355-145d7bc53a3f h1:6E4kyvCnk1Vb0L/wEfvBZFF4vkkbvpsOOHoRkDu60I8= -github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240827183355-145d7bc53a3f/go.mod h1:ko0HcPe7AkwMukddKa13Qq5zyvi8V9KYxBZmqU8a9cE= +github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240904211550-7ea64adab752 h1:KtwYfSreTB6WRoHmYvvPzCAXPaJeKzsCeFNA0ErzLas= +github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240904211550-7ea64adab752/go.mod h1:ko0HcPe7AkwMukddKa13Qq5zyvi8V9KYxBZmqU8a9cE= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= From 1871b387a98f366fc9b0381cda8155c7d133b784 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Mon, 23 Sep 2024 15:59:53 -0400 Subject: [PATCH 23/55] Update `terraform-plugin-go` dependency --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8571a3290..cf160b6a4 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.24.0 + github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240830180900-5eafe6ae6def github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index 3fe9c7360..635b63694 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOs github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= -github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= +github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240830180900-5eafe6ae6def h1:/RKsl9EoVaSGf4PgyuDEmnPd2f/x2jdntwx+q0kY2xA= +github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240830180900-5eafe6ae6def/go.mod h1:ko0HcPe7AkwMukddKa13Qq5zyvi8V9KYxBZmqU8a9cE= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= From 627a49bf3060aa8d1c681847ff3c682681ea3cd2 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 24 Sep 2024 13:32:37 -0400 Subject: [PATCH 24/55] remove `config` from renew --- ephemeral/renew.go | 4 -- go.mod | 12 ++-- go.sum | 24 ++++---- internal/fromproto5/renewephemeralresource.go | 6 -- .../fromproto5/renewephemeralresource_test.go | 28 ---------- internal/fromproto6/renewephemeralresource.go | 6 -- .../fromproto6/renewephemeralresource_test.go | 28 ---------- .../fwserver/server_renewephemeralresource.go | 8 --- .../server_renewephemeralresource_test.go | 47 ---------------- .../proto5server/server_getmetadata_test.go | 3 + .../server_getproviderschema_test.go | 3 + .../server_renewephemeralresource_test.go | 55 ------------------- .../proto6server/server_getmetadata_test.go | 3 + .../server_getproviderschema_test.go | 3 + .../server_renewephemeralresource_test.go | 55 ------------------- 15 files changed, 30 insertions(+), 255 deletions(-) diff --git a/ephemeral/renew.go b/ephemeral/renew.go index b208b0921..1db1e5c10 100644 --- a/ephemeral/renew.go +++ b/ephemeral/renew.go @@ -18,10 +18,6 @@ type RenewRequest struct { // resource following the Open operation. State tfsdk.EphemeralState - // Config is the configuration the user supplied for the ephemeral - // resource. - Config tfsdk.Config - // Private is provider-defined ephemeral resource private state data // which was previously provided by the latest Open or Renew operation. // Any existing data is copied to RenewResponse.Private to prevent diff --git a/go.mod b/go.mod index 8571a3290..6fabbc478 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.24.0 + github.com/hashicorp/terraform-plugin-go v0.24.1-0.20240924154457-cd3b6654adf0 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -25,10 +25,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/grpc v1.66.2 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 3fe9c7360..4ea1430e3 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOs github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= -github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= +github.com/hashicorp/terraform-plugin-go v0.24.1-0.20240924154457-cd3b6654adf0 h1:WgAlLzllo3v6BeZccPW5IETHgRylr/9y37X314C+09I= +github.com/hashicorp/terraform-plugin-go v0.24.1-0.20240924154457-cd3b6654adf0/go.mod h1:zoSM9LyEFI4iVkDeKXgwBHV0uTuIIXydtK1fy9J4wBA= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -46,21 +46,21 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/fromproto5/renewephemeralresource.go b/internal/fromproto5/renewephemeralresource.go index e82bb3b58..02ce0990f 100644 --- a/internal/fromproto5/renewephemeralresource.go +++ b/internal/fromproto5/renewephemeralresource.go @@ -42,12 +42,6 @@ func RenewEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.RenewE EphemeralResourceSchema: ephemeralResourceSchema, } - config, configDiags := Config(ctx, proto5.Config, ephemeralResourceSchema) - - diags.Append(configDiags...) - - fw.Config = config - state, stateDiags := EphemeralState(ctx, proto5.State, ephemeralResourceSchema) diags.Append(stateDiags...) diff --git a/internal/fromproto5/renewephemeralresource_test.go b/internal/fromproto5/renewephemeralresource_test.go index e5bc70c14..5537b51e7 100644 --- a/internal/fromproto5/renewephemeralresource_test.go +++ b/internal/fromproto5/renewephemeralresource_test.go @@ -79,34 +79,6 @@ func TestRenewEphemeralResourceRequest(t *testing.T) { ), }, }, - "config-missing-schema": { - input: &tfprotov5.RenewEphemeralResourceRequest{ - Config: &testProto5DynamicValue, - }, - expected: nil, - expectedDiagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Missing EphemeralResource Schema", - "An unexpected error was encountered when handling the request. "+ - "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ - "Please report this to the provider developer:\n\n"+ - "Missing schema.", - ), - }, - }, - "config": { - input: &tfprotov5.RenewEphemeralResourceRequest{ - Config: &testProto5DynamicValue, - }, - ephemeralResourceSchema: testFwSchema, - expected: &fwserver.RenewEphemeralResourceRequest{ - Config: &tfsdk.Config{ - Raw: testProto5Value, - Schema: testFwSchema, - }, - EphemeralResourceSchema: testFwSchema, - }, - }, "private": { input: &tfprotov5.RenewEphemeralResourceRequest{ Private: privatestate.MustMarshalToJson(map[string][]byte{ diff --git a/internal/fromproto6/renewephemeralresource.go b/internal/fromproto6/renewephemeralresource.go index ee6f13b71..669b24791 100644 --- a/internal/fromproto6/renewephemeralresource.go +++ b/internal/fromproto6/renewephemeralresource.go @@ -42,12 +42,6 @@ func RenewEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.RenewE EphemeralResourceSchema: ephemeralResourceSchema, } - config, configDiags := Config(ctx, proto6.Config, ephemeralResourceSchema) - - diags.Append(configDiags...) - - fw.Config = config - state, stateDiags := EphemeralState(ctx, proto6.State, ephemeralResourceSchema) diags.Append(stateDiags...) diff --git a/internal/fromproto6/renewephemeralresource_test.go b/internal/fromproto6/renewephemeralresource_test.go index 8453f3bff..803833817 100644 --- a/internal/fromproto6/renewephemeralresource_test.go +++ b/internal/fromproto6/renewephemeralresource_test.go @@ -79,34 +79,6 @@ func TestRenewEphemeralResourceRequest(t *testing.T) { ), }, }, - "config-missing-schema": { - input: &tfprotov6.RenewEphemeralResourceRequest{ - Config: &testProto6DynamicValue, - }, - expected: nil, - expectedDiagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Missing EphemeralResource Schema", - "An unexpected error was encountered when handling the request. "+ - "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ - "Please report this to the provider developer:\n\n"+ - "Missing schema.", - ), - }, - }, - "config": { - input: &tfprotov6.RenewEphemeralResourceRequest{ - Config: &testProto6DynamicValue, - }, - ephemeralResourceSchema: testFwSchema, - expected: &fwserver.RenewEphemeralResourceRequest{ - Config: &tfsdk.Config{ - Raw: testProto6Value, - Schema: testFwSchema, - }, - EphemeralResourceSchema: testFwSchema, - }, - }, "private": { input: &tfprotov6.RenewEphemeralResourceRequest{ Private: privatestate.MustMarshalToJson(map[string][]byte{ diff --git a/internal/fwserver/server_renewephemeralresource.go b/internal/fwserver/server_renewephemeralresource.go index 2cbe96646..03942936a 100644 --- a/internal/fwserver/server_renewephemeralresource.go +++ b/internal/fwserver/server_renewephemeralresource.go @@ -18,7 +18,6 @@ import ( // RenewEphemeralResourceRequest is the framework server request for the // RenewEphemeralResource RPC. type RenewEphemeralResourceRequest struct { - Config *tfsdk.Config State *tfsdk.EphemeralState Private *privatestate.Data EphemeralResourceSchema fwschema.Schema @@ -92,9 +91,6 @@ func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeral } renewReq := ephemeral.RenewRequest{ - Config: tfsdk.Config{ - Schema: req.EphemeralResourceSchema, - }, State: tfsdk.EphemeralState{ Schema: req.EphemeralResourceSchema, Raw: req.State.Raw.Copy(), @@ -105,10 +101,6 @@ func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeral Private: renewReq.Private, } - if req.Config != nil { - renewReq.Config = *req.Config - } - logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Renew") resourceWithRenew.Renew(ctx, renewReq, &renewResp) logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Renew") diff --git a/internal/fwserver/server_renewephemeralresource_test.go b/internal/fwserver/server_renewephemeralresource_test.go index 567baff09..e653785c2 100644 --- a/internal/fwserver/server_renewephemeralresource_test.go +++ b/internal/fwserver/server_renewephemeralresource_test.go @@ -34,11 +34,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, } - testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }) - testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, "test-state-value"), "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), @@ -55,11 +50,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, } - testConfig := &tfsdk.Config{ - Raw: testConfigValue, - Schema: testSchema, - } - testState := &tfsdk.EphemeralState{ Raw: testStateValue, Schema: testSchema, @@ -102,39 +92,11 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, expectedResponse: &fwserver.RenewEphemeralResourceResponse{}, }, - "request-config": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, - State: testState, - EphemeralResourceSchema: testSchema, - EphemeralResource: &testprovider.EphemeralResourceWithRenew{ - RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - - if config.TestRequired.ValueString() != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) - } - }, - }, - }, - expectedResponse: &fwserver.RenewEphemeralResourceResponse{ - Private: testEmptyPrivate, - }, - }, "request-state-missing": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, @@ -155,7 +117,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ @@ -186,7 +147,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ @@ -214,7 +174,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ @@ -242,7 +201,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, // Doesn't implement Renew interface @@ -265,7 +223,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithConfigureAndRenew{ @@ -304,7 +261,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ @@ -321,7 +277,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ @@ -350,7 +305,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ @@ -369,7 +323,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.RenewEphemeralResourceRequest{ - Config: testConfig, State: testState, EphemeralResourceSchema: testSchema, EphemeralResource: &testprovider.EphemeralResourceWithRenew{ diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 2ed45828c..39d2985bd 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -198,6 +198,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -244,6 +245,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -282,6 +284,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index fc038e872..41a632391 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -305,6 +305,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -372,6 +373,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -413,6 +415,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, diff --git a/internal/proto5server/server_renewephemeralresource_test.go b/internal/proto5server/server_renewephemeralresource_test.go index 768146b5e..27b8ee8b0 100644 --- a/internal/proto5server/server_renewephemeralresource_test.go +++ b/internal/proto5server/server_renewephemeralresource_test.go @@ -28,11 +28,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, } - testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }) - testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ @@ -82,58 +77,11 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov5.RenewEphemeralResourceRequest{ - Config: testEmptyDynamicValue, State: testEmptyDynamicValue, TypeName: "test_ephemeral_resource", }, expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{}, }, - "request-config": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResourceWithRenew{ - EphemeralResource: &testprovider.EphemeralResource{ - SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - resp.Schema = testSchema - }, - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - }, - RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - - if config.TestRequired.ValueString() != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value for test_required: %s", config.TestRequired.ValueString()) - } - - if !config.TestComputed.IsNull() { - resp.Diagnostics.AddError("unexpected req.Config value for test_computed: %s", config.TestComputed.ValueString()) - } - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.RenewEphemeralResourceRequest{ - Config: testConfigDynamicValue, - State: testStateDynamicValue, - TypeName: "test_ephemeral_resource", - }, - expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{}, - }, "request-state": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -174,7 +122,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov5.RenewEphemeralResourceRequest{ - Config: testConfigDynamicValue, State: testStateDynamicValue, TypeName: "test_ephemeral_resource", }, @@ -208,7 +155,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov5.RenewEphemeralResourceRequest{ - Config: testConfigDynamicValue, State: testStateDynamicValue, TypeName: "test_ephemeral_resource", }, @@ -254,7 +200,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov5.RenewEphemeralResourceRequest{ - Config: testEmptyDynamicValue, State: testEmptyDynamicValue, TypeName: "test_ephemeral_resource", }, diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 600ab24c7..5a2395f7e 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -153,6 +153,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -197,6 +198,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -243,6 +245,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index 4ae964c7b..24337c998 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -223,6 +223,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -304,6 +305,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -371,6 +373,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, diff --git a/internal/proto6server/server_renewephemeralresource_test.go b/internal/proto6server/server_renewephemeralresource_test.go index e14231538..b237dc3e9 100644 --- a/internal/proto6server/server_renewephemeralresource_test.go +++ b/internal/proto6server/server_renewephemeralresource_test.go @@ -28,11 +28,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, } - testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }) - testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ @@ -82,58 +77,11 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov6.RenewEphemeralResourceRequest{ - Config: testEmptyDynamicValue, State: testEmptyDynamicValue, TypeName: "test_ephemeral_resource", }, expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{}, }, - "request-config": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResourceWithRenew{ - EphemeralResource: &testprovider.EphemeralResource{ - SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - resp.Schema = testSchema - }, - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - }, - RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { - var config struct { - TestComputed types.String `tfsdk:"test_computed"` - TestRequired types.String `tfsdk:"test_required"` - } - - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - - if config.TestRequired.ValueString() != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value for test_required: %s", config.TestRequired.ValueString()) - } - - if !config.TestComputed.IsNull() { - resp.Diagnostics.AddError("unexpected req.Config value for test_computed: %s", config.TestComputed.ValueString()) - } - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.RenewEphemeralResourceRequest{ - Config: testConfigDynamicValue, - State: testStateDynamicValue, - TypeName: "test_ephemeral_resource", - }, - expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{}, - }, "request-state": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -174,7 +122,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov6.RenewEphemeralResourceRequest{ - Config: testConfigDynamicValue, State: testStateDynamicValue, TypeName: "test_ephemeral_resource", }, @@ -208,7 +155,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov6.RenewEphemeralResourceRequest{ - Config: testConfigDynamicValue, State: testStateDynamicValue, TypeName: "test_ephemeral_resource", }, @@ -254,7 +200,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { }, }, request: &tfprotov6.RenewEphemeralResourceRequest{ - Config: testEmptyDynamicValue, State: testEmptyDynamicValue, TypeName: "test_ephemeral_resource", }, From 90cbd29d1e8f29c2019088f77110ea212e5beb98 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 25 Sep 2024 14:22:47 -0400 Subject: [PATCH 25/55] Implement write only attributes in the `resource/schema` package --- internal/fwschema/attribute.go | 12 +- .../write_only_nested_attribute_validation.go | 80 + ...e_only_nested_attribute_validation_test.go | 1446 +++++++++++++++++ resource/schema/bool_attribute.go | 14 + resource/schema/bool_attribute_test.go | 34 + resource/schema/dynamic_attribute.go | 14 + resource/schema/dynamic_attribute_test.go | 34 + resource/schema/float32_attribute.go | 14 + resource/schema/float32_attribute_test.go | 34 + resource/schema/float64_attribute.go | 14 + resource/schema/float64_attribute_test.go | 34 + resource/schema/int32_attribute.go | 14 + resource/schema/int32_attribute_test.go | 34 + resource/schema/int64_attribute.go | 14 + resource/schema/int64_attribute_test.go | 34 + resource/schema/list_attribute.go | 14 + resource/schema/list_attribute_test.go | 34 + resource/schema/list_nested_attribute.go | 22 + resource/schema/list_nested_attribute_test.go | 122 ++ resource/schema/map_attribute.go | 14 + resource/schema/map_attribute_test.go | 34 + resource/schema/map_nested_attribute.go | 22 + resource/schema/map_nested_attribute_test.go | 122 ++ resource/schema/number_attribute.go | 14 + resource/schema/number_attribute_test.go | 34 + resource/schema/object_attribute.go | 14 + resource/schema/object_attribute_test.go | 34 + resource/schema/set_attribute.go | 14 + resource/schema/set_attribute_test.go | 34 + resource/schema/set_nested_attribute.go | 22 + resource/schema/set_nested_attribute_test.go | 122 ++ resource/schema/single_nested_attribute.go | 23 + .../schema/single_nested_attribute_test.go | 114 ++ resource/schema/string_attribute.go | 14 + resource/schema/string_attribute_test.go | 34 + 35 files changed, 2682 insertions(+), 1 deletion(-) create mode 100644 internal/fwtype/write_only_nested_attribute_validation.go create mode 100644 internal/fwtype/write_only_nested_attribute_validation_test.go diff --git a/internal/fwschema/attribute.go b/internal/fwschema/attribute.go index 4face5a95..f8a27a547 100644 --- a/internal/fwschema/attribute.go +++ b/internal/fwschema/attribute.go @@ -4,8 +4,9 @@ package fwschema import ( - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" ) // Attribute is the core interface required for implementing Terraform @@ -63,6 +64,11 @@ type Attribute interface { // sensitive. This is named differently than Sensitive to prevent a // conflict with the tfsdk.Attribute field name. IsSensitive() bool + + // IsWriteOnly should return true if the attribute configuration value is + // write-only. This is named differently than WriteOnly to prevent a + // conflict with the tfsdk.Attribute field name. + IsWriteOnly() bool } // AttributesEqual is a helper function to perform equality testing on two @@ -101,5 +107,9 @@ func AttributesEqual(a, b Attribute) bool { return false } + if a.IsWriteOnly() != b.IsWriteOnly() { + return false + } + return true } diff --git a/internal/fwtype/write_only_nested_attribute_validation.go b/internal/fwtype/write_only_nested_attribute_validation.go new file mode 100644 index 000000000..a9454b9b8 --- /dev/null +++ b/internal/fwtype/write_only_nested_attribute_validation.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwtype + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" +) + +// ContainsAllWriteOnlyChildAttributes will return true if all child attributes for the +// given nested attribute have WriteOnly set to true. +func ContainsAllWriteOnlyChildAttributes(nestedAttr metaschema.NestedAttribute) bool { + if !nestedAttr.IsWriteOnly() { + return false + } + nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() + + for _, childAttr := range nestedObjAttrs { + nestedAttribute, ok := childAttr.(metaschema.NestedAttribute) + if ok { + if !ContainsAllWriteOnlyChildAttributes(nestedAttribute) { + return false + } + } + + if !childAttr.IsWriteOnly() { + return false + } + } + + return true +} + +// ContainsAnyWriteOnlyChildAttributes will return true if any child attribute for the +// given nested attribute has WriteOnly set to true. +func ContainsAnyWriteOnlyChildAttributes(nestedAttr metaschema.NestedAttribute) bool { + if nestedAttr.IsWriteOnly() { + return true + } + nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() + + for _, childAttr := range nestedObjAttrs { + nestedAttribute, ok := childAttr.(metaschema.NestedAttribute) + if ok { + if ContainsAnyWriteOnlyChildAttributes(nestedAttribute) { + return true + } + } + + if childAttr.IsWriteOnly() { + return true + } + } + + return false +} + +func InvalidWriteOnlyNestedAttributeDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ) +} + +func InvalidComputedNestedAttributeWithWriteOnlyDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a Computed nested attribute that contains a WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ) +} diff --git a/internal/fwtype/write_only_nested_attribute_validation_test.go b/internal/fwtype/write_only_nested_attribute_validation_test.go new file mode 100644 index 000000000..b72ee60e2 --- /dev/null +++ b/internal/fwtype/write_only_nested_attribute_validation_test.go @@ -0,0 +1,1446 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwtype_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { + tests := map[string]struct { + nestedAttr metaschema.NestedAttribute + expected bool + }{ + "empty nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{}, + expected: false, + }, + "writeOnly list nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "writeOnly list nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "writeOnly list nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "writeOnly list nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "writeOnly list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "writeOnly list nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly list nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly list nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly list nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly set nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "writeOnly set nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "writeOnly set nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "writeOnly set nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "writeOnly set nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "writeOnly set nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly set nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly set nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly set nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly set nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly map nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "writeOnly map nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "writeOnly map nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "writeOnly map nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "writeOnly map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "writeOnly map nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly map nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly map nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly map nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly single nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "writeOnly single nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "writeOnly single nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "writeOnly single nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "writeOnly single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + "writeOnly single nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly single nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "writeOnly single nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + expected: false, + }, + "non-writeOnly single nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "non-writeOnly single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + if got := fwtype.ContainsAllWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { + tests := map[string]struct { + nestedAttr metaschema.NestedAttribute + expected bool + }{ + "empty nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{}, + expected: false, + }, + "list nested attribute with writeOnly returns true": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly returns true": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly returns true": { + nestedAttr: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with writeOnly returns true": { + nestedAttr: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: true, + }, + "single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + if got := fwtype.ContainsAnyWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index abb0b8708..e9fc10a21 100644 --- a/resource/schema/bool_attribute.go +++ b/resource/schema/bool_attribute.go @@ -152,6 +152,15 @@ type BoolAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Bool + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -229,6 +238,11 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a BoolAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/bool_attribute_test.go b/resource/schema/bool_attribute_test.go index d4b85ef2d..e2d77dd42 100644 --- a/resource/schema/bool_attribute_test.go +++ b/resource/schema/bool_attribute_test.go @@ -512,6 +512,40 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } } +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.BoolAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestBoolAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamic_attribute.go b/resource/schema/dynamic_attribute.go index 7b97625d9..0ef295a02 100644 --- a/resource/schema/dynamic_attribute.go +++ b/resource/schema/dynamic_attribute.go @@ -153,6 +153,15 @@ type DynamicAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Dynamic + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -215,6 +224,11 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a DynamicAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // DynamicDefaultValue returns the Default field value. func (a DynamicAttribute) DynamicDefaultValue() defaults.Dynamic { return a.Default diff --git a/resource/schema/dynamic_attribute_test.go b/resource/schema/dynamic_attribute_test.go index f99dc598c..93c5010f3 100644 --- a/resource/schema/dynamic_attribute_test.go +++ b/resource/schema/dynamic_attribute_test.go @@ -397,6 +397,40 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.DynamicAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go index 9e8e7a22a..836a13310 100644 --- a/resource/schema/float32_attribute.go +++ b/resource/schema/float32_attribute.go @@ -155,6 +155,15 @@ type Float32Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Float32 + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +241,11 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float32_attribute_test.go b/resource/schema/float32_attribute_test.go index 75f1a9637..3c90d0081 100644 --- a/resource/schema/float32_attribute_test.go +++ b/resource/schema/float32_attribute_test.go @@ -512,6 +512,40 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } } +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Float32Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat32AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go index 7d762a4a2..679031952 100644 --- a/resource/schema/float64_attribute.go +++ b/resource/schema/float64_attribute.go @@ -155,6 +155,15 @@ type Float64Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Float64 + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +241,11 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float64_attribute_test.go b/resource/schema/float64_attribute_test.go index a7d155c9f..939080080 100644 --- a/resource/schema/float64_attribute_test.go +++ b/resource/schema/float64_attribute_test.go @@ -512,6 +512,40 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } } +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Float64Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat64AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go index 41b74bcf3..e26184be2 100644 --- a/resource/schema/int32_attribute.go +++ b/resource/schema/int32_attribute.go @@ -155,6 +155,15 @@ type Int32Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Int32 + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +241,11 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int32_attribute_test.go b/resource/schema/int32_attribute_test.go index 152d957f7..48eb3d2f2 100644 --- a/resource/schema/int32_attribute_test.go +++ b/resource/schema/int32_attribute_test.go @@ -512,6 +512,40 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } } +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Int32Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt32AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go index 65ec795e9..f0b00a446 100644 --- a/resource/schema/int64_attribute.go +++ b/resource/schema/int64_attribute.go @@ -155,6 +155,15 @@ type Int64Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Int64 + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +241,11 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int64_attribute_test.go b/resource/schema/int64_attribute_test.go index a961c1fb8..032f523c9 100644 --- a/resource/schema/int64_attribute_test.go +++ b/resource/schema/int64_attribute_test.go @@ -512,6 +512,40 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } } +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Int64Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt64AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index 1dc0e0e8c..46fd5d839 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -168,6 +168,15 @@ type ListAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.List + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a list @@ -232,6 +241,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue returns the Default field value. func (a ListAttribute) ListDefaultValue() defaults.List { return a.Default diff --git a/resource/schema/list_attribute_test.go b/resource/schema/list_attribute_test.go index 784ec70d6..07db0c333 100644 --- a/resource/schema/list_attribute_test.go +++ b/resource/schema/list_attribute_test.go @@ -403,6 +403,40 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ListAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 95fd2ba01..79f68cd5c 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -178,6 +178,15 @@ type ListNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.List + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -260,6 +269,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue returns the Default field value. func (a ListNestedAttribute) ListDefaultValue() defaults.List { return a.Default @@ -284,6 +298,14 @@ func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fws resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.ListDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/list_nested_attribute_test.go b/resource/schema/list_nested_attribute_test.go index a73c7a843..290fd0dc7 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -574,6 +574,40 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListDefaultValue(t *testing.T) { t.Parallel() @@ -909,6 +943,94 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index ac50f63f8..2e55b7a40 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -171,6 +171,15 @@ type MapAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Map + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a map @@ -235,6 +244,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue returns the Default field value. func (a MapAttribute) MapDefaultValue() defaults.Map { return a.Default diff --git a/resource/schema/map_attribute_test.go b/resource/schema/map_attribute_test.go index 4f56036d5..9e48ea787 100644 --- a/resource/schema/map_attribute_test.go +++ b/resource/schema/map_attribute_test.go @@ -403,6 +403,40 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.MapAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index ab2230b3b..c29f0322d 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -178,6 +178,15 @@ type MapNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Map + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -260,6 +269,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue returns the Default field value. func (a MapNestedAttribute) MapDefaultValue() defaults.Map { return a.Default @@ -284,6 +298,14 @@ func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.MapDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/map_nested_attribute_test.go b/resource/schema/map_nested_attribute_test.go index 1fd1a7624..4c7a72489 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -574,6 +574,40 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedDefaultValue(t *testing.T) { t.Parallel() @@ -909,6 +943,94 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/number_attribute.go b/resource/schema/number_attribute.go index d2b9c59af..8252a3ff1 100644 --- a/resource/schema/number_attribute.go +++ b/resource/schema/number_attribute.go @@ -156,6 +156,15 @@ type NumberAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Number + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -218,6 +227,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a NumberAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // NumberDefaultValue returns the Default field value. func (a NumberAttribute) NumberDefaultValue() defaults.Number { return a.Default diff --git a/resource/schema/number_attribute_test.go b/resource/schema/number_attribute_test.go index 2bc8731ca..e9ff78cc8 100644 --- a/resource/schema/number_attribute_test.go +++ b/resource/schema/number_attribute_test.go @@ -398,6 +398,40 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.NumberAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index 7b9fe6a56..b58f7c965 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -170,6 +170,15 @@ type ObjectAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Object + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into an @@ -234,6 +243,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ObjectAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue returns the Default field value. func (a ObjectAttribute) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/resource/schema/object_attribute_test.go b/resource/schema/object_attribute_test.go index 071e2293c..8f1b6cc3c 100644 --- a/resource/schema/object_attribute_test.go +++ b/resource/schema/object_attribute_test.go @@ -409,6 +409,40 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ObjectAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index 7a54221bb..393318607 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -166,6 +166,15 @@ type SetAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Set + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a set @@ -230,6 +239,11 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SetAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // SetDefaultValue returns the Default field value. func (a SetAttribute) SetDefaultValue() defaults.Set { return a.Default diff --git a/resource/schema/set_attribute_test.go b/resource/schema/set_attribute_test.go index 98483f724..9f7c6d0ac 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -403,6 +403,40 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SetAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index dee37b591..861f59695 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -173,6 +173,15 @@ type SetNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Set + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -255,6 +264,11 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SetNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // SetDefaultValue returns the Default field value. func (a SetNestedAttribute) SetDefaultValue() defaults.Set { return a.Default @@ -279,6 +293,14 @@ func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.SetDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/set_nested_attribute_test.go b/resource/schema/set_nested_attribute_test.go index d148db457..8ea3cb58c 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -574,6 +574,40 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SetNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetDefaultValue(t *testing.T) { t.Parallel() @@ -909,6 +943,94 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index 3dbda942a..a0acece6f 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -167,6 +168,15 @@ type SingleNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Object + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -271,6 +281,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue returns the Default field value. func (a SingleNestedAttribute) ObjectDefaultValue() defaults.Object { return a.Default @@ -295,6 +310,14 @@ func (a SingleNestedAttribute) ValidateImplementation(ctx context.Context, req f resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) } + if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.ObjectDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/single_nested_attribute_test.go b/resource/schema/single_nested_attribute_test.go index 1f20b65b3..f1b666566 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -538,6 +538,40 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectDefaultValue(t *testing.T) { t.Parallel() @@ -819,6 +853,86 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go index 7e3b8a1c2..793414f94 100644 --- a/resource/schema/string_attribute.go +++ b/resource/schema/string_attribute.go @@ -152,6 +152,15 @@ type StringAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.String + + // WriteOnly indicates that the practitioner can choose a value for this + // attribute, but Terraform will not store this attribute in state. + // If WriteOnly is true, either Optional or Required must also be true. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -214,6 +223,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a StringAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // StringDefaultValue returns the Default field value. func (a StringAttribute) StringDefaultValue() defaults.String { return a.Default diff --git a/resource/schema/string_attribute_test.go b/resource/schema/string_attribute_test.go index 65bc15b10..20f5ccf36 100644 --- a/resource/schema/string_attribute_test.go +++ b/resource/schema/string_attribute_test.go @@ -397,6 +397,40 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.StringAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringDefaultValue(t *testing.T) { t.Parallel() From 0f84c52f23b025659d70bdcca0c2c13bdf966b06 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 25 Sep 2024 14:25:42 -0400 Subject: [PATCH 26/55] Implement write only attributes in the `datasource/schema` package --- datasource/schema/bool_attribute.go | 8 ++++- datasource/schema/bool_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/dynamic_attribute.go | 8 ++++- datasource/schema/dynamic_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/float32_attribute.go | 5 +++ datasource/schema/float32_attribute_test.go | 28 +++++++++++++++++ datasource/schema/float64_attribute.go | 8 ++++- datasource/schema/float64_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/int32_attribute.go | 5 +++ datasource/schema/int32_attribute_test.go | 28 +++++++++++++++++ datasource/schema/int64_attribute.go | 8 ++++- datasource/schema/int64_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/list_attribute.go | 8 ++++- datasource/schema/list_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/list_nested_attribute.go | 8 ++++- .../schema/list_nested_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/map_attribute.go | 8 ++++- datasource/schema/map_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/map_nested_attribute.go | 5 +++ .../schema/map_nested_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/number_attribute.go | 8 ++++- datasource/schema/number_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/object_attribute.go | 8 ++++- datasource/schema/object_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/set_attribute.go | 8 ++++- datasource/schema/set_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/set_nested_attribute.go | 5 +++ .../schema/set_nested_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/single_nested_attribute.go | 5 +++ .../schema/single_nested_attribute_test.go | 31 ++++++++++++++++++- datasource/schema/string_attribute.go | 8 ++++- datasource/schema/string_attribute_test.go | 31 ++++++++++++++++++- 32 files changed, 578 insertions(+), 25 deletions(-) diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index b9f6e3820..520bcbc4b 100644 --- a/datasource/schema/bool_attribute.go +++ b/datasource/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,6 +182,11 @@ func (a BoolAttribute) IsRequired() bool { return a.Required } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + // IsSensitive returns the Sensitive field value. func (a BoolAttribute) IsSensitive() bool { return a.Sensitive diff --git a/datasource/schema/bool_attribute_test.go b/datasource/schema/bool_attribute_test.go index 40e36a08d..b493d892d 100644 --- a/datasource/schema/bool_attribute_test.go +++ b/datasource/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index 6b1b6c83e..aa3b5a716 100644 --- a/datasource/schema/dynamic_attribute.go +++ b/datasource/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -182,6 +183,11 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators diff --git a/datasource/schema/dynamic_attribute_test.go b/datasource/schema/dynamic_attribute_test.go index 8981dbecd..95fe01fb4 100644 --- a/datasource/schema/dynamic_attribute_test.go +++ b/datasource/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index 8f3dbdc21..ed34e3168 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -189,3 +189,8 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a Float32Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/float32_attribute_test.go b/datasource/schema/float32_attribute_test.go index 0da3cc94e..1040f6245 100644 --- a/datasource/schema/float32_attribute_test.go +++ b/datasource/schema/float32_attribute_test.go @@ -424,3 +424,31 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 1313353ec..35a34f8a0 100644 --- a/datasource/schema/float64_attribute.go +++ b/datasource/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,8 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/float64_attribute_test.go b/datasource/schema/float64_attribute_test.go index f2e05ebf7..b83e16abd 100644 --- a/datasource/schema/float64_attribute_test.go +++ b/datasource/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index 89f852e8c..d06f71866 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -189,3 +189,8 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a Int32Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/int32_attribute_test.go b/datasource/schema/int32_attribute_test.go index 3df85e9c7..23ce97fd5 100644 --- a/datasource/schema/int32_attribute_test.go +++ b/datasource/schema/int32_attribute_test.go @@ -424,3 +424,31 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index ab9d5ca1b..543d5408c 100644 --- a/datasource/schema/int64_attribute.go +++ b/datasource/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,8 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/int64_attribute_test.go b/datasource/schema/int64_attribute_test.go index c1ac3c7a4..b15eeb85f 100644 --- a/datasource/schema/int64_attribute_test.go +++ b/datasource/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index 9d502067f..42e710d12 100644 --- a/datasource/schema/list_attribute.go +++ b/datasource/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -202,6 +203,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/datasource/schema/list_attribute_test.go b/datasource/schema/list_attribute_test.go index 27294a130..1e7335ce3 100644 --- a/datasource/schema/list_attribute_test.go +++ b/datasource/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index b9b70d6fb..953307553 100644 --- a/datasource/schema/list_nested_attribute.go +++ b/datasource/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -230,6 +231,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/datasource/schema/list_nested_attribute_test.go b/datasource/schema/list_nested_attribute_test.go index 5d1e7db88..fd1cf6941 100644 --- a/datasource/schema/list_nested_attribute_test.go +++ b/datasource/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/map_attribute.go b/datasource/schema/map_attribute.go index 516576a4e..3f5e7d6d4 100644 --- a/datasource/schema/map_attribute.go +++ b/datasource/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -205,6 +206,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_attribute_test.go b/datasource/schema/map_attribute_test.go index d79425462..ab3d31167 100644 --- a/datasource/schema/map_attribute_test.go +++ b/datasource/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index 9729efdc3..f0fa318ea 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -231,6 +231,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_nested_attribute_test.go b/datasource/schema/map_nested_attribute_test.go index ea6dc480d..671f55593 100644 --- a/datasource/schema/map_nested_attribute_test.go +++ b/datasource/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/number_attribute.go b/datasource/schema/number_attribute.go index ffe4e0839..a1ca61783 100644 --- a/datasource/schema/number_attribute.go +++ b/datasource/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -185,6 +186,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/datasource/schema/number_attribute_test.go b/datasource/schema/number_attribute_test.go index f8cec323d..449dda7ce 100644 --- a/datasource/schema/number_attribute_test.go +++ b/datasource/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index eafa40c6e..f1fe16864 100644 --- a/datasource/schema/object_attribute.go +++ b/datasource/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -204,6 +205,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/object_attribute_test.go b/datasource/schema/object_attribute_test.go index c517465ed..ab3c428ce 100644 --- a/datasource/schema/object_attribute_test.go +++ b/datasource/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -404,6 +405,34 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/set_attribute.go b/datasource/schema/set_attribute.go index 261b02424..4240d625c 100644 --- a/datasource/schema/set_attribute.go +++ b/datasource/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -200,6 +201,11 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_attribute_test.go b/datasource/schema/set_attribute_test.go index 793095705..9a0760b9b 100644 --- a/datasource/schema/set_attribute_test.go +++ b/datasource/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 1b17b8743..412643520 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -226,6 +226,11 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_nested_attribute_test.go b/datasource/schema/set_nested_attribute_test.go index 7f7815f70..4b064ee7c 100644 --- a/datasource/schema/set_nested_attribute_test.go +++ b/datasource/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/single_nested_attribute.go b/datasource/schema/single_nested_attribute.go index 811e76de4..95ed16950 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -240,6 +240,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/single_nested_attribute_test.go b/datasource/schema/single_nested_attribute_test.go index 25a5e565e..2a62a8e42 100644 --- a/datasource/schema/single_nested_attribute_test.go +++ b/datasource/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -530,6 +531,34 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/string_attribute.go b/datasource/schema/string_attribute.go index 0c2dd9aba..c0ec4d48d 100644 --- a/datasource/schema/string_attribute.go +++ b/datasource/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,6 +182,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +func (a StringAttribute) IsWriteOnly() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/datasource/schema/string_attribute_test.go b/datasource/schema/string_attribute_test.go index d436b9bd1..f86110feb 100644 --- a/datasource/schema/string_attribute_test.go +++ b/datasource/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() From 2c9d3cb209d6c92a94a517489710630e01a2ffc2 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 25 Sep 2024 16:31:20 -0400 Subject: [PATCH 27/55] Implement write only attributes in the `provider/schema` and `provider/metaschema` packages --- datasource/schema/bool_attribute.go | 2 +- datasource/schema/dynamic_attribute.go | 2 +- datasource/schema/float32_attribute.go | 2 +- datasource/schema/float64_attribute.go | 2 +- datasource/schema/int32_attribute.go | 2 +- datasource/schema/int64_attribute.go | 2 +- datasource/schema/list_attribute.go | 2 +- datasource/schema/list_nested_attribute.go | 2 +- datasource/schema/map_attribute.go | 2 +- datasource/schema/map_nested_attribute.go | 2 +- datasource/schema/number_attribute.go | 2 +- datasource/schema/object_attribute.go | 2 +- datasource/schema/set_attribute.go | 2 +- datasource/schema/set_nested_attribute.go | 2 +- datasource/schema/single_nested_attribute.go | 2 +- datasource/schema/string_attribute.go | 2 +- provider/metaschema/bool_attribute.go | 8 ++++- provider/metaschema/bool_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/float64_attribute.go | 8 ++++- provider/metaschema/float64_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/int64_attribute.go | 8 ++++- provider/metaschema/int64_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/list_attribute.go | 8 ++++- provider/metaschema/list_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/list_nested_attribute.go | 8 ++++- .../metaschema/list_nested_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/map_attribute.go | 8 ++++- provider/metaschema/map_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/map_nested_attribute.go | 8 ++++- .../metaschema/map_nested_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/number_attribute.go | 8 ++++- provider/metaschema/number_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/object_attribute.go | 8 ++++- provider/metaschema/object_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/set_attribute.go | 8 ++++- provider/metaschema/set_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/set_nested_attribute.go | 8 ++++- .../metaschema/set_nested_attribute_test.go | 31 ++++++++++++++++++- .../metaschema/single_nested_attribute.go | 8 ++++- .../single_nested_attribute_test.go | 31 ++++++++++++++++++- provider/metaschema/string_attribute.go | 8 ++++- provider/metaschema/string_attribute_test.go | 31 ++++++++++++++++++- provider/schema/bool_attribute.go | 8 ++++- provider/schema/bool_attribute_test.go | 31 ++++++++++++++++++- provider/schema/dynamic_attribute.go | 8 ++++- provider/schema/dynamic_attribute_test.go | 31 ++++++++++++++++++- provider/schema/float32_attribute.go | 8 ++++- provider/schema/float32_attribute_test.go | 31 ++++++++++++++++++- provider/schema/float64_attribute.go | 8 ++++- provider/schema/float64_attribute_test.go | 31 ++++++++++++++++++- provider/schema/int32_attribute.go | 5 +++ provider/schema/int32_attribute_test.go | 28 +++++++++++++++++ provider/schema/int64_attribute.go | 8 ++++- provider/schema/int64_attribute_test.go | 31 ++++++++++++++++++- provider/schema/list_attribute.go | 8 ++++- provider/schema/list_attribute_test.go | 31 ++++++++++++++++++- provider/schema/list_nested_attribute.go | 8 ++++- provider/schema/list_nested_attribute_test.go | 31 ++++++++++++++++++- provider/schema/map_attribute.go | 8 ++++- provider/schema/map_attribute_test.go | 31 ++++++++++++++++++- provider/schema/map_nested_attribute.go | 5 +++ provider/schema/map_nested_attribute_test.go | 31 ++++++++++++++++++- provider/schema/number_attribute.go | 8 ++++- provider/schema/number_attribute_test.go | 31 ++++++++++++++++++- provider/schema/object_attribute.go | 8 ++++- provider/schema/object_attribute_test.go | 31 ++++++++++++++++++- provider/schema/set_attribute.go | 8 ++++- provider/schema/set_attribute_test.go | 31 ++++++++++++++++++- provider/schema/set_nested_attribute.go | 5 +++ provider/schema/set_nested_attribute_test.go | 31 ++++++++++++++++++- provider/schema/single_nested_attribute.go | 5 +++ .../schema/single_nested_attribute_test.go | 31 ++++++++++++++++++- provider/schema/string_attribute.go | 8 ++++- provider/schema/string_attribute_test.go | 31 ++++++++++++++++++- 74 files changed, 1079 insertions(+), 69 deletions(-) diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index 520bcbc4b..1d984a8db 100644 --- a/datasource/schema/bool_attribute.go +++ b/datasource/schema/bool_attribute.go @@ -182,7 +182,7 @@ func (a BoolAttribute) IsRequired() bool { return a.Required } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a BoolAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index aa3b5a716..4659cadf6 100644 --- a/datasource/schema/dynamic_attribute.go +++ b/datasource/schema/dynamic_attribute.go @@ -183,7 +183,7 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a DynamicAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index ed34e3168..d2510f5c3 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -190,7 +190,7 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a Float32Attribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 35a34f8a0..1d893dd33 100644 --- a/datasource/schema/float64_attribute.go +++ b/datasource/schema/float64_attribute.go @@ -190,7 +190,7 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a Float64Attribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index d06f71866..d06a9b8ce 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -190,7 +190,7 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a Int32Attribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index 543d5408c..85bd4a445 100644 --- a/datasource/schema/int64_attribute.go +++ b/datasource/schema/int64_attribute.go @@ -190,7 +190,7 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a Int64Attribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index 42e710d12..2e5140602 100644 --- a/datasource/schema/list_attribute.go +++ b/datasource/schema/list_attribute.go @@ -203,7 +203,7 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a ListAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index 953307553..922320627 100644 --- a/datasource/schema/list_nested_attribute.go +++ b/datasource/schema/list_nested_attribute.go @@ -231,7 +231,7 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a ListNestedAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/map_attribute.go b/datasource/schema/map_attribute.go index 3f5e7d6d4..3d6c57680 100644 --- a/datasource/schema/map_attribute.go +++ b/datasource/schema/map_attribute.go @@ -206,7 +206,7 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a MapAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index f0fa318ea..9bf1bb957 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -231,7 +231,7 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a MapNestedAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/number_attribute.go b/datasource/schema/number_attribute.go index a1ca61783..c21f74a15 100644 --- a/datasource/schema/number_attribute.go +++ b/datasource/schema/number_attribute.go @@ -186,7 +186,7 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a NumberAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index f1fe16864..a004329ac 100644 --- a/datasource/schema/object_attribute.go +++ b/datasource/schema/object_attribute.go @@ -205,7 +205,7 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a ObjectAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/set_attribute.go b/datasource/schema/set_attribute.go index 4240d625c..859b0558d 100644 --- a/datasource/schema/set_attribute.go +++ b/datasource/schema/set_attribute.go @@ -201,7 +201,7 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a SetAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 412643520..26b14e449 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -226,7 +226,7 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a SetNestedAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/single_nested_attribute.go b/datasource/schema/single_nested_attribute.go index 95ed16950..b6f4e7f32 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -240,7 +240,7 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a SingleNestedAttribute) IsWriteOnly() bool { return false } diff --git a/datasource/schema/string_attribute.go b/datasource/schema/string_attribute.go index c0ec4d48d..95534fece 100644 --- a/datasource/schema/string_attribute.go +++ b/datasource/schema/string_attribute.go @@ -182,7 +182,7 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false (write-only attributes are not supported in data source schemas). +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. func (a StringAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 6d96a516a..7180fc7bc 100644 --- a/provider/metaschema/bool_attribute.go +++ b/provider/metaschema/bool_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -117,3 +118,8 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/bool_attribute_test.go b/provider/metaschema/bool_attribute_test.go index 617225b54..50b3e06bb 100644 --- a/provider/metaschema/bool_attribute_test.go +++ b/provider/metaschema/bool_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index 8a4478655..ef37da466 100644 --- a/provider/metaschema/float64_attribute.go +++ b/provider/metaschema/float64_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -120,3 +121,8 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/float64_attribute_test.go b/provider/metaschema/float64_attribute_test.go index 71cb2f688..f6f5ebd7f 100644 --- a/provider/metaschema/float64_attribute_test.go +++ b/provider/metaschema/float64_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index 8751d574e..bd13a67d9 100644 --- a/provider/metaschema/int64_attribute.go +++ b/provider/metaschema/int64_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -120,3 +121,8 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/int64_attribute_test.go b/provider/metaschema/int64_attribute_test.go index 28efcebd7..2aa4d585e 100644 --- a/provider/metaschema/int64_attribute_test.go +++ b/provider/metaschema/int64_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index a3ff30e65..cfedf83bd 100644 --- a/provider/metaschema/list_attribute.go +++ b/provider/metaschema/list_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -134,6 +135,11 @@ func (a ListAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/list_attribute_test.go b/provider/metaschema/list_attribute_test.go index 6c864e8d6..424c1d602 100644 --- a/provider/metaschema/list_attribute_test.go +++ b/provider/metaschema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -379,6 +380,34 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index a6b4f875a..2f99b5607 100644 --- a/provider/metaschema/list_nested_attribute.go +++ b/provider/metaschema/list_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -159,3 +160,8 @@ func (a ListNestedAttribute) IsRequired() bool { func (a ListNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/list_nested_attribute_test.go b/provider/metaschema/list_nested_attribute_test.go index dc6188822..c2aeae811 100644 --- a/provider/metaschema/list_nested_attribute_test.go +++ b/provider/metaschema/list_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -546,3 +547,31 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index 51ee02edb..e7be27532 100644 --- a/provider/metaschema/map_attribute.go +++ b/provider/metaschema/map_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -137,6 +138,11 @@ func (a MapAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/map_attribute_test.go b/provider/metaschema/map_attribute_test.go index 0956d8cc4..52ee19318 100644 --- a/provider/metaschema/map_attribute_test.go +++ b/provider/metaschema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -379,6 +380,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index 47a3b4348..616064fc0 100644 --- a/provider/metaschema/map_nested_attribute.go +++ b/provider/metaschema/map_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -159,3 +160,8 @@ func (a MapNestedAttribute) IsRequired() bool { func (a MapNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/map_nested_attribute_test.go b/provider/metaschema/map_nested_attribute_test.go index 7c320b1bf..88eb68b26 100644 --- a/provider/metaschema/map_nested_attribute_test.go +++ b/provider/metaschema/map_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -546,3 +547,31 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go index 86e45b8ab..5aea1c285 100644 --- a/provider/metaschema/number_attribute.go +++ b/provider/metaschema/number_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -121,3 +122,8 @@ func (a NumberAttribute) IsRequired() bool { func (a NumberAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/number_attribute_test.go b/provider/metaschema/number_attribute_test.go index 819f2d2a1..588dd4e0b 100644 --- a/provider/metaschema/number_attribute_test.go +++ b/provider/metaschema/number_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestNumberAttributeIsSensitive(t *testing.T) { }) } } + +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index aa4c67be9..da70a516e 100644 --- a/provider/metaschema/object_attribute.go +++ b/provider/metaschema/object_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -136,6 +137,11 @@ func (a ObjectAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/object_attribute_test.go b/provider/metaschema/object_attribute_test.go index e24db5dd1..f439faf0a 100644 --- a/provider/metaschema/object_attribute_test.go +++ b/provider/metaschema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -385,6 +386,34 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go index 919713075..f6fc456bf 100644 --- a/provider/metaschema/set_attribute.go +++ b/provider/metaschema/set_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -132,6 +133,11 @@ func (a SetAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/set_attribute_test.go b/provider/metaschema/set_attribute_test.go index 8cb5a95d5..620209b6f 100644 --- a/provider/metaschema/set_attribute_test.go +++ b/provider/metaschema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -379,6 +380,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index 233866a00..36e5d6141 100644 --- a/provider/metaschema/set_nested_attribute.go +++ b/provider/metaschema/set_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -154,3 +155,8 @@ func (a SetNestedAttribute) IsRequired() bool { func (a SetNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/set_nested_attribute_test.go b/provider/metaschema/set_nested_attribute_test.go index 623fa3668..d0d86508c 100644 --- a/provider/metaschema/set_nested_attribute_test.go +++ b/provider/metaschema/set_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -546,3 +547,31 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index 0ed1a22fd..ff11e783d 100644 --- a/provider/metaschema/single_nested_attribute.go +++ b/provider/metaschema/single_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -174,3 +175,8 @@ func (a SingleNestedAttribute) IsRequired() bool { func (a SingleNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/single_nested_attribute_test.go b/provider/metaschema/single_nested_attribute_test.go index f7c7cbda5..f04d55634 100644 --- a/provider/metaschema/single_nested_attribute_test.go +++ b/provider/metaschema/single_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -510,3 +511,31 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index 3a14d0721..49fd9cb64 100644 --- a/provider/metaschema/string_attribute.go +++ b/provider/metaschema/string_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -117,3 +118,8 @@ func (a StringAttribute) IsRequired() bool { func (a StringAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/string_attribute_test.go b/provider/metaschema/string_attribute_test.go index 544fa268a..00f34d5b2 100644 --- a/provider/metaschema/string_attribute_test.go +++ b/provider/metaschema/string_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestStringAttributeIsSensitive(t *testing.T) { }) } } + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index c411062e0..7244d3691 100644 --- a/provider/schema/bool_attribute.go +++ b/provider/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -178,3 +179,8 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/bool_attribute_test.go b/provider/schema/bool_attribute_test.go index 043f2a896..50e4af6ed 100644 --- a/provider/schema/bool_attribute_test.go +++ b/provider/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index c738d348d..49c50cc31 100644 --- a/provider/schema/dynamic_attribute.go +++ b/provider/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -175,3 +176,8 @@ func (a DynamicAttribute) IsSensitive() bool { func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/dynamic_attribute_test.go b/provider/schema/dynamic_attribute_test.go index c06eaf811..0779fe460 100644 --- a/provider/schema/dynamic_attribute_test.go +++ b/provider/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -384,6 +385,34 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index a36c5c435..36209f481 100644 --- a/provider/schema/float32_attribute.go +++ b/provider/schema/float32_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,8 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/float32_attribute_test.go b/provider/schema/float32_attribute_test.go index e779b4a0a..107216df6 100644 --- a/provider/schema/float32_attribute_test.go +++ b/provider/schema/float32_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index 786965e3a..ec5e7ece5 100644 --- a/provider/schema/float64_attribute.go +++ b/provider/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,8 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/float64_attribute_test.go b/provider/schema/float64_attribute_test.go index 96604d0e7..b08486708 100644 --- a/provider/schema/float64_attribute_test.go +++ b/provider/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index 16ff58f0f..a7c28d527 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -182,3 +182,8 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/int32_attribute_test.go b/provider/schema/int32_attribute_test.go index b3ac0687c..a612fb42f 100644 --- a/provider/schema/int32_attribute_test.go +++ b/provider/schema/int32_attribute_test.go @@ -418,3 +418,31 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 3fd9713f1..6a380846e 100644 --- a/provider/schema/int64_attribute.go +++ b/provider/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,8 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/int64_attribute_test.go b/provider/schema/int64_attribute_test.go index f5e457663..42efcaed4 100644 --- a/provider/schema/int64_attribute_test.go +++ b/provider/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index e733b297f..692bdb4d2 100644 --- a/provider/schema/list_attribute.go +++ b/provider/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -195,6 +196,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_attribute_test.go b/provider/schema/list_attribute_test.go index 3baa9382b..2a529ae13 100644 --- a/provider/schema/list_attribute_test.go +++ b/provider/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -392,6 +393,34 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 0c82da4ad..01fc09037 100644 --- a/provider/schema/list_nested_attribute.go +++ b/provider/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -223,6 +224,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_nested_attribute_test.go b/provider/schema/list_nested_attribute_test.go index 43d76a612..33d6f1723 100644 --- a/provider/schema/list_nested_attribute_test.go +++ b/provider/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -563,6 +564,34 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/map_attribute.go b/provider/schema/map_attribute.go index 77dc2b61d..74d3c24e6 100644 --- a/provider/schema/map_attribute.go +++ b/provider/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -198,6 +199,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_attribute_test.go b/provider/schema/map_attribute_test.go index 0bbef5f25..8c5548e6d 100644 --- a/provider/schema/map_attribute_test.go +++ b/provider/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -392,6 +393,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index 2eed2fa08..034693317 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -223,6 +223,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_nested_attribute_test.go b/provider/schema/map_nested_attribute_test.go index 193960869..129321f22 100644 --- a/provider/schema/map_nested_attribute_test.go +++ b/provider/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -563,6 +564,34 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/number_attribute.go b/provider/schema/number_attribute.go index f3e90e2b7..03a66ddbd 100644 --- a/provider/schema/number_attribute.go +++ b/provider/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -178,6 +179,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/provider/schema/number_attribute_test.go b/provider/schema/number_attribute_test.go index b957cc73f..306e975e5 100644 --- a/provider/schema/number_attribute_test.go +++ b/provider/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -384,6 +385,34 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index 3041f5a79..5e5c0f64f 100644 --- a/provider/schema/object_attribute.go +++ b/provider/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -197,6 +198,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/object_attribute_test.go b/provider/schema/object_attribute_test.go index 01f65b864..089d03f71 100644 --- a/provider/schema/object_attribute_test.go +++ b/provider/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/set_attribute.go b/provider/schema/set_attribute.go index 3297452b7..fbb7be645 100644 --- a/provider/schema/set_attribute.go +++ b/provider/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -193,6 +194,11 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_attribute_test.go b/provider/schema/set_attribute_test.go index 862b2d8dd..42aec99d1 100644 --- a/provider/schema/set_attribute_test.go +++ b/provider/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -392,6 +393,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index 7a2fb6060..4e1b4bfd6 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -219,6 +219,11 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_nested_attribute_test.go b/provider/schema/set_nested_attribute_test.go index 163604a9c..942cd2a25 100644 --- a/provider/schema/set_nested_attribute_test.go +++ b/provider/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -563,6 +564,34 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/single_nested_attribute.go b/provider/schema/single_nested_attribute.go index 4aa669bf1..74eb9219f 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -233,6 +233,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/single_nested_attribute_test.go b/provider/schema/single_nested_attribute_test.go index a471cbc20..6d40629a2 100644 --- a/provider/schema/single_nested_attribute_test.go +++ b/provider/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -524,6 +525,34 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/string_attribute.go b/provider/schema/string_attribute.go index 7ab7a0c42..e8ce8f099 100644 --- a/provider/schema/string_attribute.go +++ b/provider/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -174,6 +175,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/provider/schema/string_attribute_test.go b/provider/schema/string_attribute_test.go index 4d24ef73d..58b0cafeb 100644 --- a/provider/schema/string_attribute_test.go +++ b/provider/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -384,6 +385,34 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() From 1e41c4a1c86320520b7b5a9a2b8b2d070faeb079 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 25 Sep 2024 18:02:03 -0400 Subject: [PATCH 28/55] Implement write only attributes in the `internal/testing/testschema` package --- internal/testing/testschema/attribute.go | 9 ++++++++- internal/testing/testschema/attributewithbooldefault.go | 6 ++++++ .../testing/testschema/attributewithboolplanmodifiers.go | 9 ++++++++- .../testing/testschema/attributewithboolvalidators.go | 9 ++++++++- .../testing/testschema/attributewithdynamicdefault.go | 6 ++++++ .../testschema/attributewithdynamicplanmodifiers.go | 6 ++++++ .../testing/testschema/attributewithdynamicvalidators.go | 9 ++++++++- .../testing/testschema/attributewithfloat32default.go | 6 ++++++ .../testschema/attributewithfloat32planmodifiers.go | 6 ++++++ .../testing/testschema/attributewithfloat32validators.go | 6 ++++++ .../testing/testschema/attributewithfloat64default.go | 6 ++++++ .../testschema/attributewithfloat64planmodifiers.go | 6 ++++++ .../testing/testschema/attributewithfloat64validators.go | 6 ++++++ internal/testing/testschema/attributewithint32default.go | 6 ++++++ .../testschema/attributewithint32planmodifiers.go | 6 ++++++ .../testing/testschema/attributewithint32validators.go | 6 ++++++ internal/testing/testschema/attributewithint64default.go | 6 ++++++ .../testschema/attributewithint64planmodifiers.go | 6 ++++++ .../testing/testschema/attributewithint64validators.go | 6 ++++++ internal/testing/testschema/attributewithlistdefault.go | 6 ++++++ .../testing/testschema/attributewithlistplanmodifiers.go | 6 ++++++ .../testing/testschema/attributewithlistvalidators.go | 6 ++++++ internal/testing/testschema/attributewithmapdefault.go | 6 ++++++ .../testing/testschema/attributewithmapplanmodifiers.go | 6 ++++++ .../testing/testschema/attributewithmapvalidators.go | 6 ++++++ .../testing/testschema/attributewithnumberdefault.go | 6 ++++++ .../testschema/attributewithnumberplanmodifiers.go | 6 ++++++ .../testing/testschema/attributewithnumbervalidators.go | 6 ++++++ .../testing/testschema/attributewithobjectdefault.go | 6 ++++++ .../testschema/attributewithobjectplanmodifiers.go | 6 ++++++ .../testing/testschema/attributewithobjectvalidators.go | 6 ++++++ internal/testing/testschema/attributewithsetdefault.go | 6 ++++++ .../testing/testschema/attributewithsetplanmodifiers.go | 9 ++++++++- .../testing/testschema/attributewithsetvalidators.go | 6 ++++++ .../testing/testschema/attributewithstringdefault.go | 6 ++++++ .../testschema/attributewithstringplanmodifiers.go | 6 ++++++ .../testing/testschema/attributewithstringvalidators.go | 6 ++++++ internal/testing/testschema/nested_attribute.go | 6 ++++++ .../testschema/nested_attribute_with_list_default.go | 6 ++++++ .../nested_attribute_with_list_plan_modifiers.go | 6 ++++++ .../testschema/nested_attribute_with_map_default.go | 6 ++++++ .../nested_attribute_with_map_plan_modifiers.go | 6 ++++++ .../testschema/nested_attribute_with_object_default.go | 6 ++++++ .../nested_attribute_with_object_plan_modifiers.go | 6 ++++++ .../testschema/nested_attribute_with_set_default.go | 6 ++++++ .../nested_attribute_with_set_plan_modifiers.go | 6 ++++++ 46 files changed, 286 insertions(+), 5 deletions(-) diff --git a/internal/testing/testschema/attribute.go b/internal/testing/testschema/attribute.go index fccc26b42..979db68eb 100644 --- a/internal/testing/testschema/attribute.go +++ b/internal/testing/testschema/attribute.go @@ -4,9 +4,10 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwschema.Attribute = Attribute{} @@ -19,6 +20,7 @@ type Attribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -77,3 +79,8 @@ func (a Attribute) IsRequired() bool { func (a Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a Attribute) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithbooldefault.go b/internal/testing/testschema/attributewithbooldefault.go index 5d7b25671..66edc07e5 100644 --- a/internal/testing/testschema/attributewithbooldefault.go +++ b/internal/testing/testschema/attributewithbooldefault.go @@ -22,6 +22,7 @@ type AttributeWithBoolDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Bool } @@ -85,3 +86,8 @@ func (a AttributeWithBoolDefaultValue) IsRequired() bool { func (a AttributeWithBoolDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithboolplanmodifiers.go b/internal/testing/testschema/attributewithboolplanmodifiers.go index d27661894..397f18186 100644 --- a/internal/testing/testschema/attributewithboolplanmodifiers.go +++ b/internal/testing/testschema/attributewithboolplanmodifiers.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithBoolPlanModifiers = AttributeWithBoolPlanModifiers{} @@ -22,6 +23,7 @@ type AttributeWithBoolPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Bool } @@ -85,3 +87,8 @@ func (a AttributeWithBoolPlanModifiers) IsRequired() bool { func (a AttributeWithBoolPlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithboolvalidators.go b/internal/testing/testschema/attributewithboolvalidators.go index 5cc0943ca..044c25cf4 100644 --- a/internal/testing/testschema/attributewithboolvalidators.go +++ b/internal/testing/testschema/attributewithboolvalidators.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithBoolValidators = AttributeWithBoolValidators{} @@ -22,6 +23,7 @@ type AttributeWithBoolValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Bool } @@ -85,3 +87,8 @@ func (a AttributeWithBoolValidators) IsRequired() bool { func (a AttributeWithBoolValidators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithdynamicdefault.go b/internal/testing/testschema/attributewithdynamicdefault.go index d366beb9a..b562132a8 100644 --- a/internal/testing/testschema/attributewithdynamicdefault.go +++ b/internal/testing/testschema/attributewithdynamicdefault.go @@ -22,6 +22,7 @@ type AttributeWithDynamicDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Dynamic } @@ -85,3 +86,8 @@ func (a AttributeWithDynamicDefaultValue) IsRequired() bool { func (a AttributeWithDynamicDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithdynamicplanmodifiers.go b/internal/testing/testschema/attributewithdynamicplanmodifiers.go index abb7ca6bf..74a80587a 100644 --- a/internal/testing/testschema/attributewithdynamicplanmodifiers.go +++ b/internal/testing/testschema/attributewithdynamicplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithDynamicPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Dynamic } @@ -85,3 +86,8 @@ func (a AttributeWithDynamicPlanModifiers) IsSensitive() bool { func (a AttributeWithDynamicPlanModifiers) DynamicPlanModifiers() []planmodifier.Dynamic { return a.PlanModifiers } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithdynamicvalidators.go b/internal/testing/testschema/attributewithdynamicvalidators.go index 1fe086775..e4ef1024e 100644 --- a/internal/testing/testschema/attributewithdynamicvalidators.go +++ b/internal/testing/testschema/attributewithdynamicvalidators.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithDynamicValidators = AttributeWithDynamicValidators{} @@ -22,6 +23,7 @@ type AttributeWithDynamicValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Dynamic } @@ -85,3 +87,8 @@ func (a AttributeWithDynamicValidators) IsSensitive() bool { func (a AttributeWithDynamicValidators) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat32default.go b/internal/testing/testschema/attributewithfloat32default.go index e1dabdd87..c3aeb627d 100644 --- a/internal/testing/testschema/attributewithfloat32default.go +++ b/internal/testing/testschema/attributewithfloat32default.go @@ -22,6 +22,7 @@ type AttributeWithFloat32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Float32 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat32DefaultValue) IsRequired() bool { func (a AttributeWithFloat32DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat32planmodifiers.go b/internal/testing/testschema/attributewithfloat32planmodifiers.go index b3ef10ebc..f93d87229 100644 --- a/internal/testing/testschema/attributewithfloat32planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat32planmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithFloat32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Float32 } @@ -86,3 +87,8 @@ func (a AttributeWithFloat32PlanModifiers) IsRequired() bool { func (a AttributeWithFloat32PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat32validators.go b/internal/testing/testschema/attributewithfloat32validators.go index d9d38f9ea..7fb02a5ad 100644 --- a/internal/testing/testschema/attributewithfloat32validators.go +++ b/internal/testing/testschema/attributewithfloat32validators.go @@ -23,6 +23,7 @@ type AttributeWithFloat32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Float32 } @@ -86,3 +87,8 @@ func (a AttributeWithFloat32Validators) IsRequired() bool { func (a AttributeWithFloat32Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat64default.go b/internal/testing/testschema/attributewithfloat64default.go index 33b717f33..484ec37d9 100644 --- a/internal/testing/testschema/attributewithfloat64default.go +++ b/internal/testing/testschema/attributewithfloat64default.go @@ -22,6 +22,7 @@ type AttributeWithFloat64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Float64 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat64DefaultValue) IsRequired() bool { func (a AttributeWithFloat64DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat64planmodifiers.go b/internal/testing/testschema/attributewithfloat64planmodifiers.go index 6273cbd5d..ba04291da 100644 --- a/internal/testing/testschema/attributewithfloat64planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat64planmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithFloat64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Float64 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat64PlanModifiers) IsRequired() bool { func (a AttributeWithFloat64PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat64validators.go b/internal/testing/testschema/attributewithfloat64validators.go index b2b2b5f70..02ef17704 100644 --- a/internal/testing/testschema/attributewithfloat64validators.go +++ b/internal/testing/testschema/attributewithfloat64validators.go @@ -22,6 +22,7 @@ type AttributeWithFloat64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Float64 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat64Validators) IsRequired() bool { func (a AttributeWithFloat64Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint32default.go b/internal/testing/testschema/attributewithint32default.go index bbfe22af8..f332bae41 100644 --- a/internal/testing/testschema/attributewithint32default.go +++ b/internal/testing/testschema/attributewithint32default.go @@ -22,6 +22,7 @@ type AttributeWithInt32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Int32 } @@ -85,3 +86,8 @@ func (a AttributeWithInt32DefaultValue) IsRequired() bool { func (a AttributeWithInt32DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint32planmodifiers.go b/internal/testing/testschema/attributewithint32planmodifiers.go index aff453d9c..7f131df58 100644 --- a/internal/testing/testschema/attributewithint32planmodifiers.go +++ b/internal/testing/testschema/attributewithint32planmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithInt32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Int32 } @@ -86,3 +87,8 @@ func (a AttributeWithInt32PlanModifiers) IsRequired() bool { func (a AttributeWithInt32PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint32validators.go b/internal/testing/testschema/attributewithint32validators.go index 7c6913bcc..8a4546e9e 100644 --- a/internal/testing/testschema/attributewithint32validators.go +++ b/internal/testing/testschema/attributewithint32validators.go @@ -23,6 +23,7 @@ type AttributeWithInt32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Int32 } @@ -86,3 +87,8 @@ func (a AttributeWithInt32Validators) IsRequired() bool { func (a AttributeWithInt32Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint64default.go b/internal/testing/testschema/attributewithint64default.go index ca9e12b96..574a88a58 100644 --- a/internal/testing/testschema/attributewithint64default.go +++ b/internal/testing/testschema/attributewithint64default.go @@ -22,6 +22,7 @@ type AttributeWithInt64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Int64 } @@ -85,3 +86,8 @@ func (a AttributeWithInt64DefaultValue) IsRequired() bool { func (a AttributeWithInt64DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint64planmodifiers.go b/internal/testing/testschema/attributewithint64planmodifiers.go index 368a865c0..e21a43251 100644 --- a/internal/testing/testschema/attributewithint64planmodifiers.go +++ b/internal/testing/testschema/attributewithint64planmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithInt64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Int64 } @@ -85,3 +86,8 @@ func (a AttributeWithInt64PlanModifiers) IsRequired() bool { func (a AttributeWithInt64PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint64validators.go b/internal/testing/testschema/attributewithint64validators.go index 07cf28bd7..c4e23e166 100644 --- a/internal/testing/testschema/attributewithint64validators.go +++ b/internal/testing/testschema/attributewithint64validators.go @@ -22,6 +22,7 @@ type AttributeWithInt64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Int64 } @@ -85,3 +86,8 @@ func (a AttributeWithInt64Validators) IsRequired() bool { func (a AttributeWithInt64Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithlistdefault.go b/internal/testing/testschema/attributewithlistdefault.go index 1f6c65d5a..ff23c5215 100644 --- a/internal/testing/testschema/attributewithlistdefault.go +++ b/internal/testing/testschema/attributewithlistdefault.go @@ -23,6 +23,7 @@ type AttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.List } @@ -88,3 +89,8 @@ func (a AttributeWithListDefaultValue) IsRequired() bool { func (a AttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithlistplanmodifiers.go b/internal/testing/testschema/attributewithlistplanmodifiers.go index 08ed953fa..fbb50c332 100644 --- a/internal/testing/testschema/attributewithlistplanmodifiers.go +++ b/internal/testing/testschema/attributewithlistplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithListPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.List } @@ -84,6 +85,11 @@ func (a AttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a AttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithlistvalidators.go b/internal/testing/testschema/attributewithlistvalidators.go index bb47ca9d6..fefa2eb02 100644 --- a/internal/testing/testschema/attributewithlistvalidators.go +++ b/internal/testing/testschema/attributewithlistvalidators.go @@ -23,6 +23,7 @@ type AttributeWithListValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.List } @@ -84,6 +85,11 @@ func (a AttributeWithListValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // ListValidators satisfies the fwxschema.AttributeWithListValidators interface. func (a AttributeWithListValidators) ListValidators() []validator.List { return a.Validators diff --git a/internal/testing/testschema/attributewithmapdefault.go b/internal/testing/testschema/attributewithmapdefault.go index a8bf1910d..2b223cd1d 100644 --- a/internal/testing/testschema/attributewithmapdefault.go +++ b/internal/testing/testschema/attributewithmapdefault.go @@ -23,6 +23,7 @@ type AttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Map } @@ -88,3 +89,8 @@ func (a AttributeWithMapDefaultValue) IsRequired() bool { func (a AttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithmapplanmodifiers.go b/internal/testing/testschema/attributewithmapplanmodifiers.go index 1d067e1d4..06c7b2fe4 100644 --- a/internal/testing/testschema/attributewithmapplanmodifiers.go +++ b/internal/testing/testschema/attributewithmapplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithMapPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Map } @@ -84,6 +85,11 @@ func (a AttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a AttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithmapvalidators.go b/internal/testing/testschema/attributewithmapvalidators.go index 1469ceee2..5e3a9dac8 100644 --- a/internal/testing/testschema/attributewithmapvalidators.go +++ b/internal/testing/testschema/attributewithmapvalidators.go @@ -23,6 +23,7 @@ type AttributeWithMapValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Map } @@ -84,6 +85,11 @@ func (a AttributeWithMapValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // MapValidators satisfies the fwxschema.AttributeWithMapValidators interface. func (a AttributeWithMapValidators) MapValidators() []validator.Map { return a.Validators diff --git a/internal/testing/testschema/attributewithnumberdefault.go b/internal/testing/testschema/attributewithnumberdefault.go index 7bca51169..effa79507 100644 --- a/internal/testing/testschema/attributewithnumberdefault.go +++ b/internal/testing/testschema/attributewithnumberdefault.go @@ -22,6 +22,7 @@ type AttributeWithNumberDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Number } @@ -85,3 +86,8 @@ func (a AttributeWithNumberDefaultValue) IsRequired() bool { func (a AttributeWithNumberDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithnumberplanmodifiers.go b/internal/testing/testschema/attributewithnumberplanmodifiers.go index 7073a1e72..cf9299778 100644 --- a/internal/testing/testschema/attributewithnumberplanmodifiers.go +++ b/internal/testing/testschema/attributewithnumberplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithNumberPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Number } @@ -81,6 +82,11 @@ func (a AttributeWithNumberPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // NumberPlanModifiers satisfies the fwxschema.AttributeWithNumberPlanModifiers interface. func (a AttributeWithNumberPlanModifiers) NumberPlanModifiers() []planmodifier.Number { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithnumbervalidators.go b/internal/testing/testschema/attributewithnumbervalidators.go index 0e63b7db3..af1869d38 100644 --- a/internal/testing/testschema/attributewithnumbervalidators.go +++ b/internal/testing/testschema/attributewithnumbervalidators.go @@ -22,6 +22,7 @@ type AttributeWithNumberValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Number } @@ -81,6 +82,11 @@ func (a AttributeWithNumberValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // NumberValidators satisfies the fwxschema.AttributeWithNumberValidators interface. func (a AttributeWithNumberValidators) NumberValidators() []validator.Number { return a.Validators diff --git a/internal/testing/testschema/attributewithobjectdefault.go b/internal/testing/testschema/attributewithobjectdefault.go index 54e0e594e..ed25e0a90 100644 --- a/internal/testing/testschema/attributewithobjectdefault.go +++ b/internal/testing/testschema/attributewithobjectdefault.go @@ -23,6 +23,7 @@ type AttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Object } @@ -88,3 +89,8 @@ func (a AttributeWithObjectDefaultValue) IsRequired() bool { func (a AttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithobjectplanmodifiers.go b/internal/testing/testschema/attributewithobjectplanmodifiers.go index 87f5933e2..e4ec023bd 100644 --- a/internal/testing/testschema/attributewithobjectplanmodifiers.go +++ b/internal/testing/testschema/attributewithobjectplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithObjectPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Object } @@ -84,6 +85,11 @@ func (a AttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a AttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithobjectvalidators.go b/internal/testing/testschema/attributewithobjectvalidators.go index 854be0dbd..534c47cf6 100644 --- a/internal/testing/testschema/attributewithobjectvalidators.go +++ b/internal/testing/testschema/attributewithobjectvalidators.go @@ -23,6 +23,7 @@ type AttributeWithObjectValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Object } @@ -84,6 +85,11 @@ func (a AttributeWithObjectValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectValidators satisfies the fwxschema.AttributeWithObjectValidators interface. func (a AttributeWithObjectValidators) ObjectValidators() []validator.Object { return a.Validators diff --git a/internal/testing/testschema/attributewithsetdefault.go b/internal/testing/testschema/attributewithsetdefault.go index 351ce17b8..f770f956c 100644 --- a/internal/testing/testschema/attributewithsetdefault.go +++ b/internal/testing/testschema/attributewithsetdefault.go @@ -23,6 +23,7 @@ type AttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Set } @@ -88,3 +89,8 @@ func (a AttributeWithSetDefaultValue) IsRequired() bool { func (a AttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithsetplanmodifiers.go b/internal/testing/testschema/attributewithsetplanmodifiers.go index 4efb38ac4..a36f5adc4 100644 --- a/internal/testing/testschema/attributewithsetplanmodifiers.go +++ b/internal/testing/testschema/attributewithsetplanmodifiers.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithSetPlanModifiers = AttributeWithSetPlanModifiers{} @@ -23,6 +24,7 @@ type AttributeWithSetPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Set } @@ -84,6 +86,11 @@ func (a AttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a AttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithsetvalidators.go b/internal/testing/testschema/attributewithsetvalidators.go index 217b11b21..32bc5256f 100644 --- a/internal/testing/testschema/attributewithsetvalidators.go +++ b/internal/testing/testschema/attributewithsetvalidators.go @@ -23,6 +23,7 @@ type AttributeWithSetValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Set } @@ -84,6 +85,11 @@ func (a AttributeWithSetValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // SetValidators satisfies the fwxschema.AttributeWithSetValidators interface. func (a AttributeWithSetValidators) SetValidators() []validator.Set { return a.Validators diff --git a/internal/testing/testschema/attributewithstringdefault.go b/internal/testing/testschema/attributewithstringdefault.go index 88e93f004..01e1e6f4e 100644 --- a/internal/testing/testschema/attributewithstringdefault.go +++ b/internal/testing/testschema/attributewithstringdefault.go @@ -22,6 +22,7 @@ type AttributeWithStringDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.String } @@ -85,3 +86,8 @@ func (a AttributeWithStringDefaultValue) IsRequired() bool { func (a AttributeWithStringDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithstringplanmodifiers.go b/internal/testing/testschema/attributewithstringplanmodifiers.go index cbf324d66..d1b5af8d5 100644 --- a/internal/testing/testschema/attributewithstringplanmodifiers.go +++ b/internal/testing/testschema/attributewithstringplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithStringPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.String } @@ -81,6 +82,11 @@ func (a AttributeWithStringPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // StringPlanModifiers satisfies the fwxschema.AttributeWithStringPlanModifiers interface. func (a AttributeWithStringPlanModifiers) StringPlanModifiers() []planmodifier.String { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithstringvalidators.go b/internal/testing/testschema/attributewithstringvalidators.go index a864dd314..e1ddf7e59 100644 --- a/internal/testing/testschema/attributewithstringvalidators.go +++ b/internal/testing/testschema/attributewithstringvalidators.go @@ -22,6 +22,7 @@ type AttributeWithStringValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.String } @@ -81,6 +82,11 @@ func (a AttributeWithStringValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // StringValidators satisfies the fwxschema.AttributeWithStringValidators interface. func (a AttributeWithStringValidators) StringValidators() []validator.String { return a.Validators diff --git a/internal/testing/testschema/nested_attribute.go b/internal/testing/testschema/nested_attribute.go index f078f7cb9..f37301359 100644 --- a/internal/testing/testschema/nested_attribute.go +++ b/internal/testing/testschema/nested_attribute.go @@ -27,6 +27,7 @@ type NestedAttribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -157,3 +158,8 @@ func (a NestedAttribute) IsRequired() bool { func (a NestedAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/nested_attribute_with_list_default.go b/internal/testing/testschema/nested_attribute_with_list_default.go index 0980629cd..a1b70cd87 100644 --- a/internal/testing/testschema/nested_attribute_with_list_default.go +++ b/internal/testing/testschema/nested_attribute_with_list_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithListDefaultValue) ListDefaultValue() defaults.List { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go index e20d89df8..2904c21ee 100644 --- a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go @@ -27,6 +27,7 @@ type NestedAttributeWithListPlanModifiers struct { PlanModifiers []planmodifier.List Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a NestedAttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_map_default.go b/internal/testing/testschema/nested_attribute_with_map_default.go index 128ae137c..6eac9d3ff 100644 --- a/internal/testing/testschema/nested_attribute_with_map_default.go +++ b/internal/testing/testschema/nested_attribute_with_map_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithMapDefaultValue) MapDefaultValue() defaults.Map { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go index 27ba35b45..35f2a0732 100644 --- a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go @@ -27,6 +27,7 @@ type NestedAttributeWithMapPlanModifiers struct { PlanModifiers []planmodifier.Map Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a NestedAttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_object_default.go b/internal/testing/testschema/nested_attribute_with_object_default.go index 0f6a96489..e8579c2ac 100644 --- a/internal/testing/testschema/nested_attribute_with_object_default.go +++ b/internal/testing/testschema/nested_attribute_with_object_default.go @@ -28,6 +28,7 @@ type NestedAttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -105,6 +106,11 @@ func (a NestedAttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithObjectDefaultValue) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go index 00f303165..2d86af989 100644 --- a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go @@ -26,6 +26,7 @@ type NestedAttributeWithObjectPlanModifiers struct { PlanModifiers []planmodifier.Object Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -99,6 +100,11 @@ func (a NestedAttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a NestedAttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_set_default.go b/internal/testing/testschema/nested_attribute_with_set_default.go index a8f39e9a6..3a80c28a7 100644 --- a/internal/testing/testschema/nested_attribute_with_set_default.go +++ b/internal/testing/testschema/nested_attribute_with_set_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithSetDefaultValue) SetDefaultValue() defaults.Set { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go index 6c988d86a..1d886e70e 100644 --- a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go @@ -27,6 +27,7 @@ type NestedAttributeWithSetPlanModifiers struct { PlanModifiers []planmodifier.Set Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a NestedAttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers From a72206f036c364815227181590ee32308e1bbcc3 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 26 Sep 2024 15:30:37 -0400 Subject: [PATCH 29/55] Update `terraform-plugin-go` dependency --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index cf160b6a4..6f5dd3c4e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240830180900-5eafe6ae6def + github.com/hashicorp/terraform-plugin-go v0.24.1-0.20240926180758-adf4d559d7d4 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -25,10 +25,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/grpc v1.66.2 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 635b63694..60e1046cc 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOs github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240830180900-5eafe6ae6def h1:/RKsl9EoVaSGf4PgyuDEmnPd2f/x2jdntwx+q0kY2xA= -github.com/hashicorp/terraform-plugin-go v0.23.1-0.20240830180900-5eafe6ae6def/go.mod h1:ko0HcPe7AkwMukddKa13Qq5zyvi8V9KYxBZmqU8a9cE= +github.com/hashicorp/terraform-plugin-go v0.24.1-0.20240926180758-adf4d559d7d4 h1:IrFJDqDFuJ7Soy0n/M2oiAn/oH9Cvfi+MMKJh1s2M6I= +github.com/hashicorp/terraform-plugin-go v0.24.1-0.20240926180758-adf4d559d7d4/go.mod h1:zoSM9LyEFI4iVkDeKXgwBHV0uTuIIXydtK1fy9J4wBA= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -46,21 +46,21 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 7328990d93a3c6bfcdd1ba0d23900fb43a1e121a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 26 Sep 2024 15:31:01 -0400 Subject: [PATCH 30/55] Implement write only attributes in the `ephemeral/schema` package --- ephemeral/schema/bool_attribute.go | 8 ++++- ephemeral/schema/bool_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/dynamic_attribute.go | 8 ++++- ephemeral/schema/dynamic_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/float32_attribute.go | 5 +++ ephemeral/schema/float32_attribute_test.go | 28 +++++++++++++++++ ephemeral/schema/float64_attribute.go | 8 ++++- ephemeral/schema/int32_attribute.go | 5 +++ ephemeral/schema/int64_attribute.go | 8 ++++- ephemeral/schema/list_attribute.go | 8 ++++- ephemeral/schema/list_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/list_nested_attribute.go | 8 ++++- .../schema/list_nested_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/map_attribute.go | 8 ++++- ephemeral/schema/map_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/map_nested_attribute.go | 5 +++ ephemeral/schema/map_nested_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/number_attribute.go | 8 ++++- ephemeral/schema/number_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/object_attribute.go | 8 ++++- ephemeral/schema/set_attribute.go | 8 ++++- ephemeral/schema/set_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/set_nested_attribute.go | 5 +++ ephemeral/schema/set_nested_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/single_nested_attribute.go | 5 +++ .../schema/single_nested_attribute_test.go | 31 ++++++++++++++++++- ephemeral/schema/string_attribute.go | 8 ++++- ephemeral/schema/string_attribute_test.go | 31 ++++++++++++++++++- 28 files changed, 460 insertions(+), 22 deletions(-) diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go index b9f6e3820..5671d7ecb 100644 --- a/ephemeral/schema/bool_attribute.go +++ b/ephemeral/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -185,3 +186,8 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go index 077ee1f1f..16c8ad6c6 100644 --- a/ephemeral/schema/bool_attribute_test.go +++ b/ephemeral/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go index 6b1b6c83e..62dc8bfed 100644 --- a/ephemeral/schema/dynamic_attribute.go +++ b/ephemeral/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -182,6 +183,11 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators diff --git a/ephemeral/schema/dynamic_attribute_test.go b/ephemeral/schema/dynamic_attribute_test.go index a718167a6..d4cfeb738 100644 --- a/ephemeral/schema/dynamic_attribute_test.go +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go index 8f3dbdc21..0a6b87868 100644 --- a/ephemeral/schema/float32_attribute.go +++ b/ephemeral/schema/float32_attribute.go @@ -189,3 +189,8 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go index e9e45d785..f542972aa 100644 --- a/ephemeral/schema/float32_attribute_test.go +++ b/ephemeral/schema/float32_attribute_test.go @@ -424,3 +424,31 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go index 1313353ec..a1f80bf47 100644 --- a/ephemeral/schema/float64_attribute.go +++ b/ephemeral/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,8 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go index 89f852e8c..491a5301f 100644 --- a/ephemeral/schema/int32_attribute.go +++ b/ephemeral/schema/int32_attribute.go @@ -189,3 +189,8 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go index ab9d5ca1b..e3b294c1e 100644 --- a/ephemeral/schema/int64_attribute.go +++ b/ephemeral/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,8 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go index 9d502067f..ec51320bb 100644 --- a/ephemeral/schema/list_attribute.go +++ b/ephemeral/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -202,6 +203,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go index 1b22bbbdc..e5d1dde9f 100644 --- a/ephemeral/schema/list_attribute_test.go +++ b/ephemeral/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -521,3 +522,31 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go index b9b70d6fb..ecf53207f 100644 --- a/ephemeral/schema/list_nested_attribute.go +++ b/ephemeral/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -230,6 +231,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go index 3d1ae651d..8e51872a6 100644 --- a/ephemeral/schema/list_nested_attribute_test.go +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go index 516576a4e..353ef7f88 100644 --- a/ephemeral/schema/map_attribute.go +++ b/ephemeral/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -205,6 +206,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go index 94219da83..1774e2f2d 100644 --- a/ephemeral/schema/map_attribute_test.go +++ b/ephemeral/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go index 9729efdc3..fad36ed97 100644 --- a/ephemeral/schema/map_nested_attribute.go +++ b/ephemeral/schema/map_nested_attribute.go @@ -231,6 +231,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go index 0e7986f89..156369dbc 100644 --- a/ephemeral/schema/map_nested_attribute_test.go +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go index ffe4e0839..5c230d631 100644 --- a/ephemeral/schema/number_attribute.go +++ b/ephemeral/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -185,6 +186,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/ephemeral/schema/number_attribute_test.go b/ephemeral/schema/number_attribute_test.go index 7e326b90e..d3636d006 100644 --- a/ephemeral/schema/number_attribute_test.go +++ b/ephemeral/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go index eafa40c6e..965542cf7 100644 --- a/ephemeral/schema/object_attribute.go +++ b/ephemeral/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -204,6 +205,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go index 261b02424..20d426b14 100644 --- a/ephemeral/schema/set_attribute.go +++ b/ephemeral/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -200,6 +201,11 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go index 0b6903829..86249280f 100644 --- a/ephemeral/schema/set_attribute_test.go +++ b/ephemeral/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go index 1b17b8743..c418debaa 100644 --- a/ephemeral/schema/set_nested_attribute.go +++ b/ephemeral/schema/set_nested_attribute.go @@ -226,6 +226,11 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go index 1bd3daa65..bcfa627a4 100644 --- a/ephemeral/schema/set_nested_attribute_test.go +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go index 811e76de4..54d01653d 100644 --- a/ephemeral/schema/single_nested_attribute.go +++ b/ephemeral/schema/single_nested_attribute.go @@ -240,6 +240,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/single_nested_attribute_test.go b/ephemeral/schema/single_nested_attribute_test.go index 5b7209edb..f919f492a 100644 --- a/ephemeral/schema/single_nested_attribute_test.go +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -530,6 +531,34 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go index 0c2dd9aba..eaf58a92e 100644 --- a/ephemeral/schema/string_attribute.go +++ b/ephemeral/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,6 +182,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/ephemeral/schema/string_attribute_test.go b/ephemeral/schema/string_attribute_test.go index 1c95add7d..091ec6f38 100644 --- a/ephemeral/schema/string_attribute_test.go +++ b/ephemeral/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() From aacc3e87cee5f27fa25e8bc01ad34879997dde25 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 26 Sep 2024 15:48:03 -0400 Subject: [PATCH 31/55] Populate writeOnly fields in `internal/toproto5` and `internal/toproto6` --- internal/toproto5/getproviderschema_test.go | 126 ++++++++++++++++++++ internal/toproto5/schema_attribute.go | 4 +- internal/toproto6/getproviderschema_test.go | 126 ++++++++++++++++++++ internal/toproto6/schema_attribute.go | 4 +- 4 files changed, 258 insertions(+), 2 deletions(-) diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 7a6cd4761..b693e2c2d 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -278,6 +278,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "data-source-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "data-source-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1338,6 +1370,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "ephemeral-resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "ephemeral-resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ EphemeralResourceSchemas: map[string]fwschema.Schema{ @@ -2461,6 +2525,35 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "provider-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -4042,6 +4135,39 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.BoolAttribute{ + Computed: true, + WriteOnly: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto5/schema_attribute.go b/internal/toproto5/schema_attribute.go index 74d8fa551..c9bc37e3a 100644 --- a/internal/toproto5/schema_attribute.go +++ b/internal/toproto5/schema_attribute.go @@ -6,9 +6,10 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) // SchemaAttribute returns the *tfprotov5.SchemaAttribute equivalent of an @@ -34,6 +35,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa Computed: a.IsComputed(), Sensitive: a.IsSensitive(), Type: a.GetType().TerraformType(ctx), + WriteOnly: a.IsWriteOnly(), } if a.GetDeprecationMessage() != "" { diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index 2df173187..07fe8bbf8 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -278,6 +278,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "data-source-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "data-source-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1355,6 +1387,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "ephemeral-resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "ephemeral-resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ EphemeralResourceSchemas: map[string]fwschema.Schema{ @@ -2526,6 +2590,35 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "provider-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "provider-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -4183,6 +4276,39 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.BoolAttribute{ + Computed: true, + WriteOnly: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto6/schema_attribute.go b/internal/toproto6/schema_attribute.go index 492a5a2ab..d020a95d2 100644 --- a/internal/toproto6/schema_attribute.go +++ b/internal/toproto6/schema_attribute.go @@ -7,9 +7,10 @@ import ( "context" "sort" - "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) // SchemaAttribute returns the *tfprotov6.SchemaAttribute equivalent of an @@ -27,6 +28,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa Computed: a.IsComputed(), Sensitive: a.IsSensitive(), Type: a.GetType().TerraformType(ctx), + WriteOnly: a.IsWriteOnly(), } if a.GetDeprecationMessage() != "" { From ee78f76f22e208b210dfc485bf94bdbb3fab817c Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 26 Sep 2024 17:11:39 -0400 Subject: [PATCH 32/55] Implement `ValidateResourceConfigClientCapabilities` in the `ValidateResourceConfig` RPC --- internal/fromproto5/client_capabilities.go | 13 +++++++ .../fromproto5/validateresourcetypeconfig.go | 4 +- .../validateresourcetypeconfig_test.go | 38 ++++++++++++++++++- internal/fromproto6/client_capabilities.go | 13 +++++++ internal/fromproto6/validateresourceconfig.go | 4 +- .../fromproto6/validateresourceconfig_test.go | 38 ++++++++++++++++++- .../fwserver/server_validateresourceconfig.go | 5 ++- .../server_validateresourceconfig_test.go | 5 ++- resource/validate_config.go | 16 ++++++++ 9 files changed, 126 insertions(+), 10 deletions(-) diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 3a6347dc4..4dc1b6a85 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -75,3 +75,16 @@ func ImportStateClientCapabilities(in *tfprotov5.ImportResourceStateClientCapabi DeferralAllowed: in.DeferralAllowed, } } + +func ValidateResourceTypeConfigClientCapabilities(in *tfprotov5.ValidateResourceTypeConfigClientCapabilities) resource.ValidateConfigClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/fromproto5/validateresourcetypeconfig.go b/internal/fromproto5/validateresourcetypeconfig.go index ae6e41298..1919f9e9c 100644 --- a/internal/fromproto5/validateresourcetypeconfig.go +++ b/internal/fromproto5/validateresourcetypeconfig.go @@ -6,11 +6,12 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ValidateResourceTypeConfigRequest returns the *fwserver.ValidateResourceConfigRequest @@ -26,6 +27,7 @@ func ValidateResourceTypeConfigRequest(ctx context.Context, proto5 *tfprotov5.Va fw.Config = config fw.Resource = resource + fw.ClientCapabilities = ValidateResourceTypeConfigClientCapabilities(proto5.ClientCapabilities) return fw, diags } diff --git a/internal/fromproto5/validateresourcetypeconfig_test.go b/internal/fromproto5/validateresourcetypeconfig_test.go index 8aec17098..5c07807ed 100644 --- a/internal/fromproto5/validateresourcetypeconfig_test.go +++ b/internal/fromproto5/validateresourcetypeconfig_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValidateResourceTypeConfigRequest(t *testing.T) { @@ -88,6 +89,39 @@ func TestValidateResourceTypeConfigRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov5.ValidateResourceTypeConfigRequest{ + Config: &testProto5DynamicValue, + ClientCapabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + "client-capabilities-not-set": { + input: &tfprotov5.ValidateResourceTypeConfigRequest{ + Config: &testProto5DynamicValue, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index 6742a0303..8d169f222 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -75,3 +75,16 @@ func ImportStateClientCapabilities(in *tfprotov6.ImportResourceStateClientCapabi DeferralAllowed: in.DeferralAllowed, } } + +func ValidateResourceConfigClientCapabilities(in *tfprotov6.ValidateResourceConfigClientCapabilities) resource.ValidateConfigClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/fromproto6/validateresourceconfig.go b/internal/fromproto6/validateresourceconfig.go index 1eb65aa87..f3ea643c9 100644 --- a/internal/fromproto6/validateresourceconfig.go +++ b/internal/fromproto6/validateresourceconfig.go @@ -6,11 +6,12 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // ValidateResourceConfigRequest returns the *fwserver.ValidateResourceConfigRequest @@ -26,6 +27,7 @@ func ValidateResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.Valida fw.Config = config fw.Resource = resource + fw.ClientCapabilities = ValidateResourceConfigClientCapabilities(proto6.ClientCapabilities) return fw, diags } diff --git a/internal/fromproto6/validateresourceconfig_test.go b/internal/fromproto6/validateresourceconfig_test.go index 4e21d432c..f649a0d77 100644 --- a/internal/fromproto6/validateresourceconfig_test.go +++ b/internal/fromproto6/validateresourceconfig_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValidateResourceConfigRequest(t *testing.T) { @@ -88,6 +89,39 @@ func TestValidateResourceConfigRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov6.ValidateResourceConfigRequest{ + Config: &testProto6DynamicValue, + ClientCapabilities: &tfprotov6.ValidateResourceConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + "client-capabilities-not-set": { + input: &tfprotov6.ValidateResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_validateresourceconfig.go b/internal/fwserver/server_validateresourceconfig.go index 79e8ae9b7..5b22b5d5a 100644 --- a/internal/fwserver/server_validateresourceconfig.go +++ b/internal/fwserver/server_validateresourceconfig.go @@ -15,8 +15,9 @@ import ( // ValidateResourceConfigRequest is the framework server request for the // ValidateResourceConfig RPC. type ValidateResourceConfigRequest struct { - Config *tfsdk.Config - Resource resource.Resource + ClientCapabilities resource.ValidateConfigClientCapabilities + Config *tfsdk.Config + Resource resource.Resource } // ValidateResourceConfigResponse is the framework server response for the diff --git a/internal/proto6server/server_validateresourceconfig_test.go b/internal/proto6server/server_validateresourceconfig_test.go index 11e911745..36ea3f91c 100644 --- a/internal/proto6server/server_validateresourceconfig_test.go +++ b/internal/proto6server/server_validateresourceconfig_test.go @@ -8,12 +8,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerValidateResourceConfig(t *testing.T) { diff --git a/resource/validate_config.go b/resource/validate_config.go index 40e1213bf..f35c4aa3f 100644 --- a/resource/validate_config.go +++ b/resource/validate_config.go @@ -8,6 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// ValidateConfigClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the +// ValidateResourceConfig RPC, such as forward-compatible Terraform behavior +// changes. +type ValidateConfigClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + WriteOnlyAttributesAllowed bool +} + // ValidateConfigRequest represents a request to validate the // configuration of a resource. An instance of this request struct is // supplied as an argument to the Resource ValidateConfig receiver method @@ -19,6 +30,11 @@ type ValidateConfigRequest struct { // interpolation or other functionality that would prevent Terraform // from knowing the value at request time. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // the ValidateResourceConfig RPC, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateConfigClientCapabilities } // ValidateConfigResponse represents a response to a From 63adba297e15e2a1c6dee239ac76bc911c2f077a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 26 Sep 2024 17:26:39 -0400 Subject: [PATCH 33/55] Add attribute validation for write only attributes --- internal/fwserver/attribute_validation.go | 21 +- .../fwserver/attribute_validation_test.go | 207 ++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 98b05f0fb..9a41520e4 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -65,6 +65,22 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt return } + if a.IsWriteOnly() && a.IsRequired() && a.IsOptional() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with either Required, or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ) + } + + if a.IsWriteOnly() && a.IsComputed() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ) + } + configData := &fwschemadata.Data{ Description: fwschemadata.DataDescriptionConfiguration, Schema: req.Config.Schema, @@ -97,7 +113,10 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt // until Terraform CLI versions 0.12 through the release containing the // checks are considered end-of-life. // Reference: https://github.com/hashicorp/terraform/issues/30669 - if a.IsRequired() && attributeConfig.IsNull() { + // + // We don't validate Required + WriteOnly attributes here as that is + // done in PlanResourceChange (only on create). + if a.IsRequired() && !a.IsWriteOnly() && attributeConfig.IsNull() { resp.Diagnostics.AddAttributeError( req.AttributePath, "Missing Configuration for Required Attribute", diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 457264e0f..7a7e9b775 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1700,6 +1700,213 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "write-only-attr-with-required": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-required-null-value": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-optional": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Optional: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-computed": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Computed: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-missing-required-and-optional": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "Attribute missing Required, Optional, or Computed definition. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-with-required-and-optional": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + Optional: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with either Required, or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-with-computed-required-and-optional": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with either Required, or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, } for name, tc := range testCases { From 32e1f49735207a359c1b5a3f40077f23b1a9134e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 1 Oct 2024 14:33:39 -0400 Subject: [PATCH 34/55] Initial `RequiredWriteOnlyNilsAttributePaths()` implementation --- .../fwserver/server_planresourcechange.go | 99 ++++ .../server_planresourcechange_test.go | 519 ++++++++++++++++++ 2 files changed, 618 insertions(+) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index d66fc4897..f7d200b6e 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -71,6 +72,34 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange return } + // If the resource is planned for creation, verify that + // WriteOnly + Required attributes have a configuration + // value. + if req.PriorState.Raw.IsNull() && !req.Config.Raw.IsNull() { + var reqWriteOnlyPaths path.Paths + + err := tftypes.Walk(req.Config.Raw, RequiredWriteOnlyNilsAttributePaths(ctx, req.Config.Schema, &reqWriteOnlyPaths)) + if err != nil { + resp.Diagnostics.AddError( + "Error validating plan", + "There was an unexpected error validating the plan. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + for _, p := range reqWriteOnlyPaths { + resp.Diagnostics.AddAttributeError( + p, + "Invalid writeOnly attribute plan", + "Required + WriteOnly attributes must have a non-null configuration value during Create.", + ) + } + + if resp.Diagnostics.HasError() { + return + } + } + if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") @@ -500,6 +529,76 @@ func NormaliseRequiresReplace(ctx context.Context, rs path.Paths) path.Paths { return ret[:j] } +// RequiredWriteOnlyNilsAttributePaths returns a tftypes.Walk() function +// that populates reqWriteOnlyNilsPaths with the paths of Required and WriteOnly +// attributes that have a null value. +func RequiredWriteOnlyNilsAttributePaths(ctx context.Context, schema fwschema.Schema, reqWriteOnlyNilsPaths *path.Paths) func(path *tftypes.AttributePath, value tftypes.Value) (bool, error) { + return func(attrPath *tftypes.AttributePath, value tftypes.Value) (bool, error) { + // we are only modifying attributes, not the entire resource + if len(attrPath.Steps()) < 1 { + return true, nil + } + + ctx = logging.FrameworkWithAttributePath(ctx, attrPath.String()) + + attribute, err := schema.AttributeAtTerraformPath(ctx, attrPath) + + fwPath, diags := fromtftypes.AttributePath(ctx, attrPath, schema) + if diags.HasError() { + for _, diagErr := range diags.Errors() { + logging.FrameworkError(ctx, + "Error converting tftypes.AttributePath to path.Path", + map[string]any{ + logging.KeyError: diagErr.Detail(), + }, + ) + } + fwPath = path.Root("error") + + //return false, fmt.Errorf("couldn't convert tftypes.AttributePath to path.Path") + } + + if err != nil { + if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) { + // append null atomic attributes + if value.IsNull() { + reqWriteOnlyNilsPaths.Append(fwPath) + } + return true, nil + } + + if errors.Is(err, fwschema.ErrPathIsBlock) { + // blocks do not have the write-only field but can contain child write-only attributes + return true, nil + } + + if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) { + if value.IsNull() { + reqWriteOnlyNilsPaths.Append(fwPath) + } + return true, nil + } + + logging.FrameworkError(ctx, "couldn't find attribute in resource schema") + return false, fmt.Errorf("couldn't find attribute in resource schema: %w", err) + } + + if attribute.IsRequired() && attribute.IsWriteOnly() { + if value.IsNull() { + reqWriteOnlyNilsPaths.Append(fwPath) + + // if the value is nil, there is no need to continue walking + return false, nil + } + + // check for any required + writeOnly child values + return true, nil + } + + return false, nil + } +} + // planToState returns a *tfsdk.State with a copied value from a tfsdk.Plan. func planToState(plan tfsdk.Plan) *tfsdk.State { return &tfsdk.State{ diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 66e693935..5d9fd06f8 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "math/big" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -363,6 +364,524 @@ func TestMarkComputedNilsAsUnknown(t *testing.T) { } } +func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { + t.Parallel() + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string-value": schema.StringAttribute{ + Required: true, + }, + "string-nil-optional-writeonly": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-value-optional-writeonly": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-nil-required-writeonly": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-value-required-writeonly": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "list-value": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + }, + "list-nil-optional-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + WriteOnly: true, + }, + "list-value-optional-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + WriteOnly: true, + }, + "list-nil-required-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + WriteOnly: true, + }, + "list-value-required-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + WriteOnly: true, + }, + "list-value-required-writeonly-nested-list": schema.ListAttribute{ + ElementType: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "list-value-required-writeonly-nested-set": schema.ListAttribute{ + ElementType: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "list-value-required-writeonly-nested-map": schema.ListAttribute{ + ElementType: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "list-value-required-writeonly-nested-object": schema.ListAttribute{ + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + }, + Required: true, + WriteOnly: true, + }, + "dynamic-value": schema.DynamicAttribute{ + Required: true, + }, + "dynamic-nil-optional-writeonly": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-optional-writeonly": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-nil-required-writeonly": schema.DynamicAttribute{ + Required: true, + WriteOnly: true, + }, + "dynamic-value-required-writeonly": schema.DynamicAttribute{ + Required: true, + WriteOnly: true, + }, + // non-nil computed values should be left alone + // each element of this dynamic value will be visited, then skipped + "dynamic-value-with-underlying-list-required-writeonly": schema.DynamicAttribute{ + Required: true, + WriteOnly: true, + }, + "object-nil-required-writeonly": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "object-value-required-writeonly": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "object-value-required-writeonly-nested-list": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested-list-nil": types.ListType{ + ElemType: types.StringType, + }, + "nested-list-set": types.ListType{ + ElemType: types.StringType, + }, + }, + Required: true, + WriteOnly: true, + }, + "object-value-required-writeonly-nested-set": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested-set-nil": types.SetType{ + ElemType: types.StringType, + }, + "nested-set-set": types.SetType{ + ElemType: types.StringType, + }, + }, + Required: true, + WriteOnly: true, + }, + "object-value-required-writeonly-nested-map": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested-map-nil": types.MapType{ + ElemType: types.StringType, + }, + "nested-map-set": types.MapType{ + ElemType: types.StringType, + }, + }, + Required: true, + WriteOnly: true, + }, + "object-value-required-writeonly-nested-object": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested-object-nil": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + }, + "nested-object-set": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + }, + }, + Required: true, + WriteOnly: true, + }, + "nested-nil-required-writeonly": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Required: true, + WriteOnly: true, + }, + "nested-value-required-writeonly": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Required: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "block-nil-required-writeonly-attributes": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + "block-value-required-writeonly-attributes": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil-optional-writeonly": tftypes.NewValue(tftypes.String, nil), + "string-value-optional-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil-required-writeonly": tftypes.NewValue(tftypes.String, nil), + "string-value-required-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "list-value": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), + "list-nil-optional-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + "list-value-optional-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), + "list-nil-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + "list-value-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), + "list-value-required-writeonly-nested-list": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello, world"), + tftypes.NewValue(tftypes.String, nil), + }), + }, + ), + "list-value-required-writeonly-nested-set": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello, world"), + tftypes.NewValue(tftypes.String, nil), + }), + }, + ), + "list-value-required-writeonly-nested-map": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + ), + "list-value-required-writeonly-nested-object": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + ), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil-optional-writeonly": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-optional-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil-required-writeonly": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-required-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value-with-underlying-list-required-writeonly": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Bool, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, nil), + }, + ), + "object-nil-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "foo"), + }), + "object-value-required-writeonly-nested-list": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-list-nil": tftypes.List{ElementType: tftypes.String}, + "nested-list-set": tftypes.List{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "nested-list-nil": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, nil), + "nested-list-set": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, "hello, world"), + }), + }), + "object-value-required-writeonly-nested-set": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-set-nil": tftypes.Set{ElementType: tftypes.String}, + "nested-set-set": tftypes.Set{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "nested-set-nil": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, nil), + "nested-set-set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello, world"), + tftypes.NewValue(tftypes.String, nil), + }), + }), + "object-value-required-writeonly-nested-map": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-map-nil": tftypes.Map{ElementType: tftypes.String}, + "nested-map-set": tftypes.Map{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "nested-map-nil": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, nil), + "nested-map-set": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, map[string]tftypes.Value{ + "string-set": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + }), + }), + "object-value-required-writeonly-nested-object": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-object-nil": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + "nested-object-set": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-object-nil": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-object-set": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + ), + "nested-nil-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + "block-nil-required-writeonly-attributes": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-required-writeonly-attributes": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + }), + }) + expected := path.Paths{ + path.Root("block-value-required-writeonly-attributes"). + AtSetValue(types.ObjectValueMust( + map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + map[string]attr.Value{ + "string-nil": types.StringNull(), + "string-set": types.StringValue("bar"), + }, + )). + AtName("string-nil"), + path.Root("dynamic-nil-required-writeonly"), + path.Root("list-nil-required-writeonly"), + path.Root("list-value-required-writeonly-nested-list").AtListIndex(0).AtListIndex(1), + path.Root("list-value-required-writeonly-nested-map").AtListIndex(0).AtMapKey("string-nil"), + path.Root("list-value-required-writeonly-nested-object").AtListIndex(0).AtName("string-nil"), + path.Root("list-value-required-writeonly-nested-set").AtListIndex(0).AtSetValue(types.StringNull()), + path.Root("nested-value-required-writeonly").AtName("string-nil"), + path.Root("object-nil-required-writeonly"), + path.Root("object-value-required-writeonly").AtName("string-nil"), + path.Root("object-value-required-writeonly-nested-list").AtName("nested-list-nil"), + path.Root("object-value-required-writeonly-nested-list").AtName("nested-list-set").AtListIndex(0), + path.Root("object-value-required-writeonly-nested-map").AtName("nested-map-nil"), + path.Root("object-value-required-writeonly-nested-map").AtName("nested-map-set").AtMapKey("string-nil"), + path.Root("object-value-required-writeonly-nested-object").AtName("nested-object-nil"), + path.Root("object-value-required-writeonly-nested-object").AtName("nested-object-set").AtName("string-nil"), + path.Root("object-value-required-writeonly-nested-set").AtName("nested-set-nil"), + path.Root("object-value-required-writeonly-nested-set").AtName("nested-set-set").AtSetValue(types.StringNull()), + path.Root("string-nil-required-writeonly"), + path.Root("nested-nil-required-writeonly"), + } + + var got path.Paths + err := tftypes.Walk(input, fwserver.RequiredWriteOnlyNilsAttributePaths(context.Background(), s, &got)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].String() < got[j].String() + }) + + sort.Slice(expected, func(i, j int) bool { + return expected[i].String() < expected[j].String() + }) + + if diff := cmp.Diff(got, expected, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } +} + func TestNormaliseRequiresReplace(t *testing.T) { t.Parallel() From 9f83fe3b610cdde76db97113f9078caffc6ebef2 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 1 Oct 2024 16:14:50 -0400 Subject: [PATCH 35/55] Complete `RequiredWriteOnlyNilsAttributePaths()` implementation --- .../fwserver/server_planresourcechange.go | 47 ++- .../server_planresourcechange_test.go | 321 +++++------------- 2 files changed, 95 insertions(+), 273 deletions(-) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index f7d200b6e..51484cd3b 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -543,55 +543,46 @@ func RequiredWriteOnlyNilsAttributePaths(ctx context.Context, schema fwschema.Sc attribute, err := schema.AttributeAtTerraformPath(ctx, attrPath) - fwPath, diags := fromtftypes.AttributePath(ctx, attrPath, schema) - if diags.HasError() { - for _, diagErr := range diags.Errors() { - logging.FrameworkError(ctx, - "Error converting tftypes.AttributePath to path.Path", - map[string]any{ - logging.KeyError: diagErr.Detail(), - }, - ) - } - fwPath = path.Root("error") - - //return false, fmt.Errorf("couldn't convert tftypes.AttributePath to path.Path") - } - if err != nil { if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) { - // append null atomic attributes - if value.IsNull() { - reqWriteOnlyNilsPaths.Append(fwPath) - } + // atomic attributes can be nested block objects that contain child writeOnly attributes return true, nil } if errors.Is(err, fwschema.ErrPathIsBlock) { - // blocks do not have the write-only field but can contain child write-only attributes + // blocks do not have the writeOnly field but can contain child writeOnly attributes return true, nil } if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) { - if value.IsNull() { - reqWriteOnlyNilsPaths.Append(fwPath) - } - return true, nil + return false, nil } logging.FrameworkError(ctx, "couldn't find attribute in resource schema") return false, fmt.Errorf("couldn't find attribute in resource schema: %w", err) } - if attribute.IsRequired() && attribute.IsWriteOnly() { - if value.IsNull() { + if attribute.IsWriteOnly() { + if attribute.IsRequired() && value.IsNull() { + fwPath, diags := fromtftypes.AttributePath(ctx, attrPath, schema) + if diags.HasError() { + for _, diagErr := range diags.Errors() { + logging.FrameworkError(ctx, + "Error converting tftypes.AttributePath to path.Path", + map[string]any{ + logging.KeyError: diagErr.Detail(), + }, + ) + } + + return false, fmt.Errorf("couldn't convert tftypes.AttributePath to path.Path") + } reqWriteOnlyNilsPaths.Append(fwPath) // if the value is nil, there is no need to continue walking return false, nil } - - // check for any required + writeOnly child values + // check for any writeOnly child attributes return true, nil } diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 5d9fd06f8..2caa3ec82 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -412,37 +412,6 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { Required: true, WriteOnly: true, }, - "list-value-required-writeonly-nested-list": schema.ListAttribute{ - ElementType: types.ListType{ - ElemType: types.StringType, - }, - Required: true, - WriteOnly: true, - }, - "list-value-required-writeonly-nested-set": schema.ListAttribute{ - ElementType: types.SetType{ - ElemType: types.StringType, - }, - Required: true, - WriteOnly: true, - }, - "list-value-required-writeonly-nested-map": schema.ListAttribute{ - ElementType: types.MapType{ - ElemType: types.StringType, - }, - Required: true, - WriteOnly: true, - }, - "list-value-required-writeonly-nested-object": schema.ListAttribute{ - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string-nil": types.StringType, - "string-set": types.StringType, - }, - }, - Required: true, - WriteOnly: true, - }, "dynamic-value": schema.DynamicAttribute{ Required: true, }, @@ -462,8 +431,7 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { Required: true, WriteOnly: true, }, - // non-nil computed values should be left alone - // each element of this dynamic value will be visited, then skipped + // underlying values of dynamic attributes should be left alone "dynamic-value-with-underlying-list-required-writeonly": schema.DynamicAttribute{ Required: true, WriteOnly: true, @@ -484,61 +452,21 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { Required: true, WriteOnly: true, }, - "object-value-required-writeonly-nested-list": schema.ObjectAttribute{ - AttributeTypes: map[string]attr.Type{ - "nested-list-nil": types.ListType{ - ElemType: types.StringType, - }, - "nested-list-set": types.ListType{ - ElemType: types.StringType, - }, - }, - Required: true, - WriteOnly: true, - }, - "object-value-required-writeonly-nested-set": schema.ObjectAttribute{ - AttributeTypes: map[string]attr.Type{ - "nested-set-nil": types.SetType{ - ElemType: types.StringType, - }, - "nested-set-set": types.SetType{ - ElemType: types.StringType, - }, - }, - Required: true, - WriteOnly: true, - }, - "object-value-required-writeonly-nested-map": schema.ObjectAttribute{ - AttributeTypes: map[string]attr.Type{ - "nested-map-nil": types.MapType{ - ElemType: types.StringType, - }, - "nested-map-set": types.MapType{ - ElemType: types.StringType, - }, - }, - Required: true, - WriteOnly: true, - }, - "object-value-required-writeonly-nested-object": schema.ObjectAttribute{ - AttributeTypes: map[string]attr.Type{ - "nested-object-nil": types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string-nil": types.StringType, - "string-set": types.StringType, - }, + "nested-nil-required-writeonly": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, }, - "nested-object-set": types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string-nil": types.StringType, - "string-set": types.StringType, - }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, }, }, Required: true, WriteOnly: true, }, - "nested-nil-required-writeonly": schema.SingleNestedAttribute{ + "nested-value-required-writeonly": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "string-nil": schema.StringAttribute{ Required: true, @@ -552,7 +480,7 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { Required: true, WriteOnly: true, }, - "nested-value-required-writeonly": schema.SingleNestedAttribute{ + "optional-nested-value-required-writeonly-attributes": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "string-nil": schema.StringAttribute{ Required: true, @@ -563,7 +491,7 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { WriteOnly: true, }, }, - Required: true, + Optional: true, WriteOnly: true, }, }, @@ -608,73 +536,10 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { "list-nil-optional-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), "list-value-optional-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), "list-nil-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), - "list-value-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), - "list-value-required-writeonly-nested-list": tftypes.NewValue( - tftypes.List{ - ElementType: tftypes.List{ - ElementType: tftypes.String, - }, - }, - []tftypes.Value{ - tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "hello, world"), - tftypes.NewValue(tftypes.String, nil), - }), - }, - ), - "list-value-required-writeonly-nested-set": tftypes.NewValue( - tftypes.List{ - ElementType: tftypes.Set{ - ElementType: tftypes.String, - }, - }, - []tftypes.Value{ - tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "hello, world"), - tftypes.NewValue(tftypes.String, nil), - }), - }, - ), - "list-value-required-writeonly-nested-map": tftypes.NewValue( - tftypes.List{ - ElementType: tftypes.Map{ - ElementType: tftypes.String, - }, - }, - []tftypes.Value{ - tftypes.NewValue(tftypes.Map{ - ElementType: tftypes.String, - }, map[string]tftypes.Value{ - "string-nil": tftypes.NewValue(tftypes.String, nil), - "string-set": tftypes.NewValue(tftypes.String, "hello, world"), - }), - }, - ), - "list-value-required-writeonly-nested-object": tftypes.NewValue( - tftypes.List{ - ElementType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string-nil": tftypes.String, - "string-set": tftypes.String, - }, - }, - }, - []tftypes.Value{ - tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string-nil": tftypes.String, - "string-set": tftypes.String, - }, - }, map[string]tftypes.Value{ - "string-nil": tftypes.NewValue(tftypes.String, nil), - "string-set": tftypes.NewValue(tftypes.String, "hello, world"), - }), - }, - ), + "list-value-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello, world"), + tftypes.NewValue(tftypes.String, nil), + }), "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), "dynamic-nil-optional-writeonly": tftypes.NewValue(tftypes.DynamicPseudoType, nil), "dynamic-value-optional-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), @@ -704,87 +569,6 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { "string-nil": tftypes.NewValue(tftypes.String, nil), "string-set": tftypes.NewValue(tftypes.String, "foo"), }), - "object-value-required-writeonly-nested-list": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "nested-list-nil": tftypes.List{ElementType: tftypes.String}, - "nested-list-set": tftypes.List{ElementType: tftypes.String}, - }, - }, map[string]tftypes.Value{ - "nested-list-nil": tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, nil), - "nested-list-set": tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, nil), - tftypes.NewValue(tftypes.String, "hello, world"), - }), - }), - "object-value-required-writeonly-nested-set": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "nested-set-nil": tftypes.Set{ElementType: tftypes.String}, - "nested-set-set": tftypes.Set{ElementType: tftypes.String}, - }, - }, map[string]tftypes.Value{ - "nested-set-nil": tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, nil), - "nested-set-set": tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "hello, world"), - tftypes.NewValue(tftypes.String, nil), - }), - }), - "object-value-required-writeonly-nested-map": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "nested-map-nil": tftypes.Map{ElementType: tftypes.String}, - "nested-map-set": tftypes.Map{ElementType: tftypes.String}, - }, - }, map[string]tftypes.Value{ - "nested-map-nil": tftypes.NewValue(tftypes.Map{ - ElementType: tftypes.String, - }, nil), - "nested-map-set": tftypes.NewValue(tftypes.Map{ - ElementType: tftypes.String, - }, map[string]tftypes.Value{ - "string-set": tftypes.NewValue(tftypes.String, "hello, world"), - "string-nil": tftypes.NewValue(tftypes.String, nil), - }), - }), - "object-value-required-writeonly-nested-object": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "nested-object-nil": tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string-nil": tftypes.String, - "string-set": tftypes.String, - }, - }, - "nested-object-set": tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string-nil": tftypes.String, - "string-set": tftypes.String, - }, - }, - }, - }, map[string]tftypes.Value{ - "nested-object-nil": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string-nil": tftypes.String, - "string-set": tftypes.String, - }, - }, nil), - "nested-object-set": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string-nil": tftypes.String, - "string-set": tftypes.String, - }, - }, map[string]tftypes.Value{ - "string-nil": tftypes.NewValue(tftypes.String, nil), - "string-set": tftypes.NewValue(tftypes.String, "hello, world"), - }), - }, - ), "nested-nil-required-writeonly": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "string-nil": tftypes.String, @@ -800,6 +584,15 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { "string-nil": tftypes.NewValue(tftypes.String, nil), "string-set": tftypes.NewValue(tftypes.String, "bar"), }), + "optional-nested-value-required-writeonly-attributes": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), "block-nil-required-writeonly-attributes": tftypes.NewValue(tftypes.Set{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -842,21 +635,9 @@ func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { AtName("string-nil"), path.Root("dynamic-nil-required-writeonly"), path.Root("list-nil-required-writeonly"), - path.Root("list-value-required-writeonly-nested-list").AtListIndex(0).AtListIndex(1), - path.Root("list-value-required-writeonly-nested-map").AtListIndex(0).AtMapKey("string-nil"), - path.Root("list-value-required-writeonly-nested-object").AtListIndex(0).AtName("string-nil"), - path.Root("list-value-required-writeonly-nested-set").AtListIndex(0).AtSetValue(types.StringNull()), path.Root("nested-value-required-writeonly").AtName("string-nil"), path.Root("object-nil-required-writeonly"), - path.Root("object-value-required-writeonly").AtName("string-nil"), - path.Root("object-value-required-writeonly-nested-list").AtName("nested-list-nil"), - path.Root("object-value-required-writeonly-nested-list").AtName("nested-list-set").AtListIndex(0), - path.Root("object-value-required-writeonly-nested-map").AtName("nested-map-nil"), - path.Root("object-value-required-writeonly-nested-map").AtName("nested-map-set").AtMapKey("string-nil"), - path.Root("object-value-required-writeonly-nested-object").AtName("nested-object-nil"), - path.Root("object-value-required-writeonly-nested-object").AtName("nested-object-set").AtName("string-nil"), - path.Root("object-value-required-writeonly-nested-set").AtName("nested-set-nil"), - path.Root("object-value-required-writeonly-nested-set").AtName("nested-set-set").AtSetValue(types.StringNull()), + path.Root("optional-nested-value-required-writeonly-attributes").AtName("string-nil"), path.Root("string-nil-required-writeonly"), path.Root("nested-nil-required-writeonly"), } @@ -1075,6 +856,13 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional_write_only": tftypes.String, + "test_required_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -1579,6 +1367,19 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_optional_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required_write_only": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + } + testEmptyPlan := &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -3964,6 +3765,36 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-required-write-only-null-diag": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: testEmptyState, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{}, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic(path.Root("test_required_write_only"), + "Invalid writeOnly attribute plan", "Required + WriteOnly attributes must have a non-null configuration value during Create."), + }, + }, + }, "create-resourcewithmodifyplan-attributeplanmodifier-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, From 795ad6487345f7e0dd6c371b2cce0bb36b8cc10b Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 2 Oct 2024 12:04:22 -0400 Subject: [PATCH 36/55] Implement `validator.ValidateSchemaClientCapabilities` --- internal/fwserver/attribute_validation.go | 122 ++++--- .../fwserver/attribute_validation_test.go | 344 ++++++++++++++++++ internal/fwserver/block_validation.go | 41 ++- internal/fwserver/block_validation_test.go | 335 ++++++++++++++++- internal/fwserver/schema_validation.go | 8 + .../fwserver/server_validateresourceconfig.go | 11 +- .../server_validateresourceconfig_test.go | 94 ++++- ...e_only_nested_attribute_validation_test.go | 4 + schema/validator/bool.go | 5 + schema/validator/client_capabilities.go | 17 + schema/validator/dynamic.go | 5 + schema/validator/float32.go | 5 + schema/validator/float64.go | 5 + schema/validator/int32.go | 5 + schema/validator/int64.go | 5 + schema/validator/list.go | 5 + schema/validator/map.go | 5 + schema/validator/number.go | 5 + schema/validator/object.go | 5 + schema/validator/set.go | 5 + schema/validator/string.go | 5 + 21 files changed, 964 insertions(+), 72 deletions(-) create mode 100644 schema/validator/client_capabilities.go diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 9a41520e4..1744c09e1 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -34,6 +34,11 @@ type ValidateAttributeRequest struct { // Config contains the entire configuration of the data source, provider, or resource. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities validator.ValidateSchemaClientCapabilities } // ValidateAttributeResponse represents a response to a @@ -219,10 +224,11 @@ func AttributeValidateBool(ctx context.Context, attribute fwxschema.AttributeWit } validateReq := validator.BoolRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.BoolValidators() { @@ -284,10 +290,11 @@ func AttributeValidateFloat32(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.Float32Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Float32Validators() { @@ -349,10 +356,11 @@ func AttributeValidateFloat64(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.Float64Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Float64Validators() { @@ -414,10 +422,11 @@ func AttributeValidateInt32(ctx context.Context, attribute fwxschema.AttributeWi } validateReq := validator.Int32Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Int32Validators() { @@ -479,10 +488,11 @@ func AttributeValidateInt64(ctx context.Context, attribute fwxschema.AttributeWi } validateReq := validator.Int64Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Int64Validators() { @@ -544,10 +554,11 @@ func AttributeValidateList(ctx context.Context, attribute fwxschema.AttributeWit } validateReq := validator.ListRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.ListValidators() { @@ -609,10 +620,11 @@ func AttributeValidateMap(ctx context.Context, attribute fwxschema.AttributeWith } validateReq := validator.MapRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.MapValidators() { @@ -674,10 +686,11 @@ func AttributeValidateNumber(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.NumberRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.NumberValidators() { @@ -739,10 +752,11 @@ func AttributeValidateObject(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.ObjectValidators() { @@ -804,10 +818,11 @@ func AttributeValidateSet(ctx context.Context, attribute fwxschema.AttributeWith } validateReq := validator.SetRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.SetValidators() { @@ -869,10 +884,11 @@ func AttributeValidateString(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.StringRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.StringValidators() { @@ -934,10 +950,11 @@ func AttributeValidateDynamic(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.DynamicRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.DynamicValidators() { @@ -1163,10 +1180,11 @@ func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttribu } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: object, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, objectValidator := range objectWithValidators.ObjectValidators() { diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 7a7e9b775..c6a1e976f 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1993,6 +1993,32 @@ func TestAttributeValidateBool(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithBoolValidators{ Validators: []validator.Bool{ @@ -2197,6 +2223,32 @@ func TestAttributeValidateFloat32(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Float32Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(0.1), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithFloat32Validators{ Validators: []validator.Float32{ @@ -2401,6 +2453,32 @@ func TestAttributeValidateFloat64(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Float64Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(0.2), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithFloat64Validators{ Validators: []validator.Float64{ @@ -2605,6 +2683,32 @@ func TestAttributeValidateInt32(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Int32Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithInt32Validators{ Validators: []validator.Int32{ @@ -2809,6 +2913,32 @@ func TestAttributeValidateInt64(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Int64Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(2), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithInt64Validators{ Validators: []validator.Int64{ @@ -3015,6 +3145,32 @@ func TestAttributeValidateList(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithListValidators{ + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithListValidators{ ElementType: types.StringType, @@ -3240,6 +3396,35 @@ func TestAttributeValidateMap(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithMapValidators{ + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected MapRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithMapValidators{ ElementType: types.StringType, @@ -3469,6 +3654,32 @@ func TestAttributeValidateNumber(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithNumberValidators{ Validators: []validator.Number{ @@ -3685,6 +3896,35 @@ func TestAttributeValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithObjectValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithObjectValidators{ AttributeTypes: map[string]attr.Type{ @@ -3922,6 +4162,32 @@ func TestAttributeValidateSet(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithSetValidators{ + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithSetValidators{ ElementType: types.StringType, @@ -4139,6 +4405,32 @@ func TestAttributeValidateString(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testVal"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithStringValidators{ Validators: []validator.String{ @@ -4343,6 +4635,32 @@ func TestAttributeValidateDynamic(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithDynamicValidators{ + Validators: []validator.Dynamic{ + testvalidator.Dynamic{ + ValidateDynamicMethod: func(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected DynamicRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.DynamicValue(types.StringValue("test")), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithDynamicValidators{ Validators: []validator.Dynamic{ @@ -4580,6 +4898,32 @@ func TestNestedAttributeObjectValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { object: testschema.NestedAttributeObjectWithValidators{ Validators: []validator.Object{ diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index d65b1b5c7..fcf3898f8 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -76,6 +76,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: value, AttributePath: req.AttributePath.AtListIndex(idx), AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -110,6 +111,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: value, AttributePath: req.AttributePath.AtSetValue(value), AttributePathExpression: req.AttributePathExpression.AtSetValue(value), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -143,6 +145,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: o, AttributePath: req.AttributePath, AttributePathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -203,10 +206,11 @@ func BlockValidateList(ctx context.Context, block fwxschema.BlockWithListValidat } validateReq := validator.ListRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.ListValidators() { @@ -268,10 +272,11 @@ func BlockValidateObject(ctx context.Context, block fwxschema.BlockWithObjectVal } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.ObjectValidators() { @@ -333,10 +338,11 @@ func BlockValidateSet(ctx context.Context, block fwxschema.BlockWithSetValidator } validateReq := validator.SetRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.SetValidators() { @@ -395,10 +401,11 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: object, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, objectValidator := range objectWithValidators.ObjectValidators() { @@ -432,6 +439,7 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject nestedAttrReq := ValidateAttributeRequest{ AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedAttrResp := &ValidateAttributeResponse{} @@ -445,6 +453,7 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject nestedBlockReq := ValidateAttributeRequest{ AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockResp := &ValidateAttributeResponse{} diff --git a/internal/fwserver/block_validation_test.go b/internal/fwserver/block_validation_test.go index 367a0df1c..40726f22f 100644 --- a/internal/fwserver/block_validation_test.go +++ b/internal/fwserver/block_validation_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -20,7 +22,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBlockValidate(t *testing.T) { @@ -746,6 +747,77 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "list-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeList, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "set-no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -874,6 +946,77 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "set-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSet, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "single-no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -976,6 +1119,64 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "single-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, } for name, tc := range testCases { @@ -1085,6 +1286,44 @@ func TestBlockValidateList(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithListValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1402,6 +1641,37 @@ func TestBlockValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithObjectValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1679,6 +1949,44 @@ func TestBlockValidateSet(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithSetValidators{ Attributes: map[string]fwschema.Attribute{ @@ -2069,6 +2377,31 @@ func TestNestedBlockObjectValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { object: testschema.NestedBlockObjectWithValidators{ Validators: []validator.Object{ diff --git a/internal/fwserver/schema_validation.go b/internal/fwserver/schema_validation.go index 32c50f9d4..dedce9d43 100644 --- a/internal/fwserver/schema_validation.go +++ b/internal/fwserver/schema_validation.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -20,6 +21,11 @@ type ValidateSchemaRequest struct { // interpolation or other functionality that would prevent Terraform // from knowing the value at request time. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities validator.ValidateSchemaClientCapabilities } // ValidateSchemaResponse represents a response to a @@ -43,6 +49,7 @@ func SchemaValidate(ctx context.Context, s fwschema.Schema, req ValidateSchemaRe AttributePath: path.Root(name), AttributePathExpression: path.MatchRoot(name), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. @@ -58,6 +65,7 @@ func SchemaValidate(ctx context.Context, s fwschema.Schema, req ValidateSchemaRe AttributePath: path.Root(name), AttributePathExpression: path.MatchRoot(name), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateresourceconfig.go b/internal/fwserver/server_validateresourceconfig.go index 5b22b5d5a..591ce5a2f 100644 --- a/internal/fwserver/server_validateresourceconfig.go +++ b/internal/fwserver/server_validateresourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -52,7 +53,8 @@ func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResour } vdscReq := resource.ValidateConfigRequest{ - Config: *req.Config, + ClientCapabilities: req.ClientCapabilities, + Config: *req.Config, } if resourceWithConfigValidators, ok := req.Resource.(resource.ResourceWithConfigValidators); ok { @@ -97,8 +99,13 @@ func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResour resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: req.ClientCapabilities.WriteOnlyAttributesAllowed, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateresourceconfig_test.go b/internal/fwserver/server_validateresourceconfig_test.go index 859612031..489d6a234 100644 --- a/internal/fwserver/server_validateresourceconfig_test.go +++ b/internal/fwserver/server_validateresourceconfig_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerValidateResourceConfig(t *testing.T) { @@ -47,6 +48,10 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchema, } + testClientCapabilities := resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + } + testSchemaAttributeValidator := schema.Schema{ Attributes: map[string]schema.Attribute{ "test": schema.StringAttribute{ @@ -69,6 +74,28 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchemaAttributeValidator, } + testSchemaAttributeValidatorClientCapabilities := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorClientCapabilities := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorClientCapabilities, + } + testSchemaAttributeValidatorError := schema.Schema{ Attributes: map[string]schema.Attribute{ "test": schema.StringAttribute{ @@ -128,6 +155,21 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-AttributeValidator-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfigAttributeValidatorClientCapabilities, + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorClientCapabilities + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-AttributeValidator-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -185,6 +227,34 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-ResourceWithConfigValidators-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfig, + Resource: &testprovider.ResourceWithConfigValidators{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + &testprovider.ResourceConfigValidator{ + ValidateResourceMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-ResourceWithConfigValidators-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -259,6 +329,28 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-ResourceWithValidateConfig-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfig, + Resource: &testprovider.ResourceWithValidateConfig{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-ResourceWithValidateConfig-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwtype/write_only_nested_attribute_validation_test.go b/internal/fwtype/write_only_nested_attribute_validation_test.go index b72ee60e2..ad271f954 100644 --- a/internal/fwtype/write_only_nested_attribute_validation_test.go +++ b/internal/fwtype/write_only_nested_attribute_validation_test.go @@ -12,6 +12,7 @@ import ( ) func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() tests := map[string]struct { nestedAttr metaschema.NestedAttribute expected bool @@ -785,6 +786,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { } for name, tt := range tests { t.Run(name, func(t *testing.T) { + t.Parallel() if got := fwtype.ContainsAllWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) } @@ -793,6 +795,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { } func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() tests := map[string]struct { nestedAttr metaschema.NestedAttribute expected bool @@ -1438,6 +1441,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { } for name, tt := range tests { t.Run(name, func(t *testing.T) { + t.Parallel() if got := fwtype.ContainsAnyWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) } diff --git a/schema/validator/bool.go b/schema/validator/bool.go index 64115e713..26f6df1a4 100644 --- a/schema/validator/bool.go +++ b/schema/validator/bool.go @@ -35,6 +35,11 @@ type BoolRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Bool + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // BoolResponse is a response to a BoolRequest. diff --git a/schema/validator/client_capabilities.go b/schema/validator/client_capabilities.go new file mode 100644 index 000000000..30770eb3c --- /dev/null +++ b/schema/validator/client_capabilities.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +// ValidateSchemaClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the schema validation +// RPCs, such as forward-compatible Terraform behavior changes. +type ValidateSchemaClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + // + // This client capability is only available for resource schema + // attributes. + WriteOnlyAttributesAllowed bool +} diff --git a/schema/validator/dynamic.go b/schema/validator/dynamic.go index b035175a1..e5d2cb58c 100644 --- a/schema/validator/dynamic.go +++ b/schema/validator/dynamic.go @@ -35,6 +35,11 @@ type DynamicRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Dynamic + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // DynamicResponse is a response to a DynamicRequest. diff --git a/schema/validator/float32.go b/schema/validator/float32.go index c1cd8421d..9f38507b2 100644 --- a/schema/validator/float32.go +++ b/schema/validator/float32.go @@ -35,6 +35,11 @@ type Float32Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Float32 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Float32Response is a response to a Float32Request. diff --git a/schema/validator/float64.go b/schema/validator/float64.go index f09111ac7..7c788d8f3 100644 --- a/schema/validator/float64.go +++ b/schema/validator/float64.go @@ -35,6 +35,11 @@ type Float64Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Float64 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Float64Response is a response to a Float64Request. diff --git a/schema/validator/int32.go b/schema/validator/int32.go index 2cbbc3cc2..d13185226 100644 --- a/schema/validator/int32.go +++ b/schema/validator/int32.go @@ -35,6 +35,11 @@ type Int32Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Int32 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Int32Response is a response to a Int32Request. diff --git a/schema/validator/int64.go b/schema/validator/int64.go index 8e8accdcb..061ab1cf8 100644 --- a/schema/validator/int64.go +++ b/schema/validator/int64.go @@ -35,6 +35,11 @@ type Int64Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Int64 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Int64Response is a response to a Int64Request. diff --git a/schema/validator/list.go b/schema/validator/list.go index e5b6083d8..e2dc5ecd1 100644 --- a/schema/validator/list.go +++ b/schema/validator/list.go @@ -35,6 +35,11 @@ type ListRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.List + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // ListResponse is a response to a ListRequest. diff --git a/schema/validator/map.go b/schema/validator/map.go index 2a41cc7ac..eda09a239 100644 --- a/schema/validator/map.go +++ b/schema/validator/map.go @@ -35,6 +35,11 @@ type MapRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Map + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // MapResponse is a response to a MapRequest. diff --git a/schema/validator/number.go b/schema/validator/number.go index ef7692c20..2bccf9ac6 100644 --- a/schema/validator/number.go +++ b/schema/validator/number.go @@ -35,6 +35,11 @@ type NumberRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Number + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // NumberResponse is a response to a NumberRequest. diff --git a/schema/validator/object.go b/schema/validator/object.go index 88029e0ad..a2d96a24c 100644 --- a/schema/validator/object.go +++ b/schema/validator/object.go @@ -35,6 +35,11 @@ type ObjectRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Object + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // ObjectResponse is a response to a ObjectRequest. diff --git a/schema/validator/set.go b/schema/validator/set.go index ce7cdea34..f3aaf0b0f 100644 --- a/schema/validator/set.go +++ b/schema/validator/set.go @@ -35,6 +35,11 @@ type SetRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Set + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // SetResponse is a response to a SetRequest. diff --git a/schema/validator/string.go b/schema/validator/string.go index b453a7bfc..4427e2691 100644 --- a/schema/validator/string.go +++ b/schema/validator/string.go @@ -35,6 +35,11 @@ type StringRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.String + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // StringResponse is a response to a StringRequest. From 2d73905ca0316db8ef2d8ec86fe622ace16d3d79 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 2 Oct 2024 15:59:56 -0400 Subject: [PATCH 37/55] Implement automatic write-only value nullification during `ApplyResourceState` RPC --- internal/fromproto5/applyresourcechange.go | 2 + .../fromproto5/applyresourcechange_test.go | 24 ++ internal/fromproto5/client_capabilities.go | 14 + internal/fromproto6/applyresourcechange.go | 2 + .../fromproto6/applyresourcechange_test.go | 24 ++ internal/fromproto6/client_capabilities.go | 14 + .../data_nullify_collection_blocks.go | 3 +- .../fwserver/server_applyresourcechange.go | 54 ++-- .../server_applyresourcechange_test.go | 131 ++++++++ internal/fwserver/server_createresource.go | 32 +- .../fwserver/server_createresource_test.go | 60 ++++ internal/fwserver/server_updateresource.go | 34 ++- .../fwserver/server_updateresource_test.go | 60 ++++ internal/fwserver/write_only_nullification.go | 72 +++++ .../fwserver/write_only_nullification_test.go | 286 ++++++++++++++++++ 15 files changed, 770 insertions(+), 42 deletions(-) create mode 100644 internal/fwserver/write_only_nullification.go create mode 100644 internal/fwserver/write_only_nullification_test.go diff --git a/internal/fromproto5/applyresourcechange.go b/internal/fromproto5/applyresourcechange.go index 08d04d4ac..c0404ce61 100644 --- a/internal/fromproto5/applyresourcechange.go +++ b/internal/fromproto5/applyresourcechange.go @@ -43,6 +43,8 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso Resource: resource, } + fw.ClientCapabilities = ApplyResourceChangeClientCapabilities(proto5.ClientCapabilities) + config, configDiags := Config(ctx, proto5.Config, resourceSchema) diags.Append(configDiags...) diff --git a/internal/fromproto5/applyresourcechange_test.go b/internal/fromproto5/applyresourcechange_test.go index 6aca21466..dab76e781 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_test.go @@ -81,6 +81,30 @@ func TestApplyResourceChangeRequest(t *testing.T) { ), }, }, + "client-capabilities": { + input: &tfprotov5.ApplyResourceChangeRequest{ + ClientCapabilities: &tfprotov5.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + ResourceSchema: testFwSchema, + }, + }, + "client-capabilities-unset": { + input: &tfprotov5.ApplyResourceChangeRequest{}, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + ResourceSchema: testFwSchema, + }, + }, "config-missing-schema": { input: &tfprotov5.ApplyResourceChangeRequest{ Config: &testProto5DynamicValue, diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 4dc1b6a85..70a82aa27 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -7,10 +7,24 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) +func ApplyResourceChangeClientCapabilities(in *tfprotov5.ApplyResourceChangeClientCapabilities) fwserver.ApplyResourceChangeClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} + func ConfigureProviderClientCapabilities(in *tfprotov5.ConfigureProviderClientCapabilities) provider.ConfigureProviderClientCapabilities { if in == nil { // Client did not indicate any supported capabilities diff --git a/internal/fromproto6/applyresourcechange.go b/internal/fromproto6/applyresourcechange.go index f48eb856b..1f8d1e168 100644 --- a/internal/fromproto6/applyresourcechange.go +++ b/internal/fromproto6/applyresourcechange.go @@ -43,6 +43,8 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso Resource: resource, } + fw.ClientCapabilities = ApplyResourceChangeClientCapabilities(proto6.ClientCapabilities) + config, configDiags := Config(ctx, proto6.Config, resourceSchema) diags.Append(configDiags...) diff --git a/internal/fromproto6/applyresourcechange_test.go b/internal/fromproto6/applyresourcechange_test.go index fccbf619a..b1034ad43 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_test.go @@ -81,6 +81,30 @@ func TestApplyResourceChangeRequest(t *testing.T) { ), }, }, + "client-capabilities": { + input: &tfprotov6.ApplyResourceChangeRequest{ + ClientCapabilities: &tfprotov6.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + ResourceSchema: testFwSchema, + }, + }, + "client-capabilities-unset": { + input: &tfprotov6.ApplyResourceChangeRequest{}, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + ResourceSchema: testFwSchema, + }, + }, "config-missing-schema": { input: &tfprotov6.ApplyResourceChangeRequest{ Config: &testProto6DynamicValue, diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index 8d169f222..b2f0fac66 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -7,10 +7,24 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) +func ApplyResourceChangeClientCapabilities(in *tfprotov6.ApplyResourceChangeClientCapabilities) fwserver.ApplyResourceChangeClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} + func ConfigureProviderClientCapabilities(in *tfprotov6.ConfigureProviderClientCapabilities) provider.ConfigureProviderClientCapabilities { if in == nil { // Client did not indicate any supported capabilities diff --git a/internal/fwschemadata/data_nullify_collection_blocks.go b/internal/fwschemadata/data_nullify_collection_blocks.go index f907d6d16..a0a19ecd4 100644 --- a/internal/fwschemadata/data_nullify_collection_blocks.go +++ b/internal/fwschemadata/data_nullify_collection_blocks.go @@ -7,11 +7,12 @@ import ( "context" "errors" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // NullifyCollectionBlocks converts list and set block empty values to null diff --git a/internal/fwserver/server_applyresourcechange.go b/internal/fwserver/server_applyresourcechange.go index a11a72e47..964a2d23f 100644 --- a/internal/fwserver/server_applyresourcechange.go +++ b/internal/fwserver/server_applyresourcechange.go @@ -14,16 +14,28 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// ApplyResourceChangeClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the +// ApplyResourceChange RPC, such as forward-compatible Terraform behavior +// changes. +type ApplyResourceChangeClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + WriteOnlyAttributesAllowed bool +} + // ApplyResourceChangeRequest is the framework server request for the // ApplyResourceChange RPC. type ApplyResourceChangeRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + ClientCapabilities ApplyResourceChangeClientCapabilities + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // ApplyResourceChangeResponse is the framework server response for the @@ -45,12 +57,13 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange received no PriorState, running CreateResource") createReq := &CreateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + Resource: req.Resource, } createResp := &CreateResourceResponse{} @@ -89,13 +102,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange running UpdateResource") updateReq := &UpdateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - PriorState: req.PriorState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PriorState: req.PriorState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + Resource: req.Resource, } updateResp := &UpdateResourceResponse{} diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 59e10d76c..ae9eaf551 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -59,6 +59,31 @@ func TestServerApplyResourceChange(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional_write_only": tftypes.String, + "test_required_write_only": tftypes.String, + }, + } + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_optional_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required_write_only": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + } + + type testSchemaDataWriteOnly struct { + TestOptionalWriteOnly types.String `tfsdk:"test_optional_write_only"` + TestRequiredWriteOnly types.String `tfsdk:"test_required_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -398,6 +423,56 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "create-response-newstate-write-only": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: testEmptyState, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "create-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1259,6 +1334,62 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "old-optional-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "update-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 30c491690..147cb7d98 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -20,12 +20,13 @@ import ( // CreateResourceRequest is the framework server request for a create request // with the ApplyResourceChange RPC. type CreateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + ClientCapabilities ApplyResourceChangeClientCapabilities + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // CreateResourceResponse is the framework server response for a create request @@ -156,11 +157,22 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + if req.ClientCapabilities.WriteOnlyAttributesAllowed { + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error modifying state", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState + } } diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index d3e83c31b..df2428ecf 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -33,6 +33,13 @@ func TestServerCreateResource(t *testing.T) { }, } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -76,6 +83,18 @@ func TestServerCreateResource(t *testing.T) { }, } + testSchemaWithWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -91,6 +110,11 @@ func TestServerCreateResource(t *testing.T) { TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` } + type testSchemaDataWriteOnly struct { + TestRequired types.String `tfsdk:"test_required"` + TestWriteOnly types.String `tfsdk:"test_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -506,6 +530,42 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWithWriteOnly, + }, + ResourceSchema: testSchemaWithWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWithWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 9112c35c2..b3575bc12 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -20,13 +20,14 @@ import ( // UpdateResourceRequest is the framework server request for an update request // with the ApplyResourceChange RPC. type UpdateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + ClientCapabilities ApplyResourceChangeClientCapabilities + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // UpdateResourceResponse is the framework server response for an update request @@ -169,11 +170,22 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + if req.ClientCapabilities.WriteOnlyAttributesAllowed { + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error modifying state", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState + } } diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index 6ad7a38d6..9e9ead941 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -34,6 +34,13 @@ func TestServerUpdateResource(t *testing.T) { }, } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -77,6 +84,18 @@ func TestServerUpdateResource(t *testing.T) { }, } + testSchemaWithWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -87,6 +106,11 @@ func TestServerUpdateResource(t *testing.T) { TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` } + type testSchemaDataWriteOnly struct { + TestRequired types.String `tfsdk:"test_required"` + TestWriteOnly types.String `tfsdk:"test_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -777,6 +801,42 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWithWriteOnly, + }, + ResourceSchema: testSchemaWithWriteOnly, + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWithWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/write_only_nullification.go b/internal/fwserver/write_only_nullification.go new file mode 100644 index 000000000..e01dd2822 --- /dev/null +++ b/internal/fwserver/write_only_nullification.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func NullifyWriteOnlyAttributes(ctx context.Context, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) { + return func(path *tftypes.AttributePath, val tftypes.Value) (tftypes.Value, error) { + ctx = logging.FrameworkWithAttributePath(ctx, path.String()) + + // we are only modifying attributes, not the entire resource + if len(path.Steps()) < 1 { + return val, nil + } + + attribute, err := resourceSchema.AttributeAtTerraformPath(ctx, path) + + if err != nil { + if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) { + // ignore attributes/elements inside schema.Attributes, they have no schema of their own + logging.FrameworkTrace(ctx, "attribute is a non-schema attribute, not nullifying") + return val, nil + } + + if errors.Is(err, fwschema.ErrPathIsBlock) { + // ignore blocks, they do not have a writeOnly field + logging.FrameworkTrace(ctx, "attribute is a block, not nullifying") + return val, nil + } + + if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) { + // ignore attributes/elements inside schema.DynamicAttribute, they have no schema of their own + logging.FrameworkTrace(ctx, "attribute is inside of a dynamic attribute, not nullifying") + return val, nil + } + + logging.FrameworkError(ctx, "couldn't find attribute in resource schema") + + return tftypes.Value{}, fmt.Errorf("couldn't find attribute in resource schema: %w", err) + } + + // Value type from new state to create null with + newValueType := val.Type() + + // If the attribute is dynamic set the new value type to DynamicPseudoType + // instead of the underlying concrete type + // TODO: verify if this is the correct behavior once Terraform Core implementation is complete + _, isDynamic := attribute.GetType().(basetypes.DynamicTypable) + if isDynamic { + newValueType = tftypes.DynamicPseudoType + } + + if attribute.IsWriteOnly() && !val.IsNull() { + logging.FrameworkDebug(ctx, "Nullifying write-only attribute in the newState") + + return tftypes.NewValue(newValueType, nil), nil + } + + return val, nil + } +} diff --git a/internal/fwserver/write_only_nullification_test.go b/internal/fwserver/write_only_nullification_test.go new file mode 100644 index 000000000..ee0c7e66e --- /dev/null +++ b/internal/fwserver/write_only_nullification_test.go @@ -0,0 +1,286 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNullifyWriteOnlyAttributes(t *testing.T) { + t.Parallel() + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string-value": schema.StringAttribute{ + Required: true, + }, + "string-nil": schema.StringAttribute{ + Optional: true, + }, + "string-nil-write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-value-write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value": schema.DynamicAttribute{ + Required: true, + }, + "dynamic-nil": schema.DynamicAttribute{ + Optional: true, + }, + "dynamic-underlying-string-nil-computed": schema.DynamicAttribute{ + WriteOnly: true, + }, + "dynamic-nil-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-with-underlying-list-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "object-nil-write-only": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Optional: true, + WriteOnly: true, + }, + "object-value-write-only": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Optional: true, + WriteOnly: true, + }, + "nested-nil-write-only": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + "nested-value-write-only": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "block-nil-write-only": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "block-value-write-only": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-nil-write-only": tftypes.NewValue(tftypes.String, nil), + "string-value-write-only": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-underlying-string-nil-computed": tftypes.NewValue(tftypes.String, nil), + "dynamic-nil-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-write-only": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value-with-underlying-list-write-only": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Bool, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, false), + }, + ), + "object-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "foo"), + }), + "nested-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + "block-nil-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + }), + }) + expected := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-nil-write-only": tftypes.NewValue(tftypes.String, nil), + "string-value-write-only": tftypes.NewValue(tftypes.String, nil), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-underlying-string-nil-computed": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-nil-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-with-underlying-list-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "object-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "block-nil-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, nil), + }), + }), + }) + + got, err := tftypes.Transform(input, NullifyWriteOnlyAttributes(context.Background(), s)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + diff, err := expected.Diff(got) + if err != nil { + t.Errorf("Error diffing values: %s", err) + return + } + for _, valDiff := range diff { + t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) + } +} From 23a025bad206341d64a58397263ac844f6270506 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 3 Oct 2024 13:58:41 -0400 Subject: [PATCH 38/55] Explicitly set `ValidateSchemaClientCapabilities` during `ValidateDataSourceConfig`, `ValidateEphemeralResourceConfig`, and `ValidateProviderResourceConfig` RPCs --- internal/fwserver/server_validatedatasourceconfig.go | 12 +++++++++++- .../server_validateephemeralresourceconfig.go | 12 +++++++++++- internal/fwserver/server_validateproviderconfig.go | 12 +++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/internal/fwserver/server_validatedatasourceconfig.go b/internal/fwserver/server_validatedatasourceconfig.go index 3379b15ad..33653982d 100644 --- a/internal/fwserver/server_validatedatasourceconfig.go +++ b/internal/fwserver/server_validatedatasourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -96,8 +97,17 @@ func (s *Server) ValidateDataSourceConfig(ctx context.Context, req *ValidateData resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateephemeralresourceconfig.go b/internal/fwserver/server_validateephemeralresourceconfig.go index a99a0dbfb..6956af068 100644 --- a/internal/fwserver/server_validateephemeralresourceconfig.go +++ b/internal/fwserver/server_validateephemeralresourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -96,8 +97,17 @@ func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, req *Valid resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateproviderconfig.go b/internal/fwserver/server_validateproviderconfig.go index 588f021c2..0109e6e07 100644 --- a/internal/fwserver/server_validateproviderconfig.go +++ b/internal/fwserver/server_validateproviderconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -77,8 +78,17 @@ func (s *Server) ValidateProviderConfig(ctx context.Context, req *ValidateProvid resp.Diagnostics.Append(vpcRes.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. From b1989078a9e3de9bdbcad06b34abf6e2d9d0c8d1 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 3 Dec 2024 16:29:39 -0500 Subject: [PATCH 39/55] Nullify write-only attributes during Plan and Apply regardless of client capability --- internal/fwserver/server_createresource.go | 21 +++++++++---------- .../fwserver/server_createresource_test.go | 3 --- .../fwserver/server_planresourcechange.go | 14 ++++++++++++- internal/fwserver/server_updateresource.go | 21 +++++++++---------- .../fwserver/server_updateresource_test.go | 3 --- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 147cb7d98..1b98808e0 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -163,16 +163,15 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - if req.ClientCapabilities.WriteOnlyAttributesAllowed { - modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) - if err != nil { - resp.Diagnostics.AddError( - "Error modifying state", - "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), - ) - return - } - - resp.NewState.Raw = modifiedState + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return } + + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index df2428ecf..988a268c9 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -535,9 +535,6 @@ func TestServerCreateResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.CreateResourceRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, PlannedState: &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 51484cd3b..22b942ae9 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -81,7 +81,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange err := tftypes.Walk(req.Config.Raw, RequiredWriteOnlyNilsAttributePaths(ctx, req.Config.Schema, &reqWriteOnlyPaths)) if err != nil { resp.Diagnostics.AddError( - "Error validating plan", + "Error Validating Plan", "There was an unexpected error validating the plan. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), ) return @@ -367,6 +367,18 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange "Ensure all resource plan modifiers do not attempt to change resource plan data from being a null value if the request plan is a null value.", ) } + + // Set any write-only attributes in the plan to null + modifiedPlan, err := tftypes.Transform(resp.PlannedState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Planned State", + "There was an unexpected error modifying the PlannedState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + resp.PlannedState.Raw = modifiedPlan } func MarkComputedNilsAsUnknown(ctx context.Context, config tftypes.Value, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) { diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index b3575bc12..382cbbba9 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -176,16 +176,15 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - if req.ClientCapabilities.WriteOnlyAttributesAllowed { - modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) - if err != nil { - resp.Diagnostics.AddError( - "Error modifying state", - "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), - ) - return - } - - resp.NewState.Raw = modifiedState + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return } + + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index 9e9ead941..5c0bb5c7d 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -806,9 +806,6 @@ func TestServerUpdateResource(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.UpdateResourceRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, PlannedState: &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), From f4f97bdd3a08a5e9b831bf245d436e429ba47699 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 17 Dec 2024 13:36:56 -0500 Subject: [PATCH 40/55] remove apply client capability --- go.mod | 12 ++--- go.sum | 24 ++++----- internal/fromproto5/applyresourcechange.go | 2 - .../fromproto5/applyresourcechange_test.go | 24 --------- internal/fromproto5/client_capabilities.go | 14 ----- internal/fromproto6/applyresourcechange.go | 2 - .../fromproto6/applyresourcechange_test.go | 24 --------- internal/fromproto6/client_capabilities.go | 14 ----- .../fwserver/server_applyresourcechange.go | 54 +++++++------------ .../server_applyresourcechange_test.go | 6 --- internal/fwserver/server_createresource.go | 13 +++-- internal/fwserver/server_updateresource.go | 15 +++--- 12 files changed, 51 insertions(+), 153 deletions(-) diff --git a/go.mod b/go.mod index a170a6bb6..b01b68dda 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0 + github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -25,10 +25,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/grpc v1.67.1 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/go.sum b/go.sum index 878f4a234..856dce30d 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8Ei github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0 h1:92SnqDgZi6TgHrJlbP5UicGWgoZO+QQUrZyzwW2Ztqs= -github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0/go.mod h1:f8P2pHGkZrtdKLpCI2qIvrewUY+c4nTvtayqjJR9IcY= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa h1:GOXZVYZrfDrWxZMHdSNqZKDwayH8WBBtyOLx25ekwv8= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa/go.mod h1:OKJU8uauqiLVRWjlFB0KIgK++baq26qfvOU1IVycx9k= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -48,22 +48,22 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/fromproto5/applyresourcechange.go b/internal/fromproto5/applyresourcechange.go index c0404ce61..08d04d4ac 100644 --- a/internal/fromproto5/applyresourcechange.go +++ b/internal/fromproto5/applyresourcechange.go @@ -43,8 +43,6 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso Resource: resource, } - fw.ClientCapabilities = ApplyResourceChangeClientCapabilities(proto5.ClientCapabilities) - config, configDiags := Config(ctx, proto5.Config, resourceSchema) diags.Append(configDiags...) diff --git a/internal/fromproto5/applyresourcechange_test.go b/internal/fromproto5/applyresourcechange_test.go index dab76e781..6aca21466 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_test.go @@ -81,30 +81,6 @@ func TestApplyResourceChangeRequest(t *testing.T) { ), }, }, - "client-capabilities": { - input: &tfprotov5.ApplyResourceChangeRequest{ - ClientCapabilities: &tfprotov5.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, - }, - resourceSchema: testFwSchema, - expected: &fwserver.ApplyResourceChangeRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, - ResourceSchema: testFwSchema, - }, - }, - "client-capabilities-unset": { - input: &tfprotov5.ApplyResourceChangeRequest{}, - resourceSchema: testFwSchema, - expected: &fwserver.ApplyResourceChangeRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: false, - }, - ResourceSchema: testFwSchema, - }, - }, "config-missing-schema": { input: &tfprotov5.ApplyResourceChangeRequest{ Config: &testProto5DynamicValue, diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index e002f1416..737354888 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -8,24 +8,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) -func ApplyResourceChangeClientCapabilities(in *tfprotov5.ApplyResourceChangeClientCapabilities) fwserver.ApplyResourceChangeClientCapabilities { - if in == nil { - // Client did not indicate any supported capabilities - return fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: false, - } - } - - return fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, - } -} - func ConfigureProviderClientCapabilities(in *tfprotov5.ConfigureProviderClientCapabilities) provider.ConfigureProviderClientCapabilities { if in == nil { // Client did not indicate any supported capabilities diff --git a/internal/fromproto6/applyresourcechange.go b/internal/fromproto6/applyresourcechange.go index 1f8d1e168..f48eb856b 100644 --- a/internal/fromproto6/applyresourcechange.go +++ b/internal/fromproto6/applyresourcechange.go @@ -43,8 +43,6 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso Resource: resource, } - fw.ClientCapabilities = ApplyResourceChangeClientCapabilities(proto6.ClientCapabilities) - config, configDiags := Config(ctx, proto6.Config, resourceSchema) diags.Append(configDiags...) diff --git a/internal/fromproto6/applyresourcechange_test.go b/internal/fromproto6/applyresourcechange_test.go index b1034ad43..fccbf619a 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_test.go @@ -81,30 +81,6 @@ func TestApplyResourceChangeRequest(t *testing.T) { ), }, }, - "client-capabilities": { - input: &tfprotov6.ApplyResourceChangeRequest{ - ClientCapabilities: &tfprotov6.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, - }, - resourceSchema: testFwSchema, - expected: &fwserver.ApplyResourceChangeRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, - ResourceSchema: testFwSchema, - }, - }, - "client-capabilities-unset": { - input: &tfprotov6.ApplyResourceChangeRequest{}, - resourceSchema: testFwSchema, - expected: &fwserver.ApplyResourceChangeRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: false, - }, - ResourceSchema: testFwSchema, - }, - }, "config-missing-schema": { input: &tfprotov6.ApplyResourceChangeRequest{ Config: &testProto6DynamicValue, diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index 10f66689c..d22d81623 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -8,24 +8,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) -func ApplyResourceChangeClientCapabilities(in *tfprotov6.ApplyResourceChangeClientCapabilities) fwserver.ApplyResourceChangeClientCapabilities { - if in == nil { - // Client did not indicate any supported capabilities - return fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: false, - } - } - - return fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, - } -} - func ConfigureProviderClientCapabilities(in *tfprotov6.ConfigureProviderClientCapabilities) provider.ConfigureProviderClientCapabilities { if in == nil { // Client did not indicate any supported capabilities diff --git a/internal/fwserver/server_applyresourcechange.go b/internal/fwserver/server_applyresourcechange.go index 964a2d23f..a11a72e47 100644 --- a/internal/fwserver/server_applyresourcechange.go +++ b/internal/fwserver/server_applyresourcechange.go @@ -14,28 +14,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -// ApplyResourceChangeClientCapabilities allows Terraform to publish information -// regarding optionally supported protocol features for the -// ApplyResourceChange RPC, such as forward-compatible Terraform behavior -// changes. -type ApplyResourceChangeClientCapabilities struct { - // WriteOnlyAttributesAllowed indicates that the Terraform client - // initiating the request supports write-only attributes for managed - // resources. - WriteOnlyAttributesAllowed bool -} - // ApplyResourceChangeRequest is the framework server request for the // ApplyResourceChange RPC. type ApplyResourceChangeRequest struct { - ClientCapabilities ApplyResourceChangeClientCapabilities - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // ApplyResourceChangeResponse is the framework server response for the @@ -57,13 +45,12 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange received no PriorState, running CreateResource") createReq := &CreateResourceRequest{ - ClientCapabilities: req.ClientCapabilities, - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + Resource: req.Resource, } createResp := &CreateResourceResponse{} @@ -102,14 +89,13 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange running UpdateResource") updateReq := &UpdateResourceRequest{ - ClientCapabilities: req.ClientCapabilities, - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - PriorState: req.PriorState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PriorState: req.PriorState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + Resource: req.Resource, } updateResp := &UpdateResourceResponse{} diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index ae9eaf551..41ab43c77 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -428,9 +428,6 @@ func TestServerApplyResourceChange(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.ApplyResourceChangeRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, Config: &tfsdk.Config{ Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), @@ -1339,9 +1336,6 @@ func TestServerApplyResourceChange(t *testing.T) { Provider: &testprovider.Provider{}, }, request: &fwserver.ApplyResourceChangeRequest{ - ClientCapabilities: fwserver.ApplyResourceChangeClientCapabilities{ - WriteOnlyAttributesAllowed: true, - }, Config: &tfsdk.Config{ Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 1b98808e0..da211b612 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -20,13 +20,12 @@ import ( // CreateResourceRequest is the framework server request for a create request // with the ApplyResourceChange RPC. type CreateResourceRequest struct { - ClientCapabilities ApplyResourceChangeClientCapabilities - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // CreateResourceResponse is the framework server response for a create request diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 382cbbba9..87e97c0fa 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -20,14 +20,13 @@ import ( // UpdateResourceRequest is the framework server request for an update request // with the ApplyResourceChange RPC. type UpdateResourceRequest struct { - ClientCapabilities ApplyResourceChangeClientCapabilities - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + Resource resource.Resource } // UpdateResourceResponse is the framework server response for an update request From 4b722e03dea2adf660ea346ca505d306d8edf79b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 15:39:36 -0500 Subject: [PATCH 41/55] add validation for older terraform client versions --- internal/fwserver/attribute_validation.go | 13 +++++ .../fwserver/attribute_validation_test.go | 58 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 1744c09e1..66d0a04f5 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -130,6 +130,19 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt ) } + // If the client doesn't support write-only attributes (first supported in Terraform v1.11.0), then we raise an early validation error + // to avoid a confusing data consistency error when the provider attempts to return "null" for a write-only attribute in the planned/final state. + // + // Write-only attributes can only be successfully used with a supporting client, so the only option for a practitoner to utilize a write-only attribute + // is to upgrade their Terraform CLI version to v1.11.0 or later. + if !req.ClientCapabilities.WriteOnlyAttributesAllowed && a.IsWriteOnly() && !attributeConfig.IsNull() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "WriteOnly Attribute Not Allowed", + fmt.Sprintf("The resource contains a non-null value for WriteOnly attribute %s. Write-only attributes are only supported in Terraform 1.11 and later.", req.AttributePath.String()), + ) + } + req.AttributeConfig = attributeConfig switch attributeWithValidators := a.(type) { diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index c6a1e976f..b30847d7c 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1702,6 +1702,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-with-required": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1726,6 +1729,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-with-required-null-value": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1750,6 +1756,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-with-optional": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1774,6 +1783,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-with-computed": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1806,6 +1818,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-missing-required-and-optional": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1837,6 +1852,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-with-required-and-optional": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1870,6 +1888,9 @@ func TestAttributeValidate(t *testing.T) { }, "write-only-attr-with-computed-required-and-optional": { req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, AttributePath: path.Root("test"), Config: tfsdk.Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1907,6 +1928,43 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "write-only-attr-set-no-client-capability": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + // Client indicating it doesn't support write-only attributes + WriteOnlyAttributesAllowed: false, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "hello world!"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Required: true, + WriteOnly: true, + Type: types.StringType, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "WriteOnly Attribute Not Allowed", + "The resource contains a non-null value for WriteOnly attribute test. "+ + "Write-only attributes are only supported in Terraform 1.11 and later.", + ), + }, + }, + }, } for name, tc := range testCases { From 428efefe5acce1a7040c8cd38ef505731619842d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 16:17:25 -0500 Subject: [PATCH 42/55] add client capabilities to nested attribute validation --- internal/fwserver/attribute_validation.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 66d0a04f5..a4731ed13 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -1041,6 +1041,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtListIndex(idx), AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1075,6 +1076,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtSetValue(value), AttributePathExpression: req.AttributePathExpression.AtSetValue(value), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1109,6 +1111,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtMapKey(key), AttributePathExpression: req.AttributePathExpression.AtMapKey(key), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1146,6 +1149,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath, AttributePathExpression: req.AttributePathExpression, Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1232,6 +1236,7 @@ func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttribu AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttrResp := &ValidateAttributeResponse{} From f2c04b22a569f57af657d6cb0222360e2ebd1a1c Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 Jan 2025 14:26:30 -0500 Subject: [PATCH 43/55] Update wording of `IsWriteOnly` comment for `ephemeral/schema`, `provider/schema`, and `provider/metaschema` --- ephemeral/schema/bool_attribute.go | 3 ++- ephemeral/schema/dynamic_attribute.go | 6 ++++-- ephemeral/schema/float32_attribute.go | 3 ++- ephemeral/schema/float64_attribute.go | 6 ++++-- ephemeral/schema/int32_attribute.go | 3 ++- ephemeral/schema/int64_attribute.go | 6 ++++-- ephemeral/schema/list_attribute.go | 6 ++++-- ephemeral/schema/list_nested_attribute.go | 6 ++++-- ephemeral/schema/map_attribute.go | 6 ++++-- ephemeral/schema/map_nested_attribute.go | 3 ++- ephemeral/schema/number_attribute.go | 6 ++++-- ephemeral/schema/object_attribute.go | 3 ++- ephemeral/schema/set_attribute.go | 6 ++++-- ephemeral/schema/set_nested_attribute.go | 3 ++- ephemeral/schema/single_nested_attribute.go | 3 ++- ephemeral/schema/string_attribute.go | 6 ++++-- provider/metaschema/bool_attribute.go | 3 ++- provider/metaschema/float64_attribute.go | 3 ++- provider/metaschema/int64_attribute.go | 3 ++- provider/metaschema/list_attribute.go | 3 ++- provider/metaschema/list_nested_attribute.go | 3 ++- provider/metaschema/map_attribute.go | 3 ++- provider/metaschema/map_nested_attribute.go | 3 ++- provider/metaschema/number_attribute.go | 3 ++- provider/metaschema/object_attribute.go | 3 ++- provider/metaschema/set_attribute.go | 3 ++- provider/metaschema/set_nested_attribute.go | 3 ++- provider/metaschema/single_nested_attribute.go | 3 ++- provider/metaschema/string_attribute.go | 3 ++- provider/schema/bool_attribute.go | 3 ++- provider/schema/dynamic_attribute.go | 3 ++- provider/schema/float32_attribute.go | 3 ++- provider/schema/float64_attribute.go | 3 ++- provider/schema/int32_attribute.go | 3 ++- provider/schema/int64_attribute.go | 3 ++- provider/schema/list_attribute.go | 3 ++- provider/schema/list_nested_attribute.go | 3 ++- provider/schema/map_attribute.go | 3 ++- provider/schema/map_nested_attribute.go | 3 ++- provider/schema/number_attribute.go | 3 ++- provider/schema/object_attribute.go | 3 ++- provider/schema/set_attribute.go | 3 ++- provider/schema/set_nested_attribute.go | 3 ++- provider/schema/single_nested_attribute.go | 3 ++- provider/schema/string_attribute.go | 3 ++- 45 files changed, 108 insertions(+), 54 deletions(-) diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go index 97df54031..56790dee2 100644 --- a/ephemeral/schema/bool_attribute.go +++ b/ephemeral/schema/bool_attribute.go @@ -185,7 +185,8 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a BoolAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go index ebc7a8287..9cd22e70a 100644 --- a/ephemeral/schema/dynamic_attribute.go +++ b/ephemeral/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -180,7 +181,8 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a DynamicAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go index 6ae287d94..46af5bae2 100644 --- a/ephemeral/schema/float32_attribute.go +++ b/ephemeral/schema/float32_attribute.go @@ -188,7 +188,8 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a Float32Attribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go index c56117e0d..f4699e210 100644 --- a/ephemeral/schema/float64_attribute.go +++ b/ephemeral/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -187,7 +188,8 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a Float64Attribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go index bdeea08a7..76f6e0194 100644 --- a/ephemeral/schema/int32_attribute.go +++ b/ephemeral/schema/int32_attribute.go @@ -188,7 +188,8 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a Int32Attribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go index ca1836520..27cb2dd23 100644 --- a/ephemeral/schema/int64_attribute.go +++ b/ephemeral/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -187,7 +188,8 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a Int64Attribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go index 0e48480e5..258757f99 100644 --- a/ephemeral/schema/list_attribute.go +++ b/ephemeral/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -199,7 +200,8 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a ListAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go index b1984db1a..a4478fb48 100644 --- a/ephemeral/schema/list_nested_attribute.go +++ b/ephemeral/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -228,7 +229,8 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a ListNestedAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go index 838484780..0741747a4 100644 --- a/ephemeral/schema/map_attribute.go +++ b/ephemeral/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -203,7 +204,8 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a MapAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go index 2c20a1300..de057e106 100644 --- a/ephemeral/schema/map_nested_attribute.go +++ b/ephemeral/schema/map_nested_attribute.go @@ -229,7 +229,8 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a MapNestedAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go index c6f22645a..17d557398 100644 --- a/ephemeral/schema/number_attribute.go +++ b/ephemeral/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -183,7 +184,8 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a NumberAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go index 0fd4c5aa7..fa808e4ba 100644 --- a/ephemeral/schema/object_attribute.go +++ b/ephemeral/schema/object_attribute.go @@ -202,7 +202,8 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a ObjectAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go index 062b58f78..7ecd08ffd 100644 --- a/ephemeral/schema/set_attribute.go +++ b/ephemeral/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -198,7 +199,8 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a SetAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go index 271534611..658dc0df7 100644 --- a/ephemeral/schema/set_nested_attribute.go +++ b/ephemeral/schema/set_nested_attribute.go @@ -224,7 +224,8 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a SetNestedAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go index 0dc527ce0..167b8c136 100644 --- a/ephemeral/schema/single_nested_attribute.go +++ b/ephemeral/schema/single_nested_attribute.go @@ -238,7 +238,8 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a SingleNestedAttribute) IsWriteOnly() bool { return false } diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go index a336de56a..3cfefe97c 100644 --- a/ephemeral/schema/string_attribute.go +++ b/ephemeral/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -179,7 +180,8 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in ephemeral resource schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. func (a StringAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 7180fc7bc..374296341 100644 --- a/provider/metaschema/bool_attribute.go +++ b/provider/metaschema/bool_attribute.go @@ -119,7 +119,8 @@ func (a BoolAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a BoolAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index ef37da466..ac2b79b0a 100644 --- a/provider/metaschema/float64_attribute.go +++ b/provider/metaschema/float64_attribute.go @@ -122,7 +122,8 @@ func (a Float64Attribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a Float64Attribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index bd13a67d9..aeccd7030 100644 --- a/provider/metaschema/int64_attribute.go +++ b/provider/metaschema/int64_attribute.go @@ -122,7 +122,8 @@ func (a Int64Attribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a Int64Attribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index cfedf83bd..187d9c47c 100644 --- a/provider/metaschema/list_attribute.go +++ b/provider/metaschema/list_attribute.go @@ -135,7 +135,8 @@ func (a ListAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a ListAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index 2f99b5607..0fa1b8221 100644 --- a/provider/metaschema/list_nested_attribute.go +++ b/provider/metaschema/list_nested_attribute.go @@ -161,7 +161,8 @@ func (a ListNestedAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a ListNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index e7be27532..9103231ff 100644 --- a/provider/metaschema/map_attribute.go +++ b/provider/metaschema/map_attribute.go @@ -138,7 +138,8 @@ func (a MapAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a MapAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index 616064fc0..587c56c0a 100644 --- a/provider/metaschema/map_nested_attribute.go +++ b/provider/metaschema/map_nested_attribute.go @@ -161,7 +161,8 @@ func (a MapNestedAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a MapNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go index 5aea1c285..511e7000a 100644 --- a/provider/metaschema/number_attribute.go +++ b/provider/metaschema/number_attribute.go @@ -123,7 +123,8 @@ func (a NumberAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a NumberAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index da70a516e..aabe40d4c 100644 --- a/provider/metaschema/object_attribute.go +++ b/provider/metaschema/object_attribute.go @@ -137,7 +137,8 @@ func (a ObjectAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a ObjectAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go index f6fc456bf..f7d3e4112 100644 --- a/provider/metaschema/set_attribute.go +++ b/provider/metaschema/set_attribute.go @@ -133,7 +133,8 @@ func (a SetAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a SetAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index 36e5d6141..a3c6fbf9e 100644 --- a/provider/metaschema/set_nested_attribute.go +++ b/provider/metaschema/set_nested_attribute.go @@ -156,7 +156,8 @@ func (a SetNestedAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a SetNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index ff11e783d..160fb1c80 100644 --- a/provider/metaschema/single_nested_attribute.go +++ b/provider/metaschema/single_nested_attribute.go @@ -176,7 +176,8 @@ func (a SingleNestedAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a SingleNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index 49fd9cb64..fe25c5014 100644 --- a/provider/metaschema/string_attribute.go +++ b/provider/metaschema/string_attribute.go @@ -119,7 +119,8 @@ func (a StringAttribute) IsSensitive() bool { return false } -// IsWriteOnly returns false as write-only attributes are not supported in provider meta schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a StringAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index 7244d3691..0502821ca 100644 --- a/provider/schema/bool_attribute.go +++ b/provider/schema/bool_attribute.go @@ -180,7 +180,8 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a BoolAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index 49c50cc31..4b31279f8 100644 --- a/provider/schema/dynamic_attribute.go +++ b/provider/schema/dynamic_attribute.go @@ -177,7 +177,8 @@ func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a DynamicAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index 36209f481..8e62dc96d 100644 --- a/provider/schema/float32_attribute.go +++ b/provider/schema/float32_attribute.go @@ -183,7 +183,8 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a Float32Attribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index ec5e7ece5..f0c0cc008 100644 --- a/provider/schema/float64_attribute.go +++ b/provider/schema/float64_attribute.go @@ -183,7 +183,8 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a Float64Attribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index a7c28d527..1f8c60c39 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -183,7 +183,8 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a Int32Attribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 6a380846e..25243bbf1 100644 --- a/provider/schema/int64_attribute.go +++ b/provider/schema/int64_attribute.go @@ -183,7 +183,8 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a Int64Attribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index 692bdb4d2..b85848b58 100644 --- a/provider/schema/list_attribute.go +++ b/provider/schema/list_attribute.go @@ -196,7 +196,8 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a ListAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 01fc09037..700299c05 100644 --- a/provider/schema/list_nested_attribute.go +++ b/provider/schema/list_nested_attribute.go @@ -224,7 +224,8 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a ListNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/map_attribute.go b/provider/schema/map_attribute.go index 74d3c24e6..82b5a05d7 100644 --- a/provider/schema/map_attribute.go +++ b/provider/schema/map_attribute.go @@ -199,7 +199,8 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a MapAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index 034693317..14fb4092b 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -223,7 +223,8 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a MapNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/number_attribute.go b/provider/schema/number_attribute.go index 03a66ddbd..bb6ffc6d6 100644 --- a/provider/schema/number_attribute.go +++ b/provider/schema/number_attribute.go @@ -179,7 +179,8 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a NumberAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index 5e5c0f64f..c5c81a1ba 100644 --- a/provider/schema/object_attribute.go +++ b/provider/schema/object_attribute.go @@ -198,7 +198,8 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a ObjectAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/set_attribute.go b/provider/schema/set_attribute.go index fbb7be645..eaf73344e 100644 --- a/provider/schema/set_attribute.go +++ b/provider/schema/set_attribute.go @@ -194,7 +194,8 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a SetAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index 4e1b4bfd6..a9dad64a7 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -219,7 +219,8 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a SetNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/single_nested_attribute.go b/provider/schema/single_nested_attribute.go index 74eb9219f..aded0988e 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -233,7 +233,8 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a SingleNestedAttribute) IsWriteOnly() bool { return false } diff --git a/provider/schema/string_attribute.go b/provider/schema/string_attribute.go index e8ce8f099..eda7a02c4 100644 --- a/provider/schema/string_attribute.go +++ b/provider/schema/string_attribute.go @@ -175,7 +175,8 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } -// IsWriteOnly returns false as write-only attributes are not supported in provider schemas. +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. func (a StringAttribute) IsWriteOnly() bool { return false } From da1433ec42a5e9ac444b2d82da63507b6ed0ecf9 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 Jan 2025 14:53:42 -0500 Subject: [PATCH 44/55] Update various comments for wording --- internal/fwschema/attribute.go | 2 ++ resource/schema/bool_attribute.go | 5 +++-- resource/schema/dynamic_attribute.go | 5 +++-- resource/schema/float32_attribute.go | 5 +++-- resource/schema/float64_attribute.go | 5 +++-- resource/schema/int32_attribute.go | 5 +++-- resource/schema/int64_attribute.go | 5 +++-- resource/schema/list_attribute.go | 5 +++-- resource/schema/list_nested_attribute.go | 8 ++++++-- resource/schema/map_attribute.go | 5 +++-- resource/schema/map_nested_attribute.go | 8 ++++++-- resource/schema/number_attribute.go | 5 +++-- resource/schema/object_attribute.go | 5 +++-- resource/schema/set_attribute.go | 5 +++-- resource/schema/set_nested_attribute.go | 8 ++++++-- resource/schema/single_nested_attribute.go | 5 +++-- resource/schema/string_attribute.go | 5 +++-- schema/validator/client_capabilities.go | 4 ++-- 18 files changed, 61 insertions(+), 34 deletions(-) diff --git a/internal/fwschema/attribute.go b/internal/fwschema/attribute.go index f8a27a547..6c440b319 100644 --- a/internal/fwschema/attribute.go +++ b/internal/fwschema/attribute.go @@ -68,6 +68,8 @@ type Attribute interface { // IsWriteOnly should return true if the attribute configuration value is // write-only. This is named differently than WriteOnly to prevent a // conflict with the tfsdk.Attribute field name. + // + // Write-only attributes are a managed-resource schema concept only. IsWriteOnly() bool } diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index e9fc10a21..fa80f565c 100644 --- a/resource/schema/bool_attribute.go +++ b/resource/schema/bool_attribute.go @@ -153,9 +153,10 @@ type BoolAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Bool - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/dynamic_attribute.go b/resource/schema/dynamic_attribute.go index 0ef295a02..e06600ab4 100644 --- a/resource/schema/dynamic_attribute.go +++ b/resource/schema/dynamic_attribute.go @@ -154,9 +154,10 @@ type DynamicAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Dynamic - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go index 836a13310..3064b4ed9 100644 --- a/resource/schema/float32_attribute.go +++ b/resource/schema/float32_attribute.go @@ -156,9 +156,10 @@ type Float32Attribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Float32 - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go index 679031952..205af3f98 100644 --- a/resource/schema/float64_attribute.go +++ b/resource/schema/float64_attribute.go @@ -156,9 +156,10 @@ type Float64Attribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Float64 - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go index e26184be2..d3f97d60b 100644 --- a/resource/schema/int32_attribute.go +++ b/resource/schema/int32_attribute.go @@ -156,9 +156,10 @@ type Int32Attribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Int32 - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go index f0b00a446..c65eb41fa 100644 --- a/resource/schema/int64_attribute.go +++ b/resource/schema/int64_attribute.go @@ -156,9 +156,10 @@ type Int64Attribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Int64 - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index 46fd5d839..9c1536dbe 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -169,9 +169,10 @@ type ListAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.List - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 79f68cd5c..22dd9d86b 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -179,9 +179,13 @@ type ListNestedAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.List - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index 2e55b7a40..f76a295c3 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -172,9 +172,10 @@ type MapAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Map - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index c29f0322d..68d7f1d1f 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -179,9 +179,13 @@ type MapNestedAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Map - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/number_attribute.go b/resource/schema/number_attribute.go index 8252a3ff1..8f367592e 100644 --- a/resource/schema/number_attribute.go +++ b/resource/schema/number_attribute.go @@ -157,9 +157,10 @@ type NumberAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Number - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index b58f7c965..03f35aa00 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -171,9 +171,10 @@ type ObjectAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Object - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index 393318607..843f58fb3 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -167,9 +167,10 @@ type SetAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Set - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index 861f59695..3776433b1 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -174,9 +174,13 @@ type SetNestedAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Set - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index a0acece6f..d05067440 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -169,9 +169,10 @@ type SingleNestedAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.Object - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go index 793414f94..693327016 100644 --- a/resource/schema/string_attribute.go +++ b/resource/schema/string_attribute.go @@ -153,9 +153,10 @@ type StringAttribute struct { // should be avoided and a plan modifier should be used instead. Default defaults.String - // WriteOnly indicates that the practitioner can choose a value for this - // attribute, but Terraform will not store this attribute in state. + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. // // This functionality is only supported in Terraform 1.11 and later. // Practitioners that choose a value for this attribute with older diff --git a/schema/validator/client_capabilities.go b/schema/validator/client_capabilities.go index 30770eb3c..8f0bbe9a0 100644 --- a/schema/validator/client_capabilities.go +++ b/schema/validator/client_capabilities.go @@ -11,7 +11,7 @@ type ValidateSchemaClientCapabilities struct { // initiating the request supports write-only attributes for managed // resources. // - // This client capability is only available for resource schema - // attributes. + // This client capability is only populated during managed resource schema + // validation. WriteOnlyAttributesAllowed bool } From 46205a8bc00e9aa8e74f211f6deb6de77ffade32 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 Jan 2025 15:46:55 -0500 Subject: [PATCH 45/55] Update test cases --- internal/toproto5/getproviderschema_test.go | 4 ++-- internal/toproto6/getproviderschema_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index b693e2c2d..104814f5d 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -4141,7 +4141,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { "test_resource": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ "test_attribute": resourceschema.BoolAttribute{ - Computed: true, + Optional: true, WriteOnly: true, }, }, @@ -4157,7 +4157,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { - Computed: true, + Optional: true, Name: "test_attribute", WriteOnly: true, Type: tftypes.Bool, diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index ca0b6e641..161ae67a3 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -4250,7 +4250,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { "test_resource": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ "test_attribute": resourceschema.BoolAttribute{ - Computed: true, + Optional: true, WriteOnly: true, }, }, @@ -4266,7 +4266,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { - Computed: true, + Optional: true, Name: "test_attribute", WriteOnly: true, Type: tftypes.Bool, From 1ef9cb2a4c4d5ffca3f0670fc5eecae6b04fc5f2 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 Jan 2025 17:02:09 -0500 Subject: [PATCH 46/55] Update wording for `write-only` attribute validation errors --- internal/fwserver/attribute_validation.go | 4 +++- internal/fwserver/attribute_validation_test.go | 9 ++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index a4731ed13..901733364 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -74,8 +74,9 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid Attribute Definition", - "WriteOnly Attributes must be set with either Required, or Optional. This is always a problem with the provider and should be reported to the provider developer.", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", ) + return } if a.IsWriteOnly() && a.IsComputed() { @@ -84,6 +85,7 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt "Invalid Attribute Definition", "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", ) + return } configData := &fwschemadata.Data{ diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index b30847d7c..09c198382 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1881,7 +1881,7 @@ func TestAttributeValidate(t *testing.T) { diag.NewAttributeErrorDiagnostic( path.Root("test"), "Invalid Attribute Definition", - "WriteOnly Attributes must be set with either Required, or Optional. This is always a problem with the provider and should be reported to the provider developer.", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", ), }, }, @@ -1918,12 +1918,7 @@ func TestAttributeValidate(t *testing.T) { diag.NewAttributeErrorDiagnostic( path.Root("test"), "Invalid Attribute Definition", - "WriteOnly Attributes must be set with either Required, or Optional. This is always a problem with the provider and should be reported to the provider developer.", - ), - diag.NewAttributeErrorDiagnostic( - path.Root("test"), - "Invalid Attribute Definition", - "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", ), }, }, From 4aa117c29280f6f687d6beec1a9a6fc2333f28dd Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 14 Jan 2025 17:02:40 -0500 Subject: [PATCH 47/55] Refactor write_only_nested_attribute_validation.go and write_only_nested_attribute_validation_test.go --- .../write_only_nested_attribute_validation.go | 33 +- resource/schema/list_nested_attribute.go | 8 +- resource/schema/map_nested_attribute.go | 8 +- resource/schema/set_nested_attribute.go | 8 +- resource/schema/single_nested_attribute.go | 9 +- ...e_only_nested_attribute_validation_test.go | 413 ++++++------------ 6 files changed, 151 insertions(+), 328 deletions(-) rename internal/{fwtype => fwschema}/write_only_nested_attribute_validation.go (81%) rename {internal/fwtype => resource/schema}/write_only_nested_attribute_validation_test.go (76%) diff --git a/internal/fwtype/write_only_nested_attribute_validation.go b/internal/fwschema/write_only_nested_attribute_validation.go similarity index 81% rename from internal/fwtype/write_only_nested_attribute_validation.go rename to internal/fwschema/write_only_nested_attribute_validation.go index a9454b9b8..4cba87413 100644 --- a/internal/fwtype/write_only_nested_attribute_validation.go +++ b/internal/fwschema/write_only_nested_attribute_validation.go @@ -1,35 +1,31 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package fwtype +package fwschema import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" ) // ContainsAllWriteOnlyChildAttributes will return true if all child attributes for the // given nested attribute have WriteOnly set to true. -func ContainsAllWriteOnlyChildAttributes(nestedAttr metaschema.NestedAttribute) bool { - if !nestedAttr.IsWriteOnly() { - return false - } +func ContainsAllWriteOnlyChildAttributes(nestedAttr NestedAttribute) bool { nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() for _, childAttr := range nestedObjAttrs { - nestedAttribute, ok := childAttr.(metaschema.NestedAttribute) + if !childAttr.IsWriteOnly() { + return false + } + + nestedAttribute, ok := childAttr.(NestedAttribute) if ok { if !ContainsAllWriteOnlyChildAttributes(nestedAttribute) { return false } } - - if !childAttr.IsWriteOnly() { - return false - } } return true @@ -37,23 +33,20 @@ func ContainsAllWriteOnlyChildAttributes(nestedAttr metaschema.NestedAttribute) // ContainsAnyWriteOnlyChildAttributes will return true if any child attribute for the // given nested attribute has WriteOnly set to true. -func ContainsAnyWriteOnlyChildAttributes(nestedAttr metaschema.NestedAttribute) bool { - if nestedAttr.IsWriteOnly() { - return true - } +func ContainsAnyWriteOnlyChildAttributes(nestedAttr NestedAttribute) bool { nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() for _, childAttr := range nestedObjAttrs { - nestedAttribute, ok := childAttr.(metaschema.NestedAttribute) + if childAttr.IsWriteOnly() { + return true + } + + nestedAttribute, ok := childAttr.(NestedAttribute) if ok { if ContainsAnyWriteOnlyChildAttributes(nestedAttribute) { return true } } - - if childAttr.IsWriteOnly() { - return true - } } return false diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 22dd9d86b..ee1845bb5 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -302,12 +302,12 @@ func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fws resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } - if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) } - if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) } if a.ListDefaultValue() != nil { diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index 68d7f1d1f..db868f726 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -302,12 +302,12 @@ func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } - if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) } - if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) } if a.MapDefaultValue() != nil { diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index 3776433b1..56c449307 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -297,12 +297,12 @@ func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } - if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) } - if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) } if a.SetDefaultValue() != nil { diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index d05067440..12cab9800 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" - "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -311,12 +310,12 @@ func (a SingleNestedAttribute) ValidateImplementation(ctx context.Context, req f resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) } - if a.IsWriteOnly() && !fwtype.ContainsAllWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) } - if a.IsComputed() && fwtype.ContainsAnyWriteOnlyChildAttributes(a) { - resp.Diagnostics.Append(fwtype.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) } if a.ObjectDefaultValue() != nil { diff --git a/internal/fwtype/write_only_nested_attribute_validation_test.go b/resource/schema/write_only_nested_attribute_validation_test.go similarity index 76% rename from internal/fwtype/write_only_nested_attribute_validation_test.go rename to resource/schema/write_only_nested_attribute_validation_test.go index ad271f954..acc2ca1de 100644 --- a/internal/fwtype/write_only_nested_attribute_validation_test.go +++ b/resource/schema/write_only_nested_attribute_validation_test.go @@ -1,12 +1,12 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package fwtype_test +package schema_test import ( "testing" - "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" ) @@ -17,13 +17,12 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { nestedAttr metaschema.NestedAttribute expected bool }{ - "empty nested attribute returns false": { + "empty nested attribute returns true": { nestedAttr: schema.ListNestedAttribute{}, - expected: false, + expected: true, }, - "writeOnly list nested attribute with writeOnly child attribute returns true": { + "list nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -34,9 +33,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly list nested attribute with non-writeOnly child attribute returns false": { + "list nested attribute with non-writeOnly child attribute returns false": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -47,7 +45,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly list nested attribute with multiple writeOnly child attributes returns true": { + "list nested attribute with multiple writeOnly child attributes returns true": { nestedAttr: schema.ListNestedAttribute{ WriteOnly: true, NestedObject: schema.NestedAttributeObject{ @@ -63,9 +61,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly list nested attribute with one non-writeOnly child attribute returns false": { + "list nested attribute with one non-writeOnly child attribute returns false": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -79,9 +76,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly list nested attribute with writeOnly child nested attributes returns true": { + "list nested attribute with writeOnly child nested attributes returns true": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "list_nested_attribute": schema.ListNestedAttribute{ @@ -102,12 +98,12 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly list nested attribute with non-writeOnly child nested attribute returns false": { + "list nested attribute with non-writeOnly child nested attribute returns false": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -124,9 +120,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly list nested attribute with one non-writeOnly child nested attribute returns false": { + "list nested attribute with one non-writeOnly child nested attribute returns false": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "list_nested_attribute": schema.ListNestedAttribute{ @@ -143,6 +138,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, }, "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -159,9 +155,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly list nested attribute with one non-writeOnly nested child attribute returns false": { + "list nested attribute with one non-writeOnly nested child attribute returns false": { nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "list_nested_attribute": schema.ListNestedAttribute{ @@ -182,46 +177,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "non-writeOnly list nested attribute with one non-writeOnly child attribute returns false": { - nestedAttr: schema.ListNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: false, - }, - }, - }, - }, - expected: false, - }, - "non-writeOnly list nested attribute with writeOnly child nested attributes returns true": { - nestedAttr: schema.ListNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "list_nested_attribute": schema.ListNestedAttribute{ - WriteOnly: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: true, - }, - }, - }, - }, - }, - }, - }, - expected: false, - }, - "writeOnly set nested attribute with writeOnly child attribute returns true": { + "set nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -232,9 +189,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly set nested attribute with non-writeOnly child attribute returns false": { + "set nested attribute with non-writeOnly child attribute returns false": { nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -245,7 +201,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly set nested attribute with multiple writeOnly child attributes returns true": { + "set nested attribute with multiple writeOnly child attributes returns true": { nestedAttr: schema.SetNestedAttribute{ WriteOnly: true, NestedObject: schema.NestedAttributeObject{ @@ -261,7 +217,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly set nested attribute with one non-writeOnly child attribute returns false": { + "set nested attribute with one non-writeOnly child attribute returns false": { nestedAttr: schema.SetNestedAttribute{ WriteOnly: true, NestedObject: schema.NestedAttributeObject{ @@ -277,9 +233,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly set nested attribute with writeOnly child nested attributes returns true": { + "set nested attribute with writeOnly child nested attributes returns true": { nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "set_nested_attribute": schema.SetNestedAttribute{ @@ -300,12 +255,12 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly set nested attribute with non-writeOnly child nested attribute returns false": { + "set nested attribute with non-writeOnly child nested attribute returns false": { nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -322,9 +277,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly set nested attribute with one non-writeOnly child nested attribute returns false": { + "set nested attribute with one non-writeOnly child nested attribute returns false": { nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "set_nested_attribute": schema.SetNestedAttribute{ @@ -341,6 +295,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, }, "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -357,9 +312,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly set nested attribute with one non-writeOnly nested child attribute returns false": { + "set nested attribute with one non-writeOnly nested child attribute returns false": { nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "set_nested_attribute": schema.SetNestedAttribute{ @@ -380,46 +334,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "non-writeOnly set nested attribute with one non-writeOnly child attribute returns false": { - nestedAttr: schema.SetNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: false, - }, - }, - }, - }, - expected: false, - }, - "non-writeOnly set nested attribute with writeOnly child nested attributes returns true": { - nestedAttr: schema.SetNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "set_nested_attribute": schema.SetNestedAttribute{ - WriteOnly: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: true, - }, - }, - }, - }, - }, - }, - }, - expected: false, - }, - "writeOnly map nested attribute with writeOnly child attribute returns true": { + "map nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -430,9 +346,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly map nested attribute with non-writeOnly child attribute returns false": { + "map nested attribute with non-writeOnly child attribute returns false": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -443,9 +358,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly map nested attribute with multiple writeOnly child attributes returns true": { + "map nested attribute with multiple writeOnly child attributes returns true": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -459,9 +373,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly map nested attribute with one non-writeOnly child attribute returns false": { + "map nested attribute with one non-writeOnly child attribute returns false": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -475,9 +388,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly map nested attribute with writeOnly child nested attributes returns true": { + "map nested attribute with writeOnly child nested attributes returns true": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "map_nested_attribute": schema.MapNestedAttribute{ @@ -498,12 +410,12 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly map nested attribute with non-writeOnly child nested attribute returns false": { + "map nested attribute with non-writeOnly child nested attribute returns false": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -520,9 +432,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly map nested attribute with one non-writeOnly child nested attribute returns false": { + "map nested attribute with one non-writeOnly child nested attribute returns false": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "map_nested_attribute": schema.MapNestedAttribute{ @@ -539,6 +450,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, }, "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -555,9 +467,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly map nested attribute with one non-writeOnly nested child attribute returns false": { + "map nested attribute with one non-writeOnly nested child attribute returns false": { nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "map_nested_attribute": schema.MapNestedAttribute{ @@ -578,46 +489,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "non-writeOnly map nested attribute with one non-writeOnly child attribute returns false": { - nestedAttr: schema.MapNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: false, - }, - }, - }, - }, - expected: false, - }, - "non-writeOnly map nested attribute with writeOnly child nested attributes returns true": { - nestedAttr: schema.MapNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "map_nested_attribute": schema.MapNestedAttribute{ - WriteOnly: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: true, - }, - }, - }, - }, - }, - }, - }, - expected: false, - }, - "writeOnly single nested attribute with writeOnly child attribute returns true": { + "single nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ WriteOnly: true, @@ -626,9 +499,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly single nested attribute with non-writeOnly child attribute returns false": { + "single nested attribute with non-writeOnly child attribute returns false": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ WriteOnly: false, @@ -637,9 +509,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly single nested attribute with multiple writeOnly child attributes returns true": { + "single nested attribute with multiple writeOnly child attributes returns true": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ WriteOnly: true, @@ -651,9 +522,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly single nested attribute with one non-writeOnly child attribute returns false": { + "single nested attribute with one non-writeOnly child attribute returns false": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ WriteOnly: true, @@ -665,9 +535,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly single nested attribute with writeOnly child nested attributes returns true": { + "single nested attribute with writeOnly child nested attributes returns true": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "single_nested_attribute": schema.SingleNestedAttribute{ WriteOnly: true, @@ -684,11 +553,11 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "writeOnly single nested attribute with non-writeOnly child nested attribute returns false": { + "single nested attribute with non-writeOnly child nested attribute returns false": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ WriteOnly: true, @@ -702,9 +571,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly single nested attribute with one non-writeOnly child nested attribute returns false": { + "single nested attribute with one non-writeOnly child nested attribute returns false": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "single_nested_attribute": schema.SingleNestedAttribute{ WriteOnly: true, @@ -718,6 +586,7 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, }, "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -733,9 +602,8 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "writeOnly single nested attribute with one non-writeOnly nested child attribute returns false": { + "single nested attribute with one non-writeOnly nested child attribute returns false": { nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, Attributes: map[string]schema.Attribute{ "single_nested_attribute": schema.SingleNestedAttribute{ WriteOnly: true, @@ -752,42 +620,11 @@ func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { }, expected: false, }, - "non-writeOnly single nested attribute with one non-writeOnly child attribute returns false": { - nestedAttr: schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: false, - }, - }, - }, - expected: false, - }, - "non-writeOnly single nested attribute with writeOnly child nested attributes returns true": { - nestedAttr: schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "single_nested_attribute": schema.SingleNestedAttribute{ - WriteOnly: true, - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - WriteOnly: true, - }, - "float32_attribute": schema.Float32Attribute{ - WriteOnly: true, - }, - }, - }, - }, - }, - expected: false, - }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { t.Parallel() - if got := fwtype.ContainsAllWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + if got := fwschema.ContainsAllWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) } }) @@ -804,19 +641,6 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { nestedAttr: schema.ListNestedAttribute{}, expected: false, }, - "list nested attribute with writeOnly returns true": { - nestedAttr: schema.ListNestedAttribute{ - WriteOnly: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - Computed: true, - }, - }, - }, - }, - expected: true, - }, "list nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ @@ -880,10 +704,12 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -898,13 +724,16 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -923,22 +752,27 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, }, "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -953,6 +787,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -969,19 +804,6 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "set nested attribute with writeOnly returns true": { - nestedAttr: schema.SetNestedAttribute{ - WriteOnly: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - Computed: true, - }, - }, - }, - }, - expected: true, - }, "set nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.SetNestedAttribute{ NestedObject: schema.NestedAttributeObject{ @@ -1045,10 +867,12 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1066,10 +890,12 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1088,22 +914,27 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, }, "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1118,6 +949,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -1134,19 +966,6 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "map nested attribute with writeOnly returns true": { - nestedAttr: schema.MapNestedAttribute{ - WriteOnly: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - Computed: true, - }, - }, - }, - }, - expected: true, - }, "map nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.MapNestedAttribute{ NestedObject: schema.NestedAttributeObject{ @@ -1210,10 +1029,12 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1228,13 +1049,16 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1253,22 +1077,27 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, }, "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1283,6 +1112,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ @@ -1299,17 +1129,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { }, expected: true, }, - "single nested attribute with writeOnly returns true": { - nestedAttr: schema.SingleNestedAttribute{ - WriteOnly: true, - Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ - Computed: true, - }, - }, - }, - expected: true, - }, + "single nested attribute with writeOnly child attribute returns true": { nestedAttr: schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ @@ -1363,10 +1183,12 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { WriteOnly: true, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1378,12 +1200,15 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { nestedAttr: schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1398,21 +1223,26 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { WriteOnly: true, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, "float32_attribute": schema.Float32Attribute{ - Computed: true, + WriteOnly: false, + Computed: true, }, }, }, @@ -1425,6 +1255,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { nestedAttr: schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, Attributes: map[string]schema.Attribute{ "string_attribute": schema.StringAttribute{ WriteOnly: false, @@ -1442,7 +1273,7 @@ func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { t.Parallel() - if got := fwtype.ContainsAnyWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + if got := fwschema.ContainsAnyWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) } }) From 5c9c15cd8fbe28ccd4da08783768c11e818e8cf8 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 15 Jan 2025 16:28:17 -0500 Subject: [PATCH 48/55] Move `Required` + `WriteOnly` validations from `PlanResourceChange` RPC to `ValidateResourceConfig` RPC --- internal/fwserver/attribute_validation.go | 5 +- .../fwserver/attribute_validation_test.go | 11 +++- .../fwserver/server_planresourcechange.go | 28 ----------- .../server_planresourcechange_test.go | 50 ------------------- 4 files changed, 11 insertions(+), 83 deletions(-) diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 901733364..3c2492b14 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -120,10 +120,7 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt // until Terraform CLI versions 0.12 through the release containing the // checks are considered end-of-life. // Reference: https://github.com/hashicorp/terraform/issues/30669 - // - // We don't validate Required + WriteOnly attributes here as that is - // done in PlanResourceChange (only on create). - if a.IsRequired() && !a.IsWriteOnly() && attributeConfig.IsNull() { + if a.IsRequired() && attributeConfig.IsNull() { resp.Diagnostics.AddAttributeError( req.AttributePath, "Missing Configuration for Required Attribute", diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 09c198382..6f0fbc5ab 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1752,7 +1752,16 @@ func TestAttributeValidate(t *testing.T) { }, }, }, - resp: ValidateAttributeResponse{}, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Missing Configuration for Required Attribute", + "Must set a configuration value for the test attribute as the provider has marked it as required.\n\n"+ + "Refer to the provider documentation or contact the provider developers for additional information about configurable attributes that are required.", + ), + }, + }, }, "write-only-attr-with-optional": { req: ValidateAttributeRequest{ diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 22b942ae9..ecc23d8ef 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -72,34 +72,6 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange return } - // If the resource is planned for creation, verify that - // WriteOnly + Required attributes have a configuration - // value. - if req.PriorState.Raw.IsNull() && !req.Config.Raw.IsNull() { - var reqWriteOnlyPaths path.Paths - - err := tftypes.Walk(req.Config.Raw, RequiredWriteOnlyNilsAttributePaths(ctx, req.Config.Schema, &reqWriteOnlyPaths)) - if err != nil { - resp.Diagnostics.AddError( - "Error Validating Plan", - "There was an unexpected error validating the plan. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), - ) - return - } - - for _, p := range reqWriteOnlyPaths { - resp.Diagnostics.AddAttributeError( - p, - "Invalid writeOnly attribute plan", - "Required + WriteOnly attributes must have a non-null configuration value during Create.", - ) - } - - if resp.Diagnostics.HasError() { - return - } - } - if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 2caa3ec82..4e22b36c6 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -856,13 +856,6 @@ func TestServerPlanResourceChange(t *testing.T) { }, } - testSchemaTypeWriteOnly := tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_optional_write_only": tftypes.String, - "test_required_write_only": tftypes.String, - }, - } - testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -1367,19 +1360,6 @@ func TestServerPlanResourceChange(t *testing.T) { }, } - testSchemaWriteOnly := schema.Schema{ - Attributes: map[string]schema.Attribute{ - "test_optional_write_only": schema.StringAttribute{ - Optional: true, - WriteOnly: true, - }, - "test_required_write_only": schema.StringAttribute{ - Required: true, - WriteOnly: true, - }, - }, - } - testEmptyPlan := &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -3765,36 +3745,6 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, - "create-required-write-only-null-diag": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.PlanResourceChangeRequest{ - Config: &tfsdk.Config{ - Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ - "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), - "test_required_write_only": tftypes.NewValue(tftypes.String, nil), - }), - Schema: testSchemaWriteOnly, - }, - ProposedNewState: &tfsdk.Plan{ - Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ - "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), - "test_required_write_only": tftypes.NewValue(tftypes.String, nil), - }), - Schema: testSchemaWriteOnly, - }, - PriorState: testEmptyState, - ResourceSchema: testSchemaWriteOnly, - Resource: &testprovider.Resource{}, - }, - expectedResponse: &fwserver.PlanResourceChangeResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic(path.Root("test_required_write_only"), - "Invalid writeOnly attribute plan", "Required + WriteOnly attributes must have a non-null configuration value during Create."), - }, - }, - }, "create-resourcewithmodifyplan-attributeplanmodifier-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, From f73d27efdf5f3177862bc5914e94aee54e38be3e Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 16 Jan 2025 13:40:47 -0500 Subject: [PATCH 49/55] Add write-only value nullification to `ReadResource`, `ImportResourceState`, `UpgradeResourceState`, and `MoveResourceState` RPCs --- internal/fwserver/server_createresource.go | 2 +- .../fwserver/server_importresourcestate.go | 12 ++ .../server_importresourcestate_test.go | 67 ++++++++ internal/fwserver/server_moveresourcestate.go | 12 ++ .../fwserver/server_moveresourcestate_test.go | 57 ++++++- .../fwserver/server_planresourcechange.go | 2 +- internal/fwserver/server_readresource.go | 20 ++- internal/fwserver/server_readresource_test.go | 61 ++++++++ internal/fwserver/server_updateresource.go | 2 +- .../fwserver/server_upgraderesourcestate.go | 34 +++- .../server_upgraderesourcestate_test.go | 148 ++++++++++++++++++ 11 files changed, 408 insertions(+), 9 deletions(-) diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index da211b612..d5a0aef2e 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -163,7 +163,7 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, } // Set any write-only attributes in the state to null - modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) if err != nil { resp.Diagnostics.AddError( "Error Modifying State", diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 7288cc3e7..4abe204cd 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -142,6 +142,18 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } + // Set any write-only attributes in the import state to null + modifiedState, err := tftypes.Transform(importResp.State.Raw, NullifyWriteOnlyAttributes(ctx, importResp.State.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Import State", + "There was an unexpected error modifying the Import State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + importResp.State.Raw = modifiedState + if importResp.State.Raw.Equal(req.EmptyState.Raw) { resp.Diagnostics.AddError( "Missing Resource Import State", diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index bd0275a12..d605ef3d9 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -33,12 +33,26 @@ func TestServerImportResourceState(t *testing.T) { }, } + testTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "write-only": tftypes.String, + "required": tftypes.String, + }, + } + testEmptyStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, nil), "optional": tftypes.NewValue(tftypes.String, nil), "required": tftypes.NewValue(tftypes.String, nil), }) + testEmptyStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, nil), + "write-only": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }) + testUnknownStateValue := tftypes.NewValue(testType, tftypes.UnknownValue) testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ @@ -61,11 +75,31 @@ func TestServerImportResourceState(t *testing.T) { }, } + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required": schema.StringAttribute{ + Required: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: testEmptyStateValue, Schema: testSchema, } + testEmptyStateWriteOnly := &tfsdk.State{ + Raw: testEmptyStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testUnknownState := &tfsdk.State{ Raw: testUnknownStateValue, Schema: testSchema, @@ -416,6 +450,39 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyStateWriteOnly, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("write-only"), "write-only-val")...) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: tfsdk.State{ + Raw: tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id"), + "write-only": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_moveresourcestate.go b/internal/fwserver/server_moveresourcestate.go index 480e4a956..1a832f3fe 100644 --- a/internal/fwserver/server_moveresourcestate.go +++ b/internal/fwserver/server_moveresourcestate.go @@ -205,6 +205,18 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe return } + // Set any write-only attributes in the move resource state to null + modifiedState, err := tftypes.Transform(moveStateResp.TargetState.Raw, NullifyWriteOnlyAttributes(ctx, moveStateResp.TargetState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Move Resource State", + "There was an unexpected error modifying the Move Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + moveStateResp.TargetState.Raw = modifiedState + // If the implement has set the state in any way, return the response. if !moveStateResp.TargetState.Raw.Equal(tftypes.NewValue(req.TargetResourceSchema.Type().TerraformType(ctx), nil)) { resp.Diagnostics = moveStateResp.Diagnostics diff --git a/internal/fwserver/server_moveresourcestate_test.go b/internal/fwserver/server_moveresourcestate_test.go index 1a8e0fb77..931b84a76 100644 --- a/internal/fwserver/server_moveresourcestate_test.go +++ b/internal/fwserver/server_moveresourcestate_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerMoveResourceState(t *testing.T) { @@ -40,6 +41,22 @@ func TestServerMoveResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write_only_attribute": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.MoveResourceStateRequest @@ -757,6 +774,44 @@ func TestServerMoveResourceState(t *testing.T) { }, }, }, + "response-TargetState-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "write_only_attribute": nil, + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("write_only_attribute"), "movestate-val")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchemaWriteOnly, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index ecc23d8ef..aacd2c998 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -341,7 +341,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } // Set any write-only attributes in the plan to null - modifiedPlan, err := tftypes.Transform(resp.PlannedState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + modifiedPlan, err := tftypes.Transform(resp.PlannedState.Raw, NullifyWriteOnlyAttributes(ctx, resp.PlannedState.Schema)) if err != nil { resp.Diagnostics.AddError( "Error Modifying Planned State", diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 628a2e445..d260e2912 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -6,6 +6,8 @@ package fwserver import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -157,11 +159,21 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index a9520edd4..f6711551a 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -34,11 +34,23 @@ func TestServerReadResource(t *testing.T) { }, } + testTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_write_only": tftypes.String, + "test_required": tftypes.String, + }, + } + testCurrentStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, nil), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), }) + testCurrentStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "test_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), + }) + testNewStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, "test-newstate-value"), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), @@ -55,6 +67,18 @@ func TestServerReadResource(t *testing.T) { }, } + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -97,6 +121,11 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testCurrentStateWriteOnly := &tfsdk.State{ + Raw: testCurrentStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testNewState := &tfsdk.State{ Raw: testNewStateValue, Schema: testSchema, @@ -562,6 +591,38 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-state-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentStateWriteOnly, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestWriteOnly types.String `tfsdk:"test_write_only"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + data.TestWriteOnly = types.StringValue("test-write-only-value") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "test_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 87e97c0fa..ad1d9f998 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -176,7 +176,7 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, } // Set any write-only attributes in the state to null - modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) if err != nil { resp.Diagnostics.AddError( "Error Modifying State", diff --git a/internal/fwserver/server_upgraderesourcestate.go b/internal/fwserver/server_upgraderesourcestate.go index b2cc340e5..536c3dfd0 100644 --- a/internal/fwserver/server_upgraderesourcestate.go +++ b/internal/fwserver/server_upgraderesourcestate.go @@ -225,9 +225,19 @@ func (s *Server) UpgradeResourceState(ctx context.Context, req *UpgradeResourceS return } + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(upgradedStateValue, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Upgraded Resource State", + "There was an unexpected error modifying the Upgraded Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + resp.UpgradedState = &tfsdk.State{ Schema: req.ResourceSchema, - Raw: upgradedStateValue, + Raw: modifiedState, } return @@ -243,5 +253,27 @@ func (s *Server) UpgradeResourceState(ctx context.Context, req *UpgradeResourceS return } + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(upgradeResourceStateResponse.State.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Upgraded Resource State", + "There was an unexpected error modifying the Upgraded Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + upgradeResourceStateResponse.State.Raw = modifiedState + + // If the write-only nullification results in a null state, then this is a provider error + if upgradeResourceStateResponse.State.Raw.Type() == nil || upgradeResourceStateResponse.State.Raw.IsNull() { + resp.Diagnostics.AddError( + "Missing Upgraded Resource State", + fmt.Sprintf("After attempting a resource state upgrade to version %d, the provider did not return any state data. ", req.Version)+ + "Preventing the unexpected loss of resource state data. "+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + ) + return + } + resp.UpgradedState = &upgradeResourceStateResponse.State } diff --git a/internal/fwserver/server_upgraderesourcestate_test.go b/internal/fwserver/server_upgraderesourcestate_test.go index b720bed7e..1e733874f 100644 --- a/internal/fwserver/server_upgraderesourcestate_test.go +++ b/internal/fwserver/server_upgraderesourcestate_test.go @@ -42,6 +42,23 @@ func TestServerUpgradeResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write_only_attribute": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + Version: 1, // Must be above 0 + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.UpgradeResourceStateRequest @@ -342,6 +359,71 @@ func TestServerUpgradeResourceState(t *testing.T) { }, }, }, + "RawState-DynamicValue-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.ResourceWithUpgradeState{ + Resource: &testprovider.Resource{}, + UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var rawState struct { + Id string `json:"id"` + RequiredAttribute bool `json:"required_attribute"` + } + + if err := json.Unmarshal(req.RawState.JSON, &rawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior State", + err.Error(), + ) + return + } + + dynamicValue, err := tfprotov6.NewDynamicValue( + schemaTypeWriteOnly, + tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, rawState.Id), + "write_only_attribute": tftypes.NewValue(tftypes.String, "write-only-dynamic-value"), + "required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", rawState.RequiredAttribute)), + }), + ) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Upgraded State", + err.Error(), + ) + return + } + + resp.DynamicValue = &dynamicValue + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceStateResponse{ + UpgradedState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, "ResourceType-UpgradeState-not-implemented": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -517,6 +599,72 @@ func TestServerUpgradeResourceState(t *testing.T) { }, }, }, + "PriorSchema-and-State-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.ResourceWithUpgradeState{ + Resource: &testprovider.Resource{}, + UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "required_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData struct { + Id string `tfsdk:"id"` + RequiredAttribute bool `tfsdk:"required_attribute"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := struct { + Id string `tfsdk:"id"` + WriteOnlyAttribute string `tfsdk:"write_only_attribute"` + RequiredAttribute string `tfsdk:"required_attribute"` + }{ + Id: priorStateData.Id, + WriteOnlyAttribute: "write-only-upgraded-state", + RequiredAttribute: fmt.Sprintf("%t", priorStateData.RequiredAttribute), + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...) + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceStateResponse{ + UpgradedState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, "PriorSchema-and-State-json-mismatch": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, From 44d4a25fe9510f5a409cd3175fedfe1c57371bc4 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 17 Jan 2025 12:58:27 -0500 Subject: [PATCH 50/55] Add missing `IsWriteOnly()` unit tests for `ephemeral/schema` package --- ephemeral/schema/float64_attribute_test.go | 31 +++++++++++++++++++++- ephemeral/schema/int32_attribute_test.go | 28 +++++++++++++++++++ ephemeral/schema/int64_attribute_test.go | 31 +++++++++++++++++++++- ephemeral/schema/object_attribute_test.go | 31 +++++++++++++++++++++- 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go index 4c3f2703a..db9e8db2b 100644 --- a/ephemeral/schema/float64_attribute_test.go +++ b/ephemeral/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat54AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go index bd6d4d36c..e118cb0db 100644 --- a/ephemeral/schema/int32_attribute_test.go +++ b/ephemeral/schema/int32_attribute_test.go @@ -424,3 +424,31 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt2AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go index e61165c5f..5f07ff2ff 100644 --- a/ephemeral/schema/int64_attribute_test.go +++ b/ephemeral/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go index 429aba1e7..76f4aab38 100644 --- a/ephemeral/schema/object_attribute_test.go +++ b/ephemeral/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -438,6 +439,34 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() From cb1f813ddee700f1bed338e6a3caa4fd2a36314f Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Fri, 17 Jan 2025 13:19:15 -0500 Subject: [PATCH 51/55] Add godoc comment to `NullifyWriteOnlyAttributes()` --- internal/fwserver/write_only_nullification.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/write_only_nullification.go b/internal/fwserver/write_only_nullification.go index e01dd2822..5bfc77609 100644 --- a/internal/fwserver/write_only_nullification.go +++ b/internal/fwserver/write_only_nullification.go @@ -15,6 +15,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) +// NullifyWriteOnlyAttributes transforms a tftypes.Value, setting all write-only attribute values +// to null according to the given managed resource schema. This function is called in all managed +// resource RPCs before a response is sent to Terraform Core. Terraform Core expects all write-only +// attribute values to be null to prevent data consistency errors. This can technically be done +// manually by the provider developers, but the Framework is handling it instead for convenience. func NullifyWriteOnlyAttributes(ctx context.Context, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) { return func(path *tftypes.AttributePath, val tftypes.Value) (tftypes.Value, error) { ctx = logging.FrameworkWithAttributePath(ctx, path.String()) @@ -51,7 +56,7 @@ func NullifyWriteOnlyAttributes(ctx context.Context, resourceSchema fwschema.Sch } // Value type from new state to create null with - newValueType := val.Type() + newValueType := attribute.GetType().TerraformType(ctx) // If the attribute is dynamic set the new value type to DynamicPseudoType // instead of the underlying concrete type From 2b72dc0a12ab87712d6f311c5e8eeae7e91ca507 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 4 Feb 2025 17:15:59 -0500 Subject: [PATCH 52/55] Add testing for nested types for `NullifyWriteOnlyAttributes()` --- .../fwserver/write_only_nullification_test.go | 1424 +++++++++++++++++ 1 file changed, 1424 insertions(+) diff --git a/internal/fwserver/write_only_nullification_test.go b/internal/fwserver/write_only_nullification_test.go index ee0c7e66e..7aba06585 100644 --- a/internal/fwserver/write_only_nullification_test.go +++ b/internal/fwserver/write_only_nullification_test.go @@ -284,3 +284,1427 @@ func TestNullifyWriteOnlyAttributes(t *testing.T) { t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) } } + +func TestNullifyWriteOnlyAttributes_NestedTypes(t *testing.T) { + t.Parallel() + nestedObjectType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + }, + } + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + "map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "nested-map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + "list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + "set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "single-nested-block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-single-nested-block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + "list-nested-block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-list-nested-block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + "set-nested-block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-set-nested-block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "single-nested-attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "single-nested-attribute-wo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "map-nested-attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "list-nested-attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "set-nested-attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + "nested-single-nested-block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + "list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + }), + }), + "set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + }), + }), + }) + expected := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "single-nested-attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + }), + "single-nested-attribute-wo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, nil), + "map-nested-attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, nil), + }), + }), + "map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, nil), + "list-nested-attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + }), + }), + "list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, nil), + "set-nested-attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, nil), + }), + }), + "set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, nil), + "single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + "nested-single-nested-block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + "nested-single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + }), + }), + "list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + "nested-list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + }), + }), + }), + }), + "set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, nil), + "nested-set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, nil), + }), + }), + }), + }), + }) + got, err := tftypes.Transform(input, NullifyWriteOnlyAttributes(context.Background(), s)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + diff, err := expected.Diff(got) + if err != nil { + t.Errorf("Error diffing values: %s", err) + return + } + for _, valDiff := range diff { + t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) + } +} From e210315c7023d8cf105325d562b0abd164857a5c Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Wed, 5 Feb 2025 16:41:41 -0500 Subject: [PATCH 53/55] Add website documentation --- website/data/plugin-framework-nav-data.json | 4 + .../handling-data/attributes/bool.mdx | 12 ++ .../handling-data/attributes/dynamic.mdx | 12 ++ .../handling-data/attributes/float32.mdx | 12 ++ .../handling-data/attributes/float64.mdx | 12 ++ .../handling-data/attributes/int32.mdx | 12 ++ .../handling-data/attributes/int64.mdx | 12 ++ .../handling-data/attributes/list-nested.mdx | 14 +++ .../handling-data/attributes/list.mdx | 12 ++ .../handling-data/attributes/map-nested.mdx | 14 +++ .../handling-data/attributes/map.mdx | 12 ++ .../handling-data/attributes/number.mdx | 12 ++ .../handling-data/attributes/object.mdx | 12 ++ .../handling-data/attributes/set-nested.mdx | 14 +++ .../handling-data/attributes/set.mdx | 12 ++ .../attributes/single-nested.mdx | 14 +++ .../handling-data/attributes/string.mdx | 12 ++ .../docs/plugin/framework/resources/index.mdx | 1 + .../resources/write-only-arguments.mdx | 111 ++++++++++++++++++ 19 files changed, 316 insertions(+) create mode 100644 website/docs/plugin/framework/resources/write-only-arguments.mdx diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index 2297fad3c..b41e7901f 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -98,6 +98,10 @@ { "title": "Timeouts", "path": "resources/timeouts" + }, + { + "title": "Write-only Arguments", + "path": "resources/write-only-arguments" } ] }, diff --git a/website/docs/plugin/framework/handling-data/attributes/bool.mdx b/website/docs/plugin/framework/handling-data/attributes/bool.mdx index 2d37ffcd1..37eef22bf 100644 --- a/website/docs/plugin/framework/handling-data/attributes/bool.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/bool.mdx @@ -104,6 +104,18 @@ The [`boolplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx index 4c95a01d6..5dfe4a922 100644 --- a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx @@ -134,6 +134,18 @@ The [`dynamicplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/float32.mdx b/website/docs/plugin/framework/handling-data/attributes/float32.mdx index be23a0fc0..782158a8b 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float32.mdx @@ -110,6 +110,18 @@ The [`float32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/float64.mdx b/website/docs/plugin/framework/handling-data/attributes/float64.mdx index 9e062a18c..1bb835f18 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float64.mdx @@ -110,6 +110,18 @@ The [`float64planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/int32.mdx b/website/docs/plugin/framework/handling-data/attributes/int32.mdx index 0feb915bb..43ead57fd 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int32.mdx @@ -110,6 +110,18 @@ The [`int32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plug Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/int64.mdx b/website/docs/plugin/framework/handling-data/attributes/int64.mdx index 7e1e004a8..a168532f9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int64.mdx @@ -110,6 +110,18 @@ The [`int64planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plug Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx index f088d5e35..5b8864b5d 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx @@ -159,6 +159,20 @@ The [`listplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/list.mdx b/website/docs/plugin/framework/handling-data/attributes/list.mdx index 382696c35..7c668d141 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list.mdx @@ -128,6 +128,18 @@ The [`listplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx index 7b2c52d3c..53096aae8 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx @@ -159,6 +159,20 @@ The [`mapplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/map.mdx b/website/docs/plugin/framework/handling-data/attributes/map.mdx index 368c4e25b..cbb99b889 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map.mdx @@ -131,6 +131,18 @@ The [`mapplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/number.mdx b/website/docs/plugin/framework/handling-data/attributes/number.mdx index 1afe11e19..d9ba6e537 100644 --- a/website/docs/plugin/framework/handling-data/attributes/number.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/number.mdx @@ -110,6 +110,18 @@ The [`numberplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/object.mdx b/website/docs/plugin/framework/handling-data/attributes/object.mdx index 05be71d36..58442a0d9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/object.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/object.mdx @@ -165,6 +165,18 @@ Only the object attribute itself, not individual sub-attributes, can define its Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx index ca1ce3cce..b8049e6e0 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx @@ -159,6 +159,20 @@ The [`setplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/set.mdx b/website/docs/plugin/framework/handling-data/attributes/set.mdx index 29724eb3b..80da1b2f6 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set.mdx @@ -128,6 +128,18 @@ The [`setplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx index 5b76dcaa9..5fd1a907d 100644 --- a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx @@ -155,6 +155,20 @@ The [`objectplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/string.mdx b/website/docs/plugin/framework/handling-data/attributes/string.mdx index b3b49f80e..057117dff 100644 --- a/website/docs/plugin/framework/handling-data/attributes/string.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/string.mdx @@ -112,6 +112,18 @@ The [`stringplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/resources/index.mdx b/website/docs/plugin/framework/resources/index.mdx index c7f0691b8..0cf87dcd8 100644 --- a/website/docs/plugin/framework/resources/index.mdx +++ b/website/docs/plugin/framework/resources/index.mdx @@ -30,6 +30,7 @@ Further documentation is available for deeper resource concepts: - [Upgrade state](/terraform/plugin/framework/resources/state-upgrade) to transparently update state data outside plans. - [Validate](/terraform/plugin/framework/resources/validate-configuration) practitioner configuration against acceptable values. - [Timeouts](/terraform/plugin/framework/resources/timeouts) in practitioner configuration for use in resource create, read, update and delete functions. +- [Write-only Arguments](/terraform/plugin/framework/resources/write-only-arguments) are special types of attributes that can accept [ephemeral values](/terraform/language/resources/ephemeral) and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. ## Define Resource Type diff --git a/website/docs/plugin/framework/resources/write-only-arguments.mdx b/website/docs/plugin/framework/resources/write-only-arguments.mdx new file mode 100644 index 000000000..6248f6ed5 --- /dev/null +++ b/website/docs/plugin/framework/resources/write-only-arguments.mdx @@ -0,0 +1,111 @@ +--- +page_title: 'Plugin Development - Framework: Write-only Arguments' +description: >- + How to implement write-only arguments with the provider development framework. +--- + +# Write-only Arguments + +Write-only arguments are managed resource attributes that are configured by practitioners but are not persisted to the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. +Write-only arguments should be used to handle secret values that do not need to be persisted in Terraform state, such as passwords, API keys, etc. +The provider is expected to be the terminal point for an ephemeral value, +which should either use the value by making the appropriate change to the API or ignore the value. Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) and are not required to be consistent between plan and apply operations. + +## General Concepts + +The following are high level differences between `Required`/`Optional` arguments and write-only arguments: + +- Write-only arguments can accept ephemeral and non-ephemeral values + +- Write-only argument values are only available in the configuration. The prior state, planned state, and final state values for +write-only arguments should always be `null`. + - Provider developers do not need to explicitly set write-only argument values to `null` after using them as the plugin framework will handle the nullification of write-only arguments for all RPCs. + +- Any value that is set for a write-only argument in the state or plan (during [Plan Modification](/terraform/plugin/framework/resources/plan-modification)) by the provider will be reverted to `null` by plugin framework before the RPC response is sent to Terraform. + +- Write-only argument values cannot produce a Terraform plan difference. + - This is because the prior state value for a write-only argument will always be `null` and the planned/final state value will also be `null`, therefore, it cannot produce a diff on its own. + - The one exception to this case is if the write-only argument is added to `requires_replace` during Plan Modification (i.e., using the [`RequiresReplace()`](/terraform/plugin/framework/resources/plan-modification#requiresreplace) plan modifier), in that case, the write-only argument will always cause a diff/trigger a resource recreation + +- Since write-only arguments can accept ephemeral values, write-only argument configuration values are not expected to be consistent between plan and apply. + +## Schema + +An attribute can be made write-only by setting the `WriteOnly` field to `true` in the schema. Attributes with `WriteOnly` set to `true` must also have +one of `Required` or `Optional` set to `true`. If a nested attribute has `WriteOnly` set to `true`, all child attributes must also have `WriteOnly` set to `true`. +`Computed` cannot be set to true for write-only arguments. + +**Schema example:** + +```go +"password_wo": schema.StringAttribute{ + Required: true, + WriteOnly: true, +}, +``` + +## Retrieving Write-only Values + +Write-only argument values should be retrieved from the configuration instead of the plan. Refer to [accessing values](/terraform/plugin/framework/handling-data/accessing-values) for more details on +retrieving values from configuration. + +## PreferWriteOnlyAttribute Validators + +The `PreferWriteOnlyAttribute()` validators available in the [`terraform-plugin-framework-validators` Go module](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators) +can be used when you have a write-only version of an existing attribute, and you want to encourage practitioners to use the write-only version whenever possible. + +The validator returns a warning if the Terraform client is 1.11 or above and the value of the existing attribute is non-null. + +`PreferWriteOnlyAttribute()` is available as a resource-level validator in the [`resourcevalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator) or +as an attribute-level validator in the `[type]validator` packages (i.e., [`stringvalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator)) + +Usage: + +```go +// Resource-level validator +// Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + // Throws a warning diagnostic encouraging practitioners to use + // password_wo if password has a known value + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("password"), + path.MatchRoot("password_wo"), + ), + } + +// Attribute-level validator +// Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "password": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + // Throws a warning diagnostic encouraging practitioners to use + // password_wo if password has a known value. + stringvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("password_wo"), + ), + }, + }, + "password_wo": schema.StringAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +``` + +```hcl +resource "example_db_instance" "ex" { + username = "foo" + password = "bar" # returns a warning encouraging practitioners to use `password_wo` instead. +} +``` + +## Best Practices + +Since write-only arguments have no prior values, user intent cannot be determined with a write-only argument alone. To determine when to use/not use a write-only argument value in your provider, we recommend using other non-write-only arguments in the provider. For example: + +- Pair write-only arguments with a configuration attribute (required or optional) to “trigger” the use of the write-only argument + - For example, a `password_wo` write-only argument can be paired with a configured `password_wo_version` attribute. When the `password_wo_version` is modified, the provider will send the `password_wo` value to the API. +- Use a keepers attribute (which is used in the [Random Provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs#resource-keepers)) that will take in arbitrary key-pair values. Whenever there is a change to the `keepers` attribute, the provider will use the write-only argument value. \ No newline at end of file From fe7e644d9df0cc232e4a0844de7784937ac90715 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 6 Feb 2025 10:52:41 -0500 Subject: [PATCH 54/55] Add recommendation to use private state to store hashes --- .../docs/plugin/framework/resources/write-only-arguments.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/plugin/framework/resources/write-only-arguments.mdx b/website/docs/plugin/framework/resources/write-only-arguments.mdx index 6248f6ed5..b418e34f0 100644 --- a/website/docs/plugin/framework/resources/write-only-arguments.mdx +++ b/website/docs/plugin/framework/resources/write-only-arguments.mdx @@ -104,8 +104,9 @@ resource "example_db_instance" "ex" { ## Best Practices -Since write-only arguments have no prior values, user intent cannot be determined with a write-only argument alone. To determine when to use/not use a write-only argument value in your provider, we recommend using other non-write-only arguments in the provider. For example: +Since write-only arguments have no prior values, user intent or value changes cannot be determined with a write-only argument alone. To determine when to use/not use a write-only argument value in your provider, we recommend one of the following: - Pair write-only arguments with a configuration attribute (required or optional) to “trigger” the use of the write-only argument - For example, a `password_wo` write-only argument can be paired with a configured `password_wo_version` attribute. When the `password_wo_version` is modified, the provider will send the `password_wo` value to the API. -- Use a keepers attribute (which is used in the [Random Provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs#resource-keepers)) that will take in arbitrary key-pair values. Whenever there is a change to the `keepers` attribute, the provider will use the write-only argument value. \ No newline at end of file +- Use a keepers attribute (which is used in the [Random Provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs#resource-keepers)) that will take in arbitrary key-pair values. Whenever there is a change to the `keepers` attribute, the provider will use the write-only argument value. +- Use the resource's [private state] to store secure hashes of write-only argument values, the provider will then use the hash to determine if a write-only argument value has changed in later Terraform runs. \ No newline at end of file From fa3bc55ed4a5fc649e1d990dcf5e29aae17fb58a Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Thu, 6 Feb 2025 11:48:40 -0500 Subject: [PATCH 55/55] Add changelog entries --- .changes/unreleased/FEATURES-20250206-114700.yaml | 6 ++++++ .changes/unreleased/NOTES-20250206-114436.yaml | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20250206-114700.yaml create mode 100644 .changes/unreleased/NOTES-20250206-114436.yaml diff --git a/.changes/unreleased/FEATURES-20250206-114700.yaml b/.changes/unreleased/FEATURES-20250206-114700.yaml new file mode 100644 index 000000000..ce306a4c9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-114700.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema: Added `WriteOnly` schema field for managed resource schemas to indicate a write-only attribute. +Write-only attribute values are not saved to the Terraform plan or state artifacts.' +time: 2025-02-06T11:47:00.176842-05:00 +custom: + Issue: "1044" diff --git a/.changes/unreleased/NOTES-20250206-114436.yaml b/.changes/unreleased/NOTES-20250206-114436.yaml new file mode 100644 index 000000000..0ce8a7603 --- /dev/null +++ b/.changes/unreleased/NOTES-20250206-114436.yaml @@ -0,0 +1,5 @@ +kind: NOTES +body: Write-only attribute support is in technical preview and offered without compatibility promises until Terraform 1.11 is generally available. +time: 2025-02-06T11:44:36.156747-05:00 +custom: + Issue: "1044"