Hooks and plan modifiers
You can customize your Terraform provider using hooks and plan modifiers.
Hooks are custom functions that hook into the API request and response flow, allowing you to manipulate request data, add functionality like logging, or call additional endpoints.
Plan modifiers are custom functions that hook into the plan process, allowing you to perform attribute plan modification or resource plan modification.
Hooks
Just like with SDK generation, hooks can be used to customize the API request and response flow. The hooks code will be run before a request is sent, after a response is received, and if the API returns an error.
When you generate a Terraform provider, a Go SDK is generated for your API, and this is used internally in the provider. You can add hooks to the Go SDK client by updating the code in the hooks/terraform
folder.
You can read more about hooks in our hooks documentation.
Plan modifiers
Plan modifiers are specific to Terraform providers, and allow your SDK to modify the plan that is generated by Terraform. This includes adjusting values, adding logging or other tracing, marking resources for replacement when in-place updates are not supported, or giving warnings on resource deletion.
2 types of plan modifiers are supported:
Plan modifiers live in a folder called customPlanModifiers
in the same folder as your liblab config file. If this folder exists with the relevant plan modifier code files, then the generated SDK will include the plan modifier code. To remove this from your SDK, delete the customPlanModifiers
folder and re-generate your SDK.
.
├── customPlanModifiers
│ └──
└── liblab.config.json
Attribute plan modifiers
Attribute plan modifiers can be used to modify values for attributes, as well as adding functionality such as logging or tracing.
Create the attribute plan modifier
Attribute plan modifiers live in .go
files with whatever name makes sense to you, in a subfolder called attributes
in the customPlanModifiers
folder.
.
├── customPlanModifiers
│ └── attributes
│ └── my_plan_modifier.go
└── liblab.config.json
These plan modifiers can be shared between multiple properties. One property can have multiple plan modifiers, and multiple properties can use the same plan modifier. This is why the name of the plan modifier is not important in code, only to you.
Each file needs to implement one of the Terraform plan modifier interfaces. These are typed to the attributes type, so you need to implement the relevant set. This file does not have a package declaration statement - this code will be included in a package the generated SDK, so must not declare a package.
For example, to implement a plan modifier for a bool
property, you would need to implement the BoolPlanModifier
interface imported from github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier
.
You will also need to export a function to create the plan modifier.
For example, to implement a plan modifier for boolean
attributes that logs the value, your code might look like this:
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)
func LogBool() planmodifier.Bool {
return logBool{}
}
type logBool struct{}
func (l logBool) Description(_ context.Context) string {
return "Logs boolean values"
}
func (l logBool) MarkdownDescription(_ context.Context) string {
return "Logs boolean values"
}
func (l logBool) PlanModifyBool(ctx context.Context,
req planmodifier.BoolRequest,
resp *planmodifier.BoolResponse) {
logger.Log(fmt.Sprintf("%s bool value: %t", req.Path, req.PlanValue))
return
}
In this example the LogBool
function is exported and this created the plan modifier.
Add the attribute plan modifier to a property
Plan modifiers can be attached to a property using the x-liblab-plan-modifiers
annotation. One property can have many plan modifiers, and many properties can use the same plan modifier.
- JSON
- YAML
"properties": {
"isCold": {
"type": "boolean",
"x-liblab-plan-modifiers": ["LogBool","TraceBool"]
},
"isNew": {
"type": "boolean",
"x-liblab-plan-modifiers": ["LogBool"]
}
}
properties:
isCold:
type: boolean
x-liblab-plan-modifiers: [LogBool,TraceBool]
isNew:
type: boolean
x-liblab-plan-modifiers: [LogBool]
The value of the x-liblab-plan-modifiers
annotation is an array of strings, where each string is the name of a plan modifier. The name of the plan modifier is the name of the exported function in the plan modifier code file.
liblab will infer the schema for each endpoint using a set of rules. You may need to refer to these to determine what property to attach the annotation to. You can read more in our resource schemas guide.
Resource plan modifiers
Resource plan modifiers allow you to add logic that applies to resources as a whole such as marking resources for replacement if in-place updates are not available, or return diagnostics during resource destruction.
Create the resource plan modifier
Resource plan modifiers live in .go
files with the same name as the resource they are connected to, in a subfolder called resources
in the customPlanModifiers
folder. Resources are defined in your API spec using the x-liblab-resource
annotation.
For example, if you have the following resource:
- JSON
- YAML
"paths": {
"/soda": {
"get": {
"x-liblab-resource": "Soda#Read",
"operationId": "GetSoda",
...
}
}
}
paths:
/soda:
get:
x-liblab-resource: "Soda#Read"
operationId: GetSoda
...
It would live in a file called Soda.go
, in the customPlanModifiers/resources
folder:
.
├── customPlanModifiers
│ └── resources
│ └── Soda.go
└── liblab.config.json
This file needs to implement the ResourceWithModifyPlan
interface, which has a single method ModifyPlan
. This is implemented on a struct that is named after the resource, with the Resource
suffix.
For example, for the Soda
resource mention above, the struct would be SodaResource
. The custom plan modifier code would look like this:
func (r *SodaResource) ModifyPlan(ctx context.Context,
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse) {
// Fill in logic.
}
The contents of this file is inserted into the generated resource.go
file for the resource, therefor you do not need to declare a package. You can add imports to this file, but you do not need to add any of the following as these will already be imported:
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path/"
"github.com/hashicorp/terraform-plugin-framework/resource/"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/"
"github.com/hashicorp/terraform-plugin-framework/types/"
"internal/utils"
"<GoSdkModuleName>/pkg/client"
)
<GoSdkModuleName>/pkg/client
is the Go SDK client that is generated for your API. internal/utils
is a package that contains helper functions used by the resource struct.
Reference the HTTP client
If you need to access the underlying HTTP client to make additional API calls, you can do so by importing net/http
. For example:
import (
"net/http" // imports need to have the prefix (net/http instead of just http)
)
func (r *SodaResource) ModifyPlan(ctx context.Context,
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse) {
// perform whatever api check
_, err := http.Get("https://example.com")
...
}
Reference the SDK client
You can also access the SDK client that gives you access to the generated Go SDK that is used internally in the provider. This is useful if you want to make additional API calls, or if you want to use the SDK to get the current state of a resource. The SDK is available on the client
field on the resource. For example:
func (r *SodaResource) ModifyPlan(ctx context.Context,
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse) {
// Call the API
someResource, err := r.client.SodaService.GetFlavors(id)
if err != nil {
resp.Diagnostics.AddError("Error fetching example", err.Error())
}
...
}
Access the resource model
The interface for the resource model is defined as the resource name suffixed with ResourceModel
. For example, for the Soda
resource mentioned above, the interface would be SodaResourceModel
. The data in this model can be populated from the resource context. For example:
func (r *SodaResource) ModifyPlan(ctx context.Context,
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse) {
// Get the resource model
var data ResourceNameResourceModel
// Populate the model data
utils.PopulateModelData(ctx, &data, resp.Diagnostics, req.Plan.Get)
...
}
Tips for writing plan modifiers
Writing customer plan modifiers can be tricky, so here are some tips to help you get started.
Write the resource plan modifier in the SDK
liblab will pick up the plan modifier code from the customPlanModifiers
folder, but writing code in this folder can be tricky as you don't have access to intellisense or autocomplete as this code has no knowledge of the generated provider code, SDK client. or the required Go packages. A helpful process to use to write a plan modifier is:
- Generate the provider without any plan modifiers
- Find the
resource.go
file in the generated provider for the resource you want to write a plan modifier for - Implement the
ModifyPlan
method on theResource
struct in this file - Compile and test the provider
- Once you are happy with the plan modifier, copy the code into a new
.go
file in thecustomPlanModifiers
folder. You only need to add theModifyPlan
method, any imports you added (not the existing ones), and any additional code you wrote. - Once you have copied the code, regenerate the provider.