Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions internal/services/dns_record/list_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

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

acc = acc[:min(len(acc), maxItems)]

// Records returned in the same list may have differing concrete underlying
// types for `data.flags` (e.g. CAA records carry a number, A records omit
// the field and decode as a typeless dynamic null). Terraform's list
// serialiser requires every element of a list to share an identical type,
// so the differing per-record types crash the data source with
// "inconsistent list element types". Normalise the dynamic flags so every
// element exposes the same underlying type before constructing the list.
// See https://github.com/cloudflare/terraform-provider-cloudflare/issues/7004
normalizeFlagsTypes(ctx, acc)

result, diags := customfield.NewObjectListFromAttributes[DNSRecordsResultDataSourceModel](ctx, acc)
resp.Diagnostics.Append(diags...)
data.Result = result

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// normalizeFlagsTypes ensures every element's `data.flags` exposes the same
// concrete underlying type. When any sibling carries a known concrete value, an
// otherwise typeless dynamic null is rewrapped as a typed null of that type so
// the resulting list has a uniform element type.
func normalizeFlagsTypes(ctx context.Context, acc []attr.Value) {
if len(acc) < 2 {
return
}

models := make([]*DNSRecordsResultDataSourceModel, len(acc))

var concreteType attr.Type
for i, el := range acc {
obj, ok := el.(customfield.NestedObject[DNSRecordsResultDataSourceModel])
if !ok {
return
}
m, diags := obj.Value(ctx)
if diags.HasError() || m == nil {
return
}
models[i] = m
if concreteType != nil {
continue
}
data, diags := m.Data.Value(ctx)
if diags.HasError() || data == nil {
continue
}
dyn, _ := data.Flags.ToDynamicValue(ctx)
if dyn.IsNull() || dyn.IsUnknown() {
continue
}
if underlying := dyn.UnderlyingValue(); underlying != nil && !underlying.IsNull() && !underlying.IsUnknown() {
concreteType = underlying.Type(ctx)
}
}

if concreteType == nil {
return
}

typedNull, err := makeTypedDynamicNull(concreteType)
if err {
return
}

changed := false
for _, m := range models {
data, diags := m.Data.Value(ctx)
if diags.HasError() || data == nil {
continue
}
dyn, _ := data.Flags.ToDynamicValue(ctx)
if !dyn.IsNull() {
if underlying := dyn.UnderlyingValue(); underlying != nil && !underlying.IsNull() {
continue
}
}
data.Flags = typedNull
m.Data = customfield.NewObjectMust(ctx, data)
changed = true
}

if !changed {
return
}

for i, m := range models {
obj, diags := customfield.NewObject(ctx, m)
if diags.HasError() {
return
}
acc[i] = obj
}
}

// makeTypedDynamicNull wraps a typed null of the given concrete type into a
// NormalizedDynamicValue so that ToTerraformValue serialises a typed null
// rather than the unconstrained DynamicPseudoType.
func makeTypedDynamicNull(t attr.Type) (customfield.NormalizedDynamicValue, bool) {
var inner attr.Value
switch t.(type) {
case basetypes.Float64Type:
inner = types.Float64Null()
case basetypes.StringType:
inner = types.StringNull()
default:
return customfield.NormalizedDynamicValue{}, true
}
return customfield.RawNormalizedDynamicValueFrom(inner), false
}