Skip to content

Commit 1b56e2f

Browse files
SurbhiAgarwal1SurbhiAgarwal1
authored andcommitted
feat: Add CEL-based conditional function execution
Adds support for CEL expressions in Kptfile pipeline functions via a new 'condition' field. Functions with a condition are only executed if the CEL expression evaluates to true against the current resource list. - Add CELEvaluator in internal/fnruntime/celeval.go with k8s CEL extensions - Integrate condition check in FunctionRunner.Filter (runner.go) - Append skipped result to fnResults when condition is not met - Add 'condition' field to kptfile/v1 Function type - Update executor and runneroptions to support condition passing - Add e2e and unit tests for conditional execution - Add k8s.io/apiserver dependency for CEL library extensions Signed-off-by: Surbhi <agarwalsurbhi1807@gmail.com>
1 parent aaad95b commit 1b56e2f

7 files changed

Lines changed: 737 additions & 1 deletion

File tree

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/bytecodealliance/wasmtime-go v1.0.0
88
github.com/cpuguy83/go-md2man/v2 v2.0.7
99
github.com/go-errors/errors v1.5.1
10+
github.com/google/cel-go v0.26.0
1011
github.com/google/go-cmp v0.7.0
1112
github.com/google/go-containerregistry v0.20.6
1213
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
@@ -28,6 +29,7 @@ require (
2829
k8s.io/api v0.34.1
2930
k8s.io/apiextensions-apiserver v0.34.1
3031
k8s.io/apimachinery v0.34.1
32+
k8s.io/apiserver v0.34.1
3133
k8s.io/cli-runtime v0.34.1
3234
k8s.io/client-go v0.34.1
3335
k8s.io/component-base v0.34.1
@@ -42,9 +44,11 @@ require (
4244
)
4345

4446
require (
47+
cel.dev/expr v0.24.0 // indirect
4548
cloud.google.com/go/compute/metadata v0.9.0 // indirect
4649
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
4750
github.com/MakeNowJust/heredoc v1.0.0 // indirect
51+
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
4852
github.com/beorn7/perks v1.0.1 // indirect
4953
github.com/blang/semver/v4 v4.0.0 // indirect
5054
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -109,19 +113,23 @@ require (
109113
github.com/sergi/go-diff v1.4.0 // indirect
110114
github.com/sirupsen/logrus v1.9.3 // indirect
111115
github.com/spyzhov/ajson v0.9.6 // indirect
116+
github.com/stoewer/go-strcase v1.3.0 // indirect
112117
github.com/ulikunitz/xz v0.5.15 // indirect
113118
github.com/vbatts/tar-split v0.12.2 // indirect
114119
github.com/x448/float16 v0.8.4 // indirect
115120
go.opentelemetry.io/otel v1.38.0 // indirect
116121
go.opentelemetry.io/otel/trace v1.38.0 // indirect
117122
go.yaml.in/yaml/v2 v2.4.3 // indirect
118123
go.yaml.in/yaml/v3 v3.0.4 // indirect
124+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
119125
golang.org/x/net v0.47.0 // indirect
120126
golang.org/x/oauth2 v0.32.0 // indirect
121127
golang.org/x/sync v0.18.0 // indirect
122128
golang.org/x/sys v0.38.0 // indirect
123129
golang.org/x/term v0.37.0 // indirect
124130
golang.org/x/time v0.14.0 // indirect
131+
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
132+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
125133
google.golang.org/protobuf v1.36.10 // indirect
126134
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
127135
gopkg.in/inf.v0 v0.9.1 // indirect

go.sum

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
2+
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
13
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
24
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
35
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
@@ -6,6 +8,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
68
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
79
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
810
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
11+
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
12+
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
913
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
1014
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
1115
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -89,6 +93,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
8993
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
9094
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
9195
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
96+
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
97+
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
9298
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
9399
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
94100
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -203,13 +209,20 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
203209
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
204210
github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ=
205211
github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
212+
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
213+
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
206214
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
215+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
216+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
207217
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
208218
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
209219
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
210220
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
211221
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
212222
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
223+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
224+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
225+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
213226
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
214227
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
215228
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
@@ -239,6 +252,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
239252
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
240253
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
241254
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
255+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
256+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
242257
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
243258
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
244259
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
@@ -281,6 +296,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
281296
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
282297
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
283298
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
299+
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
300+
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
301+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
302+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
284303
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
285304
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
286305
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -307,6 +326,8 @@ k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJb
307326
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
308327
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
309328
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
329+
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
330+
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
310331
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
311332
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
312333
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=

internal/fnruntime/celeval.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2026 The kpt and Nephio Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package fnruntime
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
"github.com/google/cel-go/cel"
22+
"github.com/google/cel-go/common/types"
23+
"github.com/google/cel-go/ext"
24+
k8scellib "k8s.io/apiserver/pkg/cel/library"
25+
"sigs.k8s.io/kustomize/kyaml/yaml"
26+
)
27+
28+
const checkFrequency = 100
29+
30+
// This gives about .1 seconds of CPU time for the evaluation to run
31+
const costLimit = 1000000
32+
33+
// CELEvaluator evaluates CEL expressions against KRM resources
34+
type CELEvaluator struct {
35+
env *cel.Env
36+
prg cel.Program // Pre-compiled program for the condition
37+
}
38+
39+
// NewCELEvaluator creates a new CEL evaluator with the standard environment
40+
// for the given condition string.
41+
func NewCELEvaluator(condition string) (*CELEvaluator, error) {
42+
env, err := cel.NewEnv(
43+
cel.Variable("resources", cel.ListType(cel.DynType)),
44+
// Below is a list of Env settings that is a selection of https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/cel/environment/base.go
45+
// General rules are for maintaining this list.
46+
// 1. utility functions should be available. This allows for more compatibility with k8s's own CEL conditions
47+
// 2. AST validation is not needed as kpt will recompile CEL expressions every time, there is no cost-saving in exiting early
48+
// 3. Compile time optimisations do not make sense, as each CEL expression will be evaluated once before being discarded.
49+
// 3. Things that are helping with authorization in k8s are not needed, as they're returning either ResourceCheck or Decision types, which are not needed for kpt
50+
cel.HomogeneousAggregateLiterals(),
51+
cel.DefaultUTCTimeZone(true),
52+
k8scellib.URLs(),
53+
k8scellib.Regex(),
54+
k8scellib.Lists(),
55+
cel.CrossTypeNumericComparisons(true),
56+
cel.OptionalTypes(),
57+
k8scellib.Quantity(),
58+
ext.Strings(ext.StringsVersion(2)),
59+
ext.Sets(),
60+
k8scellib.IP(),
61+
k8scellib.CIDR(),
62+
k8scellib.Format(),
63+
ext.TwoVarComprehensions(),
64+
k8scellib.SemverLib(k8scellib.SemverVersion(1)),
65+
ext.Lists(ext.ListsVersion(3)),
66+
)
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to create CEL environment: %w", err)
69+
}
70+
71+
evaluator := &CELEvaluator{
72+
env: env,
73+
}
74+
75+
// Pre-compile the condition if provided
76+
if condition != "" {
77+
ast, issues := env.Compile(condition)
78+
if issues != nil && issues.Err() != nil {
79+
return nil, fmt.Errorf("failed to compile CEL expression: %w", issues.Err())
80+
}
81+
82+
// Validate that the expression returns a boolean
83+
if ast.OutputType() != cel.BoolType {
84+
return nil, fmt.Errorf("CEL expression must return a boolean, got %v", ast.OutputType())
85+
}
86+
87+
// Create the program with a hard cost limit and cost tracking enabled
88+
prg, err := env.Program(ast,
89+
cel.CostLimit(costLimit),
90+
cel.InterruptCheckFrequency(checkFrequency),
91+
cel.CostTracking(&k8scellib.CostEstimator{}),
92+
)
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to create CEL program: %w", err)
95+
}
96+
97+
evaluator.prg = prg
98+
}
99+
100+
return evaluator, nil
101+
}
102+
103+
// EvaluateCondition evaluates a CEL condition expression against a list of resources
104+
// Returns true if the condition is met, false otherwise
105+
// The program is pre-compiled, so this just evaluates it with the given resources
106+
func (e *CELEvaluator) EvaluateCondition(ctx context.Context, resources []*yaml.RNode) (bool, error) {
107+
if e.prg == nil {
108+
return true, nil
109+
}
110+
111+
// Convert resources to a format suitable for CEL
112+
resourceList, err := e.resourcesToList(resources)
113+
if err != nil {
114+
return false, fmt.Errorf("failed to convert resources: %w", err)
115+
}
116+
117+
// Evaluate the expression
118+
out, _, err := e.prg.ContextEval(ctx, map[string]interface{}{
119+
"resources": resourceList,
120+
})
121+
if err != nil {
122+
return false, fmt.Errorf("failed to evaluate CEL expression: %w", err)
123+
}
124+
125+
// Extract the boolean result
126+
result, ok := out.(types.Bool)
127+
if !ok {
128+
return false, fmt.Errorf("CEL expression must return a boolean, got %T", out)
129+
}
130+
131+
return bool(result), nil
132+
}
133+
134+
// resourcesToList converts RNodes to a list of maps for CEL evaluation
135+
func (e *CELEvaluator) resourcesToList(resources []*yaml.RNode) ([]interface{}, error) {
136+
result := make([]interface{}, 0, len(resources))
137+
138+
for _, resource := range resources {
139+
// Convert each resource to a map
140+
resourceMap, err := e.resourceToMap(resource)
141+
if err != nil {
142+
return nil, err
143+
}
144+
result = append(result, resourceMap)
145+
}
146+
147+
return result, nil
148+
}
149+
150+
// resourceToMap converts a single RNode to a map for CEL evaluation
151+
// Converts yaml.Node directly to avoid serialization overhead
152+
func (e *CELEvaluator) resourceToMap(resource *yaml.RNode) (map[string]interface{}, error) {
153+
// Get the underlying yaml.Node
154+
node := resource.YNode()
155+
if node == nil {
156+
return nil, fmt.Errorf("resource has nil yaml.Node")
157+
}
158+
159+
// Convert yaml.Node to map[string]interface{} directly
160+
var result map[string]interface{}
161+
if err := node.Decode(&result); err != nil {
162+
return nil, fmt.Errorf("failed to decode resource: %w", err)
163+
}
164+
165+
return result, nil
166+
}

0 commit comments

Comments
 (0)