Skip to content

Commit 71044ff

Browse files
committed
fix(manifest): normalize map(dynamic) shapes and add regressions
1 parent 12458dc commit 71044ff

File tree

11 files changed

+755
-0
lines changed

11 files changed

+755
-0
lines changed

manifest/morph/morph.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package morph
66
import (
77
"fmt"
88
"math/big"
9+
"sort"
910
"strconv"
1011
"strings"
1112

@@ -569,6 +570,18 @@ func morphMapToType(v tftypes.Value, t tftypes.Type, p *tftypes.AttributePath) (
569570
}
570571
nmvals[k] = nv
571572
}
573+
if t.(tftypes.Map).ElementType.Is(tftypes.DynamicPseudoType) {
574+
nmvals, err = NormalizeDynamicMapElements(nmvals, p)
575+
if err != nil {
576+
diags = append(diags, &tfprotov5.Diagnostic{
577+
Attribute: p,
578+
Severity: tfprotov5.DiagnosticSeverityError,
579+
Summary: "Failed to normalize map(dynamic) element types",
580+
Detail: err.Error(),
581+
})
582+
return tftypes.Value{}, diags
583+
}
584+
}
572585
return newValue(t, nmvals, p)
573586
case t.Is(tftypes.DynamicPseudoType):
574587
return v, diags
@@ -674,6 +687,18 @@ func morphObjectToType(v tftypes.Value, t tftypes.Type, p *tftypes.AttributePath
674687
}
675688
mvals[k] = nv
676689
}
690+
if t.(tftypes.Map).ElementType.Is(tftypes.DynamicPseudoType) {
691+
mvals, err = NormalizeDynamicMapElements(mvals, p)
692+
if err != nil {
693+
diags = append(diags, &tfprotov5.Diagnostic{
694+
Attribute: p,
695+
Severity: tfprotov5.DiagnosticSeverityError,
696+
Summary: "Failed to normalize map(dynamic) element types",
697+
Detail: err.Error(),
698+
})
699+
return tftypes.Value{}, diags
700+
}
701+
}
677702
return newValue(t, mvals, p)
678703
case t.Is(tftypes.DynamicPseudoType):
679704
return v, diags
@@ -754,6 +779,217 @@ func newValue(t tftypes.Type, val interface{}, p *tftypes.AttributePath) (tftype
754779
return tftypes.NewValue(t, val), nil
755780
}
756781

782+
// NormalizeDynamicMapElements normalizes the value set for a map(dynamic) so all
783+
// elements have a coherent type shape before serialization.
784+
//
785+
// If all element types already match, values are returned unchanged.
786+
// If elements are all objects with differing attribute sets/types, element
787+
// object types are merged by attribute name and missing/incompatible attributes
788+
// are represented as DynamicPseudoType.
789+
// If elements are not all objects and have differing types, an error is
790+
// returned rather than coercing values.
791+
func NormalizeDynamicMapElements(vals map[string]tftypes.Value, p *tftypes.AttributePath) (map[string]tftypes.Value, error) {
792+
if p == nil {
793+
p = tftypes.NewAttributePath()
794+
}
795+
if len(vals) == 0 {
796+
return vals, nil
797+
}
798+
799+
var firstType tftypes.Type
800+
allSameType := true
801+
allObjectTypes := true
802+
for _, v := range vals {
803+
if firstType == nil {
804+
firstType = v.Type()
805+
} else if !firstType.Equal(v.Type()) {
806+
allSameType = false
807+
}
808+
if !v.Type().Is(tftypes.Object{}) {
809+
allObjectTypes = false
810+
}
811+
}
812+
if allSameType {
813+
return vals, nil
814+
}
815+
if !allObjectTypes {
816+
return nil, p.NewErrorf(
817+
"[%s] cannot normalize map(dynamic) with incompatible element types: %s",
818+
attributePathSummary(p),
819+
describeMapElementTypes(vals),
820+
)
821+
}
822+
823+
mergedObjectType := mergeDynamicMapObjectType(vals)
824+
mergedAttrTypes := mergedObjectType.AttributeTypes
825+
normalized := make(map[string]tftypes.Value, len(vals))
826+
827+
for key, v := range vals {
828+
ep := p.WithElementKeyString(key)
829+
var objVals map[string]tftypes.Value
830+
if err := v.As(&objVals); err != nil {
831+
return nil, ep.NewError(err)
832+
}
833+
834+
outVals := make(map[string]tftypes.Value, len(mergedAttrTypes))
835+
for attrName, targetType := range mergedAttrTypes {
836+
ap := ep.WithAttributeName(attrName)
837+
if av, ok := objVals[attrName]; ok {
838+
nv, err := normalizeMapObjectAttributeValue(av, targetType, ap)
839+
if err != nil {
840+
return nil, err
841+
}
842+
outVals[attrName] = nv
843+
continue
844+
}
845+
outVals[attrName] = tftypes.NewValue(targetType, nil)
846+
}
847+
normalized[key] = tftypes.NewValue(mergedObjectType, outVals)
848+
}
849+
850+
return normalized, nil
851+
}
852+
853+
// NormalizeDynamicMapShapes recursively normalizes map(dynamic) values nested in
854+
// the supplied value to ensure serialization-safe element type structures.
855+
func NormalizeDynamicMapShapes(v tftypes.Value, p *tftypes.AttributePath) (tftypes.Value, error) {
856+
if p == nil {
857+
p = tftypes.NewAttributePath()
858+
}
859+
if !v.IsKnown() || v.IsNull() {
860+
return v, nil
861+
}
862+
863+
switch {
864+
case v.Type().Is(tftypes.Object{}):
865+
var vals map[string]tftypes.Value
866+
if err := v.As(&vals); err != nil {
867+
return tftypes.Value{}, p.NewError(err)
868+
}
869+
atts := v.Type().(tftypes.Object).AttributeTypes
870+
ovals := make(map[string]tftypes.Value, len(atts))
871+
for name, attType := range atts {
872+
np := p.WithAttributeName(name)
873+
cv, ok := vals[name]
874+
if !ok {
875+
ovals[name] = tftypes.NewValue(attType, nil)
876+
continue
877+
}
878+
nv, err := NormalizeDynamicMapShapes(cv, np)
879+
if err != nil {
880+
return tftypes.Value{}, err
881+
}
882+
ovals[name] = nv
883+
}
884+
return tftypes.NewValue(v.Type(), ovals), nil
885+
886+
case v.Type().Is(tftypes.Map{}):
887+
var vals map[string]tftypes.Value
888+
if err := v.As(&vals); err != nil {
889+
return tftypes.Value{}, p.NewError(err)
890+
}
891+
mvals := make(map[string]tftypes.Value, len(vals))
892+
for key, el := range vals {
893+
np := p.WithElementKeyString(key)
894+
nv, err := NormalizeDynamicMapShapes(el, np)
895+
if err != nil {
896+
return tftypes.Value{}, err
897+
}
898+
mvals[key] = nv
899+
}
900+
if v.Type().(tftypes.Map).ElementType.Is(tftypes.DynamicPseudoType) {
901+
var err error
902+
mvals, err = NormalizeDynamicMapElements(mvals, p)
903+
if err != nil {
904+
return tftypes.Value{}, err
905+
}
906+
}
907+
return tftypes.NewValue(v.Type(), mvals), nil
908+
909+
case v.Type().Is(tftypes.List{}) || v.Type().Is(tftypes.Set{}) || v.Type().Is(tftypes.Tuple{}):
910+
var vals []tftypes.Value
911+
if err := v.As(&vals); err != nil {
912+
return tftypes.Value{}, p.NewError(err)
913+
}
914+
out := make([]tftypes.Value, len(vals))
915+
for i, el := range vals {
916+
np := p.WithElementKeyInt(i)
917+
nv, err := NormalizeDynamicMapShapes(el, np)
918+
if err != nil {
919+
return tftypes.Value{}, err
920+
}
921+
out[i] = nv
922+
}
923+
return tftypes.NewValue(v.Type(), out), nil
924+
}
925+
926+
return v, nil
927+
}
928+
929+
func mergeDynamicMapObjectType(vals map[string]tftypes.Value) tftypes.Object {
930+
attrType := make(map[string]tftypes.Type)
931+
attrPresentCount := make(map[string]int)
932+
attrTypeConsistent := make(map[string]bool)
933+
totalElements := len(vals)
934+
935+
for _, v := range vals {
936+
objType := v.Type().(tftypes.Object)
937+
for name, t := range objType.AttributeTypes {
938+
attrPresentCount[name]++
939+
if priorType, ok := attrType[name]; !ok {
940+
attrType[name] = t
941+
attrTypeConsistent[name] = true
942+
} else if !priorType.Equal(t) {
943+
attrTypeConsistent[name] = false
944+
}
945+
}
946+
}
947+
948+
merged := make(map[string]tftypes.Type, len(attrType))
949+
for name, t := range attrType {
950+
if attrPresentCount[name] != totalElements || !attrTypeConsistent[name] {
951+
merged[name] = tftypes.DynamicPseudoType
952+
continue
953+
}
954+
merged[name] = t
955+
}
956+
return tftypes.Object{AttributeTypes: merged}
957+
}
958+
959+
func normalizeMapObjectAttributeValue(v tftypes.Value, targetType tftypes.Type, p *tftypes.AttributePath) (tftypes.Value, error) {
960+
if targetType.Is(tftypes.DynamicPseudoType) {
961+
return v, nil
962+
}
963+
if v.Type().Equal(targetType) {
964+
return v, nil
965+
}
966+
if v.IsNull() {
967+
return tftypes.NewValue(targetType, nil), nil
968+
}
969+
if !v.IsKnown() {
970+
return tftypes.NewValue(targetType, tftypes.UnknownValue), nil
971+
}
972+
return tftypes.Value{}, p.NewErrorf(
973+
"[%s] incompatible map(dynamic) attribute type: expected %s, got %s",
974+
attributePathSummary(p),
975+
typeNameNoPrefix(targetType),
976+
typeNameNoPrefix(v.Type()),
977+
)
978+
}
979+
980+
func describeMapElementTypes(vals map[string]tftypes.Value) string {
981+
types := make(map[string]struct{}, len(vals))
982+
for _, v := range vals {
983+
types[typeNameNoPrefix(v.Type())] = struct{}{}
984+
}
985+
list := make([]string, 0, len(types))
986+
for t := range types {
987+
list = append(list, t)
988+
}
989+
sort.Strings(list)
990+
return strings.Join(list, ", ")
991+
}
992+
757993
// MorphTypeStructure converts a value to have a different type structure while preserving
758994
// the underlying data. This is needed when plan and apply produce values with different
759995
// type structures due to DynamicPseudoType being converted to concrete types during

0 commit comments

Comments
 (0)