Use existing custom types or develop custom types to consistently define behaviors for a kind of value across schemas. Custom types are supported on top of any framework-defined type.
Supported behaviors for custom types include:
The following Go modules contain custom type implementations covering common use cases with validation and semantic equality logic (where appropriate).
terraform-plugin-framework-jsontypes
terraform-plugin-framework-nettypes
terraform-plugin-framework-timetypes
Individual data value handling in the framework is performed by a pair of associated Go types:
The framework defines a standard set these associated Go types referred to by the "base type" terminology. Extending these base types is referred to by the "custom type" terminology.
Use a custom type by switching the schema definition and data handling from a framework-defined type to the custom type.
Schema DefinitionThe framework schema types accept a CustomType
field where applicable, such as the resource/schema.StringAttribute
type. When the CustomType
is omitted, the framework defaults to the associated base type.
Implement the CustomType
field in a schema type to switch from the base type to a custom type.
In this example, a string attribute implements a custom type.
schema.StringAttribute{
CustomType: CustomStringType{},
// ... other fields ...
}
Data Handling
Each custom type will also include a value type, which must be used anywhere the value is referenced in data source, provider, or resource logic.
Switch any usage of a base value type to the custom value type. Any logic will need to be updated to match the custom value type implementation.
In this example, a custom value type is used in a data model approach:
type ThingModel struct {
// Instead of types.String
Timestamp CustomStringValue `tfsdk:"timestamp"`
// ... other fields ...
}
Create a custom type by extending an existing framework schema type and its associated value type. Once created, define semantic equality and/or validation logic for the custom type.
Schema TypeExtend a framework schema type by creating a Go type that implements one of the github.com/hashicorp/terraform-plugin-framework/types/basetypes
package *Typable
interfaces.
Tip
The commonly used types
package types are aliases to the basetypes
package types mentioned in this table.
It is recommended to use Go type embedding of the base type to simplify the implementation and ensure it is up to date with the latest data handling features of the framework. With type embedding, the following attr.Type
methods must be overridden by the custom type to prevent confusing errors:
Equal(attr.Type) bool
ValueFromTerraform(context.Context, tftypes.Value) (attr.Value, error)
ValueType(context.Context) attr.Value
String() string
ValueFrom{TYPE}(context.Context, basetypes.{TYPE}Value) (basetypes.{TYPE}Valuable, diag.Diagnostics)
*Typable
custom schema type interface listed above, for example basetypes.StringTypable
is defined as ValueFromString
In this example, the basetypes.StringTypable
interface is implemented to create a custom string type with an associated value type:
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringTypable = CustomStringType{}
type CustomStringType struct {
basetypes.StringType
// ... potentially other fields ...
}
func (t CustomStringType) Equal(o attr.Type) bool {
other, ok := o.(CustomStringType)
if !ok {
return false
}
return t.StringType.Equal(other.StringType)
}
func (t CustomStringType) String() string {
return "CustomStringType"
}
func (t CustomStringType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
// CustomStringValue defined in the value type section
value := CustomStringValue{
StringValue: in,
}
return value, nil
}
func (t CustomStringType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
if err != nil {
return nil, err
}
stringValue, ok := attrValue.(basetypes.StringValue)
if !ok {
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}
stringValuable, diags := t.ValueFromString(ctx, stringValue)
if diags.HasError() {
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
}
return stringValuable, nil
}
func (t CustomStringType) ValueType(ctx context.Context) attr.Value {
// CustomStringValue defined in the value type section
return CustomStringValue{}
}
Value Type
Extend a framework value type by creating a Go type that implements one of the github.com/hashicorp/terraform-plugin-framework/types/basetypes
package *Valuable
interfaces.
Tip
The commonly used types
package types are aliases to the basetypes
package types mentioned in this table.
It is recommended to use Go type embedding of the base type to simplify the implementation and ensure it is up to date with the latest data handling features of the framework. With type embedding, the following attr.Value
methods must be overridden by the custom type to prevent confusing errors:
Note
The overridden Equal(attr.Value) bool
method should not contain Semantic Equality logic. Equal
should only check the type of attr.Value
and the underlying base value.
An example of this can be found below with the CustomStringValue
implementation.
In this example, the basetypes.StringValuable
interface is implemented to create a custom string value type with an associated schema type:
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringValuable = CustomStringValue{}
type CustomStringValue struct {
basetypes.StringValue
// ... potentially other fields ...
}
func (v CustomStringValue) Equal(o attr.Value) bool {
other, ok := o.(CustomStringValue)
if !ok {
return false
}
return v.StringValue.Equal(other.StringValue)
}
func (v CustomStringValue) Type(ctx context.Context) attr.Type {
// CustomStringType defined in the schema type section
return CustomStringType{}
}
From this point, the custom type can be extended with other behaviors.
Semantic EqualitySemantic equality handling enables the value type to automatically keep a prior value when a new value is determined to be inconsequentially different. This handling can prevent unexpected drift detection for values and in some cases prevent Terraform data handling errors.
This value type functionality is checked in the following scenarios:
Read
method logic is compared to the configuration value.Read
method logic is compared to the request prior state value.Create
or Update
method logic is compared to the request plan value.The framework will only call semantic equality logic if both the prior and new values are known. Null or unknown values are unnecessary to check. When working with collection types, the framework automatically calls semantic equality logic of element types. When working with object types, the framework automatically calls semantic equality of underlying attribute types.
Implement the associated github.com/hashicorp/terraform-plugin-framework/types/basetypes
package *ValuableWithSemanticEquals
interface on the value type to define and enable this behavior.
In this example, the custom string value type will preserve the prior value if the expected RFC3339 timestamps are considered equivalent:
// CustomStringValue defined in the value type section
// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringValuableWithSemanticEquals = CustomStringValue{}
func (v CustomStringValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
// The framework should always pass the correct value type, but always check
newValue, ok := newValuable.(CustomStringValue)
if !ok {
diags.AddError(
"Semantic Equality Check Error",
"An unexpected value type was received while performing semantic equality checks. "+
"Please report this to the provider developers.\n\n"+
"Expected Value Type: "+fmt.Sprintf("%T", v)+"\n"+
"Got Value Type: "+fmt.Sprintf("%T", newValuable),
)
return false, diags
}
// Skipping error checking if CustomStringValue already implemented RFC3339 validation
priorTime, _ := time.Parse(time.RFC3339, v.StringValue.ValueString())
// Skipping error checking if CustomStringValue already implemented RFC3339 validation
newTime, _ := time.Parse(time.RFC3339, newValue.ValueString())
// If the times are equivalent, keep the prior value
return priorTime.Equal(newTime), diags
}
Validation Value Validation
Validation handling in custom value types can be enabled for schema attribute values, or provider-defined function parameters.
Implement the xattr.ValidateableAttribute
interface on the custom value type to define and enable validation handling for a schema attribute, which will automatically raise warning and/or error diagnostics when a value is determined to be invalid.
Implement the function.ValidateableParameter
interface on the custom value type to define and enable validation handling for a provider-defined function parameter, which will automatically raise an error when a value is determined to be invalid.
If the custom value type is to be used for both schema attribute values and provider-defined function parameters, implement both interfaces.
// Implementation of the xattr.ValidateableAttribute interface
func (v CustomStringValue) ValidateAttribute(ctx context.Context, req xattr.ValidateAttributeRequest, resp *xattr.ValidateAttributeResponse) {
if v.IsNull() || v.IsUnknown() {
return
}
err := v.validate(v.ValueString())
if err != nil {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid RFC 3339 String Value",
"An unexpected error occurred while converting a string value that was expected to be RFC 3339 format. "+
"The RFC 3339 string format is YYYY-MM-DDTHH:MM:SSZ, such as 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00.\n\n"+
"Path: "+req.Path.String()+"\n"+
"Given Value: "+v.ValueString()+"\n"+
"Error: "+err.Error(),
)
return
}
}
// Implementation of the function.ValidateableParameter interface
func (v CustomStringValue) ValidateParameter(ctx context.Context, req function.ValidateParameterRequest, resp *function.ValidateParameterResponse) {
if v.IsNull() || v.IsUnknown() {
return
}
err := v.validate(v.ValueString())
if err != nil {
resp.Error = function.NewArgumentFuncError(
req.Position,
"Invalid RFC 3339 String Value: "+
"An unexpected error occurred while converting a string value that was expected to be RFC 3339 format. "+
"The RFC 3339 string format is YYYY-MM-DDTHH:MM:SSZ, such as 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00.\n\n"+
fmt.Sprintf("Position: %d", req.Position)+"\n"+
"Given Value: "+v.ValueString()+"\n"+
"Error: "+err.Error(),
)
}
}
func (v CustomStringValue) validate(in string) error {
_, err := time.Parse(time.RFC3339, in)
return err
}
Type Validation
Implement the xattr.TypeWithValidate
interface on the value type to define and enable this behavior.
Note
This functionality uses the lower level tftypes
type system compared to other framework logic.
In this example, the custom string value type will ensure the string is a valid RFC3339 timestamp:
// CustomStringType defined in the schema type section
func (t CustomStringType) Validate(ctx context.Context, value tftypes.Value, valuePath path.Path) diag.Diagnostics {
if value.IsNull() || !value.IsKnown() {
return nil
}
var diags diag.Diagnostics
var valueString string
if err := value.As(&valueString); err != nil {
diags.AddAttributeError(
valuePath,
"Invalid Terraform Value",
"An unexpected error occurred while attempting to convert a Terraform value to a string. "+
"This generally is an issue with the provider schema implementation. "+
"Please contact the provider developers.\n\n"+
"Path: "+valuePath.String()+"\n"+
"Error: "+err.Error(),
)
return diags
}
if _, err := time.Parse(time.RFC3339, valueString); err != nil {
diags.AddAttributeError(
valuePath,
"Invalid RFC 3339 String Value",
"An unexpected error occurred while converting a string value that was expected to be RFC 3339 format. "+
"The RFC 3339 string format is YYYY-MM-DDTHH:MM:SSZ, such as 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00.\n\n"+
"Path: "+valuePath.String()+"\n"+
"Given Value: "+valueString+"\n"+
"Error: "+err.Error(),
)
return diags
}
return diags
}
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4