Skip to content
Closed
Show file tree
Hide file tree
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
442 changes: 430 additions & 12 deletions manifest/morph/morph.go

Large diffs are not rendered by default.

327 changes: 324 additions & 3 deletions manifest/morph/morph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func TestMorphValueToType(t *testing.T) {
},
// This covers the case were we need to represent lists that contain dynamicPseudoType sub-elements
// because the dynamicPseudoType might hold heterogenous types
// Output preserves DynamicPseudoType in type structure to ensure consistency between plan and apply
"tuple(single)->tuple": {
In: sampleInType{
V: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.String, tftypes.String}}, []tftypes.Value{
Expand All @@ -145,7 +146,7 @@ func TestMorphValueToType(t *testing.T) {
}),
T: tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.DynamicPseudoType}},
},
Out: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.String, tftypes.String}}, []tftypes.Value{
Out: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.DynamicPseudoType, tftypes.DynamicPseudoType, tftypes.DynamicPseudoType}}, []tftypes.Value{
tftypes.NewValue(tftypes.String, "foo"),
tftypes.NewValue(tftypes.String, "bar"),
tftypes.NewValue(tftypes.String, "baz"),
Expand Down Expand Up @@ -378,6 +379,73 @@ func TestMorphValueToType(t *testing.T) {
"three": tftypes.NewValue(tftypes.String, "baz"),
}),
},
"object(heterogeneous)->map(dynamic) normalized": {
In: sampleInType{
V: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"test": tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
}},
"test2": tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
}},
map[string]tftypes.Value{
"test": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
}},
map[string]tftypes.Value{
"datas": tftypes.NewValue(
tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
[]tftypes.Value{tftypes.NewValue(tftypes.String, "10.0.0.0/24")},
),
},
),
"test2": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
map[string]tftypes.Value{},
),
},
),
T: tftypes.Map{ElementType: tftypes.DynamicPseudoType},
},
Out: tftypes.NewValue(
tftypes.Map{ElementType: tftypes.DynamicPseudoType},
map[string]tftypes.Value{
"test": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.DynamicPseudoType,
}},
map[string]tftypes.Value{
"datas": tftypes.NewValue(
tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
[]tftypes.Value{tftypes.NewValue(tftypes.String, "10.0.0.0/24")},
),
},
),
"test2": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.DynamicPseudoType,
}},
map[string]tftypes.Value{
"datas": tftypes.NewValue(tftypes.DynamicPseudoType, nil),
},
),
},
),
},
"object(incompatible-primitives)->map(dynamic) error": {
In: sampleInType{
V: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"one": tftypes.String,
"two": tftypes.Number,
}}, map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.String, "foo"),
"two": tftypes.NewValue(tftypes.Number, new(big.Float).SetInt64(42)),
}),
T: tftypes.Map{ElementType: tftypes.DynamicPseudoType},
},
WantErr: true,
},
"object->object": {
In: sampleInType{
V: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{
Expand Down Expand Up @@ -484,6 +552,7 @@ func TestMorphValueToType(t *testing.T) {
}),
},
// morphing to tuple attributes to "template tuples" (containing dynamic) should result in the same number of elements as the input
// Output preserves DynamicPseudoType in type structure to ensure consistency between plan and apply
"object(dynamic) -> object": {
In: sampleInType{
V: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{
Expand All @@ -500,15 +569,34 @@ func TestMorphValueToType(t *testing.T) {
}},
},
Out: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"one": tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}},
"one": tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.DynamicPseudoType, tftypes.DynamicPseudoType}},
}}, map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}},
"one": tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.DynamicPseudoType, tftypes.DynamicPseudoType}},
[]tftypes.Value{
tftypes.NewValue(tftypes.String, "bar"),
tftypes.NewValue(tftypes.String, "baz"),
}),
}),
},
// Regression test: morphObjectToType with empty object and DynamicPseudoType schema
// Without fix, output type would be Object{data: Object{}} instead of Object{data: DynamicPseudoType}
"object(empty-dynamic) -> object": {
In: sampleInType{
V: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
}}, map[string]tftypes.Value{
"data": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, map[string]tftypes.Value{}),
}),
T: tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.DynamicPseudoType,
}},
},
Out: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.DynamicPseudoType, // Must preserve DynamicPseudoType
}}, map[string]tftypes.Value{
"data": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, map[string]tftypes.Value{}),
}),
},
}
for n, s := range samples {
t.Run(n, func(t *testing.T) {
Expand Down Expand Up @@ -716,3 +804,236 @@ func formatDiagnostics(diags []*tfprotov5.Diagnostic) string {
}
return b.String()
}

func TestMorphTypeStructure(t *testing.T) {
type sampleInType struct {
Source tftypes.Value
TargetType tftypes.Type
}
samples := map[string]struct {
In sampleInType
Expected tftypes.Value
HasError bool
}{
"dynamic-to-concrete-empty-object": {
// Simulates the KubeVirt masquerade case: source has DynamicPseudoType, target has Object{}
In: sampleInType{
Source: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"masquerade": tftypes.DynamicPseudoType,
}},
map[string]tftypes.Value{
"masquerade": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
map[string]tftypes.Value{},
),
},
),
TargetType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"masquerade": tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
}},
},
Expected: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"masquerade": tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
}},
map[string]tftypes.Value{
"masquerade": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
map[string]tftypes.Value{},
),
},
),
},
"nested-dynamic-to-concrete": {
// Deeply nested DynamicPseudoType to concrete
In: sampleInType{
Source: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"spec": tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.DynamicPseudoType,
}},
}},
map[string]tftypes.Value{
"spec": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.DynamicPseudoType,
}},
map[string]tftypes.Value{
"data": tftypes.NewValue(tftypes.String, "test"),
},
),
},
),
TargetType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"spec": tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.String,
}},
}},
},
Expected: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"spec": tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.String,
}},
}},
map[string]tftypes.Value{
"spec": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"data": tftypes.String,
}},
map[string]tftypes.Value{
"data": tftypes.NewValue(tftypes.String, "test"),
},
),
},
),
},
"list-with-dynamic-elements": {
In: sampleInType{
Source: tftypes.NewValue(
tftypes.List{ElementType: tftypes.DynamicPseudoType},
[]tftypes.Value{
tftypes.NewValue(tftypes.String, "a"),
tftypes.NewValue(tftypes.String, "b"),
},
),
TargetType: tftypes.List{ElementType: tftypes.String},
},
Expected: tftypes.NewValue(
tftypes.List{ElementType: tftypes.String},
[]tftypes.Value{
tftypes.NewValue(tftypes.String, "a"),
tftypes.NewValue(tftypes.String, "b"),
},
),
},
"same-type-passthrough": {
In: sampleInType{
Source: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
}},
map[string]tftypes.Value{
"name": tftypes.NewValue(tftypes.String, "test"),
},
),
TargetType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
}},
},
Expected: tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
}},
map[string]tftypes.Value{
"name": tftypes.NewValue(tftypes.String, "test"),
},
),
},
"null-value": {
In: sampleInType{
Source: tftypes.NewValue(tftypes.DynamicPseudoType, nil),
TargetType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
},
Expected: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, nil),
},
"unknown-value": {
In: sampleInType{
Source: tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue),
TargetType: tftypes.String,
},
Expected: tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
},
}

for name, sample := range samples {
t.Run(name, func(t *testing.T) {
result, err := MorphTypeStructure(sample.In.Source, sample.In.TargetType, tftypes.NewAttributePath())
if sample.HasError {
if err == nil {
t.Errorf("Expected error but got none")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if !result.Equal(sample.Expected) {
t.Errorf("Result mismatch:\n Expected: %s\n Got: %s", sample.Expected, result)
}
// Also verify the type structure matches
if !result.Type().Equal(sample.Expected.Type()) {
t.Errorf("Type mismatch:\n Expected: %s\n Got: %s", sample.Expected.Type(), result.Type())
}
})
}
}

func TestNormalizeDynamicMapElements(t *testing.T) {
t.Run("normalizes-heterogeneous-object-elements", func(t *testing.T) {
in := map[string]tftypes.Value{
"test": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
}},
map[string]tftypes.Value{
"datas": tftypes.NewValue(
tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
[]tftypes.Value{tftypes.NewValue(tftypes.String, "10.0.0.0/24")},
),
},
),
"test2": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{}},
map[string]tftypes.Value{},
),
}

out, err := NormalizeDynamicMapElements(in, tftypes.NewAttributePath().WithAttributeName("object").WithAttributeName("spec").WithAttributeName("mapdata"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

expected := map[string]tftypes.Value{
"test": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.DynamicPseudoType,
}},
map[string]tftypes.Value{
"datas": tftypes.NewValue(
tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}},
[]tftypes.Value{tftypes.NewValue(tftypes.String, "10.0.0.0/24")},
),
},
),
"test2": tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"datas": tftypes.DynamicPseudoType,
}},
map[string]tftypes.Value{
"datas": tftypes.NewValue(tftypes.DynamicPseudoType, nil),
},
),
}

if !cmp.Equal(out, expected, cmp.Exporter(func(t reflect.Type) bool { return true })) {
t.Fatalf("unexpected normalized map values:\nexpected: %#v\nreceived: %#v", expected, out)
}
})

t.Run("fails-on-incompatible-non-object-elements", func(t *testing.T) {
in := map[string]tftypes.Value{
"one": tftypes.NewValue(tftypes.String, "foo"),
"two": tftypes.NewValue(tftypes.Number, new(big.Float).SetInt64(42)),
}
_, err := NormalizeDynamicMapElements(in, tftypes.NewAttributePath().WithAttributeName("object").WithAttributeName("spec").WithAttributeName("mapdata"))
if err == nil {
t.Fatal("expected normalization error but got nil")
}
if !strings.Contains(err.Error(), "cannot normalize map(dynamic)") {
t.Fatalf("unexpected error: %v", err)
}
})
}
Loading
Loading