Terraform Plugin Framework: Computed Attribute Known Only After Apply
How to reduce noise from computed attributes
This post is part of a series on the Terraform Plugin Framework:
- Terraform Plugin Framework: Optional Attributes With Defaults
- Terraform Plugin Framework: Computed Attributes Known Only After Apply
- Terraform Plugin Framework: Use State For Unknown
UseStateForUnknown()
plan modifier; see Terraform Plugin Framework: Use State For Unknown
I continue to struggle to find good documentation on the recommended Terraform Plugin Framework.
Today’s journey started with a new custom provider resource with a couple computed fields:
func (r *EnvironmentResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Environment resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "internal terraform resource id (matches the uid when the Environment has been created/imported)",
Computed: true,
},
"uid": schema.StringAttribute{
MarkdownDescription: "internal contentstack identifier",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "name of the Environment",
Required: true,
},
"created_at": schema.StringAttribute{
MarkdownDescription: "created_at of the Global Field",
Computed: true,
},
"updated_at": schema.StringAttribute{
MarkdownDescription: "updated_at of the Global Field",
Computed: true,
},
"version": schema.Int64Attribute{
MarkdownDescription: "version number of the Environment",
Computed: true,
},
},
}
}
In this example name
is the only required attribute; the others are all computed.
Semantically, however some attributes (id
, uid
and created_at
) are computed only once on creation or import while other attributes (updated_at
, version
) are recomputed on every update.
Given the following resource definition:
resource "contentstack_environment" "trial" {
name = "trial"
}
We get the following plan:
Terraform will perform the following actions:
# contentstack_environment.trial will be updated in-place
~ resource "contentstack_environment" "trial" {
~ created_at = "TBD" -> (known after apply)
id = "blt8a8eefa2c95a72fd" -> (known after apply)
name = "trial"
uid = "blt8a8eefa2c95a72fd" -> (known after apply)
~ updated_at = "TBD" -> (known after apply)
~ version = 4 -> (known after apply)
}
Plan: 0 to add, 1 to change, 0 to destroy.
We can tell terraform that some of these values will not change across plans by using a custom PlanModifier
:
func (r *EnvironmentResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Environment resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "internal terraform resource id (matches the uid when the Environment has been created/imported)",
Computed: true,
PlanModifiers: []planmodifier.String{
mystringplanmodifiers.CopyStateValue(),
},
},
"uid": schema.StringAttribute{
MarkdownDescription: "internal contentstack identifier",
Computed: true,
PlanModifiers: []planmodifier.String{
mystringplanmodifiers.CopyStateValue(),
},
},
"name": schema.StringAttribute{
MarkdownDescription: "name of the Environment",
Required: true,
},
"created_at": schema.StringAttribute{
MarkdownDescription: "created_at of the Global Field",
Computed: true,
PlanModifiers: []planmodifier.String{
mystringplanmodifiers.CopyStateValue(),
},
},
"updated_at": schema.StringAttribute{
MarkdownDescription: "updated_at of the Global Field",
Computed: true,
},
"version": schema.Int64Attribute{
MarkdownDescription: "version number of the Environment",
Computed: true,
},
},
}
}
Where the CopyStateValue
plan modifier looks like this:
package stringplanmodifier
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)
type copyStateValue struct{}
// CopyStateValue return a string plan modifier that copies the
// existing value from the previous plan to the new plan so that
// it doesn't show up as pending.
func CopyStateValue() planmodifier.String {
return copyStateValue{}
}
func (m copyStateValue) Description(context.Context) string {
return fmt.Sprintf("TBD")
}
func (m copyStateValue) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}
func (m copyStateValue) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
resp.PlanValue = req.StateValue
}
Now running a plan on the same terraform resource gives us
Terraform will perform the following actions:
# contentstack_environment.trial will be updated in-place
~ resource "contentstack_environment" "trial" {
~ created_at = "TBD" -> (known after apply)
id = "blt8a8eefa2c95a72fd"
name = "trial"
uid = "blt8a8eefa2c95a72fd"
~ updated_at = "TBD"
~ version = 5 -> (known after apply)
}
Plan: 0 to add, 1 to change, 0 to destroy.