diff --git a/commands/fn/render/cmdrender.go b/commands/fn/render/cmdrender.go index 43426ebef9..ceb1cc0b01 100644 --- a/commands/fn/render/cmdrender.go +++ b/commands/fn/render/cmdrender.go @@ -85,6 +85,9 @@ type Runner struct { func (r *Runner) InitDefaults() { r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix) + // Initialize CEL environment for condition evaluation + // Ignore error as conditions are optional; if CEL init fails, conditions will error at runtime + _ = r.RunnerOptions.InitCELEnvironment() } func (r *Runner) preRunE(_ *cobra.Command, args []string) error { diff --git a/documentation/content/en/book/04-using-functions/_index.md b/documentation/content/en/book/04-using-functions/_index.md index 75ca80115f..9d53926f2d 100644 --- a/documentation/content/en/book/04-using-functions/_index.md +++ b/documentation/content/en/book/04-using-functions/_index.md @@ -375,6 +375,68 @@ will merge each function pipeline list as an associative list, using `name` as the merge key. An unspecified `name` or duplicated names may result in unexpected merges. +### Specifying `condition` + +The `condition` field lets you skip a function based on the current state of the resources in the package. +It takes a [CEL](https://cel.dev/) expression that is evaluated against the resource list. If the expression +returns `true`, the function runs. If it returns `false`, the function is skipped. + +The expression receives a variable called `resources`, which is a list of all KRM resources passed to +this function step (after `selectors` and `exclude` have been applied). Each resource is a map with +the standard fields: `apiVersion`, `kind`, `metadata`, `spec`, `status`. + +For example, only run the `set-labels` function if a `ConfigMap` named `app-config` exists in the package: + +```yaml +# wordpress/Kptfile (Excerpt) +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: wordpress +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels:latest + configMap: + app: wordpress + condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config') +``` + +When you render the package, kpt shows whether the function ran or was skipped: + +```shell +$ kpt fn render wordpress +Package "wordpress": + +[RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" +[PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" + +Successfully executed 1 function(s) in 1 package(s). +``` + +If the condition is not met: + +```shell +$ kpt fn render wordpress +Package "wordpress": + +[SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" (condition not met) + +Successfully executed 1 function(s) in 1 package(s). +``` + +Some useful CEL expression patterns: + +- Check if a resource of a specific kind exists: + `resources.exists(r, r.kind == 'Deployment')` +- Check if a specific resource exists by name: + `resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'my-config')` +- Check the count of resources: + `resources.filter(r, r.kind == 'Deployment').size() > 0` + +The `condition` field can be combined with `selectors` and `exclude`. The condition is evaluated +after selectors and exclusions are applied, so `resources` only contains the resources that +passed the selection criteria. + ### Specifying `selectors` In some cases, you want to invoke the function only on a subset of resources based on a diff --git a/documentation/content/en/reference/schema/kptfile/kptfile.yaml b/documentation/content/en/reference/schema/kptfile/kptfile.yaml index e13c8fc233..86e3820612 100644 --- a/documentation/content/en/reference/schema/kptfile/kptfile.yaml +++ b/documentation/content/en/reference/schema/kptfile/kptfile.yaml @@ -71,6 +71,16 @@ definitions: this is primarily used for merging function declaration with upstream counterparts type: string x-go-name: Name + condition: + description: |- + `Condition` is an optional CEL expression that determines whether this + function should be executed. The expression is evaluated against the list + of KRM resources passed to this function step (after `Selectors` and + `Exclude` have been applied) and should return a boolean value. + If omitted or evaluates to true, the function executes normally. + If evaluates to false, the function is skipped. + type: string + x-go-name: Condition selectors: description: |- `Selectors` are used to specify resources on which the function should be executed diff --git a/e2e/testdata/fn-render/condition/condition-met/.expected/config.yaml b/e2e/testdata/fn-render/condition/condition-met/.expected/config.yaml new file mode 100644 index 0000000000..318c6710c8 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-met/.expected/config.yaml @@ -0,0 +1,12 @@ +actualStripLines: + - " stderr: 'WARNING: The requested image''s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested'" + +stdErrStripLines: + - " Stderr:" + - " \"WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested\"" + +stdErr: | + Package: "condition-met" + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/no-op" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/no-op" in 0s + Successfully executed 1 function(s) in 1 package(s). diff --git a/e2e/testdata/fn-render/condition/condition-met/.expected/diff.patch b/e2e/testdata/fn-render/condition/condition-met/.expected/diff.patch new file mode 100644 index 0000000000..28e8b8b064 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-met/.expected/diff.patch @@ -0,0 +1,19 @@ +diff --git a/Kptfile b/Kptfile +index eb90ac3..ace574a 100644 +--- a/Kptfile ++++ b/Kptfile +@@ -5,4 +5,12 @@ metadata: + pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/no-op +- condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')" ++ condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config') ++status: ++ conditions: ++ - type: Rendered ++ status: "True" ++ reason: RenderSuccess ++ renderStatus: ++ mutationSteps: ++ - image: ghcr.io/kptdev/krm-functions-catalog/no-op ++ exitCode: 0 diff --git a/e2e/testdata/fn-render/condition/condition-met/.krmignore b/e2e/testdata/fn-render/condition/condition-met/.krmignore new file mode 100644 index 0000000000..9d7a4007d6 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-met/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-render/condition/condition-met/Kptfile b/e2e/testdata/fn-render/condition/condition-met/Kptfile new file mode 100644 index 0000000000..eb90ac3a41 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-met/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: app +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/no-op + condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')" diff --git a/e2e/testdata/fn-render/condition/condition-met/resources.yaml b/e2e/testdata/fn-render/condition/condition-met/resources.yaml new file mode 100644 index 0000000000..47bec8bc08 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-met/resources.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + key: value +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 diff --git a/e2e/testdata/fn-render/condition/condition-not-met/.expected/config.yaml b/e2e/testdata/fn-render/condition/condition-not-met/.expected/config.yaml new file mode 100644 index 0000000000..cfd2519544 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-not-met/.expected/config.yaml @@ -0,0 +1,11 @@ +actualStripLines: + - " stderr: 'WARNING: The requested image''s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested'" + +stdErrStripLines: + - " Stderr:" + - " \"WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested\"" + +stdErr: | + Package: "condition-not-met" + [SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/no-op" (condition not met) + Successfully executed 1 function(s) in 1 package(s). diff --git a/e2e/testdata/fn-render/condition/condition-not-met/.expected/diff.patch b/e2e/testdata/fn-render/condition/condition-not-met/.expected/diff.patch new file mode 100644 index 0000000000..f569330fc0 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-not-met/.expected/diff.patch @@ -0,0 +1,17 @@ +diff --git a/Kptfile b/Kptfile +index eb90ac3..ace574a 100644 +--- a/Kptfile ++++ b/Kptfile +@@ -5,4 +5,11 @@ metadata: + pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/no-op +- condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')" ++ condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config') ++status: ++ conditions: ++ - type: Rendered ++ status: "True" ++ reason: RenderSuccess ++ renderStatus: ++ mutationSteps: [] diff --git a/e2e/testdata/fn-render/condition/condition-not-met/.krmignore b/e2e/testdata/fn-render/condition/condition-not-met/.krmignore new file mode 100644 index 0000000000..9d7a4007d6 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-not-met/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-render/condition/condition-not-met/Kptfile b/e2e/testdata/fn-render/condition/condition-not-met/Kptfile new file mode 100644 index 0000000000..eb90ac3a41 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-not-met/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: app +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/no-op + condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')" diff --git a/e2e/testdata/fn-render/condition/condition-not-met/resources.yaml b/e2e/testdata/fn-render/condition/condition-not-met/resources.yaml new file mode 100644 index 0000000000..c8cdecf359 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-not-met/resources.yaml @@ -0,0 +1,6 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 diff --git a/e2e/testdata/live-apply/apply-depends-on/config.yaml b/e2e/testdata/live-apply/apply-depends-on/config.yaml index c6a5e5418b..f9167da311 100644 --- a/e2e/testdata/live-apply/apply-depends-on/config.yaml +++ b/e2e/testdata/live-apply/apply-depends-on/config.yaml @@ -17,7 +17,7 @@ parallel: true kptArgs: - "live" - "apply" - - "--reconcile-timeout=2m" + - "--reconcile-timeout=5m" stdOut: | inventory update started diff --git a/e2e/testdata/live-apply/json-output/config.yaml b/e2e/testdata/live-apply/json-output/config.yaml index 6ea446894b..c2d1e3d822 100644 --- a/e2e/testdata/live-apply/json-output/config.yaml +++ b/e2e/testdata/live-apply/json-output/config.yaml @@ -17,7 +17,7 @@ kptArgs: - "live" - "apply" - "--output=json" - - "--reconcile-timeout=2m" + - "--reconcile-timeout=5m" stdOut: | {"action":"Inventory","status":"Started","timestamp":"","type":"group"} {"action":"Inventory","status":"Finished","timestamp":"","type":"group"} diff --git a/go.mod b/go.mod index b5a1effca2..567ceee3c8 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/bytecodealliance/wasmtime-go v1.0.0 github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/go-errors/errors v1.5.1 + github.com/google/cel-go v0.26.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.6 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 @@ -25,26 +26,28 @@ require ( golang.org/x/text v0.31.0 gopkg.in/yaml.v2 v2.4.0 gotest.tools v2.2.0+incompatible - k8s.io/api v0.34.1 + k8s.io/api v0.35.0 k8s.io/apiextensions-apiserver v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/cli-runtime v0.34.1 - k8s.io/client-go v0.34.1 - k8s.io/component-base v0.34.1 + k8s.io/apimachinery v0.35.0 + k8s.io/cli-runtime v0.35.0 + k8s.io/client-go v0.35.0 + k8s.io/component-base v0.35.0 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - k8s.io/kubectl v0.34.1 + k8s.io/kubectl v0.35.0 sigs.k8s.io/cli-utils v0.37.2 sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/kustomize/api v0.20.1 - sigs.k8s.io/kustomize/kyaml v0.20.1 + sigs.k8s.io/kustomize/api v0.21.0 + sigs.k8s.io/kustomize/kyaml v0.21.0 sigs.k8s.io/yaml v1.6.0 ) require ( + cel.dev/expr v0.24.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -79,7 +82,6 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect @@ -88,14 +90,12 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/moby/spdystream v0.5.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/onsi/gomega v1.37.0 // indirect + github.com/onsi/gomega v1.38.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/otiai10/mint v1.6.3 // indirect @@ -109,6 +109,7 @@ require ( github.com/sergi/go-diff v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spyzhov/ajson v0.9.6 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -116,17 +117,21 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-helpers v0.34.1 // indirect + k8s.io/apiserver v0.34.1 + k8s.io/component-helpers v0.35.0 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index e7dab440ff..17443f021c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -6,8 +8,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -89,6 +91,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -96,14 +100,12 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -137,8 +139,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -151,14 +151,12 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI= github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -188,8 +186,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/regclient/regclient v0.11.1 h1:MtxUaEVh2bgBzAX9wqH71cB4NWom4EdZ/31Z9f7ZwCU= github.com/regclient/regclient v0.11.1/go.mod h1:4Wu8lxr/v0QzrIId6cJj/2BH8gP3dUHes37lZJP0J90= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -203,13 +201,20 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= @@ -239,6 +244,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= @@ -281,6 +288,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -301,26 +312,28 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M= -k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= -k8s.io/component-helpers v0.34.1 h1:gWhH3CCdwAx5P3oJqZKb4Lg5FYZTWVbdWtOI8n9U4XY= -k8s.io/component-helpers v0.34.1/go.mod h1:4VgnUH7UA/shuBur+OWoQC0xfb69sy/93ss0ybZqm3c= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= +k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA= +k8s.io/component-helpers v0.35.0/go.mod h1:ahX0m/LTYmu7fL3W8zYiIwnQ/5gT28Ex4o2pymF63Co= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI= -k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A= +k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= +k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= @@ -329,10 +342,10 @@ sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327U sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= -sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= -sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= -sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/kustomize/api v0.21.0 h1:I7nry5p8iDJbuRdYS7ez8MUvw7XVNPcIP5GkzzuXIIQ= +sigs.k8s.io/kustomize/api v0.21.0/go.mod h1:XGVQuR5n2pXKWbzXHweZU683pALGw/AMVO4zU4iS8SE= +sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ= +sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/internal/fnruntime/celeval_test.go b/internal/fnruntime/celeval_test.go new file mode 100644 index 0000000000..e25d444421 --- /dev/null +++ b/internal/fnruntime/celeval_test.go @@ -0,0 +1,160 @@ +// Copyright 2026 The kpt and Nephio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fnruntime + +import ( + "context" + "testing" + + "github.com/kptdev/kpt/pkg/lib/runneroptions" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func newTestEnv(t *testing.T) *runneroptions.CELEnvironment { + t.Helper() + env, err := runneroptions.NewCELEnvironment() + require.NoError(t, err) + return env +} + +func TestNewCELEnvironment(t *testing.T) { + env := newTestEnv(t) + assert.NotNil(t, env) +} + +func TestEvaluateCondition_EmptyCondition(t *testing.T) { + env := newTestEnv(t) + result, err := env.EvaluateCondition(context.Background(), "", nil) + require.NoError(t, err) + assert.True(t, result, "empty condition should return true") +} + +func TestEvaluateCondition_SimpleTrue(t *testing.T) { + env := newTestEnv(t) + result, err := env.EvaluateCondition(context.Background(), "true", nil) + require.NoError(t, err) + assert.True(t, result) +} + +func TestEvaluateCondition_SimpleFalse(t *testing.T) { + env := newTestEnv(t) + result, err := env.EvaluateCondition(context.Background(), "false", nil) + require.NoError(t, err) + assert.False(t, result) +} + +func TestEvaluateCondition_ResourceExists(t *testing.T) { + env := newTestEnv(t) + + configMap, err := yaml.Parse("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-config\ndata:\n key: value") + require.NoError(t, err) + deployment, err := yaml.Parse("apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test-deployment\nspec:\n replicas: 3") + require.NoError(t, err) + + resources := []*yaml.RNode{configMap, deployment} + + result, err := env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "ConfigMap" && r.metadata.name == "test-config")`, resources) + require.NoError(t, err) + assert.True(t, result) + + result, err = env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "ConfigMap" && r.metadata.name == "wrong-name")`, resources) + require.NoError(t, err) + assert.False(t, result) + + result, err = env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "Deployment")`, resources) + require.NoError(t, err) + assert.True(t, result) +} + +func TestEvaluateCondition_ResourceCount(t *testing.T) { + env := newTestEnv(t) + + deployment, err := yaml.Parse("apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test-deployment\nspec:\n replicas: 3") + require.NoError(t, err) + resources := []*yaml.RNode{deployment} + + result, err := env.EvaluateCondition(context.Background(), + `resources.filter(r, r.kind == "Deployment").size() > 0`, resources) + require.NoError(t, err) + assert.True(t, result) + + result, err = env.EvaluateCondition(context.Background(), + `resources.filter(r, r.kind == "ConfigMap").size() == 0`, resources) + require.NoError(t, err) + assert.True(t, result) +} + +func TestEvaluateCondition_InvalidExpression(t *testing.T) { + env := newTestEnv(t) + _, err := env.EvaluateCondition(context.Background(), "this is not valid CEL", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to compile") +} + +func TestEvaluateCondition_NonBooleanResult(t *testing.T) { + env := newTestEnv(t) + _, err := env.EvaluateCondition(context.Background(), "1 + 1", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must return a boolean") +} + +func TestEvaluateCondition_Immutability(t *testing.T) { + env := newTestEnv(t) + + configMap, err := yaml.Parse("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-config\n namespace: default\ndata:\n key: original-value") + require.NoError(t, err) + + originalYAML, err := configMap.String() + require.NoError(t, err) + + _, err = env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "ConfigMap")`, []*yaml.RNode{configMap}) + require.NoError(t, err) + + afterYAML, err := configMap.String() + require.NoError(t, err) + assert.Equal(t, originalYAML, afterYAML, "CEL evaluation should not mutate input resources") +} + +func TestEvaluateCondition_MissingMetadata(t *testing.T) { + env := newTestEnv(t) + + // Resource with no metadata at all + noMetadata, err := yaml.Parse("apiVersion: v1\nkind: ConfigMap\ndata:\n key: value") + require.NoError(t, err) + + // Resource with metadata but no name + noName, err := yaml.Parse("apiVersion: v1\nkind: ConfigMap\nmetadata: {}\ndata:\n key: other") + require.NoError(t, err) + + resources := []*yaml.RNode{noMetadata, noName} + + // Should not error — missing metadata.name defaults to "" + result, err := env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "ConfigMap" && r.metadata.name == "test-config")`, resources) + require.NoError(t, err) + assert.False(t, result, "no resource should match when metadata.name is missing") + + // kind check should still work + result, err = env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "ConfigMap")`, resources) + require.NoError(t, err) + assert.True(t, result) +} diff --git a/internal/fnruntime/condition_test.go b/internal/fnruntime/condition_test.go new file mode 100644 index 0000000000..b8e2ac720d --- /dev/null +++ b/internal/fnruntime/condition_test.go @@ -0,0 +1,117 @@ +// Copyright 2026 The kpt and Nephio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fnruntime + +import ( + "context" + "io" + "testing" + + "github.com/kptdev/kpt/internal/types" + fnresult "github.com/kptdev/kpt/pkg/api/fnresult/v1" + kptfile "github.com/kptdev/kpt/pkg/api/kptfile/v1" + "github.com/kptdev/kpt/pkg/lib/runneroptions" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/filesys" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestFunctionRunner_Conditions(t *testing.T) { + ctx := context.Background() + fsys := filesys.MakeFsInMemory() + celEnv, err := runneroptions.NewCELEnvironment() + require.NoError(t, err) + + inputNodes := []*yaml.RNode{ + yaml.MustParse("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app-config"), + } + + testCases := []struct { + name string + fn *kptfile.Function + condition string + expectRun bool + }{ + { + name: "builtin runtime - condition met", + fn: &kptfile.Function{ + Image: runneroptions.FuncGenPkgContext, + }, + condition: "resources.exists(r, r.kind == 'ConfigMap')", + expectRun: true, + }, + { + name: "builtin runtime - condition not met", + fn: &kptfile.Function{ + Image: runneroptions.FuncGenPkgContext, + }, + condition: "resources.exists(r, r.kind == 'Deployment')", + expectRun: false, + }, + { + name: "executable runtime - condition met", + fn: &kptfile.Function{ + Exec: "my-exec", + }, + condition: "resources.size() > 0", + expectRun: true, + }, + { + name: "executable runtime - condition not met", + fn: &kptfile.Function{ + Exec: "my-exec", + }, + condition: "resources.size() == 0", + expectRun: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.fn.Condition = tc.condition + results := fnresult.NewResultList() + + // Mock runner options + opts := runneroptions.RunnerOptions{ + CELEnvironment: celEnv, + ResolveToImage: func(image string) string { return image }, + } + + // We use a mock runner to avoid actual execution + runner, err := NewRunner(ctx, fsys, tc.fn, types.UniquePath("pkg"), results, opts, nil) + require.NoError(t, err) + + // Override the Run function to track if it's called + wasRun := false + runner.filter.Run = func(_ io.Reader, _ io.Writer) error { + wasRun = true + return nil + } + + _, err = runner.Filter(inputNodes) + require.NoError(t, err) + + assert.Equal(t, tc.expectRun, wasRun, "Run state mismatch for: %s", tc.name) + assert.Equal(t, !tc.expectRun, runner.WasSkipped(), "Skip state mismatch for: %s", tc.name) + + if !tc.expectRun { + require.NotEmpty(t, results.Items) + assert.True(t, results.Items[0].Skipped) + assert.Equal(t, 0, results.Items[0].ExitCode) + } + }) + } +} diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go index ca9c968b81..ae528dbf29 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -150,7 +150,26 @@ func NewRunner( } } } - return NewFunctionRunner(ctx, fltr, pkgPath, fnResult, fnResults, opts) + + fr, err := NewFunctionRunner(ctx, fltr, pkgPath, fnResult, fnResults, opts) + if err != nil { + return nil, err + } + + // Set condition; the shared CEL environment from opts is used at evaluation time. + if f.Condition != "" { + if opts.CELEnvironment == nil { + name := f.Image + if name == "" { + name = f.Exec + } + return nil, fmt.Errorf("condition specified for function %q but no CEL environment is configured in RunnerOptions", name) + } + fr.condition = f.Condition + fr.celEnv = opts.CELEnvironment + } + + return fr, nil } // NewFunctionRunner returns a FunctionRunner given a specification of a function @@ -191,10 +210,44 @@ type FunctionRunner struct { fnResult *fnresult.Result fnResults *fnresult.ResultList opts runneroptions.RunnerOptions + condition string // CEL condition expression + celEnv *runneroptions.CELEnvironment // shared CEL environment for condition evaluation + skipped bool // true if function execution was skipped due to condition +} + +func (fr *FunctionRunner) SetCondition(condition string, celEnv *runneroptions.CELEnvironment) { + fr.condition = condition + fr.celEnv = celEnv +} + +func (fr *FunctionRunner) WasSkipped() bool { + return fr.skipped } func (fr *FunctionRunner) Filter(input []*yaml.RNode) (output []*yaml.RNode, err error) { pr := printer.FromContextOrDie(fr.ctx) + + // Check condition before executing function + if fr.celEnv != nil && fr.condition != "" { + shouldExecute, err := fr.celEnv.EvaluateCondition(fr.ctx, fr.condition, input) + if err != nil { + return nil, fmt.Errorf("failed to evaluate condition for function %q: %w", fr.name, err) + } + + if !shouldExecute { + if !fr.disableCLIOutput { + pr.Printf("[SKIPPED] %q (condition not met)\n", fr.name) + } + // Append a skipped result so consumers get one result per pipeline step + fr.fnResult.ExitCode = 0 + fr.fnResult.Skipped = true + fr.fnResults.Items = append(fr.fnResults.Items, *fr.fnResult) + // Return input unchanged - function is skipped + fr.skipped = true + return input, nil + } + } + if !fr.disableCLIOutput { if fr.opts.AllowWasm { if fr.opts.DisplayResourceCount { diff --git a/internal/util/get/get.go b/internal/util/get/get.go index 1d74f85c40..b839b9af6e 100644 --- a/internal/util/get/get.go +++ b/internal/util/get/get.go @@ -150,6 +150,9 @@ func (c Command) Run(ctx context.Context) error { pr.Printf("\nCustomizing package for deployment.\n") hookCmd := hook.Executor{} hookCmd.RunnerOptions.InitDefaults(c.DefaultKrmFunctionImagePrefix) + // Initialize CEL environment for condition evaluation + // Ignore error as conditions are optional; if CEL init fails, conditions will error at runtime + _ = hookCmd.RunnerOptions.InitCELEnvironment() hookCmd.PkgPath = c.Destination builtinHooks := []kptfilev1.Function{ diff --git a/internal/util/render/executor.go b/internal/util/render/executor.go index 8bd5c652da..975108fa80 100644 --- a/internal/util/render/executor.go +++ b/internal/util/render/executor.go @@ -812,7 +812,9 @@ func (pn *pkgNode) runMutators(ctx context.Context, hctx *hydrationContext, inpu hctx.mutationSteps = append(hctx.mutationSteps, captureStepResult(pl.Mutators[i], hctx.fnResults, resultCountBeforeExec, err)) return input, err } - hctx.executedFunctionCnt++ + if !mutator.WasSkipped() { + hctx.executedFunctionCnt++ + } hctx.mutationSteps = append(hctx.mutationSteps, captureStepResult(pl.Mutators[i], hctx.fnResults, resultCountBeforeExec, nil)) if len(selectors) > 0 || len(exclusions) > 0 { @@ -870,11 +872,14 @@ func (pn *pkgNode) runValidators(ctx context.Context, hctx *hydrationContext, in hctx.validationSteps = append(hctx.validationSteps, preExecFailureStep(function, err)) return err } - if _, err = validator.Filter(cloneResources(selectedResources)); err != nil { + validatorRunner := validator.(*fnruntime.FunctionRunner) + if _, err = validatorRunner.Filter(cloneResources(selectedResources)); err != nil { hctx.validationSteps = append(hctx.validationSteps, captureStepResult(function, hctx.fnResults, resultCountBeforeExec, err)) return err } - hctx.executedFunctionCnt++ + if !validatorRunner.WasSkipped() { + hctx.executedFunctionCnt++ + } hctx.validationSteps = append(hctx.validationSteps, captureStepResult(function, hctx.fnResults, resultCountBeforeExec, nil)) } return nil @@ -1059,9 +1064,10 @@ func captureStepResult(fn kptfilev1.Function, fnResults *fnresult.ResultList, re step.Stderr = last.Stderr step.ExitCode = last.ExitCode step.Results = frameworkResultsToItems(last.Results) - for _, ri := range step.Results { - if ri.Severity == string(framework.Error) { - step.ErrorResults = append(step.ErrorResults, ri) + step.Skipped = last.Skipped + for _, item := range step.Results { + if item.Severity == string(framework.Error) { + step.ErrorResults = append(step.ErrorResults, item) } } } else if execErr != nil { diff --git a/pkg/api/fnresult/v1/types.go b/pkg/api/fnresult/v1/types.go index 6a318a83d4..b47a83403a 100644 --- a/pkg/api/fnresult/v1/types.go +++ b/pkg/api/fnresult/v1/types.go @@ -39,6 +39,8 @@ type Result struct { ExitCode int `yaml:"exitCode"` // Results is the list of results for the function Results framework.Results `yaml:"results,omitempty"` + // Skipped indicates if the function was skipped due to a condition + Skipped bool `yaml:"skipped,omitempty"` } const ( diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go index 3fff13720c..249b0d129f 100644 --- a/pkg/api/kptfile/v1/types.go +++ b/pkg/api/kptfile/v1/types.go @@ -361,6 +361,20 @@ type Function struct { // `Exclude` are used to specify resources on which the function should NOT be executed. // If not specified, all resources selected by `Selectors` are selected. Exclusions []Selector `yaml:"exclude,omitempty" json:"exclude,omitempty"` + + // `Condition` is an optional CEL expression that determines whether this + // function should be executed. The expression is evaluated against the list + // of KRM resources passed to this function step (after `Selectors` and + // `Exclude` have been applied) and should return a boolean value. + // If omitted or evaluates to true, the function executes normally. + // If evaluates to false, the function is skipped. + // + // Example: Check if a specific ConfigMap exists among the selected resources: + // condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'my-config')" + // + // Example: Check resource count among the selected resources: + // condition: "resources.filter(r, r.kind == 'Deployment').size() > 0" + Condition string `yaml:"condition,omitempty" json:"condition,omitempty"` } // Selector specifies the selection criteria @@ -433,6 +447,8 @@ type PipelineStepResult struct { ExitCode int `yaml:"exitCode" json:"exitCode"` Results []ResultItem `yaml:"results,omitempty" json:"results,omitempty"` ErrorResults []ResultItem `yaml:"errorResults,omitempty" json:"errorResults,omitempty"` + // Skipped indicates if the function was skipped due to a condition + Skipped bool `yaml:"skipped,omitempty" json:"skipped,omitempty"` } // ResultItem mirrors framework.Result with only the fields needed for Kptfile status. diff --git a/pkg/lib/kptops/fs_test.go b/pkg/lib/kptops/fs_test.go index d989f2fe15..9dcebea9b6 100644 --- a/pkg/lib/kptops/fs_test.go +++ b/pkg/lib/kptops/fs_test.go @@ -107,6 +107,9 @@ spec: Runtime: &runtime{}, } r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix) + if err := r.RunnerOptions.InitCELEnvironment(); err != nil { + t.Fatalf("Failed to initialize CEL environment: %v", err) + } r.RunnerOptions.ImagePullPolicy = runneroptions.IfNotPresentPull _, err := r.Execute(fake.CtxWithDefaultPrinter()) if err != nil { @@ -221,6 +224,9 @@ spec: Runtime: &runtime{}, } r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix) + if err := r.RunnerOptions.InitCELEnvironment(); err != nil { + t.Fatalf("Failed to initialize CEL environment: %v", err) + } r.RunnerOptions.ImagePullPolicy = runneroptions.IfNotPresentPull _, err := r.Execute(fake.CtxWithDefaultPrinter()) diff --git a/pkg/lib/kptops/render_test.go b/pkg/lib/kptops/render_test.go index 8170e236b7..de61adfecd 100644 --- a/pkg/lib/kptops/render_test.go +++ b/pkg/lib/kptops/render_test.go @@ -73,6 +73,9 @@ func TestRender(t *testing.T) { Output: &output, } r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix) + if err := r.RunnerOptions.InitCELEnvironment(); err != nil { + t.Fatalf("Failed to initialize CEL environment: %v", err) + } if _, err := r.Execute(fake.CtxWithDefaultPrinter()); err != nil { t.Errorf("Render failed: %v", err) diff --git a/pkg/lib/runneroptions/celenv.go b/pkg/lib/runneroptions/celenv.go new file mode 100644 index 0000000000..aeda0bc3b0 --- /dev/null +++ b/pkg/lib/runneroptions/celenv.go @@ -0,0 +1,157 @@ +// Copyright 2026 The kpt and Nephio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runneroptions + +import ( + "context" + "fmt" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/ext" + k8scellib "k8s.io/apiserver/pkg/cel/library" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +const ( + celCheckFrequency = 100 + // celCostLimit gives about .1 seconds of CPU time for the evaluation to run + celCostLimit = 1000000 +) + +// CELEnvironment holds a shared CEL environment for evaluating conditions. +// The environment is created once and reused; programs are compiled per condition call. +type CELEnvironment struct { + env *cel.Env +} + +// NewCELEnvironment creates a new CELEnvironment with the standard KRM variable bindings. +// Includes cel-go built-in extensions and k8s-specific validators (IP, CIDR, Quantity, SemVer) +// from k8s.io/apiserver/pkg/cel/library for full Kubernetes CEL compatibility. +func NewCELEnvironment() (*CELEnvironment, error) { + env, err := cel.NewEnv( + cel.Variable("resources", cel.ListType(cel.DynType)), + cel.HomogeneousAggregateLiterals(), + cel.DefaultUTCTimeZone(true), + cel.CrossTypeNumericComparisons(true), + cel.OptionalTypes(), + ext.Strings(ext.StringsVersion(2)), + ext.Sets(), + ext.TwoVarComprehensions(), + ext.Lists(ext.ListsVersion(3)), + k8scellib.IP(), + k8scellib.CIDR(), + k8scellib.Quantity(), + k8scellib.SemverLib(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create CEL environment: %w", err) + } + return &CELEnvironment{env: env}, nil +} + +// EvaluateCondition compiles and evaluates a CEL condition against a list of KRM resources. +// Returns true if the condition is met, false otherwise. +// An empty condition always returns true (function executes unconditionally). +func (e *CELEnvironment) EvaluateCondition(ctx context.Context, condition string, resources []*yaml.RNode) (bool, error) { + if condition == "" { + return true, nil + } + + ast, issues := e.env.Compile(condition) + if issues != nil && issues.Err() != nil { + return false, fmt.Errorf("failed to compile CEL expression: %w", issues.Err()) + } + + if ast.OutputType() != cel.BoolType { + return false, fmt.Errorf("CEL expression must return a boolean, got %v", ast.OutputType()) + } + + prg, err := e.env.Program(ast, + cel.CostLimit(celCostLimit), + cel.InterruptCheckFrequency(celCheckFrequency), + ) + if err != nil { + return false, fmt.Errorf("failed to create CEL program: %w", err) + } + + resourceList, err := resourcesToList(resources) + if err != nil { + return false, fmt.Errorf("failed to convert resources: %w", err) + } + + out, _, err := prg.ContextEval(ctx, map[string]any{ + "resources": resourceList, + }) + if err != nil { + return false, fmt.Errorf("failed to evaluate CEL expression: %w", err) + } + + result, ok := out.(types.Bool) + if !ok { + return false, fmt.Errorf("CEL expression must return a boolean, got %T", out) + } + + return bool(result), nil +} + +func resourcesToList(resources []*yaml.RNode) ([]any, error) { + result := make([]any, 0, len(resources)) + for _, resource := range resources { + m, err := resourceToMap(resource) + if err != nil { + return nil, err + } + result = append(result, m) + } + return result, nil +} + +func resourceToMap(resource *yaml.RNode) (map[string]any, error) { + node := resource.YNode() + if node == nil { + return nil, fmt.Errorf("resource has nil yaml.Node") + } + var result map[string]any + if err := node.Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode resource: %w", err) + } + // Ensure standard KRM fields are always present so CEL expressions like + // r.kind == "Deployment" never error with "no such key". + if _, ok := result["apiVersion"]; !ok { + result["apiVersion"] = "" + } + if _, ok := result["kind"]; !ok { + result["kind"] = "" + } + // Ensure metadata and its common nested keys exist so expressions like + // r.metadata.name and r.metadata.namespace do not fail on missing keys. + if mdVal, ok := result["metadata"]; ok { + if mdMap, ok := mdVal.(map[string]any); ok { + if _, ok := mdMap["name"]; !ok { + mdMap["name"] = "" + } + if _, ok := mdMap["namespace"]; !ok { + mdMap["namespace"] = "" + } + result["metadata"] = mdMap + } else { + result["metadata"] = map[string]any{"name": "", "namespace": ""} + } + } else { + result["metadata"] = map[string]any{"name": "", "namespace": ""} + } + return result, nil +} diff --git a/pkg/lib/runneroptions/runneroptions.go b/pkg/lib/runneroptions/runneroptions.go index be5c2437c6..01bc622b15 100644 --- a/pkg/lib/runneroptions/runneroptions.go +++ b/pkg/lib/runneroptions/runneroptions.go @@ -57,6 +57,10 @@ type RunnerOptions struct { // ResolveToImage will resolve a partial image to a fully-qualified one ResolveToImage ImageResolveFunc + + // CELEnvironment is the shared CEL environment used to evaluate function conditions. + // It is initialised by InitDefaults and reused across all function runners. + CELEnvironment *CELEnvironment } func (opts *RunnerOptions) InitDefaults(defaultImagePrefix string) { @@ -64,6 +68,18 @@ func (opts *RunnerOptions) InitDefaults(defaultImagePrefix string) { opts.ResolveToImage = ResolveToImageForCLIFunc(defaultImagePrefix) } +// InitCELEnvironment initializes the CEL environment for condition evaluation. +// This should be called separately after InitDefaults to allow proper error handling. +// Returns an error if CEL environment creation fails. +func (opts *RunnerOptions) InitCELEnvironment() error { + celEnv, err := NewCELEnvironment() + if err != nil { + return fmt.Errorf("failed to initialise CEL environment: %w", err) + } + opts.CELEnvironment = celEnv + return nil +} + // ResolveToImageForCLIFunc returns a func that converts the KRM function short path to the full image url. // If the function is a catalog function, it prepends `prefix`, e.g. "set-namespace:v0.1" --> prefix + "set-namespace:v0.1". // A "/" is appended to `prefix` if it is not an empty string and does not end with a "/". diff --git a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go index 5cc0ffbea2..2631f11472 100644 --- a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go +++ b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go @@ -117,6 +117,8 @@ func GetEvalFnRunner(ctx context.Context, parent string) *EvalFnRunner { &r.excludeAnnotations, "exclude-annotations", []string{}, "exclude resources matching the given annotations") r.Command.Flags().StringArrayVar( &r.excludeLabels, "exclude-labels", []string{}, "exclude resources matching the given labels") + r.Command.Flags().StringVar( + &r.Condition, "condition", "", "conditional expression to determine if function should be run") if err := r.Command.Flags().MarkHidden("include-meta-resources"); err != nil { panic(err) @@ -161,11 +163,16 @@ type EvalFnRunner struct { excludeLabels []string excludeAnnotations []string + Condition string + runFns runfn.RunFns } func (r *EvalFnRunner) InitDefaults() { r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix) + // Initialize CEL environment for condition evaluation + // Ignore error as conditions are optional; if CEL init fails, conditions will error at runtime + _ = r.RunnerOptions.InitCELEnvironment() } func (r *EvalFnRunner) runE(c *cobra.Command, _ []string) error { @@ -199,6 +206,7 @@ func (r *EvalFnRunner) NewFunction() *kptfile.Function { if !r.Exclusion.IsEmpty() { newFn.Exclusions = []kptfile.Selector{r.Exclusion} } + newFn.Condition = r.Condition if r.FnConfigPath != "" { fnConfigAbsPath, _, _ := pathutil.ResolveAbsAndRelPaths(r.FnConfigPath) pkgAbsPath, _, _ := pathutil.ResolveAbsAndRelPaths(r.runFns.Path) @@ -553,6 +561,7 @@ func (r *EvalFnRunner) preRunE(c *cobra.Command, args []string) error { ContinueOnEmptyResult: true, Selector: r.Selector, Exclusion: r.Exclusion, + Condition: r.Condition, RunnerOptions: r.RunnerOptions, } diff --git a/thirdparty/cmdconfig/commands/cmdeval/cmdeval_test.go b/thirdparty/cmdconfig/commands/cmdeval/cmdeval_test.go index a7e862dfc8..78ad390ba1 100644 --- a/thirdparty/cmdconfig/commands/cmdeval/cmdeval_test.go +++ b/thirdparty/cmdconfig/commands/cmdeval/cmdeval_test.go @@ -432,6 +432,7 @@ apiVersion: v1 r.runFns.Function = nil r.runFns.FnConfig = nil r.runFns.RunnerOptions.ResolveToImage = nil + r.runFns.RunnerOptions.CELEnvironment = nil tt.expectedStruct.FnConfigPath = tt.fnConfigPath if !assert.Equal(t, *tt.expectedStruct, r.runFns) { t.FailNow() diff --git a/thirdparty/kyaml/runfn/runfn.go b/thirdparty/kyaml/runfn/runfn.go index df6c57af2c..9ed580c4f2 100644 --- a/thirdparty/kyaml/runfn/runfn.go +++ b/thirdparty/kyaml/runfn/runfn.go @@ -97,6 +97,7 @@ type RunFns struct { Selector kptfile.Selector Exclusion kptfile.Selector + Condition string } // Execute runs the command @@ -413,5 +414,9 @@ func (r *RunFns) defaultFnFilterProvider(spec runtimeutil.FunctionSpec, fnConfig opts.DisplayResourceCount = true } - return fnruntime.NewFunctionRunner(r.Ctx, fltr, "", fnResult, r.fnResults, opts) + runner, _ := fnruntime.NewFunctionRunner(r.Ctx, fltr, "", fnResult, r.fnResults, opts) + if r.Condition != "" { + runner.SetCondition(r.Condition, opts.CELEnvironment) + } + return runner, nil }