Skip to content

Commit dd04825

Browse files
Use partial unstructured converter to reduce memory consumption (#155)
1 parent b22281c commit dd04825

File tree

4 files changed

+113
-17
lines changed

4 files changed

+113
-17
lines changed

status/controller.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sync"
1010
"time"
1111

12+
opunstructured "github.com/awslabs/operatorpkg/unstructured"
1213
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1314
"k8s.io/apimachinery/pkg/runtime"
1415
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -146,12 +147,12 @@ func (c *Controller[T]) Reconcile(ctx context.Context, req reconcile.Request) (r
146147
}
147148

148149
type GenericObjectController[T client.Object] struct {
149-
*Controller[*unstructuredAdapter[T]]
150+
*Controller[*UnstructuredAdapter[T]]
150151
}
151152

152153
func NewGenericObjectController[T client.Object](client client.Client, eventRecorder record.EventRecorder, opts ...option.Function[Option]) *GenericObjectController[T] {
153154
return &GenericObjectController[T]{
154-
Controller: NewController[*unstructuredAdapter[T]](client, eventRecorder, opts...),
155+
Controller: NewController[*UnstructuredAdapter[T]](client, eventRecorder, opts...),
155156
}
156157
}
157158

@@ -168,9 +169,9 @@ func (c *GenericObjectController[T]) Reconcile(ctx context.Context, req reconcil
168169
}
169170

170171
func (c *Controller[T]) toAdditionalMetricLabels(obj Object) map[string]string {
172+
u := opunstructured.ToPartialUnstructured(obj, lo.Values(c.additionalMetricFields)...)
171173
return lo.Assign(
172174
lo.MapEntries(c.additionalMetricFields, func(k string, v string) (string, string) {
173-
u := lo.Must(runtime.DefaultUnstructuredConverter.ToUnstructured(obj))
174175
elem, _, _ := unstructured.NestedString(u, lo.Filter(strings.Split(v, "."), func(s string, _ int) bool { return s != "" })...)
175176
return toPrometheusLabel(k), elem
176177
}),
@@ -179,9 +180,9 @@ func (c *Controller[T]) toAdditionalMetricLabels(obj Object) map[string]string {
179180
}
180181

181182
func (c *Controller[T]) toAdditionalGaugeMetricLabels(obj Object) map[string]string {
183+
u := opunstructured.ToPartialUnstructured(obj, lo.Values(c.additionalGaugeMetricFields)...)
182184
return lo.Assign(
183185
lo.MapEntries(c.additionalGaugeMetricFields, func(k string, v string) (string, string) {
184-
u := lo.Must(runtime.DefaultUnstructuredConverter.ToUnstructured(obj))
185186
elem, _, _ := unstructured.NestedString(u, lo.Filter(strings.Split(v, "."), func(s string, _ int) bool { return s != "" })...)
186187
return toPrometheusLabel(k), elem
187188
}),

status/unstructured_adapter.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,38 @@ import (
44
"time"
55

66
"github.com/awslabs/operatorpkg/object"
7+
opunstructured "github.com/awslabs/operatorpkg/unstructured"
78
"github.com/samber/lo"
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10-
"k8s.io/apimachinery/pkg/runtime"
1111
"k8s.io/apimachinery/pkg/runtime/schema"
1212
"sigs.k8s.io/controller-runtime/pkg/client"
1313
)
1414

15-
// unstructuredAdapter is an adapter for the status.Object interface. unstructuredAdapter
15+
// UnstructuredAdapter is an adapter for the status.Object interface. unstructuredAdapter
1616
// makes the assumption that status conditions are found on status.conditions path.
17-
type unstructuredAdapter[T client.Object] struct {
17+
type UnstructuredAdapter[T client.Object] struct {
1818
unstructured.Unstructured
1919
}
2020

21-
func NewUnstructuredAdapter[T client.Object](obj client.Object) *unstructuredAdapter[T] {
22-
u := unstructured.Unstructured{Object: lo.Must(runtime.DefaultUnstructuredConverter.ToUnstructured(obj))}
23-
ua := &unstructuredAdapter[T]{Unstructured: u}
21+
func NewUnstructuredAdapter[T client.Object](obj client.Object) *UnstructuredAdapter[T] {
22+
u := unstructured.Unstructured{Object: opunstructured.ToPartialUnstructured(obj, ".status.conditions")}
23+
ua := &UnstructuredAdapter[T]{Unstructured: u}
2424
ua.SetGroupVersionKind(object.GVK(obj))
2525
return ua
2626
}
2727

28-
func (u *unstructuredAdapter[T]) GetObjectKind() schema.ObjectKind {
28+
func (u *UnstructuredAdapter[T]) GetObjectKind() schema.ObjectKind {
2929
return u
3030
}
31-
func (u *unstructuredAdapter[T]) SetGroupVersionKind(gvk schema.GroupVersionKind) {
31+
func (u *UnstructuredAdapter[T]) SetGroupVersionKind(gvk schema.GroupVersionKind) {
3232
u.Unstructured.SetGroupVersionKind(gvk)
3333
}
34-
func (u *unstructuredAdapter[T]) GroupVersionKind() schema.GroupVersionKind {
34+
func (u *UnstructuredAdapter[T]) GroupVersionKind() schema.GroupVersionKind {
3535
return object.GVK(object.New[T]())
3636
}
3737

38-
func (u *unstructuredAdapter[T]) GetConditions() []Condition {
38+
func (u *UnstructuredAdapter[T]) GetConditions() []Condition {
3939
conditions, _, _ := unstructured.NestedFieldNoCopy(u.Object, "status", "conditions")
4040
if conditions == nil {
4141
return nil
@@ -58,7 +58,7 @@ func (u *unstructuredAdapter[T]) GetConditions() []Condition {
5858
return newCondition
5959
})
6060
}
61-
func (u *unstructuredAdapter[T]) SetConditions(conditions []Condition) {
61+
func (u *UnstructuredAdapter[T]) SetConditions(conditions []Condition) {
6262
unstructured.SetNestedSlice(u.Object, lo.Map(conditions, func(condition Condition, _ int) interface{} {
6363
b := map[string]interface{}{}
6464
if condition.Type != "" {
@@ -83,7 +83,7 @@ func (u *unstructuredAdapter[T]) SetConditions(conditions []Condition) {
8383
}), "status", "conditions")
8484
}
8585

86-
func (u *unstructuredAdapter[T]) StatusConditions() ConditionSet {
86+
func (u *UnstructuredAdapter[T]) StatusConditions() ConditionSet {
8787
conditionTypes := lo.Map(u.GetConditions(), func(condition Condition, _ int) string {
8888
return condition.Type
8989
})

status/unstructured_adapter_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var _ = Describe("Unstructured Adapter", func() {
7272
}
7373
conditionObj := status.NewUnstructuredAdapter[*test.CustomObject](testObject)
7474
conditionObj.SetConditions(conditions)
75-
c, found, err := unstructured.NestedSlice(testObject.Object, "status", "conditions")
75+
c, found, err := unstructured.NestedSlice(conditionObj.Object, "status", "conditions")
7676
Expect(err).To(BeNil())
7777
Expect(found).To(BeTrue())
7878
Expect(len(c)).To(BeEquivalentTo(1))

unstructured/unstructured.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package unstructured
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/samber/lo"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10+
)
11+
12+
// ToPartialUnstructured converts an object to unstructured, but only converts specific field paths
13+
// This is more memory efficient than using runtime.DefaultUnstructuredConverter since that requires the full
14+
// object to be converted and stored before extracting specific values from that object
15+
func ToPartialUnstructured(obj interface{}, fieldPaths ...string) map[string]interface{} {
16+
if u, ok := obj.(unstructured.Unstructured); ok {
17+
obj = u.UnstructuredContent()
18+
}
19+
if u, ok := obj.(*unstructured.Unstructured); ok {
20+
obj = u.UnstructuredContent()
21+
}
22+
23+
result := make(map[string]interface{})
24+
for _, fieldPath := range fieldPaths {
25+
_ = extractNestedField(obj, result, lo.Filter(strings.Split(fieldPath, "."), func(s string, _ int) bool { return s != "" })...)
26+
}
27+
return result
28+
}
29+
30+
// extractNestedField extracts a field using a path and populates the result map accordingly
31+
func extractNestedField(obj interface{}, result map[string]interface{}, field ...string) error {
32+
v := reflect.ValueOf(obj)
33+
if v.Kind() == reflect.Ptr {
34+
v = v.Elem()
35+
}
36+
var val reflect.Value
37+
switch v.Kind() {
38+
case reflect.Struct:
39+
for i := range v.Type().NumField() {
40+
f := v.Type().Field(i)
41+
tag := getJSONKey(f)
42+
if f.Name == field[0] || tag == field[0] {
43+
val = v.Field(i)
44+
break
45+
}
46+
}
47+
case reflect.Map:
48+
for _, key := range v.MapKeys() {
49+
if key.String() == field[0] {
50+
val = v.MapIndex(key)
51+
break
52+
}
53+
}
54+
default:
55+
}
56+
if !val.IsValid() {
57+
return fmt.Errorf("field %q not found in %T", field[0], obj)
58+
}
59+
if len(field) == 1 {
60+
// Final field — assign directly
61+
result[field[0]] = val.Interface()
62+
return nil
63+
}
64+
// Intermediate map — recurse
65+
childMap := map[string]interface{}{}
66+
err := extractNestedField(val.Interface(), childMap, field[1:]...)
67+
if err != nil {
68+
return err
69+
}
70+
// Merge into parent map
71+
if _, ok := result[field[0]]; !ok {
72+
result[field[0]] = map[string]interface{}{}
73+
}
74+
for k, v := range childMap {
75+
m, ok := result[field[0]].(map[string]interface{})
76+
// In general, this should never happen because we have a check higher up in the function for field existence
77+
if !ok {
78+
panic(fmt.Sprintf("full field path %q not found in %T", field, obj))
79+
}
80+
m[k] = v
81+
}
82+
return nil
83+
}
84+
85+
// getJSONKey returns the JSON key from a struct tag
86+
func getJSONKey(field reflect.StructField) string {
87+
tag := field.Tag.Get("json")
88+
if tag == "" {
89+
return field.Name
90+
}
91+
if commaIdx := strings.Index(tag, ","); commaIdx != -1 {
92+
return tag[:commaIdx]
93+
}
94+
return tag
95+
}

0 commit comments

Comments
 (0)