Provider-Defined Functions

7 minute read

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

Updated: