From 85451feed8dcb9c69aab4d170c5dce9ccaa65323 Mon Sep 17 00:00:00 2001 From: Alexander Olzem Date: Wed, 26 Aug 2026 10:35:50 +0200 Subject: [PATCH] feature(mutation): add support for list mutations using cel Signed-off-by: Alexander Olzem --- internal/mutation/transformer/cel.go | 60 ++++++++++++++++++++++- internal/mutation/transformer/cel_test.go | 49 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/internal/mutation/transformer/cel.go b/internal/mutation/transformer/cel.go index 1c3ee78..62b1114 100644 --- a/internal/mutation/transformer/cel.go +++ b/internal/mutation/transformer/cel.go @@ -18,9 +18,12 @@ package transformer import ( "fmt" + "reflect" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/decls" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -84,11 +87,66 @@ func (m *celTransformer) Apply(toMutate *unstructured.Unstructured, otherObj *un return nil, fmt.Errorf("failed to evaluate CEL expression: %w", err) } + // convert the result to its native go representation + value, err := celToNative(out) + if err != nil { + return nil, fmt.Errorf("failed to convert CEL result to native value: %w", err) + } + // update the object - updated, err := sjson.Set(encoded, m.path, out) + updated, err := sjson.Set(encoded, m.path, value) if err != nil { return nil, fmt.Errorf("failed to set updated value: %w", err) } return DecodeObject(updated) } + +// celToNative recursively converts a given value to its native Go +// representation according to the reflected type description, or error if the +// conversion is not feasible. +func celToNative(value ref.Val) (any, error) { + switch value.Type() { + case types.ListType: + l, err := value.ConvertToNative(reflect.TypeFor[[]ref.Val]()) + if err != nil { + return nil, err + } + list := l.([]ref.Val) + + result := make([]any, len(list)) + for i, item := range list { + result[i], err = celToNative(item) + if err != nil { + return nil, err + } + } + return result, nil + + case types.MapType: + m, err := value.ConvertToNative(reflect.TypeFor[map[ref.Val]ref.Val]()) + if err != nil { + return nil, err + } + mmap := m.(map[ref.Val]ref.Val) + + result := make(map[string]any, len(mmap)) + for key, item := range mmap { + k, err := key.ConvertToNative(reflect.TypeFor[string]()) + if err != nil { + return nil, err + } + + v, err := celToNative(item) + if err != nil { + return nil, err + } + + result[k.(string)] = v + } + return result, nil + + default: + return value.ConvertToNative(reflect.TypeFor[any]()) + } +} diff --git a/internal/mutation/transformer/cel_test.go b/internal/mutation/transformer/cel_test.go index bf4ce68..e39611a 100644 --- a/internal/mutation/transformer/cel_test.go +++ b/internal/mutation/transformer/cel_test.go @@ -38,6 +38,20 @@ spec: image: ubuntu:latest `) + listInputObject := utils.YAMLToUnstructured(t, ` +apiVersion: kcp.example.com/v1 +kind: List +metadata: + namespace: default + name: my-list +spec: + items: + - name: foo + image: ubuntu:latest + - name: bar + image: alpine:latest +`) + testcases := []struct { name string inputData *unstructured.Unstructured @@ -116,6 +130,41 @@ metadata: spec: cronSpec: ubuntu:latest image: ubuntu:latest +`), + }, + { + name: "recreate list value", + inputData: listInputObject, + otherObj: listInputObject, + mutation: syncagentv1alpha1.ResourceCELMutation{ + Path: "spec.items", + Expression: `other.spec.items`, + }, + expected: listInputObject, + }, + { + name: "modify list value", + inputData: listInputObject, + otherObj: listInputObject, + mutation: syncagentv1alpha1.ResourceCELMutation{ + Path: "spec.items", + Expression: `self.spec.items.map(a, { + "name": a.name, + "image": "registry.example.com/" + a.image + })`, + }, + expected: utils.YAMLToUnstructured(t, ` +apiVersion: kcp.example.com/v1 +kind: List +metadata: + namespace: default + name: my-list +spec: + items: + - name: foo + image: registry.example.com/ubuntu:latest + - name: bar + image: registry.example.com/alpine:latest `), }, }