Terraform Plugin Framework Custom Types

One of the great new things the Terraform Plugin Framework provides is a stronger focus on strict typing and with that, the ability to develop custom types.

One of the first issues I discovered straight out of the gate when attempting to build a new Terraform Provider with the Plugin Framework was that the data I was modelling for a resource used UUIDs for its record ids. I quickly tried to reach for an equivalent UUID validator in the plugin framework. Unfortunately, the UUID validator hasn’t been brought across from SDKv2. Remembering the benefits the Plugin Framework was supposed to afford us, I started to dig into the custom types.

When I initially dug into custom types, the Framework was only at v0.14.X and coming back to it almost a year later with the Framework having gone Generally Available (GA) with v1.X.X in February of 2023, there were several improvements and interfaces I needed to update my custom UUID type to use.

Sidenote: Shout out to Brian Flad (bflad) who initially created a custom type for time values while building out the Plugin Framework to see how the interface would look for implementors.

Interface-Driven Design

The Framework now provides a core-type system that can be extended, which makes a lot more sense and makes fantastic use of Go’s composability to reduce the amount of code required. Pre-v1.X.X types were simply tfsdk Attributes that would then take in your custom type:

1
2
3
4
5
    "id": tfsdk.Attribute{
        Required: true,
        Type:     uuidtypes.UUIDType{},
        // Potentially previous Validators
    }

But now custom types are extended from the Framework’s base types, which roughly match the underlying base types that Go (the programming language) provides, so my UUID Type now implements basetypes.StringTypable.

1
2
3
4
5
    "id": schema.StringAttribute{
        CustomType: uuidtypes.UUIDType{},
        Required:   true,
        // ...
    },

Surprisingly, once I worked out the new interfaces that were required to implement a basetypes.StringTypable, I was able to remove a lot of my code and migrate the remaining methods to wrap the overloaded methods where my custom type (UUIDType or UUIDValue) was required to be specified.

Maintenance

Take the following method that implements the attr.Attribute interface that overloads Equal from the composed basetypes.StringType struct.

1
2
3
4
5
6
7
8
9
// Equal returns true if the two types are equivalent.
func (u UUIDType) Equal(o attr.Type) bool {
	other, ok := o.(UUIDType)
	if !ok {
		return false
	}

	return u.StringType.Equal(other.StringType)
}

While a trivial example of how the new Plugin Framework’s composition helps, this means that as the framework grows and new features and methods get added to the base String Type, the maintenance overhead required should be minimised as the base types will, hopefully in most cases, be able to implement upgraded functionality for us for free.

Rolling up from v0.14.X to v1.X.X, while expected, was a bit of a pain as I was developing on top of a beta framework with many underlying changes expected - Custom Types being one of the last features of the framework to get a look at. In truth, custom types weren’t documented and supported until Terraform Plugin Framework v1.3.X. The new documentation is fantastic, enabling a developer to easily integrate and extend the Plugin Framework.

Follow Along

Feel free to follow along with my development at GitHub or chat via my socials:

Addendum

  • Time to Code: 6h
  • Time to Blog: 1.5h