@shiyuhang0 one possibility is that there another plan difference occurring somewhere, but that it is not displayed in the human-readable output from the CLI (i.e., only the cluster_status change is being displayed).
Could I ask you to check the following:
- Set the logging to trace and see if the framework is reporting a difference and triggering its unknown marking, and whether logging has anything about which attribute(s) caused the behavior to occur? Trace-level logging can be activated by using TF_LOG=TRACE terraform plan
- Examine the machine-readable/JSON plan output to see if there are any additional plan differences that are not displayed in the CLI? You can use
terraform plan -json for this purpose.
- Another option is to use Terraform
1.8.0-beta1 and see if more information is visible for the planned changes, as this version has some updates which will display certain instances in which there were changes between empty strings (“”) and null in the CLI plan output.
There are a few other considerations in the context of the cluster_status attribute:
- Whether that attribute needs to be there at all (is it useful to practitioners)?
- Whether that attribute needs to be changed on update? If not one option would be not to set the API response value for it into state.
- Whether that attribute should be planned to change to “modifying” to match the API behavior?
- Whether the resource update should wait for the modifications before returning, which would theoretically allow this attribute to remain “available”.
I tried to reproduce what you are seeing with the following minimal code. You can see that even though the value for cluster_status changes on Create, Read, and Update, there is no “drift” which is one of the reasons for being suspicious that there may be other changes in the plan that are hidden from the CLI output.
I’d be interested to know whether you’re able to reproduce what you are seeing by modifying the following:
var _ resource.Resource = (*playgroundResource)(nil)
type playgroundResource struct {
}
func NewResource() resource.Resource {
return &playgroundResource{}
}
func (e *playgroundResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_resource"
}
func (e *playgroundResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"configurable_attribute": schema.StringAttribute{
Optional: true,
},
"status": schema.SingleNestedAttribute{
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"cluster_status": schema.StringAttribute{
Computed: true,
},
},
},
},
}
}
type clusterResourceData struct {
ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
Status types.Object `tfsdk:"status"`
}
func (e *playgroundResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data clusterResourceData
diags := req.Plan.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
statusObj, diags := types.ObjectValue(
map[string]attr.Type{
"cluster_status": types.StringType,
},
map[string]attr.Value{
"cluster_status": types.StringValue(time.Now().String()),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Status = statusObj
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
func (e *playgroundResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data clusterResourceData
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
statusObj, diags := types.ObjectValue(
map[string]attr.Type{
"cluster_status": types.StringType,
},
map[string]attr.Value{
"cluster_status": types.StringValue(time.Now().String()),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Status = statusObj
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
func (e *playgroundResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data clusterResourceData
diags := req.Plan.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
statusObj, diags := types.ObjectValue(
map[string]attr.Type{
"cluster_status": types.StringType,
},
map[string]attr.Value{
"cluster_status": types.StringValue(time.Now().String()),
},
)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.Status = statusObj
diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
func (e *playgroundResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data clusterResourceData
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}