Provider-Defined Functions
What are Provider-Defined Functions?
Provider-defined functions allow providers to expose custom functions that users can call in their Terraform configurations, extending HCL’s built-in functions.
Why Use Functions?
Benefits
- Data Transformation: Process data without external tools
- Reusability: Centralize common calculations
- User Experience: Simplify complex operations
- Type Safety: Leverage Terraform’s type system
- Testing: Unit test business logic
When to Use
✅ Use functions for:
- Complex calculations
- Data transformation
- Validation logic
- Helper utilities
- Common operations
❌ Don't use for:
- API calls (use data sources)
- State management (use resources)
- Side effects
- Long-running operations
Function Basics
Function Interface
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/function"
)
type Function interface {
// Metadata returns function name and description
Metadata(context.Context, function.MetadataRequest, *function.MetadataResponse)
// Definition returns parameter and return type definitions
Definition(context.Context, function.DefinitionRequest, *function.DefinitionResponse)
// Run executes the function logic
Run(context.Context, function.RunRequest, *function.RunResponse)
}
Register Functions
// In provider implementation
func (p *provider) Functions(ctx context.Context) []function.Function {
return []function.Function{
NewUppercaseFunction(),
NewCalculateCIDRFunction(),
NewGeneratePasswordFunction(),
}
}
Simple Function Example
Uppercase Function
package provider
import (
"context"
"strings"
"github.com/hashicorp/terraform-plugin-framework/function"
)
// UppercaseFunction converts string to uppercase
type UppercaseFunction struct{}
func NewUppercaseFunction() function.Function {
return &UppercaseFunction{}
}
// Metadata sets the function name
func (f *UppercaseFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "uppercase"
}
// Definition defines parameters and return type
func (f *UppercaseFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Convert string to uppercase",
Description: "Takes a string and returns it in uppercase.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "input",
Description: "The string to convert to uppercase",
},
},
Return: function.StringReturn{},
}
}
// Run executes the function logic
func (f *UppercaseFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var input string
// Get argument value
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &input))
if resp.Error != nil {
return
}
// Execute logic
result := strings.ToUpper(input)
// Set result
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, result))
}
Usage in Terraform
terraform {
required_providers {
cachecloud = {
source = "example/cachecloud"
}
}
}
provider "cachecloud" {}
output "uppercase_name" {
value = provider::cachecloud::uppercase("hello world")
# Output: "HELLO WORLD"
}
Parameter Types
String Parameter
function.StringParameter{
Name: "text",
Description: "Input text",
}
Number Parameter
function.Int64Parameter{
Name: "count",
Description: "Number of items",
}
function.Float64Parameter{
Name: "percentage",
Description: "Percentage value",
}
Boolean Parameter
function.BoolParameter{
Name: "enabled",
Description: "Whether feature is enabled",
}
List Parameter
function.ListParameter{
Name: "items",
Description: "List of items",
ElementType: types.StringType,
}
Map Parameter
function.MapParameter{
Name: "config",
Description: "Configuration map",
ElementType: types.StringType,
}
Object Parameter
function.ObjectParameter{
Name: "settings",
Description: "Settings object",
AttributeTypes: map[string]attr.Type{
"name": types.StringType,
"enabled": types.BoolType,
},
}
Complex Function Example
CIDR Subnet Calculator
package provider
import (
"context"
"fmt"
"math"
"net"
"github.com/hashicorp/terraform-plugin-framework/function"
)
type CIDRSubnetFunction struct{}
func NewCIDRSubnetFunction() function.Function {
return &CIDRSubnetFunction{}
}
func (f *CIDRSubnetFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "cidr_subnet"
}
func (f *CIDRSubnetFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Calculate CIDR subnet",
Description: "Calculates a subnet address within a given IP network address prefix.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "prefix",
Description: "Base CIDR block (e.g., '10.0.0.0/16')",
},
function.Int64Parameter{
Name: "newbits",
Description: "Number of additional bits for subnet",
},
function.Int64Parameter{
Name: "netnum",
Description: "Subnet number",
},
},
Return: function.StringReturn{},
}
}
func (f *CIDRSubnetFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var prefix string
var newbits, netnum int64
// Get arguments
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &prefix, &newbits, &netnum))
if resp.Error != nil {
return
}
// Validate and parse CIDR
ip, ipnet, err := net.ParseCIDR(prefix)
if err != nil {
resp.Error = function.NewArgumentFuncError(0, fmt.Sprintf("Invalid CIDR notation: %s", err))
return
}
// Get original prefix length
originalPrefixLen, _ := ipnet.Mask.Size()
// Calculate new prefix length
newPrefixLen := originalPrefixLen + int(newbits)
if newPrefixLen > 32 {
resp.Error = function.NewArgumentFuncError(1, "New prefix length exceeds 32 bits")
return
}
// Calculate maximum subnet number
maxSubnets := int64(math.Pow(2, float64(newbits)))
if netnum >= maxSubnets {
resp.Error = function.NewArgumentFuncError(2,
fmt.Sprintf("Subnet number %d exceeds maximum %d", netnum, maxSubnets-1))
return
}
// Calculate subnet
ipInt := ipToInt(ip.To4())
subnetSize := int64(math.Pow(2, float64(32-newPrefixLen)))
subnetIP := intToIP(ipInt + (netnum * subnetSize))
// Format result
result := fmt.Sprintf("%s/%d", subnetIP.String(), newPrefixLen)
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, result))
}
// Helper functions
func ipToInt(ip net.IP) int64 {
return int64(ip[0])<<24 | int64(ip[1])<<16 | int64(ip[2])<<8 | int64(ip[3])
}
func intToIP(n int64) net.IP {
return net.IPv4(byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
}
Usage
# Calculate subnets
locals {
vpc_cidr = "10.0.0.0/16"
subnet_bits = 8 # Creates /24 subnets
}
output "subnet_1" {
value = provider::cachecloud::cidr_subnet(local.vpc_cidr, local.subnet_bits, 0)
# Output: "10.0.0.0/24"
}
output "subnet_2" {
value = provider::cachecloud::cidr_subnet(local.vpc_cidr, local.subnet_bits, 1)
# Output: "10.0.1.0/24"
}
output "subnet_3" {
value = provider::cachecloud::cidr_subnet(local.vpc_cidr, local.subnet_bits, 2)
# Output: "10.0.2.0/24"
}
Error Handling
Argument Errors
func (f *Function) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var count int64
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &count))
if count < 0 {
resp.Error = function.NewArgumentFuncError(0, "Count must be non-negative")
return
}
if count > 100 {
resp.Error = function.NewArgumentFuncError(0, "Count must not exceed 100")
return
}
}
General Errors
func (f *Function) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
result, err := complexCalculation()
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Calculation failed: %s", err))
return
}
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, result))
}
Testing Functions
Unit Test
package provider
import (
"context"
"testing"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func TestUppercaseFunction(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "lowercase",
input: "hello",
expected: "HELLO",
},
{
name: "mixed case",
input: "HeLLo WoRLd",
expected: "HELLO WORLD",
},
{
name: "already uppercase",
input: "HELLO",
expected: "HELLO",
},
{
name: "empty string",
input: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := NewUppercaseFunction()
req := function.RunRequest{
Arguments: function.NewArgumentsData([]attr.Value{
types.StringValue(tt.input),
}),
}
resp := function.RunResponse{
Result: function.NewResultData(types.StringType),
}
f.Run(context.Background(), req, &resp)
if resp.Error != nil {
t.Fatalf("unexpected error: %s", resp.Error)
}
var result string
resp.Result.Get(context.Background(), &result)
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
Error Cases
func TestCIDRSubnetFunction_Errors(t *testing.T) {
f := NewCIDRSubnetFunction()
tests := []struct {
name string
prefix string
newbits int64
netnum int64
expectErr bool
}{
{
name: "invalid CIDR",
prefix: "not-a-cidr",
newbits: 8,
netnum: 0,
expectErr: true,
},
{
name: "too many bits",
prefix: "10.0.0.0/16",
newbits: 20, // Would create /36
netnum: 0,
expectErr: true,
},
{
name: "netnum too large",
prefix: "10.0.0.0/16",
newbits: 8,
netnum: 256, // Only 0-255 available
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := function.RunRequest{
Arguments: function.NewArgumentsData([]attr.Value{
types.StringValue(tt.prefix),
types.Int64Value(tt.newbits),
types.Int64Value(tt.netnum),
}),
}
resp := function.RunResponse{
Result: function.NewResultData(types.StringType),
}
f.Run(context.Background(), req, &resp)
if tt.expectErr && resp.Error == nil {
t.Error("expected error but got none")
}
if !tt.expectErr && resp.Error != nil {
t.Errorf("unexpected error: %s", resp.Error)
}
})
}
}
Real-World Examples
Password Generator
type GeneratePasswordFunction struct{}
func (f *GeneratePasswordFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Generate secure password",
Parameters: []function.Parameter{
function.Int64Parameter{
Name: "length",
Description: "Password length",
},
function.BoolParameter{
Name: "special_chars",
Description: "Include special characters",
},
},
Return: function.StringReturn{},
}
}
func (f *GeneratePasswordFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var length int64
var specialChars bool
resp.Error = function.ConcatFuncErrors(resp.Error,
req.Arguments.Get(ctx, &length, &specialChars))
if resp.Error != nil {
return
}
if length < 8 {
resp.Error = function.NewArgumentFuncError(0, "Password must be at least 8 characters")
return
}
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if specialChars {
charset += "!@#$%^&*"
}
password := make([]byte, length)
for i := range password {
password[i] = charset[rand.Intn(len(charset))]
}
resp.Error = function.ConcatFuncErrors(resp.Error,
resp.Result.Set(ctx, string(password)))
}
JSON Parser
type ParseJSONFunction struct{}
func (f *ParseJSONFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Parse JSON string",
Parameters: []function.Parameter{
function.StringParameter{
Name: "json",
Description: "JSON string to parse",
},
},
Return: function.DynamicReturn{},
}
}
func (f *ParseJSONFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var jsonStr string
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &jsonStr))
if resp.Error != nil {
return
}
var data interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Invalid JSON: %s", err))
return
}
// Convert to Terraform value
tfValue, err := convertToTerraformValue(data)
if err != nil {
resp.Error = function.NewFuncError(fmt.Sprintf("Conversion error: %s", err))
return
}
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, tfValue))
}
Best Practices
1. Clear Naming
// ✅ Good: Descriptive name
func NewCalculateCIDRSubnetFunction() function.Function
// ❌ Bad: Vague name
func NewCalcFunction() function.Function
2. Comprehensive Documentation
// ✅ Good: Complete documentation
resp.Definition = function.Definition{
Summary: "Calculate CIDR subnet",
Description: "Calculates a subnet address within a given IP network address prefix. Returns a CIDR notation string.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "prefix",
Description: "Base CIDR block in format '10.0.0.0/16'",
},
},
}
// ❌ Bad: Minimal documentation
resp.Definition = function.Definition{
Summary: "Calc subnet",
}
3. Input Validation
// ✅ Good: Validate inputs
if length < 8 || length > 128 {
resp.Error = function.NewArgumentFuncError(0,
"Length must be between 8 and 128")
return
}
// ❌ Bad: No validation
result := generate(length)
4. Error Context
// ✅ Good: Specific error
resp.Error = function.NewArgumentFuncError(1,
fmt.Sprintf("Subnet number %d exceeds maximum %d", netnum, maxSubnets-1))
// ❌ Bad: Generic error
resp.Error = function.NewFuncError("Invalid input")
Key Takeaways
- ✅ Functions extend Terraform’s capabilities
- ✅ Implement Function interface (Metadata, Definition, Run)
- ✅ Support various parameter types
- ✅ Provide clear documentation
- ✅ Validate inputs thoroughly
- ✅ Return specific error messages
- ✅ Write comprehensive tests
- ✅ Use for data transformation, not side effects
What’s Next?
Learn to design complex attribute types for sophisticated resource schemas.
Next: Complex Attribute Types →
Previous: ← Advanced Topics
Next: Complex Attribute Types →
Last updated: July 1, 2026