fix: preserve empty list vs null distinction for optional list attributes
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

The API returns null for empty arrays, but OpenTofu requires that the
state match the plan exactly — an empty list [] in the plan must remain
[] in the state, not become null. This caused "inconsistent result after
apply" errors on every resource with empty optional list fields like
mutable_patterns and ban_tags.
This commit is contained in:
2026-06-21 18:42:14 +10:00
parent 7c94f06be6
commit d9d8cc7b6d
3 changed files with 67 additions and 3 deletions
+11
View File
@@ -181,6 +181,7 @@ func (r *remoteResource) Create(ctx context.Context, req resource.CreateRequest,
}
state := r.apiToModel(ctx, created)
reconcileOptionalLists(&plan, &state)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
@@ -203,6 +204,7 @@ func (r *remoteResource) Read(ctx context.Context, req resource.ReadRequest, res
}
newState := r.apiToModel(ctx, remote)
reconcileOptionalLists(&state, &newState)
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
}
@@ -223,6 +225,7 @@ func (r *remoteResource) Update(ctx context.Context, req resource.UpdateRequest,
}
state := r.apiToModel(ctx, updated)
reconcileOptionalLists(&plan, &state)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
@@ -243,6 +246,14 @@ func (r *remoteResource) ImportState(ctx context.Context, req resource.ImportSta
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
func reconcileOptionalLists(prior, current *remoteResourceModel) {
current.Patterns = preserveListNullEmptySemantics(prior.Patterns, current.Patterns)
current.Blocklist = preserveListNullEmptySemantics(prior.Blocklist, current.Blocklist)
current.MutablePatterns = preserveListNullEmptySemantics(prior.MutablePatterns, current.MutablePatterns)
current.ImmutablePatterns = preserveListNullEmptySemantics(prior.ImmutablePatterns, current.ImmutablePatterns)
current.BanTags = preserveListNullEmptySemantics(prior.BanTags, current.BanTags)
}
func (r *remoteResource) modelToAPI(ctx context.Context, m remoteResourceModel) remoteAPI {
api := remoteAPI{
Name: m.Name.ValueString(),