Skip to content
Merged
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
60 changes: 59 additions & 1 deletion internal/mutation/transformer/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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]())
}
}
49 changes: 49 additions & 0 deletions internal/mutation/transformer/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
`),
},
}
Expand Down