Terraform
Implement Functions
The framework supports implementing functions based on Terraform's concepts for provider-defined functions. It is recommended to understand those concepts before implementing a function since the terminology is used throughout this page and there are details that simplify function handling as compared to other provider concepts. Provider-defined functions are supported in Terraform 1.8 and later.
The main code components of a function implementation are:
- Defining the function including its name, expected data types, descriptions, and logic.
- Adding the function to the provider so it is accessible by Terraform and practitioners.
Once the code is implemented, it is always recommended to also add:
- Testing to ensure expected function behaviors.
- Documentation to ensure the function is discoverable by practitioners with usage information.
Define Function Type
Implement the function.Function
interface. Each of the methods is described in more detail below.
In this example, a function named echo
is defined, which takes a string argument and returns that value as the result:
import (
"github.com/hashicorp/terraform-plugin-framework/function"
)
// Ensure the implementation satisfies the desired interfaces.
var _ function.Function = &EchoFunction{}
type EchoFunction struct {}
func (f *EchoFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "echo"
}
func (f *EchoFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Echo a string",
Description: "Given a string value, returns the same value.",
Parameters: []function.Parameter{
function.StringParameter{
Name: "input",
Description: "Value to echo",
},
},
Return: function.StringReturn{},
}
}
func (f *EchoFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var input string
// Read Terraform argument data into the variable
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &input))
// Set the result to the same data
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, input))
}
Metadata Method
The function.Function
interface Metadata
method defines the function name as it would appear in Terraform configurations. Unlike resources and data sources, this name should NOT include the provider name as the configuration language syntax for calling functions will separately include the provider name. Refer to naming for additional best practice details.
In this example, the function name is set to example
:
// With the function.Function implementation
func (f *ExampleFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "example"
}
Definition Method
The function.Function
interface Definition
method defines the parameters, return, and various descriptions for documentation of the function.
In this example, the function definition includes one string parameter, a string return, and descriptions for documentation:
func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Echo a string",
Description: "Given a string value, returns the same value.",
Parameters: []function.Parameter{
function.StringParameter{
Description: "Value to echo",
Name: "input",
},
},
Return: function.StringReturn{},
}
}
Return
The Return
field must be defined as all functions must return a result. This influences how the Run method must set the result data. Refer to the returns documentation for details about all available types and how to handle data with each type.
Parameters
There may be zero or more parameters, which are defined with the Parameters
field. They are ordered, which influences how practitioners call the function in their configurations and how the Run method must read the argument data. Refer to the parameters documentation for details about all available types and how to handle data with each type.
An optional VariadicParameter
field enables a final variadic parameter which accepts zero, one, or more values of the same type. It may be optionally combined with Parameters
, meaning it represents the any argument data after the final parameter. When reading argument data, a VariadicParameter
is represented as a tuple, with each element matching the parameter type; the tuple has zero or more elements to match the given arguments.
By default, Terraform will not pass null or unknown values to the provider logic when a function is called. Within each parameter, use the AllowNullValue
and/or AllowUnknownValues
fields to explicitly allow those kinds of values. Enabling AllowNullValue
requires using a pointer type or framework type when reading argument data. Enabling AllowUnknownValues
requires using a framework type when reading argument data.
Documentation
The function documentation page describes how to implement documentation so it is available to Terraform, downstream tooling such as practitioner configuration editor integrations, and in the Terraform Registry.
Deprecation
If a function is being deprecated, such as for future removal, the DeprecationMessage
field should be set. The message should be actionable for practitioners, such as telling them what to do with their configuration instead of calling this function.
Run Method
The function.Function
interface Run
method defines the logic that is invoked when Terraform calls the function. Only argument data is provided when a function is called. Refer to HashiCorp Provider Design Principles for additional best practice details.
Implement the Run
method by:
- Creating variables for argument data, based on the parameter definitions. Refer to the parameters documentation for details about all available parameter types and how to handle data with each type.
- Reading argument data from the
function.RunRequest.Arguments
field. - Performing any computational logic.
- Setting the result value, based on the return definition, into the
function.RunResponse.Result
field. Refer to the returns documentation for details about all available return types and how to handle data with each type.
If the logic needs to return a function error, it can be added into the function.RunResponse.Error
field.
Reading Argument Data
The framework supports two methodologies for reading argument data from the function.RunRequest.Arguments
field, which is of the function.ArgumentsData
type.
The first option is using the (function.ArgumentsData).Get()
method to read all arguments at once. The framework will return an error if the number and types of target variables does not match the argument data.
In this example, the parameters are defined as a boolean and string which are read into Go built-in bool
and string
variables since they do not opt into null or unknown value handling:
func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
// ... other fields ...
Parameters: []function.Parameter{
function.BoolParameter{
Name: "bool_param",
// ... other fields ...
},
function.StringParameter{
Name: "string_param",
// ... other fields ...
},
},
}
}
func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var boolArg bool
var stringArg string
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &stringArg))
// ... other logic ...
}
The second option is using (function.ArgumentsData).GetArgument()
method to read individual arguments. The framework will return an error if the argument position does not exist or if the type of the target variable does not match the argument data.
In this example, the parameters are defined as a boolean and string and the first argument is read into a Go built-in bool
variable since it does not opt into null or unknown value handling:
func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
// ... other fields ...
Parameters: []function.Parameter{
function.BoolParameter{
Name: "bool_param",
// ... other fields ...
},
function.StringParameter{
Name: "string_param",
// ... other fields ...
},
},
}
}
func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var boolArg bool
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 0, &boolArg))
// ... other logic ...
}
Reading Variadic Parameter Argument Data
The optional VariadicParameter
field in a function definition enables a final variadic parameter which accepts zero, one, or more values of the same type. It may be optionally combined with Parameters
, meaning it represents the argument data after the final parameter. When reading argument data, a VariadicParameter
is represented as a tuple, with each element matching the parameter type; the tuple has zero or more elements to match the given arguments.
Use either the framework tuple type or a Go slice of an appropriate type to match the variadic parameter []T
.
In this example, there is a boolean parameter and string variadic parameter, where the variadic parameter argument data is always fetched as a slice of string
:
func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
// ... other fields ...
Parameters: []function.Parameter{
function.BoolParameter{
Name: "bool_param",
// ... other fields ...
},
},
VariadicParameter: function.StringParameter{
Name: "variadic_param",
// ... other fields ...
},
}
}
func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var boolArg bool
var stringVarg []string
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &stringVarg))
// ... other logic ...
}
If it is necessary to return a function error for a specific variadic argument, note that Terraform treats each zero-based argument position individually unlike how the framework exposes the argument data. Add the number of non-variadic parameters (if any) to the variadic argument tuple element index to ensure the error is aligned to the correct argument in the configuration.
In this example with two parameters and one variadic parameter, an error is returned for variadic arguments:
func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
// ... other fields ...
Parameters: []function.Parameter{
function.BoolParameter{
Name: "bool_param",
// ... other fields ...
},
function.Int64Parameter{
Name: "int64_param",
// ... other fields ...
},
},
VariadicParameter: function.StringParameter{
Name: "variadic_param",
// ... other fields ...
},
}
}
func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var boolArg bool
var int64Arg int64
var stringVarg []string
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &int64arg, &stringVarg))
for index, element := range stringVarg {
// Added by 2 to match the definition including two parameters.
resp.Error = function.ConcatFuncErrors(resp.Error, function.NewArgumentFuncError(2+index, "example summary: example detail"))
}
// ... other logic ...
}
Setting Result Data
The framework supports setting a result value into the function.RunResponse.Result
field, which is of the function.ResultData
type. The result value must match the return type, otherwise the framework or Terraform will return an error.
In this example, the return is defined as a string and a string value is set:
func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
// ... other fields ...
Return: function.StringReturn{},
}
}
func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
// ... other logic ...
// Value based on the return type. Returns can also use the framework type system.
result := "hardcoded example"
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, result))
}
Add Function to Provider
Functions become available to practitioners when they are included in the provider implementation via the provider.ProviderWithFunctions
interface Functions
method.
In this example, the EchoFunction
type, which implements the function.Function
interface, is added to the provider implementation:
// With the provider.Provider implementation
func (p *ExampleCloudProvider) Functions(_ context.Context) []func() function.Function {
return []func() function.Function{
func() function.Function {
return &EchoFunction{},
},
}
}
To simplify provider implementations, a named function can be created with the function implementation.
In this example, the EchoFunction
code includes an additional NewEchoFunction
function, which simplifies the provider implementation:
// With the provider.Provider implementation
func (p *ExampleCloudProvider) Functions(_ context.Context) []func() function.Function {
return []func() function.Function{
NewEchoFunction,
}
}
// With the function.Function implementation
func NewEchoFunction() function.Function {
return &EchoFunction{}
}