Hi,
I’m using the terraform-plugin-framework for one of our internal apis.
I have a small resource that looks something like this:
{
"id" : "123-123-123-123",
// other fields ....
"group": {
// other non-remote fields with static defaults
"name": "GROUP1"
}
}
The GROUP1
is a value that needs to be fetched from a datasource (api list endpoint, that requires authorization).
It is a required value, but in order for the user to rely on sane defaults, I would like to provide a default value for the whole (nested) object in case that the user did not define that (nested) object in his terraform configuration file (= nil).
The credentials for that api are configured on the provider level so that I can pass them around wherever my http clients need them.
My approach for fetching those defaults was to use a plan modifier for that whole (nested) object which requires an http client to be injected into the schema.Schema
in the Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse)
method (see below)
My problem is that, due to the order of code execution, my client in groupPlanModifier
is never initialized and stays as nil
in the custom plan modifier implementation.
My question is, whether this approach is the correct one or what would be the better approach to get this dynamic remote value to be set as default value
- dynamic: the value may change depending on what is in the backend database
- remote: must be fetched from an api
- default value: provided by the provider in case that the user did not configure anything
Maybe my whole approach in putting this into the provider is incorrect, as it might belong into the actual backend api?
package resourcename
type Resource struct {
client *resourceclient.ResourceClient
}
func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = model.NewResourceSchema(&r.client)
}
func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
// initialize resource client with provider credentials
resp.Diagnostics.Append(model.SetResourceClient(req, &r.client)...)
}
func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// client usage works in here just fine
}
func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// client usage works in here just fine
}
func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// client usage works in here just fine
}
func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// client usage works in here just fine
}
package model
func NewResourceSchema(client **resourceclient.ResourceClient) schema.Schema {
// last phase is the default phase
return schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"group": schema.SingleNestedAttribute{
Computed: true,
PlanModifiers: []planmodifier.Object{
newGroupPlanModifier(client),
},
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Name of the group.",
},
},
},
},
}
}
var (
_ planmodifier.Object = (*groupPlanModifier)(nil)
)
type groupPlanModifier struct {
client **resourceclient.ResourceClient
}
func newGroupPlanModifier(client **ransombridgeclient.RansomBridgeClient) *groupPlanModifier {
return &groupPlanModifier{
client: client,
}
}
// PlanModifyObject should perform the modification.
func (m *groupPlanModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) {
if !req.State.Raw.IsNull() {
// https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification
// resource is not being created, do nothing
return
}
// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}
client := *m.client // client is nil
name, err := client.GetDefaultGroupName(ctx)
if err != nil {
resp.Diagnostics.AddError("Failed to get default group name", err.Error())
return
}
defaultValue := struct {
GroupName string `tfsdk:"name"`
}{
GroupName: name,
}
value, d := types.ObjectValueFrom(ctx, GroupributeTypes, defaultValue)
if d.HasError() {
resp.Diagnostics.Append(d...)
return
}
resp.PlanValue = value
}