Skip to content

Commit cb7889b

Browse files
committed
fix(dns_record): unify dynamic flags type in list data source
Records returned by the dns_records data source can have differing concrete underlying types for data.flags (CAA records carry a number while A records omit the field and decode as a typeless dynamic null). Terraform's list serialiser requires a uniform element type, so the differing per-record types crashed the provider with 'inconsistent list element types'. Normalise the dynamic flags so every element exposes the same underlying type before constructing the list. Closes #7004 Signed-off-by: SAY-5 <saiasish.cnp@gmail.com>
1 parent b2277b8 commit cb7889b

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

internal/services/dns_record/list_data_source.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
1212
"github.com/hashicorp/terraform-plugin-framework/attr"
1313
"github.com/hashicorp/terraform-plugin-framework/datasource"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
1416
)
1517

1618
type DNSRecordsDataSource struct {
@@ -92,9 +94,113 @@ func (d *DNSRecordsDataSource) Read(ctx context.Context, req datasource.ReadRequ
9294
}
9395

9496
acc = acc[:min(len(acc), maxItems)]
97+
98+
// Records returned in the same list may have differing concrete underlying
99+
// types for `data.flags` (e.g. CAA records carry a number, A records omit
100+
// the field and decode as a typeless dynamic null). Terraform's list
101+
// serialiser requires every element of a list to share an identical type,
102+
// so the differing per-record types crash the data source with
103+
// "inconsistent list element types". Normalise the dynamic flags so every
104+
// element exposes the same underlying type before constructing the list.
105+
// See https://github.com/cloudflare/terraform-provider-cloudflare/issues/7004
106+
normalizeFlagsTypes(ctx, acc)
107+
95108
result, diags := customfield.NewObjectListFromAttributes[DNSRecordsResultDataSourceModel](ctx, acc)
96109
resp.Diagnostics.Append(diags...)
97110
data.Result = result
98111

99112
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
100113
}
114+
115+
// normalizeFlagsTypes ensures every element's `data.flags` exposes the same
116+
// concrete underlying type. When any sibling carries a known concrete value, an
117+
// otherwise typeless dynamic null is rewrapped as a typed null of that type so
118+
// the resulting list has a uniform element type.
119+
func normalizeFlagsTypes(ctx context.Context, acc []attr.Value) {
120+
if len(acc) < 2 {
121+
return
122+
}
123+
124+
models := make([]*DNSRecordsResultDataSourceModel, len(acc))
125+
126+
var concreteType attr.Type
127+
for i, el := range acc {
128+
obj, ok := el.(customfield.NestedObject[DNSRecordsResultDataSourceModel])
129+
if !ok {
130+
return
131+
}
132+
m, diags := obj.Value(ctx)
133+
if diags.HasError() || m == nil {
134+
return
135+
}
136+
models[i] = m
137+
if concreteType != nil {
138+
continue
139+
}
140+
data, diags := m.Data.Value(ctx)
141+
if diags.HasError() || data == nil {
142+
continue
143+
}
144+
dyn, _ := data.Flags.ToDynamicValue(ctx)
145+
if dyn.IsNull() || dyn.IsUnknown() {
146+
continue
147+
}
148+
if underlying := dyn.UnderlyingValue(); underlying != nil && !underlying.IsNull() && !underlying.IsUnknown() {
149+
concreteType = underlying.Type(ctx)
150+
}
151+
}
152+
153+
if concreteType == nil {
154+
return
155+
}
156+
157+
typedNull, err := makeTypedDynamicNull(concreteType)
158+
if err {
159+
return
160+
}
161+
162+
changed := false
163+
for _, m := range models {
164+
data, diags := m.Data.Value(ctx)
165+
if diags.HasError() || data == nil {
166+
continue
167+
}
168+
dyn, _ := data.Flags.ToDynamicValue(ctx)
169+
if !dyn.IsNull() {
170+
if underlying := dyn.UnderlyingValue(); underlying != nil && !underlying.IsNull() {
171+
continue
172+
}
173+
}
174+
data.Flags = typedNull
175+
m.Data = customfield.NewObjectMust(ctx, data)
176+
changed = true
177+
}
178+
179+
if !changed {
180+
return
181+
}
182+
183+
for i, m := range models {
184+
obj, diags := customfield.NewObject(ctx, m)
185+
if diags.HasError() {
186+
return
187+
}
188+
acc[i] = obj
189+
}
190+
}
191+
192+
// makeTypedDynamicNull wraps a typed null of the given concrete type into a
193+
// NormalizedDynamicValue so that ToTerraformValue serialises a typed null
194+
// rather than the unconstrained DynamicPseudoType.
195+
func makeTypedDynamicNull(t attr.Type) (customfield.NormalizedDynamicValue, bool) {
196+
var inner attr.Value
197+
switch t.(type) {
198+
case basetypes.Float64Type:
199+
inner = types.Float64Null()
200+
case basetypes.StringType:
201+
inner = types.StringNull()
202+
default:
203+
return customfield.NormalizedDynamicValue{}, true
204+
}
205+
return customfield.RawNormalizedDynamicValueFrom(inner), false
206+
}

0 commit comments

Comments
 (0)