Skip to main content

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.

"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:

"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:

  1. Generate the provider without any plan modifiers
  2. Find the resource.go file in the generated provider for the resource you want to write a plan modifier for
  3. Implement the ModifyPlan method on the Resource struct in this file
  4. Compile and test the provider
  5. Once you are happy with the plan modifier, copy the code into a new .go file in the customPlanModifiers folder. You only need to add the ModifyPlan method, any imports you added (not the existing ones), and any additional code you wrote.
  6. Once you have copied the code, regenerate the provider.