Skip to content

Latest commit

 

History

History
334 lines (248 loc) · 17 KB

File metadata and controls

334 lines (248 loc) · 17 KB
page_title description
Plugin Development - Framework: Implement Functions
How to implement provider-defined functions in the provider development framework.

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:

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:

  1. 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.
  2. Reading argument data from the function.RunRequest.Arguments field.
  3. Performing any computational logic.
  4. 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{}
}