Skip to content

Commit 3ce4645

Browse files
biancasilvavtexcezar-guimaraesCezar Guimaraes
authored
[SFS-1439] Implement custom sort for Unstructured (#18)
* feat(SFS-1439): implement custom sort for Unstructured * docs(SFS-1439): add Go docs * feat(SFS-1439): generalize sort for all Comparer types * chore(SFS-1439): update libs * feat(SFS-1439): add sort_by * feat(SFS-1439): create reverse function overload * refac(SFS-1439): solve duplicates * refac(SFS-1439): use slices.Reverse Co-authored-by: cezar-guimaraes <[email protected]> * chore(SFS-1490): bump go version to 1.22 in github workflow * chore(SFS-1490): bump go version to 1.22 in Dockerfile * chore(SFS-1490): copy custom_cel dir in Dockerfile * upgrade controller-tools --------- Co-authored-by: cezar-guimaraes <[email protected]> Co-authored-by: Cezar Guimaraes <[email protected]>
1 parent 90db8d1 commit 3ce4645

File tree

10 files changed

+923
-893
lines changed

10 files changed

+923
-893
lines changed

.github/workflows/publish.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ jobs:
2727

2828
- uses: actions/setup-go@v4
2929
with:
30-
go-version: '1.19'
30+
go-version: '1.22'
3131

3232
- name: Build, tag, and push docker image to Amazon ECR Public
3333
uses: int128/kaniko-action@v1
3434
with:
3535
push: true
36-
tags: ${{ steps.login-ecr-public.outputs.registry }}/f8y0w2c4/cleaner-controller:${{ github.ref_name }}
36+
tags: ${{ steps.login-ecr-public.outputs.registry }}/f8y0w2c4/cleaner-controller:manager-${{ github.ref_name }}
3737

3838
- name: Publish helm chart
3939
run: |

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build the manager binary
2-
FROM golang:1.19 as builder
2+
FROM golang:1.22 as builder
33
ARG TARGETOS
44
ARG TARGETARCH
55

@@ -15,6 +15,7 @@ RUN go mod download
1515
COPY main.go main.go
1616
COPY api/ api/
1717
COPY controllers/ controllers/
18+
COPY custom_cel/ custom_cel/
1819

1920
# Build
2021
# the GOARCH has not a default value to allow the binary be built according to the host where the command

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ CRD_REF_DOCS ?= $(LOCALBIN)/crd-ref-docs
196196

197197
## Tool Versions
198198
KUSTOMIZE_VERSION ?= v3.8.7
199-
CONTROLLER_TOOLS_VERSION ?= v0.10.0
199+
CONTROLLER_TOOLS_VERSION ?= v0.16.3
200200

201201
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
202202
.PHONY: kustomize

controllers/conditionalttl_controller.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"github.com/vtex/cleaner-controller/custom_cel"
2324
"time"
2425

2526
cloudevents "github.com/cloudevents/sdk-go/v2"
@@ -138,13 +139,13 @@ func (r *ConditionalTTLReconciler) Reconcile(ctx context.Context, req ctrl.Reque
138139
return ctrl.Result{}, err
139140
}
140141

141-
celCtx := buildCELContext(ts, t)
142-
celOpts := buildCELOptions(cTTL)
142+
celCtx := custom_cel.BuildCELContext(ts, t)
143+
celOpts := custom_cel.BuildCELOptions(cTTL)
143144

144145
readyCondition := metav1.Condition{
145146
ObservedGeneration: cTTL.GetGeneration(),
146147
}
147-
condsMet, retryable := evaluateCELConditions(celOpts, celCtx, cTTL.Spec.Conditions, &readyCondition)
148+
condsMet, retryable := custom_cel.EvaluateCELConditions(celOpts, celCtx, cTTL.Spec.Conditions, &readyCondition)
148149
apimeta.SetStatusCondition(&cTTL.Status.Conditions, readyCondition)
149150

150151
if !condsMet {

controllers/cel.go renamed to custom_cel/cel.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package controllers
1+
package custom_cel
22

33
import (
44
"fmt"
@@ -10,12 +10,14 @@ import (
1010
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1111
)
1212

13-
// buildCELOptions builds the list of env options to be used when
13+
// BuildCELOptions builds the list of env options to be used when
1414
// building the CEL environment used to evaluated the conditions
1515
// of a given cTTL.
16-
func buildCELOptions(cTTL *cleanerv1alpha1.ConditionalTTL) []cel.EnvOption {
16+
func BuildCELOptions(cTTL *cleanerv1alpha1.ConditionalTTL) []cel.EnvOption {
1717
r := []cel.EnvOption{
18-
ext.Strings(), // helper string functions
18+
ext.Strings(), // helper string functions
19+
ext.Bindings(), // helper binding functions
20+
Lists(), // custom VTEX helper for list functions
1921
cel.Variable("time", cel.TimestampType),
2022
}
2123
for _, t := range cTTL.Spec.Targets {
@@ -26,9 +28,9 @@ func buildCELOptions(cTTL *cleanerv1alpha1.ConditionalTTL) []cel.EnvOption {
2628
return r
2729
}
2830

29-
// buildCELContext builds the map of parameters to be passed to the CEL
31+
// BuildCELContext builds the map of parameters to be passed to the CEL
3032
// evaluation given a list of TargetStatus and an evaluation time.
31-
func buildCELContext(targets []cleanerv1alpha1.TargetStatus, time time.Time) map[string]interface{} {
33+
func BuildCELContext(targets []cleanerv1alpha1.TargetStatus, time time.Time) map[string]interface{} {
3234
ctx := make(map[string]interface{})
3335
for _, ts := range targets {
3436
if !ts.IncludeWhenEvaluating {
@@ -40,12 +42,12 @@ func buildCELContext(targets []cleanerv1alpha1.TargetStatus, time time.Time) map
4042
return ctx
4143
}
4244

43-
// evaluateCELConditions compiles and evaluates all the conditions on the passed CEL context,
45+
// EvaluateCELConditions compiles and evaluates all the conditions on the passed CEL context,
4446
// returning true only when all conditions evaluate to true. It stops evaluating on the first
4547
// encountered error but otherwise all conditions are evaluated in order to find and report
4648
// compilation and/or evaluation errors early. It also updates the passed
4749
// readyCondition Status, Type, Reason and Message fields.
48-
func evaluateCELConditions(opts []cel.EnvOption, celCtx map[string]interface{}, conditions []string, readyCondition *metav1.Condition) (conditionsMet bool, retryable bool) {
50+
func EvaluateCELConditions(opts []cel.EnvOption, celCtx map[string]interface{}, conditions []string, readyCondition *metav1.Condition) (conditionsMet bool, retryable bool) {
4951
readyCondition.Status = metav1.ConditionFalse
5052
readyCondition.Type = cleanerv1alpha1.ConditionTypeReady
5153
env, err := cel.NewEnv(opts...)

custom_cel/lists.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package custom_cel
2+
3+
import (
4+
"github.com/google/cel-go/cel"
5+
"github.com/google/cel-go/common"
6+
"github.com/google/cel-go/common/ast"
7+
"github.com/google/cel-go/common/operators"
8+
"github.com/google/cel-go/common/types"
9+
"github.com/google/cel-go/common/types/ref"
10+
"github.com/google/cel-go/common/types/traits"
11+
"github.com/google/cel-go/parser"
12+
"k8s.io/apiserver/pkg/cel/library"
13+
"slices"
14+
"sort"
15+
)
16+
17+
// Lists returns a cel.EnvOption to configure extended functions Lists manipulation.
18+
//
19+
// # SortBy
20+
//
21+
// Returns a new sorted list by the field defined.
22+
// It supports all types that implements the base traits.Comparer interface.
23+
//
24+
// <list>.sort_by(obj, obj.field) ==> <list>
25+
//
26+
// Examples:
27+
//
28+
// [2,3,1].sort_by(i,i) ==> [1,2,3]
29+
//
30+
// [{Name: "c", Age: 10}, {Name: "a", Age: 30}, {Name: "b", Age: 1}].sort_by(obj, obj.age) ==> [{Name: "b", Age: 1}, {Name: "c", Age: 10}, {Name: "a", Age: 30}]
31+
//
32+
// # ReverseList
33+
//
34+
// Returns a new list in reverse order.
35+
// It supports all types that implements the base traits.Comparer interface
36+
//
37+
// <list>.reverse_list() ==> <list>
38+
//
39+
// # Examples
40+
//
41+
// [1,2,3].reverse_list() ==> [3,2,1]
42+
//
43+
// ["x", "y", "z"].reverse_list() ==> ["z", "y", "x"]
44+
func Lists() cel.EnvOption {
45+
return cel.Lib(listsLib{})
46+
}
47+
48+
type listsLib struct{}
49+
50+
// CompileOptions implements the Library interface method defining the basic compile configuration
51+
func (u listsLib) CompileOptions() []cel.EnvOption {
52+
dynListType := cel.ListType(cel.DynType)
53+
sortByMacro := parser.NewReceiverMacro("sort_by", 2, makeSortBy)
54+
return []cel.EnvOption{
55+
library.Lists(),
56+
cel.Macros(sortByMacro),
57+
cel.Function(
58+
"pair",
59+
cel.Overload(
60+
"make_pair",
61+
[]*cel.Type{cel.DynType, cel.DynType},
62+
cel.DynType,
63+
cel.BinaryBinding(makePair),
64+
),
65+
),
66+
cel.Function(
67+
"sort",
68+
cel.Overload(
69+
"sort_list",
70+
[]*cel.Type{dynListType},
71+
dynListType,
72+
cel.UnaryBinding(makeSort),
73+
),
74+
),
75+
cel.Function(
76+
"reverse_list",
77+
cel.MemberOverload(
78+
"reverse_list_id",
79+
[]*cel.Type{cel.ListType(cel.DynType)},
80+
cel.ListType(cel.DynType),
81+
cel.UnaryBinding(makeReverse),
82+
),
83+
),
84+
}
85+
}
86+
87+
// ProgramOptions implements the Library interface method defining the basic program options
88+
func (u listsLib) ProgramOptions() []cel.ProgramOption {
89+
return []cel.ProgramOption{}
90+
}
91+
92+
type pair struct {
93+
order ref.Val
94+
value ref.Val
95+
}
96+
97+
var (
98+
orderKey = types.DefaultTypeAdapter.NativeToValue("order")
99+
valueKey = types.DefaultTypeAdapter.NativeToValue("value")
100+
)
101+
102+
func makePair(order ref.Val, value ref.Val) ref.Val {
103+
if _, ok := order.(traits.Comparer); !ok {
104+
return types.ValOrErr(order, "unable to build ordered pair with value %v", order.Value())
105+
}
106+
return types.NewStringInterfaceMap(types.DefaultTypeAdapter, map[string]any{
107+
"order": order.Value(),
108+
"value": value.Value(),
109+
})
110+
}
111+
112+
func makeSort(itemsVal ref.Val) ref.Val {
113+
items, ok := itemsVal.(traits.Lister)
114+
if !ok {
115+
return types.ValOrErr(itemsVal, "unable to convert to traits.Lister")
116+
}
117+
118+
pairs := make([]pair, 0, items.Size().Value().(int64))
119+
index := 0
120+
for it := items.Iterator(); it.HasNext().(types.Bool); {
121+
curr, ok := it.Next().(traits.Mapper)
122+
if !ok {
123+
return types.NewErr("unable to convert elem %d to traits.Mapper", index)
124+
}
125+
126+
pairs = append(pairs, pair{
127+
order: curr.Get(orderKey),
128+
value: curr.Get(valueKey),
129+
})
130+
index++
131+
}
132+
133+
sort.Slice(pairs, func(i, j int) bool {
134+
return pairs[i].order.(traits.Comparer).Compare(pairs[j].order) == types.IntNegOne
135+
})
136+
137+
var ordered []interface{}
138+
for _, v := range pairs {
139+
ordered = append(ordered, v.value.Value())
140+
}
141+
142+
return types.NewDynamicList(types.DefaultTypeAdapter, ordered)
143+
}
144+
145+
func extractIdent(e ast.Expr) (string, bool) {
146+
if e.Kind() == ast.IdentKind {
147+
return e.AsIdent(), true
148+
}
149+
return "", false
150+
}
151+
152+
func makeSortBy(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
153+
v, found := extractIdent(args[0])
154+
if !found {
155+
return nil, eh.NewError(args[0].ID(), "argument is not an identifier")
156+
}
157+
158+
var fn = args[1]
159+
160+
init := eh.NewList()
161+
condition := eh.NewLiteral(types.True)
162+
163+
step := eh.NewCall(operators.Add, eh.NewAccuIdent(), eh.NewList(
164+
eh.NewCall("pair", fn, args[0]),
165+
))
166+
167+
/*
168+
This comprehension is expanded to:
169+
__result__ = [] # init expr
170+
for $v in $target:
171+
__result__ += [pair(fn(v), v)] # step expr
172+
return sort(__result__) # result expr
173+
*/
174+
mapped := eh.NewComprehension(
175+
target,
176+
v,
177+
parser.AccumulatorName,
178+
init,
179+
condition,
180+
step,
181+
eh.NewCall(
182+
"sort",
183+
eh.NewAccuIdent(),
184+
),
185+
)
186+
187+
return mapped, nil
188+
}
189+
190+
func makeReverse(itemsVal ref.Val) ref.Val {
191+
items, ok := itemsVal.(traits.Lister)
192+
if !ok {
193+
return types.ValOrErr(itemsVal, "unable to convert to traits.Lister")
194+
}
195+
196+
orderedItems := make([]ref.Val, 0, items.Size().Value().(int64))
197+
for it := items.Iterator(); it.HasNext().(types.Bool); {
198+
orderedItems = append(orderedItems, it.Next())
199+
}
200+
201+
slices.Reverse(orderedItems)
202+
203+
return types.NewDynamicList(types.DefaultTypeAdapter, orderedItems)
204+
}

0 commit comments

Comments
 (0)