mirror of
https://github.com/kubernetes-sigs/descheduler.git
synced 2026-08-30 12:47:17 +00:00
Compare commits
33
Commits
release-1.36
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a0830ae0b | ||
|
|
9914e71ece | ||
|
|
8bbd0bd661 | ||
|
|
b66b54f6c0 | ||
|
|
5aca81876e | ||
|
|
ef090e93de | ||
|
|
bdb6f3e42d | ||
|
|
4233637de9 | ||
|
|
6fe697934e | ||
|
|
fb908401dd | ||
|
|
ad19067697 | ||
|
|
73bcc99a5e | ||
|
|
82b96a392e | ||
|
|
5c3469c3ff | ||
|
|
9d09c0d65f | ||
|
|
0b2a396ac1 | ||
|
|
e08a32dfa8 | ||
|
|
b5a214909c | ||
|
|
ec736d7140 | ||
|
|
51ecda87fb | ||
|
|
39a2b0c685 | ||
|
|
3691abd9ac | ||
|
|
4471d02801 | ||
|
|
0614367253 | ||
|
|
7d2b28bf2b | ||
|
|
cf2e95cb60 | ||
|
|
ea957b271b | ||
|
|
bb14e73150 | ||
|
|
341aba2723 | ||
|
|
607b578980 | ||
|
|
436b0ac076 | ||
|
|
86183b15a2 | ||
|
|
81fb28c18c |
@@ -13,7 +13,7 @@ jobs:
|
||||
descheduler-version: ["v0.36.0"]
|
||||
descheduler-api: ["v1alpha2"]
|
||||
manifest: ["deployment"]
|
||||
kind-version: ["v0.31.0"] # keep in sync with test/run-e2e-tests.sh
|
||||
kind-version: ["v0.31.0"] # keep in sync with test/lib/e2e-versions.env
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
|
||||
@@ -62,9 +62,17 @@ The following table lists the configurable parameters of the _descheduler_ chart
|
||||
| `ttlSecondsAfterFinished` | If set, configure `ttlSecondsAfterFinished` for the _descheduler_ job | `nil` |
|
||||
| `deschedulingInterval` | If using kind:Deployment, sets time between consecutive descheduler executions. | `5m` |
|
||||
| `replicas` | The replica count for Deployment | `1` |
|
||||
| `revisionHistoryLimit | The revision history limit for Deployment | `10` |
|
||||
| `leaderElection` | The options for high availability when running replicated components | _see values.yaml_ |
|
||||
| `podDisruptionBudget.enabled` | If `true` and `kind` is `Deployment`, create a PodDisruptionBudget for the descheduler | `false` |
|
||||
| `podDisruptionBudget.minAvailable` | Minimum descheduler pods that must stay available (mutually exclusive with `maxUnavailable`) | `1` |
|
||||
| `podDisruptionBudget.maxUnavailable`| Maximum descheduler pods that can be unavailable (mutually exclusive with `minAvailable`) | `""` |
|
||||
| `podDisruptionBudget.annotations` | Annotations to add to the PodDisruptionBudget | `{}` |
|
||||
| `cmdOptions` | The options to pass to the _descheduler_ command | _see values.yaml_ |
|
||||
| `priorityClassName` | The name of the priority class to add to pods | `system-cluster-critical` |
|
||||
| `schedulerName` | The name of the scheduler used to schedule the descheduler cronjob/deployment pods | `""` |
|
||||
| `runtimeClassName` | The RuntimeClass applied to the descheduler cronjob/deployment pods | `""` |
|
||||
| `hostUsers` | If set (`true`/`false`), sets `hostUsers` on the pod spec to control user-namespace sharing | `nil` |
|
||||
| `rbac.create` | If `true`, create & use RBAC resources | `true` |
|
||||
| `resources` | Descheduler container CPU and memory requests/limits | _see values.yaml_ |
|
||||
| `serviceAccount.create` | If `true`, create a service account for the cron job | `true` |
|
||||
|
||||
@@ -17,7 +17,7 @@ rules:
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list", "delete"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/eviction"]
|
||||
verbs: ["create"]
|
||||
|
||||
@@ -87,6 +87,15 @@ spec:
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: {{ .Values.priorityClassName }}
|
||||
{{- end }}
|
||||
{{- if .Values.schedulerName }}
|
||||
schedulerName: {{ .Values.schedulerName }}
|
||||
{{- end }}
|
||||
{{- if .Values.runtimeClassName }}
|
||||
runtimeClassName: {{ .Values.runtimeClassName }}
|
||||
{{- end }}
|
||||
{{- if kindIs "bool" .Values.hostUsers }}
|
||||
hostUsers: {{ .Values.hostUsers }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ template "descheduler.serviceAccountName" . }}
|
||||
{{- if kindIs "bool" .Values.automountServiceAccountToken }}
|
||||
automountServiceAccountToken: {{ .Values.automountServiceAccountToken }}
|
||||
|
||||
@@ -18,6 +18,7 @@ spec:
|
||||
{{- else }}
|
||||
replicas: 1
|
||||
{{- end }}
|
||||
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "descheduler.selectorLabels" . | nindent 6 }}
|
||||
@@ -41,6 +42,15 @@ spec:
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: {{ .Values.priorityClassName }}
|
||||
{{- end }}
|
||||
{{- if .Values.schedulerName }}
|
||||
schedulerName: {{ .Values.schedulerName }}
|
||||
{{- end }}
|
||||
{{- if .Values.runtimeClassName }}
|
||||
runtimeClassName: {{ .Values.runtimeClassName }}
|
||||
{{- end }}
|
||||
{{- if kindIs "bool" .Values.hostUsers }}
|
||||
hostUsers: {{ .Values.hostUsers }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ template "descheduler.serviceAccountName" . }}
|
||||
{{- if kindIs "bool" .Values.automountServiceAccountToken }}
|
||||
automountServiceAccountToken: {{ .Values.automountServiceAccountToken }}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{- if and (eq .Values.kind "Deployment") (.Values.podDisruptionBudget).enabled }}
|
||||
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}
|
||||
apiVersion: policy/v1
|
||||
{{- else }}
|
||||
apiVersion: policy/v1beta1
|
||||
{{- end }}
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ template "descheduler.fullname" . }}
|
||||
namespace: {{ include "descheduler.namespace" . }}
|
||||
labels:
|
||||
{{- include "descheduler.labels" . | nindent 4 }}
|
||||
{{- with .Values.podDisruptionBudget.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.podDisruptionBudget.minAvailable }}
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
{{- end }}
|
||||
{{- if .Values.podDisruptionBudget.maxUnavailable }}
|
||||
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
|
||||
{{- end }}
|
||||
{{- with .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }}
|
||||
unhealthyPodEvictionPolicy: {{ . }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "descheduler.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -15,3 +15,53 @@ tests:
|
||||
asserts:
|
||||
- isKind:
|
||||
of: CronJob
|
||||
|
||||
- it: sets schedulerName when provided
|
||||
set:
|
||||
schedulerName: custom-scheduler
|
||||
template: templates/cronjob.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.jobTemplate.spec.template.spec.schedulerName
|
||||
value: custom-scheduler
|
||||
|
||||
- it: sets runtimeClassName when provided
|
||||
set:
|
||||
runtimeClassName: gvisor
|
||||
template: templates/cronjob.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.jobTemplate.spec.template.spec.runtimeClassName
|
||||
value: gvisor
|
||||
|
||||
- it: omits schedulerName and runtimeClassName by default
|
||||
template: templates/cronjob.yaml
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.jobTemplate.spec.template.spec.schedulerName
|
||||
- notExists:
|
||||
path: spec.jobTemplate.spec.template.spec.runtimeClassName
|
||||
|
||||
- it: sets hostUsers to false when configured
|
||||
set:
|
||||
hostUsers: false
|
||||
template: templates/cronjob.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.jobTemplate.spec.template.spec.hostUsers
|
||||
value: false
|
||||
|
||||
- it: sets hostUsers to true when configured
|
||||
set:
|
||||
hostUsers: true
|
||||
template: templates/cronjob.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.jobTemplate.spec.template.spec.hostUsers
|
||||
value: true
|
||||
|
||||
- it: omits hostUsers by default
|
||||
template: templates/cronjob.yaml
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.jobTemplate.spec.template.spec.hostUsers
|
||||
|
||||
@@ -47,3 +47,53 @@ tests:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].args
|
||||
content: --leader-elect-resource-namespace=typo
|
||||
|
||||
- it: sets schedulerName when provided
|
||||
set:
|
||||
schedulerName: custom-scheduler
|
||||
template: templates/deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.schedulerName
|
||||
value: custom-scheduler
|
||||
|
||||
- it: sets runtimeClassName when provided
|
||||
set:
|
||||
runtimeClassName: gvisor
|
||||
template: templates/deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.runtimeClassName
|
||||
value: gvisor
|
||||
|
||||
- it: omits schedulerName and runtimeClassName by default
|
||||
template: templates/deployment.yaml
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.template.spec.schedulerName
|
||||
- notExists:
|
||||
path: spec.template.spec.runtimeClassName
|
||||
|
||||
- it: sets hostUsers to false when configured
|
||||
set:
|
||||
hostUsers: false
|
||||
template: templates/deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.hostUsers
|
||||
value: false
|
||||
|
||||
- it: sets hostUsers to true when configured
|
||||
set:
|
||||
hostUsers: true
|
||||
template: templates/deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.hostUsers
|
||||
value: true
|
||||
|
||||
- it: omits hostUsers by default
|
||||
template: templates/deployment.yaml
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.template.spec.hostUsers
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
suite: Test Descheduler PodDisruptionBudget
|
||||
|
||||
templates:
|
||||
- "*.yaml"
|
||||
|
||||
release:
|
||||
name: descheduler
|
||||
|
||||
tests:
|
||||
- it: is not created for the default CronJob kind even when enabled
|
||||
template: templates/poddisruptionbudget.yaml
|
||||
set:
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: is not created for Deployment kind unless enabled
|
||||
template: templates/poddisruptionbudget.yaml
|
||||
set:
|
||||
kind: Deployment
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: creates a PodDisruptionBudget for Deployment kind when enabled
|
||||
template: templates/poddisruptionbudget.yaml
|
||||
set:
|
||||
kind: Deployment
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
asserts:
|
||||
- isKind:
|
||||
of: PodDisruptionBudget
|
||||
- equal:
|
||||
path: spec.minAvailable
|
||||
value: 1
|
||||
- notExists:
|
||||
path: spec.maxUnavailable
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/name"]
|
||||
value: descheduler
|
||||
|
||||
- it: supports maxUnavailable instead of minAvailable
|
||||
template: templates/poddisruptionbudget.yaml
|
||||
set:
|
||||
kind: Deployment
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: ""
|
||||
maxUnavailable: 1
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.maxUnavailable
|
||||
value: 1
|
||||
- notExists:
|
||||
path: spec.minAvailable
|
||||
|
||||
- it: supports unhealthyPodEvictionPolicy and annotations
|
||||
template: templates/poddisruptionbudget.yaml
|
||||
set:
|
||||
kind: Deployment
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
unhealthyPodEvictionPolicy: IfHealthyBudget
|
||||
annotations:
|
||||
example.com/team: platform
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.unhealthyPodEvictionPolicy
|
||||
value: IfHealthyBudget
|
||||
- equal:
|
||||
path: metadata.annotations["example.com/team"]
|
||||
value: platform
|
||||
@@ -68,6 +68,8 @@ deschedulingInterval: 5m
|
||||
# only if that node is in the same zone as at least one already-running descheduler
|
||||
replicas: 1
|
||||
|
||||
revisionHistoryLimit: 10
|
||||
|
||||
# Specifies whether Leader Election resources should be created
|
||||
# Required when running as a Deployment
|
||||
# NOTE: Leader election can't be activated if DryRun enabled
|
||||
@@ -80,6 +82,20 @@ leaderElection: {}
|
||||
# resourceName: "descheduler"
|
||||
# resourceNamespace: "kube-system"
|
||||
|
||||
# Pod Disruption Budget for the descheduler Deployment. Optional.
|
||||
# Only rendered when kind is "Deployment". The descheduler runs a single replica
|
||||
# by default, so this is disabled by default. Enable it for highly available
|
||||
# setups (replicas > 1 with leaderElection enabled) to keep a descheduler pod
|
||||
# available during voluntary node disruptions.
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
# minAvailable and maxUnavailable are mutually exclusive; set only one.
|
||||
minAvailable: 1
|
||||
maxUnavailable: ""
|
||||
# See https://kubernetes.io/docs/tasks/run-application/configure-pdb/
|
||||
# unhealthyPodEvictionPolicy: IfHealthyBudget
|
||||
annotations: {}
|
||||
|
||||
command:
|
||||
- "/bin/descheduler"
|
||||
|
||||
@@ -150,6 +166,19 @@ deschedulerPolicy:
|
||||
|
||||
priorityClassName: system-cluster-critical
|
||||
|
||||
# schedulerName is the name of the scheduler used to schedule the descheduler pods.
|
||||
# Leave empty to use the cluster default scheduler.
|
||||
schedulerName: ""
|
||||
|
||||
# runtimeClassName is the RuntimeClass applied to the descheduler pods.
|
||||
# Leave empty to use the cluster default runtime.
|
||||
runtimeClassName: ""
|
||||
|
||||
# Set the Pod spec `hostUsers` field to control user-namespace sharing.
|
||||
# Default is not set (the cluster default applies). Set to `false` to opt into
|
||||
# user-namespace isolation, or `true` to share the host user namespace.
|
||||
# hostUsers: false
|
||||
|
||||
nodeSelector: {}
|
||||
# foo: bar
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ import (
|
||||
|
||||
"sigs.k8s.io/descheduler/cmd/descheduler/app/options"
|
||||
"sigs.k8s.io/descheduler/pkg/descheduler"
|
||||
"sigs.k8s.io/descheduler/pkg/features"
|
||||
"sigs.k8s.io/descheduler/pkg/tracing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/component-base/featuregate"
|
||||
"k8s.io/component-base/logs"
|
||||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
_ "k8s.io/component-base/logs/json/register"
|
||||
@@ -49,7 +49,6 @@ func NewDeschedulerCommand(out io.Writer) *cobra.Command {
|
||||
klog.ErrorS(err, "unable to initialize server")
|
||||
}
|
||||
|
||||
featureGate := featuregate.NewFeatureGate()
|
||||
logConfig := logsapi.NewLoggingConfiguration()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -58,7 +57,10 @@ func NewDeschedulerCommand(out io.Writer) *cobra.Command {
|
||||
Long: "The descheduler evicts pods which may be bound to less desired nodes",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
logs.InitLogs()
|
||||
if logsapi.ValidateAndApply(logConfig, featureGate); err != nil {
|
||||
if err := features.DefaultMutableFeatureGate.SetFromMap(s.FeatureGates); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := logsapi.ValidateAndApply(logConfig, features.DefaultMutableFeatureGate); err != nil {
|
||||
return err
|
||||
}
|
||||
descheduler.SetupPlugins()
|
||||
@@ -80,9 +82,9 @@ func NewDeschedulerCommand(out io.Writer) *cobra.Command {
|
||||
}
|
||||
cmd.SetOut(out)
|
||||
flags := cmd.Flags()
|
||||
runtime.Must(logsapi.AddFeatureGates(features.DefaultMutableFeatureGate))
|
||||
s.AddFlags(flags)
|
||||
|
||||
runtime.Must(logsapi.AddFeatureGates(featureGate))
|
||||
logsapi.AddFlags(logConfig, flags)
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2026 The Kubernetes 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 app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeschedulerCommand_LoggingAlphaOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "LoggingAlphaOptions=false rejects --log-json-split-stream",
|
||||
args: []string{
|
||||
"--logging-format=json",
|
||||
"--log-json-split-stream",
|
||||
"--feature-gates=LoggingAlphaOptions=false",
|
||||
},
|
||||
wantErr: "LoggingAlphaOptions is disabled",
|
||||
},
|
||||
{
|
||||
name: "LoggingAlphaOptions=true accepts --log-json-split-stream",
|
||||
args: []string{
|
||||
"--logging-format=json",
|
||||
"--log-json-split-stream",
|
||||
"--feature-gates=LoggingAlphaOptions=true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewDeschedulerCommand(io.Discard)
|
||||
if err := cmd.ParseFlags(tc.args); err != nil {
|
||||
t.Fatalf("ParseFlags(%v): %v", tc.args, err)
|
||||
}
|
||||
err := cmd.PreRunE(cmd, cmd.Flags().Args())
|
||||
if tc.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("expected PreRunE to succeed, got error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("expected PreRunE to fail with %q, got nil", tc.wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("expected PreRunE error to contain %q, got :%v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,10 @@ descheduler [flags]
|
||||
--feature-gates mapStringBool A set of key=value pairs that describe feature gates for alpha/experimental features. Options are:
|
||||
AllAlpha=true|false (ALPHA - default=false)
|
||||
AllBeta=true|false (BETA - default=false)
|
||||
ContextualLogging=true|false (BETA - default=true)
|
||||
EvictionsInBackground=true|false (ALPHA - default=false)
|
||||
LoggingAlphaOptions=true|false (ALPHA - default=false)
|
||||
LoggingBetaOptions=true|false (BETA - default=true)
|
||||
-h, --help help for descheduler
|
||||
--http2-max-streams-per-connection int The limit that the server gives to clients for the maximum number of streams in an HTTP/2 connection. Zero means to use golang's default.
|
||||
--kubeconfig string File with kube configuration. Deprecated, use client-connection-kubeconfig instead.
|
||||
|
||||
@@ -29,6 +29,25 @@ View all CLI options.
|
||||
```
|
||||
|
||||
## Run Tests
|
||||
|
||||
### All-in-one e2e (kind cluster created by the test script)
|
||||
|
||||
```
|
||||
KIND_E2E=1 make test-e2e
|
||||
```
|
||||
|
||||
Optional skip flags (set to any non-empty value to skip that step):
|
||||
|
||||
| Variable | Skips |
|
||||
|----------|-------|
|
||||
| `SKIP_INSTALL` | kind node-image build and cluster create |
|
||||
| `SKIP_KUBECTL_INSTALL` | kubectl download |
|
||||
| `SKIP_KIND_INSTALL` | kind binary download |
|
||||
| `SKIP_KUBEVIRT_INSTALL` | KubeVirt operator install |
|
||||
| `SKIP_METRICS_SERVER_INSTALL` | metrics-server install |
|
||||
|
||||
### Manual kind cluster (iterative development)
|
||||
|
||||
```
|
||||
GOOS=linux make dev-image
|
||||
make kind-multi-node
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||
google.golang.org/grpc v1.81.1
|
||||
google.golang.org/grpc v1.82.1
|
||||
k8s.io/api v0.36.1
|
||||
k8s.io/apimachinery v0.36.1
|
||||
k8s.io/apiserver v0.36.1
|
||||
@@ -28,8 +28,8 @@ require (
|
||||
k8s.io/klog/v2 v2.140.0
|
||||
k8s.io/metrics v0.36.1
|
||||
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2
|
||||
kubevirt.io/api v1.8.2
|
||||
kubevirt.io/client-go v1.8.2
|
||||
kubevirt.io/api v1.9.0
|
||||
kubevirt.io/client-go v1.9.0
|
||||
sigs.k8s.io/mdtoc v1.4.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
@@ -38,7 +38,7 @@ require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
@@ -75,13 +75,13 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/cel-go v0.30.0 // 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/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -96,7 +96,6 @@ require (
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.8 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect
|
||||
@@ -110,18 +109,18 @@ require (
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
@@ -137,7 +136,7 @@ require (
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/release-utils v0.8.3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||
)
|
||||
|
||||
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102
|
||||
|
||||
@@ -7,8 +7,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
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/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
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=
|
||||
@@ -141,8 +141,8 @@ github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IR
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
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/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo=
|
||||
github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -170,8 +170,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -268,8 +268,6 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
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=
|
||||
@@ -364,8 +362,8 @@ golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvm
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
@@ -392,8 +390,9 @@ golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -431,8 +430,8 @@ golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
@@ -455,8 +454,8 @@ golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -496,8 +495,8 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4=
|
||||
@@ -526,8 +525,8 @@ golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -549,8 +548,8 @@ golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -581,8 +580,9 @@ golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
@@ -600,15 +600,15 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -690,10 +690,10 @@ k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
|
||||
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
kubevirt.io/api v1.8.2 h1:h8AACJlity2n8gRE6e9PgikLAwzmNz6gzVjT1kbsDZY=
|
||||
kubevirt.io/api v1.8.2/go.mod h1:EUaPeD1GQK4IIEp8TVtk5dsFlYQGzFfcWi4CV4rs0rY=
|
||||
kubevirt.io/client-go v1.8.2 h1:jKJe5j/M7GDfpqTreoBA+BVoyQPCPTbLpuGHHaBfjdk=
|
||||
kubevirt.io/client-go v1.8.2/go.mod h1:eoH34Z/Wa1JuHeIezT/tcGDMa6zLLEf3FPRx5beJU3k=
|
||||
kubevirt.io/api v1.9.0 h1:FWqx9yGk/GHUP5o4IthGXZ8FeAlgVNv2It48wD86UUo=
|
||||
kubevirt.io/api v1.9.0/go.mod h1:tFKAVaJMm7bXCMLxOoUdAqBprKAXoCa0sp0gBmnfBgY=
|
||||
kubevirt.io/client-go v1.9.0 h1:ksR37OV0Zft6g/9YPFEjevlkRpTvWuyVE44zj9xGuGQ=
|
||||
kubevirt.io/client-go v1.9.0/go.mod h1:cgGTWTqnghJdyiGXx4y9KXAHholmF2BwodUKXuQa9Ns=
|
||||
kubevirt.io/containerized-data-importer-api v1.64.0 h1:yBLY6qEogUp8F+XSJvabIkx9uEEDeToYDfRr0mbUxJc=
|
||||
kubevirt.io/containerized-data-importer-api v1.64.0/go.mod h1:VGp35wxpLXU18b7cnEpmcThI3AjcZUSfg/Zfql44U4o=
|
||||
kubevirt.io/controller-lifecycle-operator-sdk/api v0.2.4 h1:fZYvD3/Vnitfkx6IJxjLAk8ugnZQ7CXVYcRfkSKmuZY=
|
||||
@@ -711,8 +711,9 @@ sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxO
|
||||
sigs.k8s.io/release-utils v0.8.3 h1:KtOtA4qDmzJyeQ2zkDsFVI25+NViwms/o5eL2NftFdA=
|
||||
sigs.k8s.io/release-utils v0.8.3/go.mod h1:fp82Fma06OXBhEJ+GUJKqvcplDBomruK1R/1fWJnsrQ=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Used by e2e tests (test/run-e2e-tests.sh) and make kind-multi-node.
|
||||
# Worker nodes carry topology zone labels required by topology spread constraint e2e tests.
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
|
||||
@@ -15,7 +15,7 @@ rules:
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list", "delete"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/eviction"]
|
||||
verbs: ["create"]
|
||||
|
||||
@@ -235,8 +235,10 @@ func initDescheduler(t *testing.T, ctx context.Context, featureGates featuregate
|
||||
if dryRun {
|
||||
if err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
for _, obj := range objects {
|
||||
// Only check for nodes - secrets are handled by namespacedSharedInformerFactory
|
||||
if _, ok := obj.(*v1.Node); !ok {
|
||||
// Only check for nodes and pods - secrets are handled by namespacedSharedInformerFactory
|
||||
switch obj.(type) {
|
||||
case *v1.Node, *v1.Pod:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
exists, err := descheduler.kubeClientSandbox.hasRuntimeObjectInIndexer(obj)
|
||||
@@ -256,7 +258,7 @@ func initDescheduler(t *testing.T, ctx context.Context, featureGates featuregate
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("nodes did not propagate to the indexer: %v", err)
|
||||
t.Fatalf("objects did not propagate to the indexer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ type evictionRequestsCache struct {
|
||||
requestsPerNamespace map[string]uint
|
||||
requestsTotal uint
|
||||
assumedRequestTimeoutSeconds uint
|
||||
onAssumedTimeout func(item evictionRequestItem)
|
||||
}
|
||||
|
||||
func newEvictionRequestsCache(assumedRequestTimeoutSeconds uint) *evictionRequestsCache {
|
||||
@@ -95,6 +96,9 @@ func (erc *evictionRequestsCache) cleanCache(ctx context.Context) {
|
||||
requestAgeSeconds := uint(metav1.Now().Sub(item.assumedTimestamp.Local()).Seconds())
|
||||
if requestAgeSeconds > erc.assumedRequestTimeoutSeconds {
|
||||
klog.V(4).InfoS("Assumed eviction request in background timed out, deleting", "timeout", erc.assumedRequestTimeoutSeconds, "podNamespace", item.podNamespace, "podName", item.podName)
|
||||
if erc.onAssumedTimeout != nil {
|
||||
erc.onAssumedTimeout(item)
|
||||
}
|
||||
erc.deleteItem(uid)
|
||||
}
|
||||
}
|
||||
@@ -290,6 +294,12 @@ func NewPodEvictor(
|
||||
|
||||
if featureGates.Enabled(features.EvictionsInBackground) {
|
||||
erCache := newEvictionRequestsCache(assumedEvictionRequestTimeoutSeconds)
|
||||
if podEvictor.metricsEnabled {
|
||||
erCache.onAssumedTimeout = func(item evictionRequestItem) {
|
||||
metrics.PodsEvicted.With(map[string]string{"result": "error", "strategy": item.strategyName, "namespace": item.podNamespace, "node": item.podNodeName, "profile": item.profileName}).Inc()
|
||||
metrics.PodsEvictedTotal.With(map[string]string{"result": "error", "strategy": item.strategyName, "namespace": item.podNamespace, "node": item.podNodeName, "profile": item.profileName}).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
handlerRegistration, err := podInformer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
@@ -330,8 +340,18 @@ func NewPodEvictor(
|
||||
}
|
||||
// Remove completed/suceeeded or failed pods from the cache
|
||||
if newPod.Status.Phase == v1.PodSucceeded || newPod.Status.Phase == v1.PodFailed {
|
||||
klog.V(3).InfoS("Pod with eviction in background completed. Removing pod from the cache.", "pod", klog.KObj(newPod))
|
||||
erCache.deletePod(newPod)
|
||||
if item, exists := erCache.getPod(newPod); exists {
|
||||
klog.V(3).InfoS("Pod with eviction in background completed. Removing pod from the cache.", "pod", klog.KObj(newPod))
|
||||
if item.evictionAssumed && podEvictor.metricsEnabled {
|
||||
result := "success"
|
||||
if newPod.Status.Phase == v1.PodFailed {
|
||||
result = "error"
|
||||
}
|
||||
metrics.PodsEvicted.With(map[string]string{"result": result, "strategy": item.strategyName, "namespace": item.podNamespace, "node": item.podNodeName, "profile": item.profileName}).Inc()
|
||||
metrics.PodsEvictedTotal.With(map[string]string{"result": result, "strategy": item.strategyName, "namespace": item.podNamespace, "node": item.podNodeName, "profile": item.profileName}).Inc()
|
||||
}
|
||||
erCache.deletePod(newPod)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Ignore any pod that does not have eviction in progress
|
||||
@@ -458,12 +478,6 @@ func (pe *PodEvictor) evictionRequestsPerNamespace(ns string) uint {
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *PodEvictor) EvictionRequests(node *v1.Node) uint {
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
return pe.evictionRequestsTotal()
|
||||
}
|
||||
|
||||
func (pe *PodEvictor) TotalEvictionRequests() uint {
|
||||
pe.mu.RLock()
|
||||
defer pe.mu.RUnlock()
|
||||
|
||||
@@ -683,6 +683,159 @@ func TestEvictionInBackgroundMetrics_InformerRace(t *testing.T) {
|
||||
metricstest.AssertVectorCount(t, "descheduler_pods_evicted_total", map[string]string{"result": "success"}, 1)
|
||||
}
|
||||
|
||||
// TestEvictionInBackgroundMetrics_PodCompleted verifies that when a pod
|
||||
// transitions to a terminal phase via UpdateFunc (without ever being deleted),
|
||||
// the correct metric is emitted: "success" for PodSucceeded, "error" for PodFailed.
|
||||
func TestEvictionInBackgroundMetrics_PodCompleted(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
phase v1.PodPhase
|
||||
expectedResult string
|
||||
}{
|
||||
{name: "PodSucceeded emits success", phase: v1.PodSucceeded, expectedResult: "success"},
|
||||
{name: "PodFailed emits error", phase: v1.PodFailed, expectedResult: "error"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), wait.ForeverTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
deschedulermetrics.Register()
|
||||
deschedulermetrics.PodsEvicted.Reset()
|
||||
deschedulermetrics.PodsEvictedTotal.Reset()
|
||||
|
||||
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
|
||||
ownerRef1 := test.GetReplicaSetOwnerRefList()
|
||||
p1 := test.BuildTestPod("p1", 100, 0, node1.Name, func(pod *v1.Pod) {
|
||||
pod.Namespace = "dev"
|
||||
pod.ObjectMeta.OwnerReferences = ownerRef1
|
||||
pod.Annotations = map[string]string{
|
||||
EvictionRequestAnnotationKey: "",
|
||||
}
|
||||
})
|
||||
|
||||
client := fakeclientset.NewSimpleClientset(node1, p1)
|
||||
sharedInformerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
_, eventRecorder := utils.GetRecorderAndBroadcaster(ctx, client)
|
||||
|
||||
podEvictor, err := NewPodEvictor(
|
||||
ctx,
|
||||
client,
|
||||
eventRecorder,
|
||||
sharedInformerFactory.Core().V1().Pods().Informer(),
|
||||
initFeatureGates(),
|
||||
NewOptions().WithMetricsEnabled(true),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error when creating a pod evictor: %v", err)
|
||||
}
|
||||
|
||||
client.PrependReactor("create", "pods", func(action core.Action) (bool, runtime.Object, error) {
|
||||
if action.GetSubresource() != "eviction" {
|
||||
return false, nil, nil
|
||||
}
|
||||
return true, nil, &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Reason: metav1.StatusReasonTooManyRequests,
|
||||
Message: "Eviction triggered evacuation",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
sharedInformerFactory.Start(ctx.Done())
|
||||
sharedInformerFactory.WaitForCacheSync(ctx.Done())
|
||||
|
||||
evictOpts := EvictOptions{StrategyName: "TestStrategy", ProfileName: "TestProfile"}
|
||||
podEvictor.EvictPod(ctx, p1, evictOpts)
|
||||
|
||||
metricstest.AssertVectorCount(t, "descheduler_pods_evicted_total", map[string]string{"result": "background"}, 1)
|
||||
|
||||
// Transition the pod to a terminal phase without deleting it.
|
||||
p1Updated := p1.DeepCopy()
|
||||
p1Updated.Status.Phase = tc.phase
|
||||
client.CoreV1().Pods(p1.Namespace).UpdateStatus(ctx, p1Updated, metav1.UpdateOptions{})
|
||||
|
||||
// Wait for UpdateFunc to fire and remove the pod from the cache.
|
||||
if err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
||||
return !podEvictor.erCache.hasPod(p1), nil
|
||||
}); err != nil {
|
||||
t.Fatalf("Timed out waiting for background eviction to complete: %v", err)
|
||||
}
|
||||
|
||||
metricstest.AssertVectorCount(t, "descheduler_pods_evicted_total", map[string]string{"result": tc.expectedResult}, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvictionInBackgroundMetrics_AssumedTimeout verifies that when an assumed
|
||||
// eviction entry expires in cleanCache, the "error" metric is emitted.
|
||||
func TestEvictionInBackgroundMetrics_AssumedTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), wait.ForeverTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
deschedulermetrics.Register()
|
||||
deschedulermetrics.PodsEvicted.Reset()
|
||||
deschedulermetrics.PodsEvictedTotal.Reset()
|
||||
|
||||
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
|
||||
ownerRef1 := test.GetReplicaSetOwnerRefList()
|
||||
p1 := test.BuildTestPod("p1", 100, 0, node1.Name, func(pod *v1.Pod) {
|
||||
pod.Namespace = "dev"
|
||||
pod.ObjectMeta.OwnerReferences = ownerRef1
|
||||
pod.Annotations = map[string]string{
|
||||
EvictionRequestAnnotationKey: "",
|
||||
}
|
||||
})
|
||||
|
||||
client := fakeclientset.NewSimpleClientset(node1, p1)
|
||||
sharedInformerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
_, eventRecorder := utils.GetRecorderAndBroadcaster(ctx, client)
|
||||
|
||||
podEvictor, err := NewPodEvictor(
|
||||
ctx,
|
||||
client,
|
||||
eventRecorder,
|
||||
sharedInformerFactory.Core().V1().Pods().Informer(),
|
||||
initFeatureGates(),
|
||||
NewOptions().WithMetricsEnabled(true),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error when creating a pod evictor: %v", err)
|
||||
}
|
||||
|
||||
client.PrependReactor("create", "pods", func(action core.Action) (bool, runtime.Object, error) {
|
||||
if action.GetSubresource() != "eviction" {
|
||||
return false, nil, nil
|
||||
}
|
||||
return true, nil, &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Reason: metav1.StatusReasonTooManyRequests,
|
||||
Message: "Eviction triggered evacuation",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
sharedInformerFactory.Start(ctx.Done())
|
||||
sharedInformerFactory.WaitForCacheSync(ctx.Done())
|
||||
|
||||
evictOpts := EvictOptions{StrategyName: "TestStrategy", ProfileName: "TestProfile"}
|
||||
podEvictor.EvictPod(ctx, p1, evictOpts)
|
||||
|
||||
metricstest.AssertVectorCount(t, "descheduler_pods_evicted_total", map[string]string{"result": "background"}, 1)
|
||||
|
||||
// Back-date all assumed entries so they appear expired, then run cleanCache.
|
||||
podEvictor.erCache.mu.Lock()
|
||||
for uid, item := range podEvictor.erCache.requests {
|
||||
item.assumedTimestamp = metav1.NewTime(time.Now().Add(-time.Hour))
|
||||
podEvictor.erCache.requests[uid] = item
|
||||
}
|
||||
podEvictor.erCache.mu.Unlock()
|
||||
podEvictor.erCache.cleanCache(ctx)
|
||||
|
||||
metricstest.AssertVectorCount(t, "descheduler_pods_evicted_total", map[string]string{"result": "error"}, 1)
|
||||
}
|
||||
|
||||
func assertEqualEvents(t *testing.T, expected []string, actual <-chan string) {
|
||||
t.Logf("Assert for events: %v", expected)
|
||||
c := time.After(wait.ForeverTestTimeout)
|
||||
|
||||
@@ -46,4 +46,4 @@ var defaultDeschedulerFeatureGates = map[featuregate.Feature]featuregate.Feature
|
||||
// Tests that need to modify feature gates for the duration of their test should use:
|
||||
//
|
||||
// defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, <value>)()
|
||||
var DefaultMutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()
|
||||
var DefaultMutableFeatureGate featuregate.MutableVersionedFeatureGate = featuregate.NewFeatureGate()
|
||||
|
||||
@@ -50,7 +50,7 @@ type notSupportedError struct {
|
||||
}
|
||||
|
||||
func (e notSupportedError) Error() string {
|
||||
return "maximum number of evicted pods per node reached"
|
||||
return "pod usage quantification is not supported by this usage client"
|
||||
}
|
||||
|
||||
func newNotSupportedError(usageClientType UsageClientType) *notSupportedError {
|
||||
|
||||
@@ -189,52 +189,10 @@ func TestRemoveDuplicates(t *testing.T) {
|
||||
tc.removeDuplicatesArgs.Namespaces = &api.Namespaces{
|
||||
Include: []string{testNamespace.Name},
|
||||
}
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(removeDuplicatesPolicy(tc.removeDuplicatesArgs, tc.evictorArgs))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
t.Logf("Creating %q policy CM with RemoveDuplicates configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.Logf("Deleting %q CM...", deschedulerPolicyConfigMapObj.Name)
|
||||
err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Delete(ctx, deschedulerPolicyConfigMapObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
}()
|
||||
createPolicyConfigMap(t, ctx, clientSet, removeDuplicatesPolicy(tc.removeDuplicatesArgs, tc.evictorArgs))
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment(testNamespace.Name)
|
||||
t.Logf("Creating descheduler deployment %v", deschedulerDeploymentObj.Name)
|
||||
_, err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
deschedulerPodName := ""
|
||||
defer func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(ctx, t, clientSet, deschedulerPodName)
|
||||
}
|
||||
|
||||
t.Logf("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
}()
|
||||
|
||||
t.Logf("Waiting for the descheduler pod running")
|
||||
deschedulerPods := waitForPodsRunning(ctx, t, clientSet, deschedulerDeploymentObj.Labels, 1, deschedulerDeploymentObj.Namespace)
|
||||
if len(deschedulerPods) != 0 {
|
||||
deschedulerPodName = deschedulerPods[0].Name
|
||||
}
|
||||
createDeschedulerDeploymentWithCleanup(t, ctx, clientSet, deschedulerDeploymentObj)
|
||||
|
||||
// Run RemoveDuplicates strategy
|
||||
var meetsExpectations bool
|
||||
|
||||
@@ -34,12 +34,16 @@ import (
|
||||
|
||||
const (
|
||||
vmiCount = 3
|
||||
// virtLauncherSelector selects KubeVirt virt-launcher pods only,
|
||||
// avoiding listing unrelated pods in the namespace.
|
||||
virtLauncherSelector = "kubevirt.io=virt-launcher"
|
||||
)
|
||||
|
||||
func virtualMachineInstance(idx int) *kvcorev1.VirtualMachineInstance {
|
||||
func virtualMachineInstance(idx int, namespace string) *kvcorev1.VirtualMachineInstance {
|
||||
return &kvcorev1.VirtualMachineInstance{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("kubevirtvmi-%v", idx),
|
||||
Name: fmt.Sprintf("kubevirtvmi-%v", idx),
|
||||
Namespace: namespace,
|
||||
Annotations: map[string]string{
|
||||
"descheduler.alpha.kubernetes.io/request-evict-only": "",
|
||||
},
|
||||
@@ -58,20 +62,12 @@ func virtualMachineInstance(idx int) *kvcorev1.VirtualMachineInstance {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cloudinitdisk",
|
||||
DiskDevice: kvcorev1.DiskDevice{
|
||||
Disk: &kvcorev1.DiskTarget{
|
||||
Bus: kvcorev1.DiskBusVirtio,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Rng: &kvcorev1.Rng{},
|
||||
},
|
||||
Resources: kvcorev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceMemory: resource.MustParse("1024M"),
|
||||
corev1.ResourceMemory: resource.MustParse("128M"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -81,27 +77,7 @@ func virtualMachineInstance(idx int) *kvcorev1.VirtualMachineInstance {
|
||||
Name: "containerdisk",
|
||||
VolumeSource: kvcorev1.VolumeSource{
|
||||
ContainerDisk: &kvcorev1.ContainerDiskSource{
|
||||
Image: "quay.io/kubevirt/fedora-with-test-tooling-container-disk:20240710_1265d1090",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cloudinitdisk",
|
||||
VolumeSource: kvcorev1.VolumeSource{
|
||||
CloudInitNoCloud: &kvcorev1.CloudInitNoCloudSource{
|
||||
UserData: `#cloud-config
|
||||
password: fedora
|
||||
chpasswd: { expire: False }
|
||||
packages:
|
||||
- nginx
|
||||
runcmd:
|
||||
- [ "systemctl", "enable", "--now", "nginx" ]`,
|
||||
NetworkData: `version: 2
|
||||
ethernets:
|
||||
eth0:
|
||||
addresses: [ fd10:0:2::2/120 ]
|
||||
dhcp4: true
|
||||
gateway6: fd10:0:2::1`,
|
||||
Image: kubevirtCirrosContainerDiskImage(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -110,31 +86,83 @@ ethernets:
|
||||
}
|
||||
}
|
||||
|
||||
func waitForKubevirtReady(t *testing.T, ctx context.Context, kvClient kubevirtclient.Interface) {
|
||||
obj, err := kvClient.KubevirtV1().KubeVirts("kubevirt").Get(ctx, "kubevirt", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to get kubevirt/kubevirt: %v", err)
|
||||
}
|
||||
available := false
|
||||
for _, condition := range obj.Status.Conditions {
|
||||
if condition.Type == kvcorev1.KubeVirtConditionAvailable {
|
||||
if condition.Status == corev1.ConditionTrue {
|
||||
available = true
|
||||
func formatContainerStatuses(pod *corev1.Pod) string {
|
||||
var parts []string
|
||||
formatList := func(prefix string, statuses []corev1.ContainerStatus) {
|
||||
for _, cs := range statuses {
|
||||
stateStr := "unknown"
|
||||
if cs.State.Waiting != nil {
|
||||
stateStr = fmt.Sprintf("waiting(reason=%q, message=%q)", cs.State.Waiting.Reason, cs.State.Waiting.Message)
|
||||
} else if cs.State.Terminated != nil {
|
||||
stateStr = fmt.Sprintf("terminated(exitCode=%d, reason=%q, message=%q)", cs.State.Terminated.ExitCode, cs.State.Terminated.Reason, cs.State.Terminated.Message)
|
||||
} else if cs.State.Running != nil {
|
||||
stateStr = "running"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s%s: state=%s, ready=%v, restarts=%d", prefix, cs.Name, stateStr, cs.Ready, cs.RestartCount))
|
||||
}
|
||||
}
|
||||
if !available {
|
||||
t.Fatalf("Kubevirt is not available")
|
||||
formatList("init:", pod.Status.InitContainerStatuses)
|
||||
formatList("", pod.Status.ContainerStatuses)
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// ensureVMIsLiveMigratable waits until every VMI reports the LiveMigratable condition with status True.
|
||||
func ensureVMIsLiveMigratable(t *testing.T, ctx context.Context, kvClient kubevirtclient.Interface, namespace string) {
|
||||
t.Helper()
|
||||
err := wait.PollUntilContextTimeout(ctx, 3*time.Second, 180*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
vmiList, err := kvClient.KubevirtV1().VirtualMachineInstances(namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil || len(vmiList.Items) != vmiCount {
|
||||
return false, nil
|
||||
}
|
||||
for _, vmi := range vmiList.Items {
|
||||
migratable := false
|
||||
for _, c := range vmi.Status.Conditions {
|
||||
if c.Type == kvcorev1.VirtualMachineInstanceIsMigratable && c.Status == corev1.ConditionTrue {
|
||||
migratable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !migratable {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("VMIs never became LiveMigratable: %v", err)
|
||||
}
|
||||
klog.Infof("All VMIs are LiveMigratable")
|
||||
}
|
||||
|
||||
func waitForKubevirtReady(t *testing.T, ctx context.Context, kvClient kubevirtclient.Interface) {
|
||||
t.Helper()
|
||||
err := wait.PollUntilContextTimeout(ctx, 3*time.Second, 180*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
obj, err := kvClient.KubevirtV1().KubeVirts("kubevirt").Get(ctx, "kubevirt", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
klog.Infof("Unable to get kubevirt/kubevirt: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
for _, condition := range obj.Status.Conditions {
|
||||
if condition.Type == kvcorev1.KubeVirtConditionAvailable && condition.Status == corev1.ConditionTrue {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Kubevirt is not available: %v", err)
|
||||
}
|
||||
klog.Infof("Kubevirt is available")
|
||||
}
|
||||
|
||||
func allVMIsHaveRunningPods(t *testing.T, ctx context.Context, kubeClient clientset.Interface, kvClient kubevirtclient.Interface) (bool, error) {
|
||||
func allVMIsHaveRunningPods(t *testing.T, ctx context.Context, kubeClient clientset.Interface, kvClient kubevirtclient.Interface, namespace string) (bool, error) {
|
||||
klog.Infof("Checking all vmi active pods are running")
|
||||
uidMap := make(map[types.UID]*corev1.Pod)
|
||||
podList, err := kubeClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
|
||||
podList, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: virtLauncherSelector,
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "client rate limiter") {
|
||||
if isClientRateLimiterError(err) {
|
||||
klog.Infof("Unable to list pods: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
@@ -148,7 +176,7 @@ func allVMIsHaveRunningPods(t *testing.T, ctx context.Context, kubeClient client
|
||||
uidMap[item.UID] = &pod
|
||||
}
|
||||
|
||||
vmiList, err := kvClient.KubevirtV1().VirtualMachineInstances("default").List(ctx, metav1.ListOptions{})
|
||||
vmiList, err := kvClient.KubevirtV1().VirtualMachineInstances(namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
klog.Infof("Unable to list VMIs: %v", err)
|
||||
return false, err
|
||||
@@ -165,14 +193,19 @@ func allVMIsHaveRunningPods(t *testing.T, ctx context.Context, kubeClient client
|
||||
klog.Infof("Active pod %v not found", activePod)
|
||||
return false, nil
|
||||
}
|
||||
klog.Infof("Checking whether active pod %v (uid=%v) is running", uidMap[activePod].Name, activePod)
|
||||
// ignore completed/failed pods
|
||||
if uidMap[activePod].Status.Phase == corev1.PodFailed || uidMap[activePod].Status.Phase == corev1.PodSucceeded {
|
||||
klog.Infof("Ignoring active pod %v, phase=%v", uidMap[activePod].Name, uidMap[activePod].Status.Phase)
|
||||
pod := uidMap[activePod]
|
||||
klog.Infof("Checking whether active pod %v (uid=%v) is running", pod.Name, activePod)
|
||||
if pod.Status.Phase == corev1.PodFailed {
|
||||
details := fmt.Sprintf("pod %s (phase=Failed, reason=%q, message=%q, containers=[%s])", pod.Name, pod.Status.Reason, pod.Status.Message, formatContainerStatuses(pod))
|
||||
klog.Infof("Active pod failed: %s", details)
|
||||
continue
|
||||
}
|
||||
if uidMap[activePod].Status.Phase != corev1.PodRunning {
|
||||
klog.Infof("activePod %v is not running: %v\n", uidMap[activePod].Name, uidMap[activePod].Status.Phase)
|
||||
if pod.Status.Phase == corev1.PodSucceeded {
|
||||
klog.Infof("Ignoring active pod %v, phase=%v", pod.Name, pod.Status.Phase)
|
||||
continue
|
||||
}
|
||||
if pod.Status.Phase != corev1.PodRunning {
|
||||
klog.Infof("activePod %v is not running: %v\n", pod.Name, pod.Status.Phase)
|
||||
return false, nil
|
||||
}
|
||||
atLeastOneVmiIsRunning = true
|
||||
@@ -186,7 +219,7 @@ func allVMIsHaveRunningPods(t *testing.T, ctx context.Context, kubeClient client
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func podLifeTimePolicy() *apiv1alpha2.DeschedulerPolicy {
|
||||
func podLifeTimePolicy(namespace string) *apiv1alpha2.DeschedulerPolicy {
|
||||
return &apiv1alpha2.DeschedulerPolicy{
|
||||
Profiles: []apiv1alpha2.DeschedulerProfile{
|
||||
{
|
||||
@@ -198,7 +231,7 @@ func podLifeTimePolicy() *apiv1alpha2.DeschedulerPolicy {
|
||||
Object: &podlifetime.PodLifeTimeArgs{
|
||||
MaxPodLifeTimeSeconds: utilptr.To[uint](1), // set it to immediate eviction
|
||||
Namespaces: &api.Namespaces{
|
||||
Include: []string{"default"},
|
||||
Include: []string{namespace},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -229,10 +262,12 @@ func podLifeTimePolicy() *apiv1alpha2.DeschedulerPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
func kVirtRunningPodNames(t *testing.T, ctx context.Context, kubeClient clientset.Interface) []string {
|
||||
func kVirtRunningPodNames(t *testing.T, ctx context.Context, kubeClient clientset.Interface, namespace string) []string {
|
||||
names := []string{}
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
podList, err := kubeClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
|
||||
podList, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: virtLauncherSelector,
|
||||
})
|
||||
if err != nil {
|
||||
if isClientRateLimiterError(err) {
|
||||
t.Log(err)
|
||||
@@ -243,9 +278,6 @@ func kVirtRunningPodNames(t *testing.T, ctx context.Context, kubeClient clientse
|
||||
}
|
||||
|
||||
for _, item := range podList.Items {
|
||||
if !strings.HasPrefix(item.Name, "virt-launcher-kubevirtvmi-") {
|
||||
t.Fatalf("Only pod names with 'virt-launcher-kubevirtvmi-' prefix are expected, got %q instead", item.Name)
|
||||
}
|
||||
if item.Status.Phase == corev1.PodRunning {
|
||||
names = append(names, item.Name)
|
||||
}
|
||||
@@ -258,13 +290,13 @@ func kVirtRunningPodNames(t *testing.T, ctx context.Context, kubeClient clientse
|
||||
return names
|
||||
}
|
||||
|
||||
func observeLiveMigration(t *testing.T, ctx context.Context, kubeClient clientset.Interface, usedRunningPodNames map[string]struct{}) {
|
||||
func observeLiveMigration(t *testing.T, ctx context.Context, kubeClient clientset.Interface, namespace string, usedRunningPodNames map[string]struct{}) {
|
||||
prevTotal := uint(0)
|
||||
jumps := 0
|
||||
// keep running the descheduling cycle until the migration is triggered and completed few times or times out
|
||||
for i := 0; i < 240; i++ {
|
||||
// monitor how many pods get evicted
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient)
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient, namespace)
|
||||
klog.Infof("vmi pods: %#v\n", names)
|
||||
// The number of pods need to be kept between vmiCount and vmiCount+1.
|
||||
// At most two pods are expected to have virt-launcher-kubevirtvmi-X prefix name in common.
|
||||
@@ -299,16 +331,18 @@ func observeLiveMigration(t *testing.T, ctx context.Context, kubeClient clientse
|
||||
if prevTotal != 0 && prevTotal != total {
|
||||
jumps++
|
||||
}
|
||||
// Expect at least 3 finished live migrations (two should be enough as well, though ...)
|
||||
if jumps >= 6 {
|
||||
// Expect at least 2 finished live migrations
|
||||
if jumps >= 4 {
|
||||
break
|
||||
}
|
||||
prevTotal = total
|
||||
time.Sleep(time.Second)
|
||||
time.Sleep(4 * time.Second)
|
||||
}
|
||||
|
||||
if jumps < 6 {
|
||||
podList, err := kubeClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
|
||||
if jumps < 4 {
|
||||
podList, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: virtLauncherSelector,
|
||||
})
|
||||
if err != nil {
|
||||
klog.Infof("Unable to list pods: %v", err)
|
||||
} else {
|
||||
@@ -317,9 +351,9 @@ func observeLiveMigration(t *testing.T, ctx context.Context, kubeClient clientse
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("Expected at least 3 finished live migrations, got less: %v", jumps/2.0)
|
||||
t.Fatalf("Expected at least 2 finished live migrations, got less: %v", jumps/2.0)
|
||||
}
|
||||
klog.Infof("The live migration finished 3 times")
|
||||
klog.Infof("The live migration finished 2 times")
|
||||
|
||||
// len(usedRunningPodNames) is expected to be vmiCount + jumps/2 + 1 (one more live migration could still be initiated)
|
||||
klog.Infof("len(usedRunningPodNames): %v, upper limit: %v\n", len(usedRunningPodNames), vmiCount+jumps/2+1)
|
||||
@@ -328,7 +362,7 @@ func observeLiveMigration(t *testing.T, ctx context.Context, kubeClient clientse
|
||||
}
|
||||
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient)
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient, namespace)
|
||||
klog.Infof("vmi pods: %#v\n", names)
|
||||
lNames := len(names)
|
||||
if lNames != vmiCount {
|
||||
@@ -346,7 +380,13 @@ func createAndWaitForDeschedulerRunning(t *testing.T, ctx context.Context, kubeC
|
||||
klog.Infof("Creating descheduler deployment %v", deschedulerDeploymentObj.Name)
|
||||
_, err := kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
_ = kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
_, err = kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
klog.Infof("Waiting for the descheduler pod running")
|
||||
@@ -385,143 +425,46 @@ func createKubevirtClient() (kubevirtclient.Interface, error) {
|
||||
return kubevirtclient.NewForConfig(config)
|
||||
}
|
||||
|
||||
func TestLiveMigrationInBackground(t *testing.T) {
|
||||
initPluginRegistry()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
kubeClient, err := client.CreateClient(componentbaseconfig.ClientConnectionConfiguration{Kubeconfig: os.Getenv("KUBECONFIG")}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Error during kubernetes client creation with %v", err)
|
||||
func setupE2ELiveMigrationNamespace(t *testing.T, ctx context.Context, kubeClient clientset.Interface, kvClient kubevirtclient.Interface, vmiNamespace string) {
|
||||
t.Helper()
|
||||
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: vmiNamespace}}
|
||||
if _, err := kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) {
|
||||
t.Fatalf("Unable to create namespace %v: %v", vmiNamespace, err)
|
||||
}
|
||||
|
||||
kvClient, err := createKubevirtClient()
|
||||
if err != nil {
|
||||
t.Fatalf("Error during kvClient creation with %v", err)
|
||||
}
|
||||
|
||||
waitForKubevirtReady(t, ctx, kvClient)
|
||||
|
||||
// Delete all VMIs
|
||||
defer func() {
|
||||
t.Cleanup(func() {
|
||||
cleanupCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
for i := 1; i <= vmiCount; i++ {
|
||||
vmi := virtualMachineInstance(i)
|
||||
err := kvClient.KubevirtV1().VirtualMachineInstances("default").Delete(context.Background(), vmi.Name, metav1.DeleteOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
vmi := virtualMachineInstance(i, vmiNamespace)
|
||||
if err := kvClient.KubevirtV1().VirtualMachineInstances(vmiNamespace).Delete(cleanupCtx, vmi.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
klog.Infof("Unable to delete vmi %v: %v", vmi.Name, err)
|
||||
}
|
||||
}
|
||||
wait.PollUntilContextTimeout(ctx, 5*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
podList, err := kubeClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
|
||||
wait.PollUntilContextTimeout(cleanupCtx, 5*time.Second, 30*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
podList, err := kubeClient.CoreV1().Pods(vmiNamespace).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: virtLauncherSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
lPods := len(podList.Items)
|
||||
if lPods > 0 {
|
||||
klog.Infof("Waiting until all pods under default namespace are gone, %v remaining", lPods)
|
||||
klog.Infof("Waiting until all virt-launcher pods under %v namespace are gone, %v remaining", vmiNamespace, lPods)
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}()
|
||||
|
||||
// Create N vmis and wait for the corresponding vm pods to be ready and running
|
||||
for i := 1; i <= vmiCount; i++ {
|
||||
vmi := virtualMachineInstance(i)
|
||||
_, err = kvClient.KubevirtV1().VirtualMachineInstances("default").Create(context.Background(), vmi, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create KubeVirt vmi: %v\n", err)
|
||||
if err := kubeClient.CoreV1().Namespaces().Delete(cleanupCtx, vmiNamespace, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
klog.Infof("Unable to delete namespace %v: %v", vmiNamespace, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until all VMIs have running pods
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 300*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
return allVMIsHaveRunningPods(t, ctx, kubeClient, kvClient)
|
||||
}); err != nil {
|
||||
t.Fatalf("Error waiting for all vmi active pods to be running: %v", err)
|
||||
}
|
||||
|
||||
usedRunningPodNames := make(map[string]struct{})
|
||||
// vmiCount number of names is expected
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient)
|
||||
klog.Infof("vmi pods: %#v\n", names)
|
||||
if len(names) != vmiCount {
|
||||
t.Fatalf("Expected %v vmi pods, got %v instead", vmiCount, len(names))
|
||||
}
|
||||
for _, name := range names {
|
||||
usedRunningPodNames[name] = struct{}{}
|
||||
}
|
||||
|
||||
policy := podLifeTimePolicy()
|
||||
// Allow only a single eviction simultaneously
|
||||
policy.MaxNoOfPodsToEvictPerNamespace = utilptr.To[uint](1)
|
||||
// Deploy the descheduler with the configured policy
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(policy)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
klog.Infof("Creating %q policy CM with RemovePodsHavingTooManyRestarts configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = kubeClient.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
defer func() {
|
||||
klog.Infof("Deleting %q CM...", deschedulerPolicyConfigMapObj.Name)
|
||||
err = kubeClient.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Delete(ctx, deschedulerPolicyConfigMapObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment("kube-system")
|
||||
// Set the descheduling interval to 10s
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args = []string{"--policy-config-file", "/policy-dir/policy.yaml", "--descheduling-interval", "10s", "--v", "4", "--feature-gates", "EvictionsInBackground=true"}
|
||||
|
||||
deschedulerPodName := ""
|
||||
defer func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(ctx, t, kubeClient, deschedulerPodName)
|
||||
}
|
||||
|
||||
klog.Infof("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
err = kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
waitForPodsToDisappear(ctx, t, kubeClient, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
}()
|
||||
|
||||
deschedulerPodName = createAndWaitForDeschedulerRunning(t, ctx, kubeClient, deschedulerDeploymentObj)
|
||||
|
||||
observeLiveMigration(t, ctx, kubeClient, usedRunningPodNames)
|
||||
|
||||
printPodLogs(ctx, t, kubeClient, deschedulerPodName)
|
||||
|
||||
klog.Infof("Deleting the current descheduler pod")
|
||||
err = kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error deleting %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
remainingPods := make(map[string]struct{})
|
||||
for _, name := range kVirtRunningPodNames(t, ctx, kubeClient) {
|
||||
remainingPods[name] = struct{}{}
|
||||
}
|
||||
|
||||
klog.Infof("Configuring the descheduler policy %v for PodLifetime with no limits", deschedulerPolicyConfigMapObj.Name)
|
||||
policy.MaxNoOfPodsToEvictPerNamespace = nil
|
||||
updateDeschedulerPolicy(t, ctx, kubeClient, policy)
|
||||
|
||||
deschedulerDeploymentObj = deschedulerDeployment("kube-system")
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args = []string{"--policy-config-file", "/policy-dir/policy.yaml", "--descheduling-interval", "100m", "--v", "4", "--feature-gates", "EvictionsInBackground=true"}
|
||||
deschedulerPodName = createAndWaitForDeschedulerRunning(t, ctx, kubeClient, deschedulerDeploymentObj)
|
||||
})
|
||||
}
|
||||
|
||||
func waitForVMIEvictionsWithNoLimits(t *testing.T, ctx context.Context, kubeClient clientset.Interface, vmiNamespace string, remainingPods map[string]struct{}) {
|
||||
t.Helper()
|
||||
klog.Infof("Waiting until all pods are evicted (no limit set)")
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 120*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient)
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient, vmiNamespace)
|
||||
for _, name := range names {
|
||||
if _, exists := remainingPods[name]; exists {
|
||||
klog.Infof("Waiting for %v to disappear", name)
|
||||
@@ -539,3 +482,107 @@ func TestLiveMigrationInBackground(t *testing.T) {
|
||||
t.Fatalf("Error waiting for %v new vmi active pods to be running: %v", vmiCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveMigrationInBackground(t *testing.T) {
|
||||
if *kubevirtVersionTag == "" {
|
||||
t.Fatal("--kubevirt-version-tag must be set for KubeVirt e2e tests")
|
||||
}
|
||||
|
||||
initPluginRegistry()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
kubeClient, err := client.CreateClient(componentbaseconfig.ClientConnectionConfiguration{Kubeconfig: os.Getenv("KUBECONFIG")}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Error during kubernetes client creation with %v", err)
|
||||
}
|
||||
|
||||
kvClient, err := createKubevirtClient()
|
||||
if err != nil {
|
||||
t.Fatalf("Error during kvClient creation with %v", err)
|
||||
}
|
||||
|
||||
waitForKubevirtReady(t, ctx, kvClient)
|
||||
|
||||
vmiNamespace := "e2e-livemigration"
|
||||
setupE2ELiveMigrationNamespace(t, ctx, kubeClient, kvClient, vmiNamespace)
|
||||
|
||||
for i := 1; i <= vmiCount; i++ {
|
||||
vmi := virtualMachineInstance(i, vmiNamespace)
|
||||
_, err = kvClient.KubevirtV1().VirtualMachineInstances(vmiNamespace).Create(context.Background(), vmi, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create KubeVirt vmi: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 300*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
return allVMIsHaveRunningPods(t, ctx, kubeClient, kvClient, vmiNamespace)
|
||||
}); err != nil {
|
||||
t.Fatalf("Error waiting for all vmi active pods to be running: %v", err)
|
||||
}
|
||||
|
||||
ensureVMIsLiveMigratable(t, ctx, kvClient, vmiNamespace)
|
||||
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 300*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
return allVMIsHaveRunningPods(t, ctx, kubeClient, kvClient, vmiNamespace)
|
||||
}); err != nil {
|
||||
t.Fatalf("Error waiting for all vmi active pods to be running after recreate: %v", err)
|
||||
}
|
||||
|
||||
usedRunningPodNames := make(map[string]struct{})
|
||||
names := kVirtRunningPodNames(t, ctx, kubeClient, vmiNamespace)
|
||||
klog.Infof("vmi pods: %#v\n", names)
|
||||
if len(names) != vmiCount {
|
||||
t.Fatalf("Expected %v vmi pods, got %v instead", vmiCount, len(names))
|
||||
}
|
||||
for _, name := range names {
|
||||
usedRunningPodNames[name] = struct{}{}
|
||||
}
|
||||
|
||||
policy := podLifeTimePolicy(vmiNamespace)
|
||||
policy.MaxNoOfPodsToEvictPerNamespace = utilptr.To[uint](1)
|
||||
deschedulerPolicyConfigMapObj := createPolicyConfigMap(t, ctx, kubeClient, policy)
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment("kube-system")
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args = []string{"--policy-config-file", "/policy-dir/policy.yaml", "--descheduling-interval", "10s", "--v", "4", "--feature-gates", "EvictionsInBackground=true"}
|
||||
|
||||
deschedulerPodName := ""
|
||||
t.Cleanup(func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(context.Background(), t, kubeClient, deschedulerPodName)
|
||||
}
|
||||
|
||||
klog.Infof("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
if err := kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(context.Background(), deschedulerDeploymentObj.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
klog.Infof("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
waitForPodsToDisappear(context.Background(), t, kubeClient, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
})
|
||||
|
||||
deschedulerPodName = createAndWaitForDeschedulerRunning(t, ctx, kubeClient, deschedulerDeploymentObj)
|
||||
|
||||
observeLiveMigration(t, ctx, kubeClient, vmiNamespace, usedRunningPodNames)
|
||||
|
||||
printPodLogs(ctx, t, kubeClient, deschedulerPodName)
|
||||
|
||||
klog.Infof("Deleting the current descheduler pod")
|
||||
err = kubeClient.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error deleting %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
remainingPods := make(map[string]struct{})
|
||||
for _, name := range kVirtRunningPodNames(t, ctx, kubeClient, vmiNamespace) {
|
||||
remainingPods[name] = struct{}{}
|
||||
}
|
||||
|
||||
klog.Infof("Configuring the descheduler policy %v for PodLifetime with no limits", deschedulerPolicyConfigMapObj.Name)
|
||||
policy.MaxNoOfPodsToEvictPerNamespace = nil
|
||||
updateDeschedulerPolicy(t, ctx, kubeClient, policy)
|
||||
|
||||
deschedulerDeploymentObj = deschedulerDeployment("kube-system")
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args = []string{"--policy-config-file", "/policy-dir/policy.yaml", "--descheduling-interval", "100m", "--v", "4", "--feature-gates", "EvictionsInBackground=true"}
|
||||
deschedulerPodName = createAndWaitForDeschedulerRunning(t, ctx, kubeClient, deschedulerDeploymentObj)
|
||||
|
||||
waitForVMIEvictionsWithNoLimits(t, ctx, kubeClient, vmiNamespace, remainingPods)
|
||||
}
|
||||
|
||||
@@ -145,52 +145,10 @@ func TestFailedPods(t *testing.T) {
|
||||
Include: []string{testNamespace.Name},
|
||||
}
|
||||
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(removeFailedPodsPolicy(tc.removeFailedPodsArgs, evictorArgs))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
t.Logf("Creating %q policy CM with RemoveDuplicates configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.Logf("Deleting %q CM...", deschedulerPolicyConfigMapObj.Name)
|
||||
err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Delete(ctx, deschedulerPolicyConfigMapObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
}()
|
||||
createPolicyConfigMap(t, ctx, clientSet, removeFailedPodsPolicy(tc.removeFailedPodsArgs, evictorArgs))
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment(testNamespace.Name)
|
||||
t.Logf("Creating descheduler deployment %v", deschedulerDeploymentObj.Name)
|
||||
_, err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
deschedulerPodName := ""
|
||||
defer func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(ctx, t, clientSet, deschedulerPodName)
|
||||
}
|
||||
|
||||
t.Logf("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
}()
|
||||
|
||||
t.Logf("Waiting for the descheduler pod running")
|
||||
deschedulerPods := waitForPodsRunning(ctx, t, clientSet, deschedulerDeploymentObj.Labels, 1, deschedulerDeploymentObj.Namespace)
|
||||
if len(deschedulerPods) != 0 {
|
||||
deschedulerPodName = deschedulerPods[0].Name
|
||||
}
|
||||
createDeschedulerDeploymentWithCleanup(t, ctx, clientSet, deschedulerDeploymentObj)
|
||||
|
||||
// Run RemoveDuplicates strategy
|
||||
var meetsExpectations bool
|
||||
|
||||
@@ -135,9 +135,9 @@ func TestLeaderElection(t *testing.T) {
|
||||
t.Logf("Removed kube-system/descheduler lease")
|
||||
|
||||
t.Log("Starting deschedulers")
|
||||
pod1Name, deploy1, cm1 := startDeschedulerServer(t, ctx, clientSet, ns1)
|
||||
pod1Name, deploy1, _ := startDeschedulerServer(t, ctx, clientSet, ns1)
|
||||
time.Sleep(1 * time.Second)
|
||||
pod2Name, deploy2, cm2 := startDeschedulerServer(t, ctx, clientSet, ns2)
|
||||
pod2Name, deploy2, _ := startDeschedulerServer(t, ctx, clientSet, ns2)
|
||||
defer func() {
|
||||
for _, podName := range []string{pod1Name, pod2Name} {
|
||||
printPodLogs(ctx, t, clientSet, podName)
|
||||
@@ -153,14 +153,6 @@ func TestLeaderElection(t *testing.T) {
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deploy.Labels, deploy.Namespace)
|
||||
}
|
||||
|
||||
for _, cm := range []*v1.ConfigMap{cm1, cm2} {
|
||||
t.Logf("Deleting %q CM...", cm.Name)
|
||||
err = clientSet.CoreV1().ConfigMaps(cm.Namespace).Delete(ctx, cm.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", cm.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
clientSet.CoordinationV1().Leases("kube-system").Delete(ctx, "descheduler", metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
@@ -223,21 +215,21 @@ func startDeschedulerServer(t *testing.T, ctx context.Context, clientSet clients
|
||||
EvictFailedBarePods: false,
|
||||
}
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(podlifetimePolicy(podLifeTimeArgs, evictorArgs))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
deschedulerPolicyConfigMapObj.Name = fmt.Sprintf("%s-%s", deschedulerPolicyConfigMapObj.Name, testName)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
t.Logf("Creating %q policy CM with RemoveDuplicates configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
createConfigMapWithCleanup(t, ctx, clientSet, deschedulerPolicyConfigMapObj)
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment(testName)
|
||||
deschedulerDeploymentObj.Name = fmt.Sprintf("%s-%s", deschedulerDeploymentObj.Name, testName)
|
||||
args := deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args = append(args, "--leader-elect", "--leader-elect-retry-period", "1s")
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Containers[0].Args = []string{
|
||||
"--policy-config-file", "/policy-dir/policy.yaml",
|
||||
"--descheduling-interval", "3s",
|
||||
"--v", "4",
|
||||
"--leader-elect",
|
||||
"--leader-elect-retry-period", "1s",
|
||||
}
|
||||
deschedulerDeploymentObj.Spec.Template.Spec.Volumes = []v1.Volume{
|
||||
{
|
||||
Name: "policy-volume",
|
||||
|
||||
@@ -105,7 +105,10 @@ func TestLowNodeUtilizationKubernetesMetrics(t *testing.T) {
|
||||
if _, err := clientSet.CoreV1().Namespaces().Create(ctx, testNamespace, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("Unable to create ns %v: %v", testNamespace.Name, err)
|
||||
}
|
||||
defer clientSet.CoreV1().Namespaces().Delete(ctx, testNamespace.Name, metav1.DeleteOptions{})
|
||||
|
||||
t.Cleanup(func() {
|
||||
clientSet.CoreV1().Namespaces().Delete(ctx, testNamespace.Name, metav1.DeleteOptions{})
|
||||
})
|
||||
|
||||
t.Log("Creating duplicates pods")
|
||||
testLabel := map[string]string{"app": "test-lownodeutilization-kubernetes-metrics", "name": "test-lownodeutilization-kubernetes-metrics"}
|
||||
@@ -164,11 +167,11 @@ func TestLowNodeUtilizationKubernetesMetrics(t *testing.T) {
|
||||
expectedEvictedPodCount: 2,
|
||||
lowNodeUtilizationArgs: &nodeutilization.LowNodeUtilizationArgs{
|
||||
Thresholds: api.ResourceThresholds{
|
||||
v1.ResourceCPU: 30,
|
||||
v1.ResourceCPU: 8,
|
||||
v1.ResourcePods: 30,
|
||||
},
|
||||
TargetThresholds: api.ResourceThresholds{
|
||||
v1.ResourceCPU: 50,
|
||||
v1.ResourceCPU: 15,
|
||||
v1.ResourcePods: 50,
|
||||
},
|
||||
MetricsUtilization: &nodeutilization.MetricsUtilization{
|
||||
@@ -194,21 +197,22 @@ func TestLowNodeUtilizationKubernetesMetrics(t *testing.T) {
|
||||
}
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
|
||||
t.Cleanup(func() {
|
||||
clientSet.AppsV1().Deployments(deploymentObj.Namespace).Delete(ctx, deploymentObj.Name, metav1.DeleteOptions{})
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deploymentObj.Labels, deploymentObj.Namespace)
|
||||
}()
|
||||
})
|
||||
waitForPodsRunning(ctx, t, clientSet, deploymentObj.Labels, tc.replicasNum, deploymentObj.Namespace)
|
||||
// wait until workerNodes[0].Name has the right actual cpu utilization and all the testing pods are running
|
||||
// and producing ~12 cores in total
|
||||
wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(context.Context) (done bool, err error) {
|
||||
// and producing ~4 cores in total
|
||||
if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, 60*time.Second, true, func(ctx context.Context) (done bool, err error) {
|
||||
item, err := metricsClient.MetricsV1beta1().NodeMetricses().Get(ctx, workerNodes[0].Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Logf("unable to list nodemetricses: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
t.Logf("Waiting for %q nodemetrics cpu utilization to get over 12, currently %v", workerNodes[0].Name, item.Usage.Cpu().Value())
|
||||
if item.Usage.Cpu().Value() < 12 {
|
||||
t.Logf("Waiting for %q nodemetrics cpu utilization to get over 3, currently %v", workerNodes[0].Name, item.Usage.Cpu().Value())
|
||||
if item.Usage.Cpu().Value() < 3 {
|
||||
return false, nil
|
||||
}
|
||||
totalCpu := resource.NewMilliQuantity(0, resource.DecimalSI)
|
||||
@@ -225,60 +229,20 @@ func TestLowNodeUtilizationKubernetesMetrics(t *testing.T) {
|
||||
totalCpu.Add(container.Usage[v1.ResourceCPU])
|
||||
}
|
||||
}
|
||||
// Value() will round up (e.g. 11.1 -> 12), which is still ok
|
||||
t.Logf("Waiting for totalCpu to get to 12 at least, got %v\n", totalCpu.Value())
|
||||
return totalCpu.Value() >= 12, nil
|
||||
})
|
||||
// Value() will round up (e.g. 3.1 -> 4), which is still ok
|
||||
t.Logf("Waiting for totalCpu to get to 3 at least, got %v\n", totalCpu.Value())
|
||||
return totalCpu.Value() >= 3, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("Error waiting for node/pod metrics: %v", err)
|
||||
}
|
||||
|
||||
preRunNames := sets.NewString(getCurrentPodNames(ctx, clientSet, testNamespace.Name, t)...)
|
||||
|
||||
// Deploy the descheduler with the configured policy
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(lowNodeUtilizationPolicy(tc.lowNodeUtilizationArgs, tc.evictorArgs, tc.metricsCollectorEnabled))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
t.Logf("Creating %q policy CM with LowNodeUtilization configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.Logf("Deleting %q CM...", deschedulerPolicyConfigMapObj.Name)
|
||||
err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Delete(ctx, deschedulerPolicyConfigMapObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
}()
|
||||
createPolicyConfigMap(t, ctx, clientSet, lowNodeUtilizationPolicy(tc.lowNodeUtilizationArgs, tc.evictorArgs, tc.metricsCollectorEnabled))
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment(testNamespace.Name)
|
||||
t.Logf("Creating descheduler deployment %v", deschedulerDeploymentObj.Name)
|
||||
_, err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
deschedulerPodName := ""
|
||||
defer func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(ctx, t, clientSet, deschedulerPodName)
|
||||
}
|
||||
|
||||
t.Logf("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
}()
|
||||
|
||||
t.Logf("Waiting for the descheduler pod running")
|
||||
deschedulerPods := waitForPodsRunning(ctx, t, clientSet, deschedulerDeploymentObj.Labels, 1, deschedulerDeploymentObj.Namespace)
|
||||
if len(deschedulerPods) != 0 {
|
||||
deschedulerPodName = deschedulerPods[0].Name
|
||||
}
|
||||
createDeschedulerDeploymentWithCleanup(t, ctx, clientSet, deschedulerDeploymentObj)
|
||||
|
||||
// Run LowNodeUtilization plugin
|
||||
var meetsExpectations bool
|
||||
|
||||
@@ -289,21 +289,7 @@ func TestProtectPodsWithPVC(t *testing.T) {
|
||||
t.Fatalf("Error creating %q CM: %v", policycm.Name, err)
|
||||
}
|
||||
|
||||
t.Logf("creating %q policy CM with PodsWithPVC protection enabled...", policycm.Name)
|
||||
if _, err = cli.CoreV1().ConfigMaps(policycm.Namespace).Create(
|
||||
ctx, policycm, metav1.CreateOptions{},
|
||||
); err != nil {
|
||||
t.Fatalf("error creating %q CM: %v", policycm.Name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.Logf("deleting %q CM...", policycm.Name)
|
||||
if err := cli.CoreV1().ConfigMaps(policycm.Namespace).Delete(
|
||||
ctx, policycm.Name, metav1.DeleteOptions{},
|
||||
); err != nil {
|
||||
t.Fatalf("unable to delete %q CM: %v", policycm.Name, err)
|
||||
}
|
||||
}()
|
||||
createConfigMapWithCleanup(t, ctx, cli, policycm)
|
||||
|
||||
desdep := deschedulerDeployment(namespace.Name)
|
||||
t.Logf("creating descheduler deployment %v", desdep.Name)
|
||||
|
||||
+87
-4
@@ -31,6 +31,7 @@ import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
schedulingv1 "k8s.io/api/scheduling/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
@@ -67,11 +68,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
deschedulerImage = flag.String("descheduler-image", "", "descheduler image to set in the pod spec")
|
||||
podRunAsUserId = flag.Int64("pod-run-as-user-id", 0, ".spec.securityContext.runAsUser setting, not set if 0")
|
||||
podRunAsGroupId = flag.Int64("pod-run-as-group-id", 0, ".spec.securityContext.runAsGroup setting, not set if 0")
|
||||
deschedulerImage = flag.String("descheduler-image", "", "descheduler image to set in the pod spec")
|
||||
kubevirtVersionTag = flag.String("kubevirt-version-tag", "", "KubeVirt release tag for container disk images in KubeVirt e2e tests (e.g. v1.9.0)")
|
||||
podRunAsUserId = flag.Int64("pod-run-as-user-id", 0, ".spec.securityContext.runAsUser setting, not set if 0")
|
||||
podRunAsGroupId = flag.Int64("pod-run-as-group-id", 0, ".spec.securityContext.runAsGroup setting, not set if 0")
|
||||
)
|
||||
|
||||
func kubevirtCirrosContainerDiskImage() string {
|
||||
return fmt.Sprintf("quay.io/kubevirt/cirros-container-disk-demo:%s", *kubevirtVersionTag)
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
@@ -84,7 +90,12 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
func isClientRateLimiterError(err error) bool {
|
||||
return strings.Contains(err.Error(), "client rate limiter")
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), "client rate limiter") &&
|
||||
!strings.Contains(err.Error(), "context deadline exceeded") &&
|
||||
!strings.Contains(err.Error(), "context canceled")
|
||||
}
|
||||
|
||||
func initFeatureGates() featuregate.FeatureGate {
|
||||
@@ -112,6 +123,78 @@ func deschedulerPolicyConfigMap(policy *deschedulerapiv1alpha2.DeschedulerPolicy
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// createPolicyConfigMap generates, creates (or recreates if already existing), and registers cleanup for a descheduler policy ConfigMap.
|
||||
func createPolicyConfigMap(t *testing.T, ctx context.Context, kubeClient clientset.Interface, policy *deschedulerapiv1alpha2.DeschedulerPolicy) *v1.ConfigMap {
|
||||
t.Helper()
|
||||
cm, err := deschedulerPolicyConfigMap(policy)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating policy CM object: %v", err)
|
||||
}
|
||||
return createConfigMapWithCleanup(t, ctx, kubeClient, cm)
|
||||
}
|
||||
|
||||
// createConfigMapWithCleanup creates (or recreates if already existing) a ConfigMap and registers a t.Cleanup callback to delete it.
|
||||
func createConfigMapWithCleanup(t *testing.T, ctx context.Context, kubeClient clientset.Interface, cm *v1.ConfigMap) *v1.ConfigMap {
|
||||
t.Helper()
|
||||
t.Logf("Creating %q policy CM...", cm.Name)
|
||||
_, err := kubeClient.CoreV1().ConfigMaps(cm.Namespace).Create(ctx, cm, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
_ = kubeClient.CoreV1().ConfigMaps(cm.Namespace).Delete(ctx, cm.Name, metav1.DeleteOptions{})
|
||||
_, err = kubeClient.CoreV1().ConfigMaps(cm.Namespace).Create(ctx, cm, metav1.CreateOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", cm.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
t.Logf("Deleting %q CM...", cm.Name)
|
||||
if err := kubeClient.CoreV1().ConfigMaps(cm.Namespace).Delete(context.Background(), cm.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Logf("Unable to delete %q CM: %v", cm.Name, err)
|
||||
}
|
||||
})
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// createDeschedulerDeploymentWithCleanup creates (or recreates if already existing) a descheduler deployment and registers a t.Cleanup callback.
|
||||
func createDeschedulerDeploymentWithCleanup(t *testing.T, ctx context.Context, kubeClient clientset.Interface, deployment *appsv1.Deployment) string {
|
||||
t.Helper()
|
||||
t.Logf("Creating descheduler deployment %v", deployment.Name)
|
||||
_, err := kubeClient.AppsV1().Deployments(deployment.Namespace).Create(ctx, deployment, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
_ = kubeClient.AppsV1().Deployments(deployment.Namespace).Delete(ctx, deployment.Name, metav1.DeleteOptions{})
|
||||
_, err = kubeClient.AppsV1().Deployments(deployment.Namespace).Create(ctx, deployment, metav1.CreateOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deployment.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
deschedulerPodName := ""
|
||||
t.Cleanup(func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(context.Background(), t, kubeClient, deschedulerPodName)
|
||||
}
|
||||
|
||||
t.Logf("Deleting %q deployment...", deployment.Name)
|
||||
if err := kubeClient.AppsV1().Deployments(deployment.Namespace).Delete(context.Background(), deployment.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Logf("Unable to delete %q deployment: %v", deployment.Name, err)
|
||||
}
|
||||
|
||||
waitForPodsToDisappear(context.Background(), t, kubeClient, deployment.Labels, deployment.Namespace)
|
||||
})
|
||||
|
||||
t.Logf("Waiting for the descheduler pod running")
|
||||
deschedulerPods := waitForPodsRunning(ctx, t, kubeClient, deployment.Labels, 1, deployment.Namespace)
|
||||
if len(deschedulerPods) != 0 {
|
||||
deschedulerPodName = deschedulerPods[0].Name
|
||||
}
|
||||
return deschedulerPodName
|
||||
}
|
||||
|
||||
func deschedulerDeployment(testName string) *appsv1.Deployment {
|
||||
deploymentObject := &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
||||
@@ -153,51 +153,10 @@ func TestTooManyRestarts(t *testing.T) {
|
||||
rs.DefaultFeatureGates = initFeatureGates()
|
||||
|
||||
preRunNames := sets.NewString(getCurrentPodNames(ctx, clientSet, testNamespace.Name, t)...)
|
||||
// Deploy the descheduler with the configured policy
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(tc.policy)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
t.Logf("Creating %q policy CM with RemovePodsHavingTooManyRestarts configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.Logf("Deleting %q CM...", deschedulerPolicyConfigMapObj.Name)
|
||||
err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Delete(ctx, deschedulerPolicyConfigMapObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
}()
|
||||
createPolicyConfigMap(t, ctx, clientSet, tc.policy)
|
||||
|
||||
deschedulerDeploymentObj := deschedulerDeployment(testNamespace.Name)
|
||||
t.Logf("Creating descheduler deployment %v", deschedulerDeploymentObj.Name)
|
||||
_, err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
deschedulerPodName := ""
|
||||
defer func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(ctx, t, clientSet, deschedulerPodName)
|
||||
}
|
||||
|
||||
t.Logf("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
}()
|
||||
|
||||
t.Logf("Waiting for the descheduler pod running")
|
||||
deschedulerPods := waitForPodsRunning(ctx, t, clientSet, deschedulerDeploymentObj.Labels, 1, deschedulerDeploymentObj.Namespace)
|
||||
if len(deschedulerPods) != 0 {
|
||||
deschedulerPodName = deschedulerPods[0].Name
|
||||
}
|
||||
createDeschedulerDeploymentWithCleanup(t, ctx, clientSet, deschedulerDeploymentObj)
|
||||
// Run RemovePodsHavingTooManyRestarts strategy
|
||||
if err := wait.PollUntilContextTimeout(ctx, 1*time.Second, 50*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
currentRunNames := sets.NewString(getCurrentPodNames(ctx, clientSet, testNamespace.Name, t)...)
|
||||
|
||||
@@ -219,50 +219,9 @@ func TestTopologySpreadConstraint(t *testing.T) {
|
||||
Include: []string{testNamespace.Name},
|
||||
},
|
||||
}
|
||||
deschedulerPolicyConfigMapObj, err := deschedulerPolicyConfigMap(topologySpreadConstraintPolicy(constraintArgs, evictorArgs))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
t.Logf("Creating %q policy CM with RemovePodsHavingTooManyRestarts configured...", deschedulerPolicyConfigMapObj.Name)
|
||||
_, err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Create(ctx, deschedulerPolicyConfigMapObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
t.Logf("Deleting %q CM...", deschedulerPolicyConfigMapObj.Name)
|
||||
err = clientSet.CoreV1().ConfigMaps(deschedulerPolicyConfigMapObj.Namespace).Delete(ctx, deschedulerPolicyConfigMapObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q CM: %v", deschedulerPolicyConfigMapObj.Name, err)
|
||||
}
|
||||
}()
|
||||
createPolicyConfigMap(t, ctx, clientSet, topologySpreadConstraintPolicy(constraintArgs, evictorArgs))
|
||||
deschedulerDeploymentObj := deschedulerDeployment(testNamespace.Name)
|
||||
t.Logf("Creating descheduler deployment %v", deschedulerDeploymentObj.Name)
|
||||
_, err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Create(ctx, deschedulerDeploymentObj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
|
||||
deschedulerPodName := ""
|
||||
defer func() {
|
||||
if deschedulerPodName != "" {
|
||||
printPodLogs(ctx, t, clientSet, deschedulerPodName)
|
||||
}
|
||||
|
||||
t.Logf("Deleting %q deployment...", deschedulerDeploymentObj.Name)
|
||||
err = clientSet.AppsV1().Deployments(deschedulerDeploymentObj.Namespace).Delete(ctx, deschedulerDeploymentObj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to delete %q deployment: %v", deschedulerDeploymentObj.Name, err)
|
||||
}
|
||||
waitForPodsToDisappear(ctx, t, clientSet, deschedulerDeploymentObj.Labels, deschedulerDeploymentObj.Namespace)
|
||||
}()
|
||||
|
||||
t.Logf("Waiting for the descheduler pod running")
|
||||
deschedulerPods := waitForPodsRunning(ctx, t, clientSet, deschedulerDeploymentObj.Labels, 1, deschedulerDeploymentObj.Namespace)
|
||||
if len(deschedulerPods) != 0 {
|
||||
deschedulerPodName = deschedulerPods[0].Name
|
||||
}
|
||||
createDeschedulerDeploymentWithCleanup(t, ctx, clientSet, deschedulerDeploymentObj)
|
||||
|
||||
// Run RemovePodsHavingTooManyRestarts strategy
|
||||
var meetsEvictedExpectations bool
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Used by the manifests GitHub Actions workflow (.github/workflows/manifests.yaml).
|
||||
# Three-node cluster without topology zone labels; not used by e2e tests.
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 The Kubernetes 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.
|
||||
|
||||
e2e_repo_root() {
|
||||
local script_dir="$1"
|
||||
cd "${script_dir}/.." && pwd
|
||||
}
|
||||
|
||||
collect_logs() {
|
||||
local namespace pod
|
||||
echo "Collecting pods and logs"
|
||||
for namespace in "$@"; do
|
||||
kubectl get pods -n "${namespace}"
|
||||
for pod in $(kubectl get pods -n "${namespace}" -o name); do
|
||||
echo "Logs for ${pod}"
|
||||
kubectl logs -n "${namespace}" "${pod}"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
kind_load_image() {
|
||||
local image="$1"
|
||||
if [ "${CONTAINER_ENGINE}" = "podman" ]; then
|
||||
local archive
|
||||
archive="/tmp/$(echo "${image}" | tr '/:' '_').tar"
|
||||
podman save "${image}" -o "${archive}"
|
||||
${KIND_SUDO} kind load image-archive "${archive}"
|
||||
rm "${archive}"
|
||||
else
|
||||
${KIND_SUDO} kind load docker-image "${image}"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# E2E tool version defaults. Override via environment variables.
|
||||
# Keep .github/workflows/manifests.yaml matrix versions in sync with these values.
|
||||
|
||||
K8S_VERSION=${KUBERNETES_VERSION:-v1.36.1}
|
||||
KIND_VERSION=${KIND_VERSION:-v0.31.0}
|
||||
METRICS_SERVER_VERSION=${METRICS_SERVER_VERSION:-v0.8.1}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 The Kubernetes 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.
|
||||
|
||||
kubectl create -f "https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-operator.yaml"
|
||||
kubectl create -f "https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-cr.yaml"
|
||||
# TODO(1.37): drop this patch when the k8s 1.34 e2e lane is removed. ImageVolume needs k8s >= 1.35.
|
||||
if [[ "${K8S_VERSION}" == v1.34* ]]; then
|
||||
kubectl -n kubevirt patch kubevirt kubevirt --type=merge --patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true,"disabledFeatureGates":["ImageVolume"]}}}}'
|
||||
else
|
||||
kubectl -n kubevirt patch kubevirt kubevirt --type=merge --patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'
|
||||
fi
|
||||
kubectl wait --timeout=300s --for=condition=Available -n kubevirt kv/kubevirt
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 The Kubernetes 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.
|
||||
|
||||
kubectl apply -f "https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml"
|
||||
kubectl patch -n kube-system deployment metrics-server --type=json \
|
||||
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
|
||||
kubectl wait --timeout=180s --for=condition=Available -n kube-system deployment/metrics-server
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 The Kubernetes 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.
|
||||
|
||||
KIND_NODE_IMAGE=${KIND_NODE_IMAGE:-localhost/kindest/node:${K8S_VERSION}}
|
||||
|
||||
if [ -z "${SKIP_KUBECTL_INSTALL}" ]; then
|
||||
curl -Lo kubectl "https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kubectl" && chmod +x kubectl && mv kubectl /usr/local/bin/
|
||||
fi
|
||||
if [ -z "${SKIP_KIND_INSTALL}" ]; then
|
||||
wget "https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64"
|
||||
chmod +x kind-linux-amd64
|
||||
mv kind-linux-amd64 kind
|
||||
export PATH=$PATH:$PWD
|
||||
fi
|
||||
|
||||
if [ -z "${SKIP_INSTALL}" ]; then
|
||||
${KIND_SUDO} kind build node-image "${K8S_VERSION}" --image "${KIND_NODE_IMAGE}"
|
||||
${KIND_SUDO} kind create cluster --image "${KIND_NODE_IMAGE}" --config="${REPO_ROOT}/hack/kind_config.yaml"
|
||||
fi
|
||||
${CONTAINER_ENGINE} pull registry.k8s.io/pause
|
||||
kind_load_image registry.k8s.io/pause
|
||||
kind_load_image "${DESCHEDULER_IMAGE}"
|
||||
${KIND_SUDO} kind get kubeconfig > /tmp/admin.conf
|
||||
|
||||
export KUBECONFIG="/tmp/admin.conf"
|
||||
mkdir -p ~/gopath/src/sigs.k8s.io/
|
||||
+22
-72
@@ -18,100 +18,50 @@ set -x
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
# shellcheck source=test/lib/e2e-common.sh
|
||||
source "${BASEDIR}/lib/e2e-common.sh"
|
||||
# shellcheck source=test/lib/e2e-versions.env
|
||||
source "${BASEDIR}/lib/e2e-versions.env"
|
||||
REPO_ROOT="$(e2e_repo_root "${BASEDIR}")"
|
||||
|
||||
# Set to empty if unbound/empty
|
||||
SKIP_INSTALL=${SKIP_INSTALL:-}
|
||||
KIND_E2E=${KIND_E2E:-}
|
||||
CONTAINER_ENGINE=${CONTAINER_ENGINE:-docker}
|
||||
KIND_SUDO=${KIND_SUDO:-}
|
||||
KIND_VERSION=${KIND_VERSION:-v0.31.0}
|
||||
SKIP_KUBECTL_INSTALL=${SKIP_KUBECTL_INSTALL:-}
|
||||
SKIP_KIND_INSTALL=${SKIP_KIND_INSTALL:-}
|
||||
SKIP_KUBEVIRT_INSTALL=${SKIP_KUBEVIRT_INSTALL:-}
|
||||
# v1.9.0-alpha.0 (or newer) is required for Kubernetes v1.36+, which
|
||||
# enforces stricter CRD numeric format validation
|
||||
# (https://github.com/kubernetes/kubernetes/pull/136582) and rejects the
|
||||
# pre-fix VMI checksum status schema present in v1.8.x. Fixed upstream
|
||||
# by https://github.com/kubevirt/kubevirt/pull/17469 (not backported to
|
||||
# v1.8.x). See https://github.com/kubevirt/kubevirt/issues/17858.
|
||||
KUBEVIRT_VERSION=${KUBEVIRT_VERSION:-v1.9.0-alpha.0}
|
||||
SKIP_METRICS_SERVER_INSTALL=${SKIP_METRICS_SERVER_INSTALL:-}
|
||||
KUBEVIRT_VERSION=${KUBEVIRT_VERSION:-$(grep 'kubevirt.io/api ' "${REPO_ROOT}/go.mod" | awk '{print $2}')}
|
||||
|
||||
# Build a descheduler image
|
||||
IMAGE_TAG=v$(date +%Y%m%d)-$(git describe --tags)
|
||||
BASEDIR=$(dirname "$0")
|
||||
VERSION="${IMAGE_TAG}" make -C ${BASEDIR}/.. image
|
||||
VERSION="${IMAGE_TAG}" make -C "${REPO_ROOT}" image
|
||||
|
||||
export DESCHEDULER_IMAGE="docker.io/library/descheduler:${IMAGE_TAG}"
|
||||
echo "DESCHEDULER_IMAGE: ${DESCHEDULER_IMAGE}"
|
||||
|
||||
# This just runs e2e tests.
|
||||
if [ -n "$KIND_E2E" ]; then
|
||||
K8S_VERSION=${KUBERNETES_VERSION:-v1.36.1}
|
||||
KIND_NODE_IMAGE=${KIND_NODE_IMAGE:-localhost/kindest/node:${K8S_VERSION}}
|
||||
if [ -z "${SKIP_KUBECTL_INSTALL}" ]; then
|
||||
curl -Lo kubectl https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin/
|
||||
fi
|
||||
if [ -z "${SKIP_KIND_INSTALL}" ]; then
|
||||
wget https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64
|
||||
chmod +x kind-linux-amd64
|
||||
mv kind-linux-amd64 kind
|
||||
export PATH=$PATH:$PWD
|
||||
fi
|
||||
|
||||
# If we did not set SKIP_INSTALL
|
||||
if [ -z "$SKIP_INSTALL" ]; then
|
||||
${KIND_SUDO} kind build node-image ${K8S_VERSION} --image ${KIND_NODE_IMAGE}
|
||||
${KIND_SUDO} kind create cluster --image ${KIND_NODE_IMAGE} --config=./hack/kind_config.yaml
|
||||
fi
|
||||
${CONTAINER_ENGINE} pull registry.k8s.io/pause
|
||||
if [ "${CONTAINER_ENGINE}" == "podman" ]; then
|
||||
podman save registry.k8s.io/pause -o /tmp/pause.tar
|
||||
${KIND_SUDO} kind load image-archive /tmp/pause.tar
|
||||
rm /tmp/pause.tar
|
||||
podman save ${DESCHEDULER_IMAGE} -o /tmp/descheduler.tar
|
||||
${KIND_SUDO} kind load image-archive /tmp/descheduler.tar
|
||||
rm /tmp/descheduler.tar
|
||||
else
|
||||
${KIND_SUDO} kind load docker-image registry.k8s.io/pause
|
||||
${KIND_SUDO} kind load docker-image ${DESCHEDULER_IMAGE}
|
||||
fi
|
||||
${KIND_SUDO} kind get kubeconfig > /tmp/admin.conf
|
||||
|
||||
export KUBECONFIG="/tmp/admin.conf"
|
||||
mkdir -p ~/gopath/src/sigs.k8s.io/
|
||||
if [ -n "${KIND_E2E}" ]; then
|
||||
# shellcheck source=test/lib/setup-kind.sh
|
||||
source "${BASEDIR}/lib/setup-kind.sh"
|
||||
fi
|
||||
|
||||
# Deploy rbac, sa and binding for a descheduler running through a deployment
|
||||
kubectl apply -f kubernetes/base/rbac.yaml
|
||||
kubectl apply -f "${REPO_ROOT}/kubernetes/base/rbac.yaml"
|
||||
|
||||
collect_logs() {
|
||||
echo "Collecting pods and logs"
|
||||
kubectl get pods -n default
|
||||
kubectl get pods -n kubevirt
|
||||
|
||||
for pod in $(kubectl get pods -n default -o name); do
|
||||
echo "Logs for ${pod}"
|
||||
kubectl logs -n default ${pod}
|
||||
done
|
||||
|
||||
for pod in $(kubectl get pods -n kubevirt -o name); do
|
||||
echo "Logs for ${pod}"
|
||||
kubectl logs -n kubevirt ${pod}
|
||||
done
|
||||
}
|
||||
|
||||
trap "collect_logs" ERR
|
||||
trap 'collect_logs default kubevirt' ERR
|
||||
|
||||
if [ -z "${SKIP_KUBEVIRT_INSTALL}" ]; then
|
||||
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-operator.yaml
|
||||
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-cr.yaml
|
||||
kubectl wait --timeout=180s --for=condition=Available -n kubevirt kv/kubevirt
|
||||
kubectl -n kubevirt patch kubevirt kubevirt --type=merge --patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'
|
||||
# shellcheck source=test/lib/install-kubevirt.sh
|
||||
source "${BASEDIR}/lib/install-kubevirt.sh"
|
||||
fi
|
||||
|
||||
METRICS_SERVER_VERSION="v0.8.1"
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml
|
||||
kubectl patch -n kube-system deployment metrics-server --type=json \
|
||||
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
|
||||
if [ -z "${SKIP_METRICS_SERVER_INSTALL}" ]; then
|
||||
# shellcheck source=test/lib/install-metrics-server.sh
|
||||
source "${BASEDIR}/lib/install-metrics-server.sh"
|
||||
fi
|
||||
|
||||
PRJ_PREFIX="sigs.k8s.io/descheduler"
|
||||
go test ${PRJ_PREFIX}/test/e2e/ -v -timeout 0 --args --descheduler-image ${DESCHEDULER_IMAGE} --pod-run-as-user-id 1000 --pod-run-as-group-id 1000
|
||||
go test ${PRJ_PREFIX}/test/e2e/ -v -timeout 0 --args --descheduler-image "${DESCHEDULER_IMAGE}" --kubevirt-version-tag "${KUBEVIRT_VERSION}" --pod-run-as-user-id 1000 --pod-run-as-group-id 1000
|
||||
|
||||
+4
-4
@@ -17,9 +17,9 @@ ANTLR4 that it is compatible with (I.E. uses the /v4 path).
|
||||
However, this was found to be problematic, as it meant that with the runtime embedded so far underneath the root
|
||||
of the repo, the `go get` and related commands could not properly resolve the location of the go runtime source code.
|
||||
This meant that the reference to the runtime in your `go.mod` file would refer to the correct source code, but would not
|
||||
list the release tag such as @4.12.0 - this was confusing, to say the least.
|
||||
list the release tag such as @4.13.1 - this was confusing, to say the least.
|
||||
|
||||
As of 4.12.1, the runtime is now available as a go module in its own repo, and can be imported as `github.com/antlr4-go/antlr`
|
||||
As of 4.13.0, the runtime is now available as a go module in its own repo, and can be imported as `github.com/antlr4-go/antlr`
|
||||
(the go get command should also be used with this path). See the main documentation for the ANTLR4 project for more information,
|
||||
which is available at [ANTLR docs]. The documentation for using the Go runtime is available at [Go runtime docs].
|
||||
|
||||
@@ -49,7 +49,7 @@ Here is a general/recommended template for an ANTLR based recognizer in Go:
|
||||
.
|
||||
├── parser
|
||||
│ ├── mygrammar.g4
|
||||
│ ├── antlr-4.12.1-complete.jar
|
||||
│ ├── antlr-4.13.1-complete.jar
|
||||
│ ├── generate.go
|
||||
│ └── generate.sh
|
||||
├── parsing - generated code goes here
|
||||
@@ -71,7 +71,7 @@ And the generate.sh file will look similar to this:
|
||||
|
||||
#!/bin/sh
|
||||
|
||||
alias antlr4='java -Xmx500M -cp "./antlr4-4.12.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
|
||||
alias antlr4='java -Xmx500M -cp "./antlr4-4.13.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
|
||||
antlr4 -Dlanguage=Go -no-visitor -package parsing *.g4
|
||||
|
||||
depending on whether you want visitors or listeners or any other ANTLR options. Not that another option here
|
||||
|
||||
+3
-5
@@ -4,8 +4,6 @@
|
||||
|
||||
package antlr
|
||||
|
||||
import "sync"
|
||||
|
||||
// ATNInvalidAltNumber is used to represent an ALT number that has yet to be calculated or
|
||||
// which is invalid for a particular struct such as [*antlr.BaseRuleContext]
|
||||
var ATNInvalidAltNumber int
|
||||
@@ -56,9 +54,9 @@ type ATN struct {
|
||||
//
|
||||
states []ATNState
|
||||
|
||||
mu sync.Mutex
|
||||
stateMu sync.RWMutex
|
||||
edgeMu sync.RWMutex
|
||||
mu Mutex
|
||||
stateMu RWMutex
|
||||
edgeMu RWMutex
|
||||
}
|
||||
|
||||
// NewATN returns a new ATN struct representing the given grammarType and is used
|
||||
|
||||
-3
@@ -73,9 +73,6 @@ func NewATNConfig1(c *ATNConfig, state ATNState, context *PredictionContext) *AT
|
||||
// NewATNConfig creates a new ATNConfig instance given an existing config, a state, a context and a semantic context, other 'constructors'
|
||||
// are just wrappers around this one.
|
||||
func NewATNConfig(c *ATNConfig, state ATNState, context *PredictionContext, semanticContext SemanticContext) *ATNConfig {
|
||||
if semanticContext == nil {
|
||||
panic("semanticContext cannot be nil") // TODO: Remove this - probably put here for some bug that is now fixed
|
||||
}
|
||||
b := &ATNConfig{}
|
||||
b.InitATNConfig(c, state, c.GetAlt(), context, semanticContext)
|
||||
b.cType = parserConfig
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ func (is *InputStream) GetTextFromInterval(i Interval) string {
|
||||
}
|
||||
|
||||
func (*InputStream) GetSourceName() string {
|
||||
return ""
|
||||
return "Obtained from string"
|
||||
}
|
||||
|
||||
// String returns the entire input stream as a string
|
||||
|
||||
+2
-3
@@ -8,7 +8,6 @@ import (
|
||||
"container/list"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Collectable is an interface that a struct should implement if it is to be
|
||||
@@ -587,12 +586,12 @@ type VisitRecord struct {
|
||||
|
||||
type VisitList struct {
|
||||
cache *list.List
|
||||
lock sync.RWMutex
|
||||
lock RWMutex
|
||||
}
|
||||
|
||||
var visitListPool = VisitList{
|
||||
cache: list.New(),
|
||||
lock: sync.RWMutex{},
|
||||
lock: RWMutex{},
|
||||
}
|
||||
|
||||
// NewVisitRecord returns a new VisitRecord instance from the pool if available.
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ func (b *BaseLexer) NextToken() Token {
|
||||
for {
|
||||
b.thetype = TokenInvalidType
|
||||
|
||||
ttype := b.safeMatch()
|
||||
ttype := b.safeMatch() // Defaults to LexerSkip
|
||||
|
||||
if b.input.LA(1) == TokenEOF {
|
||||
b.hitEOF = true
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ func (la *LL1Analyzer) getDecisionLookahead(s ATNState) []*IntervalSet {
|
||||
for alt := 0; alt < count; alt++ {
|
||||
|
||||
look[alt] = NewIntervalSet()
|
||||
// TODO: This is one of the reasons that ATNConfigs are allocated and freed all the time - fix this tomorrow jim!
|
||||
lookBusy := NewJStore[*ATNConfig, Comparator[*ATNConfig]](aConfEqInst, ClosureBusyCollection, "LL1Analyzer.getDecisionLookahead for lookBusy")
|
||||
la.look1(s.GetTransitions()[alt].getTarget(), nil, BasePredictionContextEMPTY, look[alt], lookBusy, NewBitSet(), false, false)
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//go:build !antlr.nomutex
|
||||
// +build !antlr.nomutex
|
||||
|
||||
package antlr
|
||||
|
||||
import "sync"
|
||||
|
||||
// Mutex is a simple mutex implementation which just delegates to sync.Mutex, it
|
||||
// is used to provide a mutex implementation for the antlr package, which users
|
||||
// can turn off with the build tag -tags antlr.nomutex
|
||||
type Mutex struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *Mutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *Mutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
type RWMutex struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *RWMutex) Lock() {
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
||||
func (m *RWMutex) Unlock() {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *RWMutex) RLock() {
|
||||
m.mu.RLock()
|
||||
}
|
||||
|
||||
func (m *RWMutex) RUnlock() {
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
//go:build antlr.nomutex
|
||||
// +build antlr.nomutex
|
||||
|
||||
package antlr
|
||||
|
||||
type Mutex struct{}
|
||||
|
||||
func (m *Mutex) Lock() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
func (m *Mutex) Unlock() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
type RWMutex struct{}
|
||||
|
||||
func (m *RWMutex) Lock() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
func (m *RWMutex) Unlock() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
func (m *RWMutex) RLock() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
func (m *RWMutex) RUnlock() {
|
||||
// No-op
|
||||
}
|
||||
+1
-3
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
// ClosureBusy is a store of ATNConfigs and is a tiny abstraction layer over
|
||||
// a standard JStore so that we can use Lazy instantiation of the JStore, mostly
|
||||
// to avoid polluting the stats module with a ton of JStore instances with nothing in them.
|
||||
@@ -883,7 +881,7 @@ func (p *ParserATNSimulator) getPredicatePredictions(ambigAlts *BitSet, altToPre
|
||||
// the ERROR state was reached, outerContext as the initial parser context from the paper
|
||||
// or the parser stack at the instant before prediction commences.
|
||||
//
|
||||
// Teh func returns the value to return from [AdaptivePredict], or
|
||||
// The func returns the value to return from [AdaptivePredict], or
|
||||
// [ATNInvalidAltNumber] if a suitable alternative was not
|
||||
// identified and [AdaptivePredict] should report an error instead.
|
||||
func (p *ParserATNSimulator) getSynValidOrSemInvalidAltThatFinishedDecisionEntryRule(configs *ATNConfigSet, outerContext ParserRuleContext) int {
|
||||
|
||||
+30
-30
@@ -6,7 +6,6 @@ package antlr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/exp/slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -101,7 +100,7 @@ func NewArrayPredictionContext(parents []*PredictionContext, returnStates []int)
|
||||
hash = murmurUpdate(hash, returnState)
|
||||
}
|
||||
hash = murmurFinish(hash, len(parents)<<1)
|
||||
|
||||
|
||||
nec := &PredictionContext{}
|
||||
nec.cachedHash = hash
|
||||
nec.pcType = PredictionContextArray
|
||||
@@ -115,6 +114,9 @@ func (p *PredictionContext) Hash() int {
|
||||
}
|
||||
|
||||
func (p *PredictionContext) Equals(other Collectable[*PredictionContext]) bool {
|
||||
if p == other {
|
||||
return true
|
||||
}
|
||||
switch p.pcType {
|
||||
case PredictionContextEmpty:
|
||||
otherP := other.(*PredictionContext)
|
||||
@@ -138,13 +140,11 @@ func (p *PredictionContext) ArrayEquals(o Collectable[*PredictionContext]) bool
|
||||
if p.cachedHash != other.Hash() {
|
||||
return false // can't be same if hash is different
|
||||
}
|
||||
|
||||
|
||||
// Must compare the actual array elements and not just the array address
|
||||
//
|
||||
return slices.Equal(p.returnStates, other.returnStates) &&
|
||||
slices.EqualFunc(p.parents, other.parents, func(x, y *PredictionContext) bool {
|
||||
return x.Equals(y)
|
||||
})
|
||||
return intSlicesEqual(p.returnStates, other.returnStates) &&
|
||||
pcSliceEqual(p.parents, other.parents)
|
||||
}
|
||||
|
||||
func (p *PredictionContext) SingletonEquals(other Collectable[*PredictionContext]) bool {
|
||||
@@ -152,23 +152,23 @@ func (p *PredictionContext) SingletonEquals(other Collectable[*PredictionContext
|
||||
return false
|
||||
}
|
||||
otherP := other.(*PredictionContext)
|
||||
if otherP == nil {
|
||||
if otherP == nil || otherP.pcType != PredictionContextSingleton {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if p.cachedHash != otherP.Hash() {
|
||||
return false // Can't be same if hash is different
|
||||
}
|
||||
|
||||
|
||||
if p.returnState != otherP.getReturnState(0) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Both parents must be nil if one is
|
||||
if p.parentCtx == nil {
|
||||
return otherP.parentCtx == nil
|
||||
}
|
||||
|
||||
|
||||
return p.parentCtx.Equals(otherP.parentCtx)
|
||||
}
|
||||
|
||||
@@ -225,27 +225,27 @@ func (p *PredictionContext) String() string {
|
||||
return "$"
|
||||
case PredictionContextSingleton:
|
||||
var up string
|
||||
|
||||
|
||||
if p.parentCtx == nil {
|
||||
up = ""
|
||||
} else {
|
||||
up = p.parentCtx.String()
|
||||
}
|
||||
|
||||
|
||||
if len(up) == 0 {
|
||||
if p.returnState == BasePredictionContextEmptyReturnState {
|
||||
return "$"
|
||||
}
|
||||
|
||||
|
||||
return strconv.Itoa(p.returnState)
|
||||
}
|
||||
|
||||
|
||||
return strconv.Itoa(p.returnState) + " " + up
|
||||
case PredictionContextArray:
|
||||
if p.isEmpty() {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
|
||||
s := "["
|
||||
for i := 0; i < len(p.returnStates); i++ {
|
||||
if i > 0 {
|
||||
@@ -263,7 +263,7 @@ func (p *PredictionContext) String() string {
|
||||
}
|
||||
}
|
||||
return s + "]"
|
||||
|
||||
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@@ -309,18 +309,18 @@ func predictionContextFromRuleContext(a *ATN, outerContext RuleContext) *Predict
|
||||
parent := predictionContextFromRuleContext(a, outerContext.GetParent().(RuleContext))
|
||||
state := a.states[outerContext.GetInvokingState()]
|
||||
transition := state.GetTransitions()[0]
|
||||
|
||||
|
||||
return SingletonBasePredictionContextCreate(parent, transition.(*RuleTransition).followState.GetStateNumber())
|
||||
}
|
||||
|
||||
func merge(a, b *PredictionContext, rootIsWildcard bool, mergeCache *JPCMap) *PredictionContext {
|
||||
|
||||
|
||||
// Share same graph if both same
|
||||
//
|
||||
if a == b || a.Equals(b) {
|
||||
return a
|
||||
}
|
||||
|
||||
|
||||
if a.pcType == PredictionContextSingleton && b.pcType == PredictionContextSingleton {
|
||||
return mergeSingletons(a, b, rootIsWildcard, mergeCache)
|
||||
}
|
||||
@@ -334,7 +334,7 @@ func merge(a, b *PredictionContext, rootIsWildcard bool, mergeCache *JPCMap) *Pr
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert either Singleton or Empty to arrays, so that we can merge them
|
||||
//
|
||||
ara := convertToArray(a)
|
||||
@@ -395,7 +395,7 @@ func mergeSingletons(a, b *PredictionContext, rootIsWildcard bool, mergeCache *J
|
||||
return previous
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rootMerge := mergeRoot(a, b, rootIsWildcard)
|
||||
if rootMerge != nil {
|
||||
if mergeCache != nil {
|
||||
@@ -564,7 +564,7 @@ func mergeArrays(a, b *PredictionContext, rootIsWildcard bool, mergeCache *JPCMa
|
||||
i := 0 // walks a
|
||||
j := 0 // walks b
|
||||
k := 0 // walks target M array
|
||||
|
||||
|
||||
mergedReturnStates := make([]int, len(a.returnStates)+len(b.returnStates))
|
||||
mergedParents := make([]*PredictionContext, len(a.returnStates)+len(b.returnStates))
|
||||
// walk and merge to yield mergedParents, mergedReturnStates
|
||||
@@ -626,9 +626,9 @@ func mergeArrays(a, b *PredictionContext, rootIsWildcard bool, mergeCache *JPCMa
|
||||
mergedParents = mergedParents[0:k]
|
||||
mergedReturnStates = mergedReturnStates[0:k]
|
||||
}
|
||||
|
||||
|
||||
M := NewArrayPredictionContext(mergedParents, mergedReturnStates)
|
||||
|
||||
|
||||
// if we created same array as a or b, return that instead
|
||||
// TODO: JI track whether this is possible above during merge sort for speed and possibly avoid an allocation
|
||||
if M.Equals(a) {
|
||||
@@ -650,7 +650,7 @@ func mergeArrays(a, b *PredictionContext, rootIsWildcard bool, mergeCache *JPCMa
|
||||
return b
|
||||
}
|
||||
combineCommonParents(&mergedParents)
|
||||
|
||||
|
||||
if mergeCache != nil {
|
||||
mergeCache.Put(a, b, M)
|
||||
}
|
||||
@@ -666,7 +666,7 @@ func mergeArrays(a, b *PredictionContext, rootIsWildcard bool, mergeCache *JPCMa
|
||||
//goland:noinspection GoUnusedFunction
|
||||
func combineCommonParents(parents *[]*PredictionContext) {
|
||||
uniqueParents := NewJStore[*PredictionContext, Comparator[*PredictionContext]](pContextEqInst, PredictionContextCollection, "combineCommonParents for PredictionContext")
|
||||
|
||||
|
||||
for p := 0; p < len(*parents); p++ {
|
||||
parent := (*parents)[p]
|
||||
_, _ = uniqueParents.Put(parent)
|
||||
@@ -685,7 +685,7 @@ func getCachedBasePredictionContext(context *PredictionContext, contextCache *Pr
|
||||
if present {
|
||||
return existing
|
||||
}
|
||||
|
||||
|
||||
existing, present = contextCache.Get(context)
|
||||
if present {
|
||||
visited.Put(context, existing)
|
||||
@@ -722,6 +722,6 @@ func getCachedBasePredictionContext(context *PredictionContext, contextCache *Pr
|
||||
contextCache.add(updated)
|
||||
visited.Put(updated, updated)
|
||||
visited.Put(context, updated)
|
||||
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ var tokenTypeMapCache = make(map[string]int)
|
||||
var ruleIndexMapCache = make(map[string]int)
|
||||
|
||||
func (b *BaseRecognizer) checkVersion(toolVersion string) {
|
||||
runtimeVersion := "4.12.0"
|
||||
runtimeVersion := "4.13.1"
|
||||
if runtimeVersion != toolVersion {
|
||||
fmt.Println("ANTLR runtime and generated code versions disagree: " + runtimeVersion + "!=" + toolVersion)
|
||||
}
|
||||
|
||||
+1
-2
@@ -9,7 +9,6 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This file allows the user to collect statistics about the runtime of the ANTLR runtime. It is not enabled by default
|
||||
@@ -30,7 +29,7 @@ type goRunStats struct {
|
||||
// within this package.
|
||||
//
|
||||
jStats []*JStatRec
|
||||
jStatsLock sync.RWMutex
|
||||
jStatsLock RWMutex
|
||||
topN int
|
||||
topNByMax []*JStatRec
|
||||
topNByUsed []*JStatRec
|
||||
|
||||
+41
-41
@@ -104,6 +104,25 @@ func (b *BaseToken) GetSource() *TokenSourceCharStreamPair {
|
||||
return b.source
|
||||
}
|
||||
|
||||
func (b *BaseToken) GetText() string {
|
||||
if b.text != "" {
|
||||
return b.text
|
||||
}
|
||||
input := b.GetInputStream()
|
||||
if input == nil {
|
||||
return ""
|
||||
}
|
||||
n := input.Size()
|
||||
if b.GetStart() < n && b.GetStop() < n {
|
||||
return input.GetTextFromInterval(NewInterval(b.GetStart(), b.GetStop()))
|
||||
}
|
||||
return "<EOF>"
|
||||
}
|
||||
|
||||
func (b *BaseToken) SetText(text string) {
|
||||
b.text = text
|
||||
}
|
||||
|
||||
func (b *BaseToken) GetTokenIndex() int {
|
||||
return b.tokenIndex
|
||||
}
|
||||
@@ -120,6 +139,28 @@ func (b *BaseToken) GetInputStream() CharStream {
|
||||
return b.source.charStream
|
||||
}
|
||||
|
||||
func (b *BaseToken) String() string {
|
||||
txt := b.GetText()
|
||||
if txt != "" {
|
||||
txt = strings.Replace(txt, "\n", "\\n", -1)
|
||||
txt = strings.Replace(txt, "\r", "\\r", -1)
|
||||
txt = strings.Replace(txt, "\t", "\\t", -1)
|
||||
} else {
|
||||
txt = "<no text>"
|
||||
}
|
||||
|
||||
var ch string
|
||||
if b.GetChannel() > 0 {
|
||||
ch = ",channel=" + strconv.Itoa(b.GetChannel())
|
||||
} else {
|
||||
ch = ""
|
||||
}
|
||||
|
||||
return "[@" + strconv.Itoa(b.GetTokenIndex()) + "," + strconv.Itoa(b.GetStart()) + ":" + strconv.Itoa(b.GetStop()) + "='" +
|
||||
txt + "',<" + strconv.Itoa(b.GetTokenType()) + ">" +
|
||||
ch + "," + strconv.Itoa(b.GetLine()) + ":" + strconv.Itoa(b.GetColumn()) + "]"
|
||||
}
|
||||
|
||||
type CommonToken struct {
|
||||
BaseToken
|
||||
}
|
||||
@@ -170,44 +211,3 @@ func (c *CommonToken) clone() *CommonToken {
|
||||
t.text = c.GetText()
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *CommonToken) GetText() string {
|
||||
if c.text != "" {
|
||||
return c.text
|
||||
}
|
||||
input := c.GetInputStream()
|
||||
if input == nil {
|
||||
return ""
|
||||
}
|
||||
n := input.Size()
|
||||
if c.start < n && c.stop < n {
|
||||
return input.GetTextFromInterval(NewInterval(c.start, c.stop))
|
||||
}
|
||||
return "<EOF>"
|
||||
}
|
||||
|
||||
func (c *CommonToken) SetText(text string) {
|
||||
c.text = text
|
||||
}
|
||||
|
||||
func (c *CommonToken) String() string {
|
||||
txt := c.GetText()
|
||||
if txt != "" {
|
||||
txt = strings.Replace(txt, "\n", "\\n", -1)
|
||||
txt = strings.Replace(txt, "\r", "\\r", -1)
|
||||
txt = strings.Replace(txt, "\t", "\\t", -1)
|
||||
} else {
|
||||
txt = "<no text>"
|
||||
}
|
||||
|
||||
var ch string
|
||||
if c.channel > 0 {
|
||||
ch = ",channel=" + strconv.Itoa(c.channel)
|
||||
} else {
|
||||
ch = ""
|
||||
}
|
||||
|
||||
return "[@" + strconv.Itoa(c.tokenIndex) + "," + strconv.Itoa(c.start) + ":" + strconv.Itoa(c.stop) + "='" +
|
||||
txt + "',<" + strconv.Itoa(c.tokenType) + ">" +
|
||||
ch + "," + strconv.Itoa(c.line) + ":" + strconv.Itoa(c.column) + "]"
|
||||
}
|
||||
|
||||
+53
@@ -326,3 +326,56 @@ func isDirectory(dir string) (bool, error) {
|
||||
}
|
||||
return fileInfo.IsDir(), err
|
||||
}
|
||||
|
||||
// intSlicesEqual returns true if the two slices of ints are equal, and is a little
|
||||
// faster than slices.Equal.
|
||||
func intSlicesEqual(s1, s2 []int) bool {
|
||||
if s1 == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
if s1 == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
if len(s1) == 0 && len(s2) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(s1) == 0 || len(s2) == 0 || len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
// If the slices are using the same memory, then they are the same slice
|
||||
if &s1[0] == &s2[0] {
|
||||
return true
|
||||
}
|
||||
for i, v := range s1 {
|
||||
if v != s2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func pcSliceEqual(s1, s2 []*PredictionContext) bool {
|
||||
if s1 == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
if s1 == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
if len(s1) == 0 && len(s2) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(s1) == 0 || len(s2) == 0 || len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
// If the slices are using the same memory, then they are the same slice
|
||||
if &s1[0] == &s2[0] {
|
||||
return true
|
||||
}
|
||||
for i, v := range s1 {
|
||||
if !v.Equals(s2[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
+14
-1
@@ -10,6 +10,7 @@ go_library(
|
||||
"cel.go",
|
||||
"decls.go",
|
||||
"env.go",
|
||||
"fieldpaths.go",
|
||||
"folding.go",
|
||||
"inlining.go",
|
||||
"io.go",
|
||||
@@ -21,10 +22,11 @@ go_library(
|
||||
"prompt.go",
|
||||
"validator.go",
|
||||
],
|
||||
embedsrcs = ["//cel/templates"],
|
||||
embedsrcs = ["templates/authoring.tmpl"],
|
||||
importpath = "github.com/google/cel-go/cel",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//cel/async:go_default_library",
|
||||
"//checker:go_default_library",
|
||||
"//checker/decls:go_default_library",
|
||||
"//common:go_default_library",
|
||||
@@ -43,6 +45,7 @@ go_library(
|
||||
"//interpreter:go_default_library",
|
||||
"//parser:go_default_library",
|
||||
"@dev_cel_expr//:expr",
|
||||
"@dev_cel_expr//conformance/proto3:go_default_library",
|
||||
"@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
|
||||
"@org_golang_google_protobuf//proto:go_default_library",
|
||||
"@org_golang_google_protobuf//reflect/protodesc:go_default_library",
|
||||
@@ -63,10 +66,12 @@ go_test(
|
||||
"cel_test.go",
|
||||
"decls_test.go",
|
||||
"env_test.go",
|
||||
"fieldpaths_test.go",
|
||||
"folding_test.go",
|
||||
"inlining_test.go",
|
||||
"io_test.go",
|
||||
"optimizer_test.go",
|
||||
"program_async_test.go",
|
||||
"prompt_test.go",
|
||||
"validator_test.go",
|
||||
],
|
||||
@@ -78,8 +83,10 @@ go_test(
|
||||
],
|
||||
embedsrcs = [
|
||||
"//cel/testdata:prompts",
|
||||
"//cel/testdata:test_fds_with_source_info",
|
||||
],
|
||||
deps = [
|
||||
"//cel/async:go_default_library",
|
||||
"//common/operators:go_default_library",
|
||||
"//common/overloads:go_default_library",
|
||||
"//common/types:go_default_library",
|
||||
@@ -89,6 +96,7 @@ go_test(
|
||||
"//test:go_default_library",
|
||||
"//test/proto2pb:go_default_library",
|
||||
"//test/proto3pb:go_default_library",
|
||||
"@com_github_google_go_cmp//cmp:go_default_library",
|
||||
"@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
|
||||
"@org_golang_google_protobuf//encoding/prototext:go_default_library",
|
||||
"@org_golang_google_protobuf//proto:go_default_library",
|
||||
@@ -96,3 +104,8 @@ go_test(
|
||||
"@org_golang_google_protobuf//types/known/wrapperspb:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
exports_files(
|
||||
["templates/authoring.tmpl"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
package(
|
||||
licenses = ["notice"], # Apache 2.0
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"async.go",
|
||||
],
|
||||
importpath = "github.com/google/cel-go/cel/async",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//common/decls:go_default_library",
|
||||
"//common/functions:go_default_library",
|
||||
"//common/types:go_default_library",
|
||||
"//common/types/ref:go_default_library",
|
||||
"//interpreter:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"async_test.go",
|
||||
],
|
||||
deps = [
|
||||
":go_default_library",
|
||||
"//common/decls:go_default_library",
|
||||
"//common/functions:go_default_library",
|
||||
"//common/types:go_default_library",
|
||||
"//common/types/ref:go_default_library",
|
||||
],
|
||||
)
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
// Copyright 2026 Google LLC
|
||||
//
|
||||
// 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 async provides helpers for configuring and executing asynchronous CEL functions,
|
||||
// including drain strategies, retry, timeout, concurrency limiting, and caching wrappers.
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/common/decls"
|
||||
"github.com/google/cel-go/common/functions"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
)
|
||||
|
||||
// Call describes a pending or completed asynchronous function call.
|
||||
// This interface exposes a safe, read-only view of the internal interpreter state.
|
||||
type Call = interpreter.AsyncCall
|
||||
|
||||
// Observer provides callbacks for monitoring the lifecycle of asynchronous function calls.
|
||||
//
|
||||
// Implementations must be safe for concurrent use: the start and finish callbacks run on different
|
||||
// goroutines, and finish callbacks for distinct calls may run concurrently. See
|
||||
// interpreter.AsyncObserver for details.
|
||||
type Observer = interpreter.AsyncObserver
|
||||
|
||||
// BlockingOp is a blocking asynchronous function operation.
|
||||
type BlockingOp = functions.BlockingAsyncOp
|
||||
|
||||
// DrainAction dictates what ConcurrentEval should do after inspecting completions.
|
||||
type DrainAction struct {
|
||||
// Reevaluate indicates that the AST should be re-evaluated immediately.
|
||||
// If true, WaitDuration is ignored.
|
||||
Reevaluate bool
|
||||
// WaitDuration indicates how long the evaluator should wait for additional
|
||||
// completions before deciding to re-evaluate. A duration of 0 means wait
|
||||
// indefinitely (block on the next completion).
|
||||
WaitDuration time.Duration
|
||||
}
|
||||
|
||||
// DrainStrategy controls when ConcurrentEval re-evaluates after async completions.
|
||||
//
|
||||
// The evaluator consults the strategy each time a completion is received.
|
||||
type DrainStrategy interface {
|
||||
// NextAction evaluates the current state of asynchronous evaluation and
|
||||
// determines the next step.
|
||||
//
|
||||
// - completed: The set of completions accumulated in the current batch.
|
||||
// - active: The number of async calls currently launched but unresolved.
|
||||
NextAction(completed []Call, active int) DrainAction
|
||||
}
|
||||
|
||||
// DrainNone returns a strategy that re-evaluates after every single completion.
|
||||
// This is the default strategy.
|
||||
func DrainNone() DrainStrategy {
|
||||
return drainNone{}
|
||||
}
|
||||
|
||||
type drainNone struct{}
|
||||
|
||||
func (drainNone) NextAction(completed []Call, active int) DrainAction {
|
||||
return DrainAction{Reevaluate: active == 0 || len(completed) > 0}
|
||||
}
|
||||
|
||||
// DrainReady returns a strategy that waits for a short duration after the first
|
||||
// completion to batch any other functions that complete at roughly the same time.
|
||||
func DrainReady(debounce time.Duration) DrainStrategy {
|
||||
return drainReady{debounce: debounce}
|
||||
}
|
||||
|
||||
type drainReady struct {
|
||||
debounce time.Duration
|
||||
}
|
||||
|
||||
func (d drainReady) NextAction(completed []Call, active int) DrainAction {
|
||||
if active == 0 {
|
||||
return DrainAction{Reevaluate: true} // Nothing left to wait for
|
||||
}
|
||||
if len(completed) == 0 {
|
||||
return DrainAction{Reevaluate: false, WaitDuration: 0} // Wait indefinitely for first
|
||||
}
|
||||
return DrainAction{Reevaluate: false, WaitDuration: d.debounce} // Wait for debounce period
|
||||
}
|
||||
|
||||
// DrainAll returns a strategy that waits for all currently pending calls to
|
||||
// complete before re-evaluating.
|
||||
//
|
||||
// Note: This strategy is optimal for independent async calls, but will over-wait
|
||||
// if some calls depend on the results of others.
|
||||
func DrainAll() DrainStrategy {
|
||||
return drainAll{}
|
||||
}
|
||||
|
||||
type drainAll struct{}
|
||||
|
||||
func (drainAll) NextAction(completed []Call, active int) DrainAction {
|
||||
return DrainAction{Reevaluate: active == 0}
|
||||
}
|
||||
|
||||
// Timeout wraps a BlockingAsyncOp with a per-call timeout.
|
||||
//
|
||||
// The timeout is enforced even when the wrapped function ignores its context: the function runs on
|
||||
// its own goroutine and Timeout selects on the deadline, returning a timeout error when it
|
||||
// fires. A function that ignores cancellation cannot be forcibly stopped (Go cannot kill a
|
||||
// goroutine), so its goroutine continues running in the background until it returns on its own;
|
||||
// only its result is abandoned. This is the recommended way to bound functions that may hang or
|
||||
// are not under the caller's control. The extra goroutine is incurred only by Timeout-wrapped
|
||||
// calls, not by async evaluation in general.
|
||||
func Timeout(fn functions.BlockingAsyncOp, timeout time.Duration) functions.BlockingAsyncOp {
|
||||
return func(ctx context.Context, args ...ref.Val) ref.Val {
|
||||
tCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
resCh := make(chan ref.Val, 1)
|
||||
go func() { resCh <- fn(tCtx, args...) }()
|
||||
select {
|
||||
case res := <-resCh:
|
||||
return res
|
||||
case <-tCtx.Done():
|
||||
return types.NewErr("operation timed out after %v: %v", timeout, tCtx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TimeoutBinding wraps a BlockingAsyncOp with a per-call timeout and returns an OverloadOpt.
|
||||
func TimeoutBinding(fn functions.BlockingAsyncOp, timeout time.Duration) decls.OverloadOpt {
|
||||
return decls.AsyncBinding(Timeout(fn, timeout))
|
||||
}
|
||||
|
||||
// RetryOption configures the behavior of RetryBinding.
|
||||
type RetryOption func(*retryConfig)
|
||||
|
||||
type retryConfig struct {
|
||||
maxAttempts int
|
||||
backoff time.Duration
|
||||
}
|
||||
|
||||
// RetryAttempts sets the maximum number of attempts (including the first one).
|
||||
func RetryAttempts(attempts int) RetryOption {
|
||||
return func(c *retryConfig) {
|
||||
c.maxAttempts = attempts
|
||||
}
|
||||
}
|
||||
|
||||
// RetryBackoff sets the fixed backoff duration between attempts.
|
||||
func RetryBackoff(backoff time.Duration) RetryOption {
|
||||
return func(c *retryConfig) {
|
||||
c.backoff = backoff
|
||||
}
|
||||
}
|
||||
|
||||
// RetryableError is an interface that errors can implement to signal whether they are retryable.
|
||||
type RetryableError interface {
|
||||
error
|
||||
IsRetryable() bool
|
||||
}
|
||||
|
||||
// Retry wraps a BlockingAsyncOp with a retry policy.
|
||||
// It will retry the operation if it returns a types.Err that wraps a RetryableError returning true for IsRetryable.
|
||||
func Retry(fn functions.BlockingAsyncOp, opts ...RetryOption) functions.BlockingAsyncOp {
|
||||
config := &retryConfig{
|
||||
maxAttempts: 3,
|
||||
backoff: 100 * time.Millisecond,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
return func(ctx context.Context, args ...ref.Val) ref.Val {
|
||||
var lastErr ref.Val
|
||||
var backoff *time.Timer
|
||||
defer func() {
|
||||
if backoff != nil {
|
||||
backoff.Stop()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < config.maxAttempts; i++ {
|
||||
if i > 0 {
|
||||
// Reuse a single timer across attempts and stop it on cancellation so the
|
||||
// pending timer is not left to fire after the call returns.
|
||||
if backoff == nil {
|
||||
backoff = time.NewTimer(config.backoff)
|
||||
} else {
|
||||
backoff.Reset(config.backoff)
|
||||
}
|
||||
select {
|
||||
case <-backoff.C:
|
||||
case <-ctx.Done():
|
||||
backoff.Stop()
|
||||
return types.NewErr("operation cancelled during retry: %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
res := fn(ctx, args...)
|
||||
if !types.IsError(res) {
|
||||
return res
|
||||
}
|
||||
|
||||
err := res.(*types.Err)
|
||||
lastErr = res
|
||||
|
||||
if !isRetryable(err) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
|
||||
// RetryBinding wraps a BlockingAsyncOp with a retry policy and returns an OverloadOpt.
|
||||
func RetryBinding(fn functions.BlockingAsyncOp, opts ...RetryOption) decls.OverloadOpt {
|
||||
return decls.AsyncBinding(Retry(fn, opts...))
|
||||
}
|
||||
|
||||
func isRetryable(err *types.Err) bool {
|
||||
var re RetryableError
|
||||
if errors.As(err, &re) {
|
||||
return re.IsRetryable()
|
||||
}
|
||||
return false
|
||||
}
|
||||
+21
@@ -17,3 +17,24 @@
|
||||
// CEL is a non-Turing complete expression language designed to parse, check, and evaluate
|
||||
// expressions against user-defined environments.
|
||||
package cel
|
||||
|
||||
// Compile is a convenience function that constructs a new Env using the provided EnvOption values,
|
||||
// compiles the expression string, and plans an executable Program.
|
||||
//
|
||||
// Warning: Creating a new environment for every compilation is expensive. Environment setup should be done once
|
||||
// and shared across expression compilations when the options remain the same.
|
||||
func Compile(expression string, opts ...EnvOption) (Program, error) {
|
||||
env, err := NewEnv(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ast, iss := env.Compile(expression)
|
||||
if iss.Err() != nil {
|
||||
return nil, iss.Err()
|
||||
}
|
||||
prg, err := env.Program(ast, EvalOptions(OptOptimize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prg, nil
|
||||
}
|
||||
|
||||
+26
@@ -346,6 +346,32 @@ func LateFunctionBinding() OverloadOpt {
|
||||
return decls.LateFunctionBinding()
|
||||
}
|
||||
|
||||
// AsyncBinding provides the implementation of an asynchronous overload. The provided function
|
||||
// is called in its own goroutine with the provided context. The function should block until
|
||||
// the result is available, and the framework manages goroutine and channel lifecycle.
|
||||
//
|
||||
// This follows the same pattern used by gRPC-Go and other major Go frameworks where user
|
||||
// code is synchronous and the framework manages concurrency.
|
||||
//
|
||||
// Context contract: the function MUST return promptly once its context is cancelled. The
|
||||
// framework cannot forcibly terminate the goroutine running the function, so a function that
|
||||
// ignores cancellation will leak its goroutine and hold a concurrency slot (see
|
||||
// AsyncMaxConcurrency) until it returns on its own. For functions that may hang or that are not
|
||||
// under your control, wrap them with async.TimeoutBinding to bound their runtime.
|
||||
func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt {
|
||||
return decls.AsyncBinding(fn)
|
||||
}
|
||||
|
||||
// SingletonAsyncBinding creates a singleton async function definition from a blocking function,
|
||||
// to be used with all function overloads. The provided function is called in its own goroutine
|
||||
// with the provided context.
|
||||
//
|
||||
// Note, this approach works well if operand is expected to have a specific trait which it implements,
|
||||
// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings.
|
||||
func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt {
|
||||
return decls.SingletonAsyncBinding(fn, traits...)
|
||||
}
|
||||
|
||||
// OverloadIsNonStrict enables the function to be called with error and unknown argument values.
|
||||
//
|
||||
// Note: do not use this option unless absoluately necessary as it should be an uncommon feature.
|
||||
|
||||
+165
-25
@@ -18,6 +18,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/checker"
|
||||
@@ -27,6 +29,7 @@ import (
|
||||
"github.com/google/cel-go/common/containers"
|
||||
"github.com/google/cel-go/common/decls"
|
||||
"github.com/google/cel-go/common/env"
|
||||
"github.com/google/cel-go/common/functions"
|
||||
"github.com/google/cel-go/common/stdlib"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
@@ -45,6 +48,10 @@ type Source = common.Source
|
||||
type Ast struct {
|
||||
source Source
|
||||
impl *celast.AST
|
||||
// loadErr captures an error detected while loading the AST (e.g. an over-deep AST ingested via
|
||||
// ParsedExprToAst / CheckedExprToAst) so it can be surfaced when the Ast is checked or planned
|
||||
// instead of recursing into the checker or planner on adversarially deep input.
|
||||
loadErr error
|
||||
}
|
||||
|
||||
// NativeRep converts the AST to a Go-native representation.
|
||||
@@ -138,10 +145,14 @@ type Env struct {
|
||||
provider types.Provider
|
||||
features map[int]bool
|
||||
appliedFeatures map[int]bool
|
||||
limits map[limitID]int
|
||||
libraries map[string]SingletonLibrary
|
||||
validators []ASTValidator
|
||||
costOptions []checker.CostOption
|
||||
|
||||
funcBindOnce sync.Once
|
||||
functionBindings []*functions.Overload
|
||||
|
||||
// Internal parser representation
|
||||
prsr *parser.Parser
|
||||
prsrOpts []parser.Option
|
||||
@@ -177,6 +188,16 @@ func (e *Env) ToConfig(name string) (*env.Config, error) {
|
||||
conf.AddImports(env.NewImport(typeName))
|
||||
}
|
||||
|
||||
// Serialize features
|
||||
for featID, enabled := range e.features {
|
||||
featName, found := featureNameByID(featID)
|
||||
if !found {
|
||||
// If the feature isn't named, it isn't intended to be publicly exposed
|
||||
continue
|
||||
}
|
||||
conf.AddFeatures(env.NewFeature(featName, enabled))
|
||||
}
|
||||
|
||||
libOverloads := map[string][]string{}
|
||||
for libName, lib := range e.libraries {
|
||||
// Track the options which have been configured by a library and
|
||||
@@ -237,7 +258,7 @@ func (e *Env) ToConfig(name string) (*env.Config, error) {
|
||||
fields := e.contextProto.Fields()
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
field := fields.Get(i)
|
||||
variable, err := fieldToVariable(field)
|
||||
variable, err := fieldToVariable(field, e.HasFeature(featureJSONFieldNames))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not serialize context field variable %q, reason: %w", field.FullName(), err)
|
||||
}
|
||||
@@ -272,16 +293,45 @@ func (e *Env) ToConfig(name string) (*env.Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize features
|
||||
for featID, enabled := range e.features {
|
||||
featName, found := featureNameByID(featID)
|
||||
if !found {
|
||||
// If the feature isn't named, it isn't intended to be publicly exposed
|
||||
for id, val := range e.limits {
|
||||
limitName, found := limitNameByID(id)
|
||||
if !found || val == 0 {
|
||||
// skip if explicitly defaulted or not supported in config
|
||||
continue
|
||||
}
|
||||
conf.AddFeatures(env.NewFeature(featName, enabled))
|
||||
conf.AddLimits(env.NewLimit(limitName, val))
|
||||
}
|
||||
|
||||
// Sort repeated fields in config where reasonable to make the export
|
||||
// stable.
|
||||
slices.SortFunc(conf.Imports, func(a *env.Import, b *env.Import) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
slices.SortFunc(conf.Extensions, func(a *env.Extension, b *env.Extension) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
slices.SortFunc(conf.Variables, func(a *env.Variable, b *env.Variable) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
slices.SortFunc(conf.Functions, func(a *env.Function, b *env.Function) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
slices.SortFunc(conf.Validators, func(a *env.Validator, b *env.Validator) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
slices.SortFunc(conf.Features, func(a *env.Feature, b *env.Feature) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
slices.SortFunc(conf.Limits, func(a *env.Limit, b *env.Limit) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
@@ -315,23 +365,25 @@ func NewEnv(opts ...EnvOption) (*Env, error) {
|
||||
// See the EnvOption helper functions for the options that can be used to configure the
|
||||
// environment.
|
||||
func NewCustomEnv(opts ...EnvOption) (*Env, error) {
|
||||
registry, err := types.NewRegistry()
|
||||
registry, err := types.NewProtoRegistry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (&Env{
|
||||
variables: []*decls.VariableDecl{},
|
||||
functions: map[string]*decls.FunctionDecl{},
|
||||
macros: []parser.Macro{},
|
||||
Container: containers.DefaultContainer,
|
||||
adapter: registry,
|
||||
provider: registry,
|
||||
features: map[int]bool{},
|
||||
appliedFeatures: map[int]bool{},
|
||||
libraries: map[string]SingletonLibrary{},
|
||||
validators: []ASTValidator{},
|
||||
progOpts: []ProgramOption{},
|
||||
costOptions: []checker.CostOption{},
|
||||
variables: []*decls.VariableDecl{},
|
||||
functions: map[string]*decls.FunctionDecl{},
|
||||
functionBindings: []*functions.Overload{},
|
||||
macros: []parser.Macro{},
|
||||
Container: containers.DefaultContainer,
|
||||
adapter: registry,
|
||||
provider: registry,
|
||||
features: map[int]bool{},
|
||||
appliedFeatures: map[int]bool{},
|
||||
limits: map[limitID]int{},
|
||||
libraries: map[string]SingletonLibrary{},
|
||||
validators: []ASTValidator{},
|
||||
progOpts: []ProgramOption{},
|
||||
costOptions: []checker.CostOption{},
|
||||
}).configure(opts)
|
||||
}
|
||||
|
||||
@@ -347,6 +399,20 @@ func NewCustomEnv(opts ...EnvOption) (*Env, error) {
|
||||
// It is possible to have both non-nil Ast and Issues values returned from this call: however,
|
||||
// the mere presence of an Ast does not imply that it is valid for use.
|
||||
func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
|
||||
// Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by
|
||||
// ParsedExprToAst / CheckedExprToAst) before recursing into the type checker on it.
|
||||
if ast != nil && ast.loadErr != nil {
|
||||
errs := common.NewErrors(ast.Source())
|
||||
errs.ReportErrorString(common.NoLocation, ast.loadErr.Error())
|
||||
return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo())
|
||||
}
|
||||
if nodeLimit := e.configuredExpressionNodeLimit(); nodeLimit > 0 && ast != nil && ast.NativeRep() != nil {
|
||||
if count := celast.NodeCount(ast.NativeRep()); count > nodeLimit {
|
||||
errs := common.NewErrors(ast.Source())
|
||||
errs.ReportErrorString(common.NoLocation, fmt.Sprintf("expression node count exceeds limit: count %d, limit %d", count, nodeLimit))
|
||||
return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo())
|
||||
}
|
||||
}
|
||||
// Construct the internal checker env, erroring if there is an issue adding the declarations.
|
||||
chk, err := e.initChecker()
|
||||
if err != nil {
|
||||
@@ -388,6 +454,24 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
|
||||
return ast, nil
|
||||
}
|
||||
|
||||
// configuredExpressionSizeLimit returns the effective expression size code point limit.
|
||||
// A zero value means "use the parser default".
|
||||
func (e *Env) configuredExpressionSizeLimit() int {
|
||||
if l := e.limits[limitCodePointSize]; l != 0 {
|
||||
return l
|
||||
}
|
||||
return 100_000
|
||||
}
|
||||
|
||||
// configuredExpressionNodeLimit returns the effective expression node limit.
|
||||
// A zero value means "use default".
|
||||
func (e *Env) configuredExpressionNodeLimit() int {
|
||||
if l := e.limits[limitExpressionNodeCount]; l != 0 {
|
||||
return l
|
||||
}
|
||||
return 100_000
|
||||
}
|
||||
|
||||
// Compile combines the Parse and Check phases CEL program compilation to produce an Ast and
|
||||
// associated issues.
|
||||
//
|
||||
@@ -397,7 +481,11 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
|
||||
//
|
||||
// Note, for parse-only uses of CEL use Parse.
|
||||
func (e *Env) Compile(txt string) (*Ast, *Issues) {
|
||||
return e.CompileSource(common.NewTextSource(txt))
|
||||
src, err := common.NewTextSourceWithLimit(txt, e.configuredExpressionSizeLimit())
|
||||
if err != nil {
|
||||
return nil, ErrorAsIssues(err)
|
||||
}
|
||||
return e.CompileSource(src)
|
||||
}
|
||||
|
||||
// CompileSource combines the Parse and Check phases CEL program compilation to produce an Ast and
|
||||
@@ -492,6 +580,10 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
|
||||
for k, v := range e.appliedFeatures {
|
||||
appliedFeaturesCopy[k] = v
|
||||
}
|
||||
limitsCopy := make(map[limitID]int, len(e.limits))
|
||||
for k, v := range e.limits {
|
||||
limitsCopy[k] = v
|
||||
}
|
||||
funcsCopy := make(map[string]*decls.FunctionDecl, len(e.functions))
|
||||
for k, v := range e.functions {
|
||||
funcsCopy[k] = v
|
||||
@@ -502,6 +594,7 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
|
||||
}
|
||||
validatorsCopy := make([]ASTValidator, len(e.validators))
|
||||
copy(validatorsCopy, e.validators)
|
||||
|
||||
costOptsCopy := make([]checker.CostOption, len(e.costOptions))
|
||||
copy(costOptsCopy, e.costOptions)
|
||||
|
||||
@@ -514,6 +607,7 @@ func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
|
||||
progOpts: progOptsCopy,
|
||||
adapter: adapter,
|
||||
features: featuresCopy,
|
||||
limits: limitsCopy,
|
||||
appliedFeatures: appliedFeaturesCopy,
|
||||
libraries: libsCopy,
|
||||
validators: validatorsCopy,
|
||||
@@ -596,7 +690,10 @@ func (e *Env) Validators() []ASTValidator {
|
||||
// This form of Parse creates a Source value for the input `txt` and forwards to the
|
||||
// ParseSource method.
|
||||
func (e *Env) Parse(txt string) (*Ast, *Issues) {
|
||||
src := common.NewTextSource(txt)
|
||||
src, err := common.NewTextSourceWithLimit(txt, e.configuredExpressionSizeLimit())
|
||||
if err != nil {
|
||||
return nil, ErrorAsIssues(err)
|
||||
}
|
||||
return e.ParseSource(src)
|
||||
}
|
||||
|
||||
@@ -617,6 +714,12 @@ func (e *Env) ParseSource(src Source) (*Ast, *Issues) {
|
||||
|
||||
// Program generates an evaluable instance of the Ast within the environment (Env).
|
||||
func (e *Env) Program(ast *Ast, opts ...ProgramOption) (Program, error) {
|
||||
// Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by
|
||||
// ParsedExprToAst / CheckedExprToAst) rather than recursing into the planner on it. This is a
|
||||
// cheap field read; the depth traversal itself runs once at conversion time, not here.
|
||||
if ast != nil && ast.loadErr != nil {
|
||||
return nil, ast.loadErr
|
||||
}
|
||||
return e.PlanProgram(ast.NativeRep(), opts...)
|
||||
}
|
||||
|
||||
@@ -780,11 +883,35 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) {
|
||||
if e.HasFeature(featureIdentEscapeSyntax) {
|
||||
prsrOpts = append(prsrOpts, parser.EnableIdentEscapeSyntax(true))
|
||||
}
|
||||
if l := e.limits[limitParseErrorRecovery]; l != 0 {
|
||||
prsrOpts = append(prsrOpts, parser.ErrorRecoveryLimit(l))
|
||||
}
|
||||
if l := e.limits[limitCodePointSize]; l != 0 {
|
||||
prsrOpts = append(prsrOpts, parser.ExpressionSizeCodePointLimit(l))
|
||||
}
|
||||
if l := e.limits[limitParseRecursionDepth]; l != 0 {
|
||||
prsrOpts = append(prsrOpts, parser.MaxRecursionDepth(l))
|
||||
}
|
||||
if l := e.limits[limitExpressionNodeCount]; l != 0 {
|
||||
prsrOpts = append(prsrOpts, parser.MaxExpressionNodeCount(l))
|
||||
}
|
||||
e.prsr, err = parser.NewParser(prsrOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enable JSON field names is using a proto-based *types.Registry
|
||||
if e.HasFeature(featureJSONFieldNames) {
|
||||
reg, isReg := e.provider.(*types.Registry)
|
||||
if !isReg {
|
||||
return nil, fmt.Errorf("JSONFieldNames() option is only compatible with *types.Registry providers")
|
||||
}
|
||||
err := reg.WithJSONFieldNames(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the checker init happens eagerly rather than lazily.
|
||||
if e.HasFeature(featureEagerlyValidateDeclarations) {
|
||||
_, err := e.initChecker()
|
||||
@@ -803,6 +930,8 @@ func (e *Env) initChecker() (*checker.Env, error) {
|
||||
chkOpts = append(chkOpts,
|
||||
checker.CrossTypeNumericComparisons(
|
||||
e.HasFeature(featureCrossTypeNumericComparisons)))
|
||||
chkOpts = append(chkOpts,
|
||||
checker.JSONFieldNames(e.HasFeature(featureJSONFieldNames)))
|
||||
|
||||
ce, err := checker.NewEnv(e.Container, e.provider, chkOpts...)
|
||||
if err != nil {
|
||||
@@ -872,6 +1001,16 @@ type Issues struct {
|
||||
info *celast.SourceInfo
|
||||
}
|
||||
|
||||
// ErrorAsIssues wraps a Golang error into a CEL common error and issue set.
|
||||
//
|
||||
// This is a convenience method for early returning from an expression validation call path due to
|
||||
// internal state or configuration which is unrelated to the source being validated.
|
||||
func ErrorAsIssues(err error) *Issues {
|
||||
errs := common.NewErrors(common.NewTextSource(""))
|
||||
errs.ReportErrorString(common.NoLocation, err.Error())
|
||||
return NewIssues(errs)
|
||||
}
|
||||
|
||||
// NewIssues returns an Issues struct from a common.Errors object.
|
||||
func NewIssues(errs *common.Errors) *Issues {
|
||||
return NewIssuesWithSourceInfo(errs, nil)
|
||||
@@ -980,9 +1119,10 @@ func (p *interopCELTypeProvider) FindStructFieldType(structType, fieldName strin
|
||||
return nil, false
|
||||
}
|
||||
return &types.FieldType{
|
||||
Type: t,
|
||||
IsSet: ft.IsSet,
|
||||
GetFrom: ft.GetFrom,
|
||||
Type: t,
|
||||
IsSet: ft.IsSet,
|
||||
GetFrom: ft.GetFrom,
|
||||
IsJSONField: ft.IsJSONField,
|
||||
}, true
|
||||
}
|
||||
return nil, false
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package cel
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
// fieldPath represents a selection path to a field from a variable in a CEL environment.
|
||||
type fieldPath struct {
|
||||
celType *Type
|
||||
// path represents the selection path to the field.
|
||||
path string
|
||||
description string
|
||||
isLeaf bool
|
||||
}
|
||||
|
||||
// Documentation implements the Documentor interface.
|
||||
func (f *fieldPath) Documentation() *common.Doc {
|
||||
return common.NewFieldDoc(f.path, f.celType.String(), f.description)
|
||||
}
|
||||
|
||||
type documentationProvider interface {
|
||||
// FindStructFieldDescription returns documentation for a field if available.
|
||||
// Returns false if the field could not be found.
|
||||
FindStructFieldDescription(typeName, fieldName string) (string, bool)
|
||||
}
|
||||
|
||||
type backtrack struct {
|
||||
// provider used to resolve types.
|
||||
provider types.Provider
|
||||
// paths of fields that have been visited along the path.
|
||||
path []string
|
||||
// types of fields that have been visited along the path. used to avoid cycles.
|
||||
types []*Type
|
||||
}
|
||||
|
||||
func (b *backtrack) push(pathStep string, celType *Type) {
|
||||
b.path = append(b.path, pathStep)
|
||||
b.types = append(b.types, celType)
|
||||
}
|
||||
|
||||
func (b *backtrack) pop() {
|
||||
b.path = b.path[:len(b.path)-1]
|
||||
b.types = b.types[:len(b.types)-1]
|
||||
}
|
||||
|
||||
func formatPath(path []string) string {
|
||||
var buffer strings.Builder
|
||||
for i, p := range path {
|
||||
if i == 0 {
|
||||
buffer.WriteString(p)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(p, "[") {
|
||||
buffer.WriteString(p)
|
||||
continue
|
||||
}
|
||||
buffer.WriteString(".")
|
||||
buffer.WriteString(p)
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (b *backtrack) expandFieldPaths(celType *Type, paths []*fieldPath) []*fieldPath {
|
||||
if slices.ContainsFunc(b.types[:len(b.types)-1], func(t *Type) bool { return t.String() == celType.String() }) {
|
||||
// Cycle detected, so stop expanding.
|
||||
paths[len(paths)-1].isLeaf = false
|
||||
return paths
|
||||
}
|
||||
switch celType.Kind() {
|
||||
case types.StructKind:
|
||||
fields, ok := b.provider.FindStructFieldNames(celType.String())
|
||||
if !ok {
|
||||
// Caller added this type to the path, so it must be a leaf.
|
||||
paths[len(paths)-1].isLeaf = true
|
||||
return paths
|
||||
}
|
||||
for _, field := range fields {
|
||||
fieldType, ok := b.provider.FindStructFieldType(celType.String(), field)
|
||||
if !ok {
|
||||
// Field not found, either hidden or an error.
|
||||
continue
|
||||
}
|
||||
b.push(field, celType)
|
||||
description := ""
|
||||
if docProvider, ok := b.provider.(documentationProvider); ok {
|
||||
description, _ = docProvider.FindStructFieldDescription(celType.String(), field)
|
||||
}
|
||||
path := &fieldPath{
|
||||
celType: fieldType.Type,
|
||||
path: formatPath(b.path),
|
||||
description: description,
|
||||
isLeaf: false,
|
||||
}
|
||||
paths = append(paths, path)
|
||||
paths = b.expandFieldPaths(fieldType.Type, paths)
|
||||
b.pop()
|
||||
}
|
||||
return paths
|
||||
case types.MapKind:
|
||||
if len(celType.Parameters()) != 2 {
|
||||
// dynamic map, so treat as a leaf.
|
||||
paths[len(paths)-1].isLeaf = true
|
||||
return paths
|
||||
}
|
||||
mapKeyType := celType.Parameters()[0]
|
||||
mapValueType := celType.Parameters()[1]
|
||||
// Add a placeholder for the map key kind (the zero value).
|
||||
keyIdentifier := ""
|
||||
switch mapKeyType.Kind() {
|
||||
case types.StringKind:
|
||||
keyIdentifier = "[\"\"]"
|
||||
case types.IntKind:
|
||||
keyIdentifier = "[0]"
|
||||
case types.UintKind:
|
||||
keyIdentifier = "[0u]"
|
||||
case types.BoolKind:
|
||||
keyIdentifier = "[false]"
|
||||
default:
|
||||
// Caller added this type to the path, so it must be a leaf.
|
||||
paths[len(paths)-1].isLeaf = true
|
||||
return paths
|
||||
}
|
||||
b.push(keyIdentifier, mapValueType)
|
||||
defer b.pop()
|
||||
return b.expandFieldPaths(mapValueType, paths)
|
||||
case types.ListKind:
|
||||
if len(celType.Parameters()) != 1 {
|
||||
// dynamic list, so treat as a leaf.
|
||||
paths[len(paths)-1].isLeaf = true
|
||||
return paths
|
||||
}
|
||||
listElemType := celType.Parameters()[0]
|
||||
b.push("[0]", listElemType)
|
||||
defer b.pop()
|
||||
return b.expandFieldPaths(listElemType, paths)
|
||||
default:
|
||||
paths[len(paths)-1].isLeaf = true
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// fieldPathsForType expands the reachable fields from the given root identifier.
|
||||
func fieldPathsForType(provider types.Provider, identifier string, celType *Type) []*fieldPath {
|
||||
b := &backtrack{
|
||||
provider: provider,
|
||||
path: []string{identifier},
|
||||
types: []*Type{celType},
|
||||
}
|
||||
paths := []*fieldPath{
|
||||
{
|
||||
celType: celType,
|
||||
path: identifier,
|
||||
isLeaf: false,
|
||||
},
|
||||
}
|
||||
|
||||
return b.expandFieldPaths(celType, paths)
|
||||
}
|
||||
+160
-32
@@ -15,6 +15,8 @@
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/cel-go/common/ast"
|
||||
@@ -38,7 +40,7 @@ func MaxConstantFoldIterations(limit int) ConstantFoldingOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Adds an Activation which provides known values for the folding evaluator
|
||||
// FoldKnownValues adds an Activation which provides known values for the folding evaluator
|
||||
//
|
||||
// Any values the activation provides will be used by the constant folder and turned into
|
||||
// literals in the AST.
|
||||
@@ -93,18 +95,18 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST)
|
||||
for _, fold := range foldableExprs {
|
||||
// If the expression could be folded because it's a non-strict call, and the
|
||||
// branches are pruned, continue to the next fold.
|
||||
if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, fold) {
|
||||
if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, a, fold) {
|
||||
continue
|
||||
}
|
||||
// Late-bound function calls cannot be folded.
|
||||
if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, a, fold) {
|
||||
if fold.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, fold) {
|
||||
continue
|
||||
}
|
||||
// Otherwise, assume all context is needed to evaluate the expression.
|
||||
err := opt.tryFold(ctx, a, fold)
|
||||
// Ignore errors for identifiers, since there is no guarantee that the environment
|
||||
// Ignore errors for identifiers or subexpressions that cannot be folded, since there is no guarantee that the environment
|
||||
// has a value for them.
|
||||
if err != nil && fold.Kind() != ast.IdentKind {
|
||||
if err != nil && fold.Kind() != ast.IdentKind && !errors.Is(err, errCannotFold) {
|
||||
ctx.ReportErrorAtID(fold.ID(), "constant-folding evaluation failed: %v", err.Error())
|
||||
return a
|
||||
}
|
||||
@@ -142,24 +144,19 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST)
|
||||
return a
|
||||
}
|
||||
|
||||
var errCannotFold = errors.New("subexpression cannot be folded")
|
||||
|
||||
// tryFold attempts to evaluate a sub-expression to a literal.
|
||||
//
|
||||
// If the evaluation succeeds, the input expr value will be modified to become a literal, otherwise
|
||||
// the method will return an error.
|
||||
func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) error {
|
||||
// Assume all context is needed to evaluate the expression.
|
||||
subAST := &Ast{
|
||||
impl: ast.NewCheckedAST(ast.NewAST(expr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()),
|
||||
}
|
||||
prg, err := ctx.Program(subAST)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activation := opt.knownValues
|
||||
if activation == nil {
|
||||
activation = NoVars()
|
||||
}
|
||||
out, _, err := prg.Eval(activation)
|
||||
navExpr := expr.(ast.NavigableExpr)
|
||||
out, err := evaluateExpr(ctx, a, navExpr, activation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -168,7 +165,31 @@ func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST,
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLateBoundFunctionCall(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) bool {
|
||||
func evaluateExpr(ctx *OptimizerContext, a *ast.AST, navigableExpr ast.NavigableExpr, activation Activation) (ref.Val, error) {
|
||||
partialActivation, err := ctx.PartialVars(activation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subAST := &Ast{
|
||||
impl: ast.NewCheckedAST(ast.NewAST(navigableExpr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()),
|
||||
}
|
||||
prg, err := ctx.Program(subAST)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Folding will not attempt to call async functions which are all marked as late-bound,
|
||||
// but the presence of such functions requires the use of `ConcurrentEval` in order to
|
||||
// avoid an early return error which blocks async functions from running in `Eval` and
|
||||
// `ContextEval` call paths.
|
||||
resCh := prg.ConcurrentEval(context.Background(), partialActivation)
|
||||
res := <-resCh
|
||||
if res.Err != nil || types.IsUnknown(res.Val) {
|
||||
return nil, errCannotFold
|
||||
}
|
||||
return res.Val, nil
|
||||
}
|
||||
|
||||
func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool {
|
||||
call := expr.AsCall()
|
||||
function := ctx.Functions()[call.FunctionName()]
|
||||
if function == nil {
|
||||
@@ -181,12 +202,12 @@ func isLateBoundFunctionCall(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) b
|
||||
// a branch can be removed. Evaluation will naturally prune logical and / or calls,
|
||||
// but conditional will not be pruned cleanly, so this is one small area where the
|
||||
// constant folding step reimplements a portion of the evaluator.
|
||||
func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool {
|
||||
func maybePruneBranches(ctx *OptimizerContext, a *ast.AST, expr ast.NavigableExpr) bool {
|
||||
call := expr.AsCall()
|
||||
args := call.Args()
|
||||
switch call.FunctionName() {
|
||||
case operators.LogicalAnd, operators.LogicalOr:
|
||||
return maybeShortcircuitLogic(ctx, call.FunctionName(), args, expr)
|
||||
return maybeShortcircuitLogic(ctx, a, call.FunctionName(), args, expr)
|
||||
case operators.Conditional:
|
||||
cond := args[0]
|
||||
truthy := args[1]
|
||||
@@ -207,11 +228,17 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool {
|
||||
return true
|
||||
}
|
||||
needle := args[0]
|
||||
if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind {
|
||||
needleValue := needle.AsLiteral()
|
||||
if (needle.Kind() == ast.LiteralKind || isSelfEqualIdent(needle)) && haystack.Kind() == ast.ListKind {
|
||||
needleIsLit := needle.Kind() == ast.LiteralKind
|
||||
needleLitVal := needle.AsLiteral()
|
||||
needleIdentVal := needle.AsIdent()
|
||||
list := haystack.AsList()
|
||||
for _, e := range list.Elements() {
|
||||
if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True {
|
||||
for _, elem := range list.Elements() {
|
||||
if needleIsLit && elem.Kind() == ast.LiteralKind && elem.AsLiteral().Equal(needleLitVal) == types.True {
|
||||
ctx.UpdateExpr(expr, ctx.NewLiteral(types.True))
|
||||
return true
|
||||
}
|
||||
if !needleIsLit && elem.Kind() == ast.IdentKind && elem.AsIdent() == needleIdentVal {
|
||||
ctx.UpdateExpr(expr, ctx.NewLiteral(types.True))
|
||||
return true
|
||||
}
|
||||
@@ -221,7 +248,7 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.Expr, expr ast.NavigableExpr) bool {
|
||||
func maybeShortcircuitLogic(ctx *OptimizerContext, a *ast.AST, function string, args []ast.Expr, expr ast.NavigableExpr) bool {
|
||||
shortcircuit := types.False
|
||||
skip := types.True
|
||||
if function == operators.LogicalOr {
|
||||
@@ -244,10 +271,14 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E
|
||||
}
|
||||
if len(newArgs) == 0 {
|
||||
newArgs = append(newArgs, args[0])
|
||||
ctx.UpdateExpr(expr, newArgs[0])
|
||||
return true
|
||||
}
|
||||
if len(newArgs) == len(args) {
|
||||
return false
|
||||
}
|
||||
if len(newArgs) == 1 {
|
||||
if !isBoolType(a, newArgs[0]) {
|
||||
return false
|
||||
}
|
||||
ctx.UpdateExpr(expr, newArgs[0])
|
||||
return true
|
||||
}
|
||||
@@ -255,6 +286,16 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E
|
||||
return true
|
||||
}
|
||||
|
||||
func isBoolType(a *ast.AST, e ast.Expr) bool {
|
||||
if a != nil && a.GetType(e.ID()) == types.BoolType {
|
||||
return true
|
||||
}
|
||||
if e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == types.BoolType {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pruneOptionalElements works from the bottom up to resolve optional elements within
|
||||
// aggregate literals.
|
||||
//
|
||||
@@ -285,9 +326,9 @@ func pruneOptionalListElements(ctx *OptimizerContext, e ast.Expr) {
|
||||
updatedElems := []ast.Expr{}
|
||||
updatedIndices := []int32{}
|
||||
newOptIndex := -1
|
||||
for _, e := range elems {
|
||||
for i, e := range elems {
|
||||
newOptIndex++
|
||||
if !l.IsOptional(int32(newOptIndex)) {
|
||||
if !l.IsOptional(int32(i)) {
|
||||
updatedElems = append(updatedElems, e)
|
||||
continue
|
||||
}
|
||||
@@ -501,7 +542,7 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext,
|
||||
sel := e.AsSelect() // guaranteed to be a navigable value
|
||||
return constantMatcher(sel.Operand().(ast.NavigableExpr))
|
||||
case ast.IdentKind:
|
||||
return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil
|
||||
return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil && !hasComprehensionVar(e)
|
||||
case ast.ComprehensionKind:
|
||||
if isNestedComprehension(e) {
|
||||
return false
|
||||
@@ -513,12 +554,15 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext,
|
||||
nested := e.AsComprehension()
|
||||
vars[nested.AccuVar()] = true
|
||||
vars[nested.IterVar()] = true
|
||||
if nested.IterVar2() != "" {
|
||||
vars[nested.IterVar2()] = true
|
||||
}
|
||||
}
|
||||
if e.Kind() == ast.IdentKind && !vars[e.AsIdent()] {
|
||||
constantExprs = false
|
||||
}
|
||||
// Late-bound function calls cannot be folded.
|
||||
if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, a, e) {
|
||||
if e.Kind() == ast.CallKind && isLateBoundFunctionCall(ctx, e) {
|
||||
constantExprs = false
|
||||
}
|
||||
})
|
||||
@@ -554,17 +598,33 @@ func constantCallMatcher(e ast.NavigableExpr) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if fnName == operators.Equals || fnName == operators.NotEquals {
|
||||
if hasComprehensionVar(e) {
|
||||
return false
|
||||
}
|
||||
if isExprConstantOfKind(children[0], types.BoolType) || isExprConstantOfKind(children[1], types.BoolType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if fnName == operators.In {
|
||||
if hasComprehensionVar(e) {
|
||||
return false
|
||||
}
|
||||
haystack := children[1]
|
||||
if haystack.Kind() == ast.ListKind && haystack.AsList().Size() == 0 {
|
||||
return true
|
||||
}
|
||||
needle := children[0]
|
||||
if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind {
|
||||
needleValue := needle.AsLiteral()
|
||||
if (needle.Kind() == ast.LiteralKind || isSelfEqualIdent(needle)) && haystack.Kind() == ast.ListKind {
|
||||
needleIsLit := needle.Kind() == ast.LiteralKind
|
||||
needleLitVal := needle.AsLiteral()
|
||||
needleIdentVal := needle.AsIdent()
|
||||
list := haystack.AsList()
|
||||
for _, e := range list.Elements() {
|
||||
if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True {
|
||||
for _, elem := range list.Elements() {
|
||||
if needleIsLit && elem.Kind() == ast.LiteralKind && elem.AsLiteral().Equal(needleLitVal) == types.True {
|
||||
return true
|
||||
}
|
||||
if !needleIsLit && elem.Kind() == ast.IdentKind && elem.AsIdent() == needleIdentVal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -579,6 +639,74 @@ func constantCallMatcher(e ast.NavigableExpr) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// isSelfEqualIdent indicates whether the expression is an identifier whose static type
|
||||
// guarantees that its runtime value is equal to itself.
|
||||
//
|
||||
// Matching an identifier against a list element by name only proves list membership when the
|
||||
// value the name resolves to is self-equal. A double may be NaN, which is not equal to itself,
|
||||
// and dyn, abstract, and struct types may all hold a NaN at runtime, so the check is limited
|
||||
// to the scalar types which cannot, and to the aggregate types whose type parameters are
|
||||
// themselves self-equal.
|
||||
func isSelfEqualIdent(e ast.Expr) bool {
|
||||
if e.Kind() != ast.IdentKind {
|
||||
return false
|
||||
}
|
||||
nav, ok := e.(ast.NavigableExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return isSelfEqualType(nav.Type())
|
||||
}
|
||||
|
||||
// isSelfEqualType indicates whether all runtime values of the given type are equal to themselves.
|
||||
func isSelfEqualType(t *types.Type) bool {
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
switch t.Kind() {
|
||||
case types.BoolKind, types.BytesKind, types.DurationKind, types.IntKind,
|
||||
types.NullTypeKind, types.StringKind, types.TimestampKind, types.TypeKind,
|
||||
types.UintKind:
|
||||
return true
|
||||
case types.ListKind, types.MapKind:
|
||||
// Aggregates compare element-wise, so they are self-equal exactly when their type
|
||||
// parameters are. A list(dyn) or map(string, double) may still contain a NaN.
|
||||
for _, p := range t.Parameters() {
|
||||
if !isSelfEqualType(p) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isExprConstantOfKind(e ast.Expr, t *types.Type) bool {
|
||||
return e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == t
|
||||
}
|
||||
|
||||
func hasComprehensionVar(e ast.NavigableExpr) bool {
|
||||
idents := ast.MatchDescendants(e, ast.KindMatcher(ast.IdentKind))
|
||||
for _, identNode := range idents {
|
||||
identName := identNode.AsIdent()
|
||||
curr := identNode
|
||||
parent, found := curr.Parent()
|
||||
for found {
|
||||
if parent.Kind() == ast.ComprehensionKind {
|
||||
compre := parent.AsComprehension()
|
||||
if (compre.AccuVar() == identName || compre.IterVar() == identName || compre.IterVar2() == identName) &&
|
||||
curr.ID() != compre.IterRange().ID() && curr.ID() != compre.AccuInit().ID() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
curr = parent
|
||||
parent, found = parent.Parent()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNestedComprehension(e ast.NavigableExpr) bool {
|
||||
parent, found := e.Parent()
|
||||
for found {
|
||||
|
||||
+65
-10
@@ -178,9 +178,38 @@ func (opt *inliningOptimizer) rewritePresenceExpr(ctx *OptimizerContext, prev, i
|
||||
))
|
||||
return
|
||||
}
|
||||
if zeroValExpr, ok := zeroValueExpr(ctx, inlinedType); ok {
|
||||
ctx.UpdateExpr(prev,
|
||||
ctx.NewCall(operators.NotEquals,
|
||||
inlined, zeroValExpr))
|
||||
return
|
||||
}
|
||||
ctx.ReportErrorAtID(prev.ID(), "unable to inline expression type %v into presence test", inlinedType)
|
||||
}
|
||||
|
||||
// zeroValueExpr creates an expression representing the empty or zero value for the given type
|
||||
// Note: bytes, lists, maps, and strings are supported via the `SizerType` trait.
|
||||
func zeroValueExpr(ctx *OptimizerContext, t *Type) (ast.Expr, bool) {
|
||||
// Note: bytes, strings, lists, and maps are covered by the "sizer-type" check
|
||||
switch t.Kind() {
|
||||
case types.BoolKind:
|
||||
return ctx.NewLiteral(types.False), true
|
||||
case types.DoubleKind:
|
||||
return ctx.NewLiteral(types.Double(0)), true
|
||||
case types.DurationKind:
|
||||
return ctx.NewCall(overloads.TypeConvertDuration, ctx.NewLiteral(types.String("0s"))), true
|
||||
case types.IntKind:
|
||||
return ctx.NewLiteral(types.IntZero), true
|
||||
case types.TimestampKind:
|
||||
return ctx.NewCall(overloads.TypeConvertTimestamp, ctx.NewLiteral(types.Int(0))), true
|
||||
case types.StructKind:
|
||||
return ctx.NewStruct(t.TypeName(), []ast.EntryExpr{}), true
|
||||
case types.UintKind:
|
||||
return ctx.NewLiteral(types.Uint(0)), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// isBindable indicates whether the inlined type can be used within a cel.bind() if the expression
|
||||
// being replaced occurs within a presence test. Value types with a size() method or field selection
|
||||
// support can be bound.
|
||||
@@ -212,17 +241,43 @@ func isBindable(matches []ast.NavigableExpr, inlined ast.Expr, inlinedType *Type
|
||||
// field selection. This may be a future refinement.
|
||||
func (opt *inliningOptimizer) matchVariable(varName string) ast.ExprMatcher {
|
||||
return func(e ast.NavigableExpr) bool {
|
||||
if e.Kind() == ast.IdentKind && e.AsIdent() == varName {
|
||||
return true
|
||||
name, found := maybeAsVariableName(e)
|
||||
if !found || name != varName {
|
||||
return false
|
||||
}
|
||||
if e.Kind() == ast.SelectKind {
|
||||
sel := e.AsSelect()
|
||||
// While the `ToQualifiedName` call could take the select directly, this
|
||||
// would skip presence tests from possible matches, which we would like
|
||||
// to include.
|
||||
qualName, found := containers.ToQualifiedName(sel.Operand())
|
||||
return found && qualName+"."+sel.FieldName() == varName
|
||||
|
||||
// Determine whether the variable being referenced has been shadowed by a comprehension
|
||||
p, hasParent := e.Parent()
|
||||
for hasParent {
|
||||
if p.Kind() != ast.ComprehensionKind {
|
||||
p, hasParent = p.Parent()
|
||||
continue
|
||||
}
|
||||
// If the inline variable name matches any of the comprehension variables at any scope,
|
||||
// return false as the variable has been shadowed.
|
||||
compre := p.AsComprehension()
|
||||
if varName == compre.AccuVar() || varName == compre.IterVar() || varName == compre.IterVar2() {
|
||||
return false
|
||||
}
|
||||
p, hasParent = p.Parent()
|
||||
}
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func maybeAsVariableName(e ast.NavigableExpr) (string, bool) {
|
||||
if e.Kind() == ast.IdentKind {
|
||||
return e.AsIdent(), true
|
||||
}
|
||||
if e.Kind() == ast.SelectKind {
|
||||
sel := e.AsSelect()
|
||||
// While the `ToQualifiedName` call could take the select directly, this
|
||||
// would skip presence tests from possible matches, which we would like
|
||||
// to include.
|
||||
if qualName, found := containers.ToQualifiedName(sel.Operand()); found {
|
||||
return qualName + "." + sel.FieldName(), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
+26
-2
@@ -52,7 +52,12 @@ func CheckedExprToAstWithSource(checkedExpr *exprpb.CheckedExpr, src Source) (*A
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Ast{source: src, impl: checked}, nil
|
||||
out := &Ast{source: src, impl: checked}
|
||||
if err := checkLoadedASTDepth(checked); err != nil {
|
||||
out.loadErr = err
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AstToCheckedExpr converts an Ast to an protobuf CheckedExpr value.
|
||||
@@ -83,7 +88,26 @@ func ParsedExprToAstWithSource(parsedExpr *exprpb.ParsedExpr, src Source) *Ast {
|
||||
src = common.NewInfoSource(parsedExpr.GetSourceInfo())
|
||||
}
|
||||
e, _ := ast.ProtoToExpr(parsedExpr.GetExpr())
|
||||
return &Ast{source: src, impl: ast.NewAST(e, info)}
|
||||
out := &Ast{source: src, impl: ast.NewAST(e, info)}
|
||||
// ParsedExprToAstWithSource has no error return, so record an over-depth violation on the Ast
|
||||
// to be surfaced when it is later checked or planned.
|
||||
out.loadErr = checkLoadedASTDepth(out.impl)
|
||||
return out
|
||||
}
|
||||
|
||||
// checkLoadedASTDepth guards ASTs that enter through the proto conversion helpers
|
||||
// (ParsedExprToAst / CheckedExprToAst) against nesting deeper than the parser's recursion limit.
|
||||
// Those entry points bypass the parser, so without this check a deeply nested loaded AST could
|
||||
// exhaust the Go stack during later checking or planning. It returns a normal error rather than
|
||||
// risking that overflow; the traversal itself is bounded so it stays safe on the same input.
|
||||
//
|
||||
// Embedders that fully control their AST inputs can skip this by building the AST through the
|
||||
// common/ast package directly instead of these conversion helpers.
|
||||
func checkLoadedASTDepth(a *ast.AST) error {
|
||||
if ast.ExceedsDepth(a, defaultMaxASTDepth) {
|
||||
return fmt.Errorf("input exceeds maximum expression nesting depth: %d", defaultMaxASTDepth)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AstToParsedExpr converts an Ast to an protobuf ParsedExpr value.
|
||||
|
||||
+41
-27
@@ -182,7 +182,6 @@ func (lib *stdLibrary) CompileOptions() []EnvOption {
|
||||
if err = lib.subset.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.variables = append(e.variables, stdlib.Types()...)
|
||||
for _, fn := range funcs {
|
||||
existing, found := e.functions[fn.Name()]
|
||||
if found {
|
||||
@@ -591,7 +590,7 @@ func (lib *optionalLib) CompileOptions() []EnvOption {
|
||||
// ProgramOptions implements the Library interface method.
|
||||
func (lib *optionalLib) ProgramOptions() []ProgramOption {
|
||||
return []ProgramOption{
|
||||
CustomDecorator(decorateOptionalOr),
|
||||
CustomDecoratorV2(decorateOptionalOr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,7 +683,7 @@ func EnableErrorOnBadPresenceTest(value bool) EnvOption {
|
||||
return features(featureEnableErrorOnBadPresenceTest, value)
|
||||
}
|
||||
|
||||
func decorateOptionalOr(i interpreter.Interpretable) (interpreter.Interpretable, error) {
|
||||
func decorateOptionalOr(i interpreter.InterpretableV2) (interpreter.InterpretableV2, error) {
|
||||
call, ok := i.(interpreter.InterpretableCall)
|
||||
if !ok {
|
||||
return i, nil
|
||||
@@ -721,8 +720,8 @@ func decorateOptionalOr(i interpreter.Interpretable) (interpreter.Interpretable,
|
||||
// the second optional expression is evaluated and returned.
|
||||
type evalOptionalOr struct {
|
||||
id int64
|
||||
lhs interpreter.Interpretable
|
||||
rhs interpreter.Interpretable
|
||||
lhs interpreter.InterpretableV2
|
||||
rhs interpreter.InterpretableV2
|
||||
}
|
||||
|
||||
// ID implements the Interpretable interface method.
|
||||
@@ -730,27 +729,34 @@ func (opt *evalOptionalOr) ID() int64 {
|
||||
return opt.id
|
||||
}
|
||||
|
||||
func (opt *evalOptionalOr) Exec(frame *interpreter.ExecutionFrame) ref.Val {
|
||||
// short-circuit lhs.
|
||||
optLHS := opt.lhs.Exec(frame)
|
||||
switch val := optLHS.(type) {
|
||||
case *types.Err, *types.Unknown:
|
||||
return optLHS
|
||||
case *types.Optional:
|
||||
if val.HasValue() {
|
||||
return optLHS
|
||||
}
|
||||
return opt.rhs.Exec(frame)
|
||||
default:
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Eval evaluates the left-hand side optional to determine whether it contains a value, else
|
||||
// proceeds with the right-hand side evaluation.
|
||||
func (opt *evalOptionalOr) Eval(ctx interpreter.Activation) ref.Val {
|
||||
// short-circuit lhs.
|
||||
optLHS := opt.lhs.Eval(ctx)
|
||||
optVal, ok := optLHS.(*types.Optional)
|
||||
if !ok {
|
||||
return optLHS
|
||||
}
|
||||
if optVal.HasValue() {
|
||||
return optVal
|
||||
}
|
||||
return opt.rhs.Eval(ctx)
|
||||
return opt.Exec(interpreter.AsFrame(ctx))
|
||||
}
|
||||
|
||||
// evalOptionalOrValue selects between an optional or a concrete value. If the optional has a value,
|
||||
// its value is returned, otherwise the alternative value expression is evaluated and returned.
|
||||
type evalOptionalOrValue struct {
|
||||
id int64
|
||||
lhs interpreter.Interpretable
|
||||
rhs interpreter.Interpretable
|
||||
lhs interpreter.InterpretableV2
|
||||
rhs interpreter.InterpretableV2
|
||||
}
|
||||
|
||||
// ID implements the Interpretable interface method.
|
||||
@@ -758,19 +764,27 @@ func (opt *evalOptionalOrValue) ID() int64 {
|
||||
return opt.id
|
||||
}
|
||||
|
||||
func (opt *evalOptionalOrValue) Exec(frame *interpreter.ExecutionFrame) ref.Val {
|
||||
// short-circuit lhs.
|
||||
optLHS := opt.lhs.Exec(frame)
|
||||
|
||||
switch val := optLHS.(type) {
|
||||
case *types.Err, *types.Unknown:
|
||||
return optLHS
|
||||
case *types.Optional:
|
||||
if val.HasValue() {
|
||||
return val.GetValue()
|
||||
}
|
||||
return opt.rhs.Exec(frame)
|
||||
default:
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Eval evaluates the left-hand side optional to determine whether it contains a value, else
|
||||
// proceeds with the right-hand side evaluation.
|
||||
func (opt *evalOptionalOrValue) Eval(ctx interpreter.Activation) ref.Val {
|
||||
// short-circuit lhs.
|
||||
optLHS := opt.lhs.Eval(ctx)
|
||||
optVal, ok := optLHS.(*types.Optional)
|
||||
if !ok {
|
||||
return optLHS
|
||||
}
|
||||
if optVal.HasValue() {
|
||||
return optVal.GetValue()
|
||||
}
|
||||
return opt.rhs.Eval(ctx)
|
||||
return opt.Exec(interpreter.AsFrame(ctx))
|
||||
}
|
||||
|
||||
type timeLegacyLibrary struct{}
|
||||
|
||||
+46
-8
@@ -15,6 +15,7 @@
|
||||
package cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/google/cel-go/common"
|
||||
@@ -29,17 +30,43 @@ import (
|
||||
// passes to ensure that the final optimized output is a valid expression with metadata consistent
|
||||
// with what would have been generated from a parsed and checked expression.
|
||||
//
|
||||
// Note: source position information is best-effort and likely wrong, but optimized expressions
|
||||
// Note: source position information is best-effort and incomplete, but optimized expressions
|
||||
// should be suitable for calls to parser.Unparse.
|
||||
type StaticOptimizer struct {
|
||||
optimizers []ASTOptimizer
|
||||
// If set, Optimize() will use this Source instead of the one from the AST.
|
||||
sourceOverride *Source
|
||||
}
|
||||
|
||||
type OptimizerOption func(*StaticOptimizer) (*StaticOptimizer, error)
|
||||
|
||||
// NewStaticOptimizer creates a StaticOptimizer with a sequence of ASTOptimizer's to be applied
|
||||
// to a checked expression.
|
||||
func NewStaticOptimizer(optimizers ...ASTOptimizer) *StaticOptimizer {
|
||||
return &StaticOptimizer{
|
||||
optimizers: optimizers,
|
||||
func NewStaticOptimizer(options ...any) (*StaticOptimizer, error) {
|
||||
so := &StaticOptimizer{}
|
||||
var err error
|
||||
for _, opt := range options {
|
||||
switch v := opt.(type) {
|
||||
case ASTOptimizer:
|
||||
so.optimizers = append(so.optimizers, v)
|
||||
case OptimizerOption:
|
||||
so, err = v(so)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported option: %v", v)
|
||||
}
|
||||
}
|
||||
return so, nil
|
||||
}
|
||||
|
||||
// OptimizeWithSource overrides the source used by the optimizer.
|
||||
// Note this will cause the source info from the AST passed to Optimize() to be discarded.
|
||||
func OptimizeWithSource(source Source) OptimizerOption {
|
||||
return func(so *StaticOptimizer) (*StaticOptimizer, error) {
|
||||
so.sourceOverride = &source
|
||||
return so, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +76,21 @@ func NewStaticOptimizer(optimizers ...ASTOptimizer) *StaticOptimizer {
|
||||
func (opt *StaticOptimizer) Optimize(env *Env, a *Ast) (*Ast, *Issues) {
|
||||
// Make a copy of the AST to be optimized.
|
||||
optimized := ast.Copy(a.NativeRep())
|
||||
source := a.Source()
|
||||
sourceInfo := optimized.SourceInfo()
|
||||
if opt.sourceOverride != nil {
|
||||
source = *opt.sourceOverride
|
||||
sourceInfo = ast.NewSourceInfo(*opt.sourceOverride)
|
||||
}
|
||||
ids := newIDGenerator(ast.MaxID(a.NativeRep()))
|
||||
|
||||
// Create the optimizer context, could be pooled in the future.
|
||||
issues := NewIssues(common.NewErrors(a.Source()))
|
||||
issues := NewIssues(common.NewErrors(source))
|
||||
baseFac := ast.NewExprFactory()
|
||||
exprFac := &optimizerExprFactory{
|
||||
idGenerator: ids,
|
||||
fac: baseFac,
|
||||
sourceInfo: optimized.SourceInfo(),
|
||||
sourceInfo: sourceInfo,
|
||||
}
|
||||
ctx := &OptimizerContext{
|
||||
optimizerExprFactory: exprFac,
|
||||
@@ -80,7 +113,7 @@ func (opt *StaticOptimizer) Optimize(env *Env, a *Ast) (*Ast, *Issues) {
|
||||
|
||||
// Recheck the updated expression for any possible type-agreement or validation errors.
|
||||
parsed := &Ast{
|
||||
source: a.Source(),
|
||||
source: source,
|
||||
impl: ast.NewAST(expr, info)}
|
||||
checked, iss := ctx.Check(parsed)
|
||||
if iss.Err() != nil {
|
||||
@@ -91,7 +124,7 @@ func (opt *StaticOptimizer) Optimize(env *Env, a *Ast) (*Ast, *Issues) {
|
||||
|
||||
// Return the optimized result.
|
||||
return &Ast{
|
||||
source: a.Source(),
|
||||
source: source,
|
||||
impl: optimized,
|
||||
}, nil
|
||||
}
|
||||
@@ -100,6 +133,8 @@ func (opt *StaticOptimizer) Optimize(env *Env, a *Ast) (*Ast, *Issues) {
|
||||
// that the ids within the expression correspond to the ids within macros.
|
||||
func normalizeIDs(idGen ast.IDGenerator, optimized ast.Expr, info *ast.SourceInfo) {
|
||||
optimized.RenumberIDs(idGen)
|
||||
info.RenumberIDs(idGen)
|
||||
|
||||
if len(info.MacroCalls()) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -260,6 +295,9 @@ func (opt *optimizerExprFactory) CopyASTAndMetadata(a *ast.AST) ast.Expr {
|
||||
for macroID, call := range copyInfo.MacroCalls() {
|
||||
opt.SetMacroCall(macroID, call)
|
||||
}
|
||||
for id, offset := range copyInfo.OffsetRanges() {
|
||||
opt.sourceInfo.SetOffsetRange(id, offset)
|
||||
}
|
||||
return copyExpr
|
||||
}
|
||||
|
||||
|
||||
+187
-30
@@ -24,6 +24,7 @@ import (
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
"google.golang.org/protobuf/types/dynamicpb"
|
||||
|
||||
"github.com/google/cel-go/cel/async"
|
||||
"github.com/google/cel-go/checker"
|
||||
"github.com/google/cel-go/common/containers"
|
||||
"github.com/google/cel-go/common/decls"
|
||||
@@ -71,12 +72,16 @@ const (
|
||||
|
||||
// Enable escape syntax for field identifiers (`).
|
||||
featureIdentEscapeSyntax
|
||||
|
||||
// Enable accessing fields by JSON names within protobuf messages
|
||||
featureJSONFieldNames
|
||||
)
|
||||
|
||||
var featureIDsToNames = map[int]string{
|
||||
featureEnableMacroCallTracking: "cel.feature.macro_call_tracking",
|
||||
featureCrossTypeNumericComparisons: "cel.feature.cross_type_numeric_comparisons",
|
||||
featureIdentEscapeSyntax: "cel.feature.backtick_escape_syntax",
|
||||
featureJSONFieldNames: "cel.feature.json_field_names",
|
||||
}
|
||||
|
||||
func featureNameByID(id int) (string, bool) {
|
||||
@@ -93,6 +98,51 @@ func featureIDByName(name string) (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// limitID is used as a key for configurable limits. These are options that
|
||||
// support exporting to YAML environment config.
|
||||
type limitID int
|
||||
|
||||
const (
|
||||
_ = limitID(iota)
|
||||
// The number of recursive calls permitted in parsing.
|
||||
limitParseRecursionDepth
|
||||
// The number of code points permitted in an input expression string.
|
||||
limitCodePointSize
|
||||
// The number of attempts to recover from a parse error.
|
||||
limitParseErrorRecovery
|
||||
// The maximum nesting depth permitted for ASTs loaded outside the parser.
|
||||
limitMaxASTDepth
|
||||
// The maximum number of expression nodes permitted in parsing (including macro expansion).
|
||||
limitExpressionNodeCount
|
||||
)
|
||||
|
||||
// defaultMaxASTDepth mirrors the parser's default maxRecursionDepth (250) and
|
||||
// is applied to ASTs that enter through non-parser ingestion paths (e.g. via
|
||||
// ParsedExprToAst / CheckedExprToAst) when no explicit limit is configured.
|
||||
const defaultMaxASTDepth = 250
|
||||
|
||||
var limitIDsToNames = map[limitID]string{
|
||||
limitCodePointSize: "cel.limit.expression_code_points",
|
||||
limitParseErrorRecovery: "cel.limit.parse_error_recovery",
|
||||
limitParseRecursionDepth: "cel.limit.parse_recursion_depth",
|
||||
limitMaxASTDepth: "cel.limit.max_ast_depth",
|
||||
limitExpressionNodeCount: "cel.limit.expression_node_count",
|
||||
}
|
||||
|
||||
func limitNameByID(id limitID) (string, bool) {
|
||||
v, ok := limitIDsToNames[id]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func limitIDByName(name string) (limitID, bool) {
|
||||
for k, v := range limitIDsToNames {
|
||||
if v == name {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
return limitID(0), false
|
||||
}
|
||||
|
||||
// EnvOption is a functional interface for configuring the environment.
|
||||
type EnvOption func(e *Env) (*Env, error)
|
||||
|
||||
@@ -275,9 +325,9 @@ func Abbrevs(qualifiedNames ...string) EnvOption {
|
||||
}
|
||||
}
|
||||
|
||||
// customTypeRegistry is an internal-only interface containing the minimum methods required to support
|
||||
// protoTypeRegistry is an internal-only interface containing the minimum methods required to support
|
||||
// custom types. It is a subset of methods from ref.TypeRegistry.
|
||||
type customTypeRegistry interface {
|
||||
type protoTypeRegistry interface {
|
||||
RegisterDescriptor(protoreflect.FileDescriptor) error
|
||||
RegisterType(...ref.Type) error
|
||||
}
|
||||
@@ -294,7 +344,7 @@ type customTypeRegistry interface {
|
||||
// Note: This option must be specified after the CustomTypeProvider option when used together.
|
||||
func Types(addTypes ...any) EnvOption {
|
||||
return func(e *Env) (*Env, error) {
|
||||
reg, isReg := e.provider.(customTypeRegistry)
|
||||
reg, isReg := e.provider.(protoTypeRegistry)
|
||||
if !isReg {
|
||||
return nil, fmt.Errorf("custom types not supported by provider: %T", e.provider)
|
||||
}
|
||||
@@ -331,7 +381,7 @@ func Types(addTypes ...any) EnvOption {
|
||||
// extension or by re-using the same EnvOption with another NewEnv() call.
|
||||
func TypeDescs(descs ...any) EnvOption {
|
||||
return func(e *Env) (*Env, error) {
|
||||
reg, isReg := e.provider.(customTypeRegistry)
|
||||
reg, isReg := e.provider.(protoTypeRegistry)
|
||||
if !isReg {
|
||||
return nil, fmt.Errorf("custom types not supported by provider: %T", e.provider)
|
||||
}
|
||||
@@ -379,7 +429,7 @@ func TypeDescs(descs ...any) EnvOption {
|
||||
}
|
||||
}
|
||||
|
||||
func registerFileSet(reg customTypeRegistry, fileSet *descpb.FileDescriptorSet) error {
|
||||
func registerFileSet(reg protoTypeRegistry, fileSet *descpb.FileDescriptorSet) error {
|
||||
files, err := protodesc.NewFiles(fileSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("protodesc.NewFiles(%v) failed: %v", fileSet, err)
|
||||
@@ -387,7 +437,7 @@ func registerFileSet(reg customTypeRegistry, fileSet *descpb.FileDescriptorSet)
|
||||
return registerFiles(reg, files)
|
||||
}
|
||||
|
||||
func registerFiles(reg customTypeRegistry, files *protoregistry.Files) error {
|
||||
func registerFiles(reg protoTypeRegistry, files *protoregistry.Files) error {
|
||||
var err error
|
||||
files.RangeFiles(func(fd protoreflect.FileDescriptor) bool {
|
||||
err = reg.RegisterDescriptor(fd)
|
||||
@@ -396,6 +446,15 @@ func registerFiles(reg customTypeRegistry, files *protoregistry.Files) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// JSONFieldNames supports accessing protocol buffer fields by json-name.
|
||||
//
|
||||
// Enabling JSON field name support will create a copy of the types.Registry with fields indexed
|
||||
// by JSON name, and whether JSON name or Proto-style names are supported will be inferred from
|
||||
// the AST extensions metadata.
|
||||
func JSONFieldNames(enabled bool) EnvOption {
|
||||
return features(featureJSONFieldNames, enabled)
|
||||
}
|
||||
|
||||
// ProgramOption is a functional interface for configuring evaluation bindings and behaviors.
|
||||
type ProgramOption func(p *prog) (*prog, error)
|
||||
|
||||
@@ -409,6 +468,14 @@ func CustomDecorator(dec interpreter.InterpretableDecorator) ProgramOption {
|
||||
}
|
||||
}
|
||||
|
||||
// CustomDecoratorV2 appends an InterpreterDecoratorV2 to the program.
|
||||
func CustomDecoratorV2(dec interpreter.InterpretableDecoratorV2) ProgramOption {
|
||||
return func(p *prog) (*prog, error) {
|
||||
p.plannerOptions = append(p.plannerOptions, interpreter.CustomDecoratorV2(dec))
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Functions adds function overloads that extend or override the set of CEL built-ins.
|
||||
//
|
||||
// Deprecated: use Function() instead to declare the function, its overload signatures,
|
||||
@@ -523,6 +590,17 @@ func configToEnvOptions(config *env.Config, provider types.Provider, optFactorie
|
||||
envOpts = append(envOpts, Abbrevs(imp.Name))
|
||||
}
|
||||
|
||||
// Configure features and common limits.
|
||||
for _, feat := range config.Features {
|
||||
// Note, if a feature is not found, it is skipped as it is possible the feature
|
||||
// is not intended to be supported publicly. In the future, a refinement of
|
||||
// to this strategy to report unrecognized features and validators should probably
|
||||
// be covered as a standard ConfigOptionFactory
|
||||
if id, found := featureIDByName(feat.Name); found {
|
||||
envOpts = append(envOpts, features(id, feat.Enabled))
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the context variable declaration
|
||||
if config.ContextVariable != nil {
|
||||
typeName := config.ContextVariable.TypeName
|
||||
@@ -564,14 +642,9 @@ func configToEnvOptions(config *env.Config, provider types.Provider, optFactorie
|
||||
envOpts = append(envOpts, FunctionDecls(funcs...))
|
||||
}
|
||||
|
||||
// Configure features
|
||||
for _, feat := range config.Features {
|
||||
// Note, if a feature is not found, it is skipped as it is possible the feature
|
||||
// is not intended to be supported publicly. In the future, a refinement of
|
||||
// to this strategy to report unrecognized features and validators should probably
|
||||
// be covered as a standard ConfigOptionFactory
|
||||
if id, found := featureIDByName(feat.Name); found {
|
||||
envOpts = append(envOpts, features(id, feat.Enabled))
|
||||
for _, limit := range config.Limits {
|
||||
if id, found := limitIDByName(limit.Name); found {
|
||||
envOpts = append(envOpts, setLimit(id, limit.Value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,6 +747,47 @@ func InterruptCheckFrequency(checkFrequency uint) ProgramOption {
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncCallObserver sets the observer for monitoring asynchronous function calls during ConcurrentEval.
|
||||
func AsyncCallObserver(observer async.Observer) ProgramOption {
|
||||
return func(p *prog) (*prog, error) {
|
||||
p.asyncObserver = observer
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncCompletionBufferSize sets the size of the buffer for the async completion channel.
|
||||
// By default, the channel is unbuffered.
|
||||
func AsyncCompletionBufferSize(size int) ProgramOption {
|
||||
return func(p *prog) (*prog, error) {
|
||||
p.asyncCompletionBufferSize = size
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncMaxConcurrency sets the maximum number of concurrently launched async calls during
|
||||
// ConcurrentEval. This bounds the number of in-flight async goroutines, so a wide fan-out (such
|
||||
// as an async call inside a comprehension over a large list) cannot exhaust memory.
|
||||
//
|
||||
// A value of 0 (unset) applies a built-in default bound. A positive value sets an explicit bound.
|
||||
// A negative value disables the limiter (unbounded launches) and should only be used when
|
||||
// concurrency is bounded by other means.
|
||||
func AsyncMaxConcurrency(maxConcurrency int) ProgramOption {
|
||||
return func(p *prog) (*prog, error) {
|
||||
p.asyncMaxConcurrency = maxConcurrency
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ConcurrentDrainStrategy configures the strategy for when to re-evaluate the program
|
||||
// during a ConcurrentEval call after receiving asynchronous completion signals.
|
||||
// By default, the program re-evaluates immediately after every completion.
|
||||
func ConcurrentDrainStrategy(strategy async.DrainStrategy) ProgramOption {
|
||||
return func(p *prog) (*prog, error) {
|
||||
p.drainStrategy = strategy
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CostEstimatorOptions configure type-check time options for estimating expression cost.
|
||||
func CostEstimatorOptions(costOpts ...checker.CostOption) EnvOption {
|
||||
return func(e *Env) (*Env, error) {
|
||||
@@ -727,8 +841,11 @@ func fieldToCELType(field protoreflect.FieldDescriptor) (*Type, error) {
|
||||
return nil, fmt.Errorf("field %s type %s not implemented", field.FullName(), field.Kind().String())
|
||||
}
|
||||
|
||||
func fieldToVariable(field protoreflect.FieldDescriptor) (*decls.VariableDecl, error) {
|
||||
func fieldToVariable(field protoreflect.FieldDescriptor, jsonFieldNames bool) (*decls.VariableDecl, error) {
|
||||
name := string(field.Name())
|
||||
if jsonFieldNames {
|
||||
name = field.JSONName()
|
||||
}
|
||||
if field.IsMap() {
|
||||
mapKey := field.MapKey()
|
||||
mapValue := field.MapValue()
|
||||
@@ -759,6 +876,8 @@ func fieldToVariable(field protoreflect.FieldDescriptor) (*decls.VariableDecl, e
|
||||
// DeclareContextProto returns an option to extend CEL environment with declarations from the given context proto.
|
||||
// Each field of the proto defines a variable of the same name in the environment.
|
||||
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#evaluation-environment
|
||||
//
|
||||
// If using JSONFieldNames(), ensure that the option is set before DeclareContextProto is provided.
|
||||
func DeclareContextProto(descriptor protoreflect.MessageDescriptor) EnvOption {
|
||||
return func(e *Env) (*Env, error) {
|
||||
if e.contextProto != nil {
|
||||
@@ -768,9 +887,10 @@ func DeclareContextProto(descriptor protoreflect.MessageDescriptor) EnvOption {
|
||||
e.contextProto = descriptor
|
||||
fields := descriptor.Fields()
|
||||
vars := make([]*decls.VariableDecl, 0, fields.Len())
|
||||
jsonFieldNames := e.HasFeature(featureJSONFieldNames)
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
field := fields.Get(i)
|
||||
variable, err := fieldToVariable(field)
|
||||
variable, err := fieldToVariable(field, jsonFieldNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -789,11 +909,15 @@ func DeclareContextProto(descriptor protoreflect.MessageDescriptor) EnvOption {
|
||||
//
|
||||
// Consider using with `DeclareContextProto` to simplify variable type declarations and publishing when using
|
||||
// protocol buffers.
|
||||
func ContextProtoVars(ctx proto.Message) (Activation, error) {
|
||||
//
|
||||
// Use the types.JSONFieldNames(true) option to populate the context proto vars using the JSON field names.
|
||||
func ContextProtoVars(ctx proto.Message, opts ...types.RegistryOption) (Activation, error) {
|
||||
if ctx == nil || !ctx.ProtoReflect().IsValid() {
|
||||
return interpreter.EmptyActivation(), nil
|
||||
}
|
||||
reg, err := types.NewRegistry(ctx)
|
||||
regOpts := []types.RegistryOption{types.ProtoTypeDefs(ctx)}
|
||||
regOpts = append(regOpts, opts...)
|
||||
reg, err := types.NewProtoRegistry(regOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -803,15 +927,19 @@ func ContextProtoVars(ctx proto.Message) (Activation, error) {
|
||||
vars := make(map[string]any, fields.Len())
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
field := fields.Get(i)
|
||||
sft, found := reg.FindStructFieldType(typeName, field.TextName())
|
||||
fieldName := field.TextName()
|
||||
if reg.JSONFieldNames() {
|
||||
fieldName = field.JSONName()
|
||||
}
|
||||
sft, found := reg.FindStructFieldType(typeName, fieldName)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("no such field: %s", field.TextName())
|
||||
return nil, fmt.Errorf("no such field: %s", fieldName)
|
||||
}
|
||||
fieldVal, err := sft.GetFrom(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vars[field.TextName()] = fieldVal
|
||||
vars[fieldName] = fieldVal
|
||||
}
|
||||
return NewActivation(vars)
|
||||
}
|
||||
@@ -847,22 +975,51 @@ func features(flag int, enabled bool) EnvOption {
|
||||
}
|
||||
}
|
||||
|
||||
// ParserRecursionLimit adjusts the AST depth the parser will tolerate.
|
||||
// Defaults defined in the parser package.
|
||||
func ParserRecursionLimit(limit int) EnvOption {
|
||||
func setLimit(id limitID, limit int) EnvOption {
|
||||
if limit < 0 {
|
||||
limit = -1
|
||||
}
|
||||
return func(e *Env) (*Env, error) {
|
||||
e.prsrOpts = append(e.prsrOpts, parser.MaxRecursionDepth(limit))
|
||||
e.limits[id] = limit
|
||||
return e, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ParserExpressionSizeLimit adjusts the number of code points the expression parser is allowed to parse.
|
||||
// ParserRecursionLimit adjusts the AST depth the parser will tolerate.
|
||||
// Defaults defined in the parser package.
|
||||
func ParserRecursionLimit(limit int) EnvOption {
|
||||
return setLimit(limitParseRecursionDepth, limit)
|
||||
}
|
||||
|
||||
// ParserErrorRecoveryLimit sets the number of attemtps the parser will take
|
||||
// to recover after encountering an error.
|
||||
func ParserErrorRecoveryLimit(limit int) EnvOption {
|
||||
return setLimit(limitParseErrorRecovery, limit)
|
||||
}
|
||||
|
||||
// ParserExpressionSizeLimit adjusts the number of code points the expression parser is allowed to parse.
|
||||
// Defaults are defined in the parser package. A negative value means unbounded.
|
||||
func ParserExpressionSizeLimit(limit int) EnvOption {
|
||||
return func(e *Env) (*Env, error) {
|
||||
e.prsrOpts = append(e.prsrOpts, parser.ExpressionSizeCodePointLimit(limit))
|
||||
return e, nil
|
||||
}
|
||||
return setLimit(limitCodePointSize, limit)
|
||||
}
|
||||
|
||||
// ExpressionNodeLimit adjusts the maximum number of expression nodes permitted during parsing
|
||||
// and checking, including nodes created by macro expansion. Defaults are defined in the parser
|
||||
// package (100,000). A negative value means unbounded.
|
||||
func ExpressionNodeLimit(limit int) EnvOption {
|
||||
return setLimit(limitExpressionNodeCount, limit)
|
||||
}
|
||||
|
||||
// ExpressionNestingDepthLimit records the maximum nesting depth permitted for ASTs in the
|
||||
// environment configuration so that the value round-trips through env.Config export/import.
|
||||
//
|
||||
// ASTs loaded outside the parser (e.g. via ParsedExprToAst / CheckedExprToAst) bypass the
|
||||
// parser's recursion limit, so those conversion paths validate nesting depth against the
|
||||
// parser-matching default (250) to avoid a Go stack overflow during later checking or planning.
|
||||
// Embedders that fully control their AST inputs and want to skip the check can construct the AST
|
||||
// through the common/ast package directly rather than the cel conversion helpers.
|
||||
func ExpressionNestingDepthLimit(limit int) EnvOption {
|
||||
return setLimit(limitMaxASTDepth, limit)
|
||||
}
|
||||
|
||||
// EnableHiddenAccumulatorName sets the parser to use the identifier '@result' for accumulators
|
||||
|
||||
+304
-172
@@ -16,10 +16,13 @@ package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/cel/async"
|
||||
"github.com/google/cel-go/common/ast"
|
||||
"github.com/google/cel-go/common/functions"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
@@ -51,6 +54,21 @@ type Program interface {
|
||||
//
|
||||
// The output contract for `ContextEval` is otherwise identical to the `Eval` method.
|
||||
ContextEval(context.Context, any) (ref.Val, *EvalDetails, error)
|
||||
|
||||
// ConcurrentEval evaluates the program concurrently, returning a channel that will receive
|
||||
// the final EvalResult when all asynchronous operations complete, or the context expires.
|
||||
//
|
||||
// The vars value may either be an `Activation` or `map[string]any`.
|
||||
//
|
||||
// Liveness: ConcurrentEval relies on context cancellation to terminate. If an async function
|
||||
// never returns and does not honor its context, and the supplied context has no deadline, the
|
||||
// call will block indefinitely. Always pass a context with a deadline or cancellation.
|
||||
//
|
||||
// Error handling is fail-fast: as soon as a re-evaluation pass yields an error, that error is
|
||||
// returned and any still in-flight async calls are cancelled (their contexts are done) and
|
||||
// their results discarded. Async functions should therefore be free of unwanted side effects
|
||||
// on partial evaluation, or guard them with idempotency/cancellation handling.
|
||||
ConcurrentEval(context.Context, any) <-chan EvalResult
|
||||
}
|
||||
|
||||
// Activation used to resolve identifiers by name and references by id.
|
||||
@@ -143,6 +161,13 @@ func (ed *EvalDetails) ActualCost() *uint64 {
|
||||
return &cost
|
||||
}
|
||||
|
||||
// EvalResult encapsulates the response from a ConcurrentEval call.
|
||||
type EvalResult struct {
|
||||
Val ref.Val
|
||||
EvalDetails *EvalDetails
|
||||
Err error
|
||||
}
|
||||
|
||||
// prog is the internal implementation of the Program interface.
|
||||
type prog struct {
|
||||
*Env
|
||||
@@ -158,11 +183,21 @@ type prog struct {
|
||||
regexOptimizations []*interpreter.RegexOptimization
|
||||
|
||||
// Interpretable configured from an Ast and aggregate decorator set based on program options.
|
||||
interpretable interpreter.Interpretable
|
||||
interpretable interpreter.InterpretableV2
|
||||
observable *interpreter.ObservableInterpretable
|
||||
callCostEstimator interpreter.ActualCostEstimator
|
||||
costOptions []interpreter.CostTrackerOption
|
||||
costLimit *uint64
|
||||
|
||||
// hasAsync indicates the planned expression contains an asynchronous function call, which can
|
||||
// only be resolved by ConcurrentEval.
|
||||
hasAsync bool
|
||||
|
||||
// Async evaluation configuration used by ConcurrentEval.
|
||||
drainStrategy async.DrainStrategy
|
||||
asyncObserver async.Observer
|
||||
asyncCompletionBufferSize int
|
||||
asyncMaxConcurrency int
|
||||
}
|
||||
|
||||
// newProgram creates a program instance with an environment, an ast, and an optional list of
|
||||
@@ -180,6 +215,7 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) {
|
||||
plannerOptions: []interpreter.PlannerOption{},
|
||||
dispatcher: disp,
|
||||
costOptions: []interpreter.CostTrackerOption{},
|
||||
drainStrategy: async.DrainReady(100 * time.Microsecond),
|
||||
}
|
||||
|
||||
// Configure the program via the ProgramOption values.
|
||||
@@ -191,15 +227,35 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the function bindings created via Function() options.
|
||||
for _, fn := range e.functions {
|
||||
bindings, err := fn.Bindings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
e.funcBindOnce.Do(func() {
|
||||
var bindings []*functions.Overload
|
||||
e.functionBindings = []*functions.Overload{}
|
||||
for _, fn := range e.functions {
|
||||
bindings, err = fn.Bindings()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
e.functionBindings = append(e.functionBindings, bindings...)
|
||||
}
|
||||
err = disp.Add(bindings...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the function bindings created via Function() options.
|
||||
err = disp.Add(e.functionBindings...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine whether the environment declares any asynchronous function. Async is a property of
|
||||
// the binding, so its presence is known from the environment alone, without inspecting the
|
||||
// program plan. The synchronous entry points (Eval, ContextEval) reject programs from an env
|
||||
// with async functions; callers needing synchronous evaluation should use a non-async env.
|
||||
for _, b := range e.functionBindings {
|
||||
if b.Async != nil {
|
||||
p.hasAsync = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +264,12 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) {
|
||||
attrFactorOpts := []interpreter.AttrFactoryOption{
|
||||
interpreter.EnableErrorOnBadPresenceTest(p.HasFeature(featureEnableErrorOnBadPresenceTest)),
|
||||
}
|
||||
if a.SourceInfo().HasExtension("json_name", ast.NewExtensionVersion(1, 1)) {
|
||||
if !e.HasFeature(featureJSONFieldNames) {
|
||||
return nil, errors.New("the AST extension 'json_name' requires the option cel.JSONFieldNames(true)")
|
||||
}
|
||||
}
|
||||
// Configure the type provider, considering whether the AST indicates whether it supports JSON field names
|
||||
if p.evalOpts&OptPartialEval == OptPartialEval {
|
||||
attrFactory = interpreter.NewPartialAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...)
|
||||
} else {
|
||||
@@ -245,8 +307,16 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) {
|
||||
if p.costLimit != nil {
|
||||
costOpts = append(costOpts, interpreter.CostTrackerLimit(*p.costLimit))
|
||||
}
|
||||
// Creating a new cost tracker for each evaluation causes significant work that
|
||||
// needs to be repeated for each evaluation even though the cost tracker is
|
||||
// mostly read-only once constructed. Therefore it gets constructed
|
||||
// once now and later a cheap clone is used for each evaluation.
|
||||
tracker, err := interpreter.NewCostTracker(p.callCostEstimator, costOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("construct cost tracker: %w", err)
|
||||
}
|
||||
trackerFactory := func() (*interpreter.CostTracker, error) {
|
||||
return interpreter.NewCostTracker(p.callCostEstimator, costOpts...)
|
||||
return tracker.Clone()
|
||||
}
|
||||
var observers []interpreter.PlannerOption
|
||||
if p.evalOpts&(OptExhaustiveEval|OptTrackState) != 0 {
|
||||
@@ -295,23 +365,25 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Build a hierarchical activation if there are default vars set.
|
||||
var vars Activation
|
||||
switch v := input.(type) {
|
||||
case Activation:
|
||||
vars = v
|
||||
case map[string]any:
|
||||
vars = activationPool.Setup(v)
|
||||
defer activationPool.Put(vars)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input)
|
||||
// Asynchronous calls cannot be resolved by a single-pass evaluation. Reject before doing any
|
||||
// work (this also covers ContextEval, which delegates here); ConcurrentEval does not call Eval.
|
||||
if p.hasAsync {
|
||||
return nil, nil, errAsyncRequiresConcurrentEval
|
||||
}
|
||||
if p.defaultVars != nil {
|
||||
vars = interpreter.NewHierarchicalActivation(p.defaultVars, vars)
|
||||
// Build a hierarchical activation if there are default vars set.
|
||||
var frame *interpreter.ExecutionFrame
|
||||
if f, ok := input.(*interpreter.ExecutionFrame); ok {
|
||||
frame = f
|
||||
} else {
|
||||
frame, err = p.newExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer frame.Close()
|
||||
}
|
||||
if p.observable != nil {
|
||||
det = &EvalDetails{}
|
||||
out = p.observable.ObserveEval(vars, func(observed any) {
|
||||
out = p.observable.ObserveExec(frame, func(observed any) {
|
||||
switch o := observed.(type) {
|
||||
case interpreter.EvalState:
|
||||
det.state = o
|
||||
@@ -320,7 +392,7 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
out = p.interpretable.Eval(vars)
|
||||
out = p.interpretable.Exec(frame)
|
||||
}
|
||||
// The output of an internal Eval may have a value (`v`) that is a types.Err. This step
|
||||
// translates the CEL value to a Go error response. This interface does not quite match the
|
||||
@@ -336,160 +408,220 @@ func (p *prog) ContextEval(ctx context.Context, input any) (ref.Val, *EvalDetail
|
||||
if ctx == nil {
|
||||
return nil, nil, fmt.Errorf("context can not be nil")
|
||||
}
|
||||
// Configure the input, making sure to wrap Activation inputs in the special ctxActivation which
|
||||
// exposes the #interrupted variable and manages rate-limited checks of the ctx.Done() state.
|
||||
var vars Activation
|
||||
switch v := input.(type) {
|
||||
case Activation:
|
||||
vars = ctxActivationPool.Setup(v, ctx.Done(), p.interruptCheckFrequency)
|
||||
defer ctxActivationPool.Put(vars)
|
||||
case map[string]any:
|
||||
rawVars := activationPool.Setup(v)
|
||||
defer activationPool.Put(rawVars)
|
||||
vars = ctxActivationPool.Setup(rawVars, ctx.Done(), p.interruptCheckFrequency)
|
||||
defer ctxActivationPool.Put(vars)
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid input, wanted Activation or map[string]any, got: (%T)%v", input, input)
|
||||
frame, err := p.newExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return p.Eval(vars)
|
||||
defer frame.Close()
|
||||
frame.SetContext(ctx, p.interruptCheckFrequency)
|
||||
out, det, errEval := p.Eval(frame)
|
||||
if errEval != nil && errors.Is(errEval, interpreter.InterruptError{}) {
|
||||
return out, det, fmt.Errorf("%w: %w", errEval, context.Cause(ctx))
|
||||
}
|
||||
return out, det, errEval
|
||||
}
|
||||
|
||||
type ctxEvalActivation struct {
|
||||
parent Activation
|
||||
interrupt <-chan struct{}
|
||||
interruptCheckCount uint
|
||||
interruptCheckFrequency uint
|
||||
// newExecutionFrame creates an ExecutionFrame for the given input without a timeout context.
|
||||
func (p *prog) newExecutionFrame(input any) (*interpreter.ExecutionFrame, error) {
|
||||
frame, err := interpreter.NewExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.defaultVars != nil {
|
||||
// Update the frame's activation in place.
|
||||
frame.Activation = interpreter.NewHierarchicalActivation(p.defaultVars, frame.Activation)
|
||||
}
|
||||
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
// ResolveName implements the Activation interface method, but adds a special #interrupted variable
|
||||
// which is capable of testing whether a 'done' signal is provided from a context.Context channel.
|
||||
func (a *ctxEvalActivation) ResolveName(name string) (any, bool) {
|
||||
if name == "#interrupted" {
|
||||
a.interruptCheckCount++
|
||||
if a.interruptCheckCount%a.interruptCheckFrequency == 0 {
|
||||
// newAsyncFrame creates an ExecutionFrame configured for asynchronous evaluation under the
|
||||
// given context, wiring the observer and concurrency limit from the program options.
|
||||
func (p *prog) newAsyncFrame(ctx context.Context, input any) (*interpreter.ExecutionFrame, error) {
|
||||
frame, err := p.newExecutionFrame(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := frame.SetContext(ctx, p.interruptCheckFrequency); err != nil {
|
||||
frame.Close()
|
||||
return nil, err
|
||||
}
|
||||
frame.SetAsyncObserver(p.asyncObserver)
|
||||
frame.SetAsyncMaxConcurrency(resolveAsyncMaxConcurrency(p.asyncMaxConcurrency))
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
// defaultAsyncMaxConcurrency bounds the number of concurrently launched async calls when the
|
||||
// program does not configure AsyncMaxConcurrency. It exists so that a wide fan-out (e.g. an async
|
||||
// call inside a comprehension over a large list) cannot spawn an unbounded number of goroutines.
|
||||
const defaultAsyncMaxConcurrency = 100
|
||||
|
||||
// resolveAsyncMaxConcurrency maps the configured concurrency to the effective launch limit:
|
||||
// - 0 (unset): apply defaultAsyncMaxConcurrency.
|
||||
// - >0: use the configured value.
|
||||
// - <0: unlimited (no launch limiter); use only if the caller bounds concurrency another way.
|
||||
func resolveAsyncMaxConcurrency(configured int) int {
|
||||
if configured == 0 {
|
||||
return defaultAsyncMaxConcurrency
|
||||
}
|
||||
return configured
|
||||
}
|
||||
|
||||
// resolveCompletionBufferSize returns the size of the async completion channel. When unset, it
|
||||
// defaults to the effective launch concurrency so that all in-flight calls can report completion
|
||||
// without blocking. An unbuffered channel would make a completed call hold its launch slot until
|
||||
// the evaluator drained it, throttling effective concurrency to the drain rate.
|
||||
func (p *prog) resolveCompletionBufferSize() int {
|
||||
if p.asyncCompletionBufferSize > 0 {
|
||||
return p.asyncCompletionBufferSize
|
||||
}
|
||||
limit := resolveAsyncMaxConcurrency(p.asyncMaxConcurrency)
|
||||
if limit < 0 {
|
||||
// Unlimited launches: fall back to the default bound for the buffer so it stays finite.
|
||||
return defaultAsyncMaxConcurrency
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
// ConcurrentEval implements the Program interface.
|
||||
func (p *prog) ConcurrentEval(ctx context.Context, input any) <-chan EvalResult {
|
||||
resCh := make(chan EvalResult, 1)
|
||||
if ctx == nil {
|
||||
resCh <- EvalResult{Err: errors.New("context can not be nil")}
|
||||
close(resCh)
|
||||
return resCh
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(resCh)
|
||||
// Ensure concurrent eval handles panic / recovery properly
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch t := r.(type) {
|
||||
case interpreter.EvalCancelledError:
|
||||
resCh <- EvalResult{Err: t}
|
||||
default:
|
||||
resCh <- EvalResult{Err: fmt.Errorf("internal error: %v", r)}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
frame, err := p.newAsyncFrame(ctx, input)
|
||||
if err != nil {
|
||||
resCh <- EvalResult{Err: err}
|
||||
return
|
||||
}
|
||||
defer frame.Close()
|
||||
|
||||
// Completions are signaled to this channel as async calls finish. The asyncCallState
|
||||
// fan-in also selects on ctx.Done(), so the sender will not leak if this loop returns early.
|
||||
completions := make(chan int64, p.resolveCompletionBufferSize())
|
||||
frame.SetCompletions(completions)
|
||||
|
||||
for {
|
||||
var out ref.Val
|
||||
var det *EvalDetails
|
||||
|
||||
if p.observable != nil {
|
||||
det = &EvalDetails{}
|
||||
out = p.observable.ObserveExec(frame, func(observed any) {
|
||||
switch o := observed.(type) {
|
||||
case interpreter.EvalState:
|
||||
det.state = o
|
||||
case *interpreter.CostTracker:
|
||||
det.costTracker = o
|
||||
}
|
||||
})
|
||||
} else {
|
||||
out = p.interpretable.Exec(frame)
|
||||
}
|
||||
|
||||
// Communicate errors quickly.
|
||||
if types.IsError(out) {
|
||||
var err error = out.(*types.Err)
|
||||
if errors.Is(err, interpreter.InterruptError{}) {
|
||||
err = fmt.Errorf("%w: %w", err, context.Cause(ctx))
|
||||
}
|
||||
resCh <- EvalResult{Val: out, EvalDetails: det, Err: err}
|
||||
return
|
||||
}
|
||||
|
||||
// A concrete (non-unknown) result is final.
|
||||
unk, isUnknown := out.(*types.Unknown)
|
||||
if !isUnknown || !unk.HasUnknownFunction() {
|
||||
resCh <- EvalResult{Val: out, EvalDetails: det, Err: nil}
|
||||
return
|
||||
}
|
||||
|
||||
// Post-execution dispatch: launch only the async calls required by the unknown result.
|
||||
frame.DispatchPendingAsyncCalls(unk.IDs())
|
||||
|
||||
// The result depends on one or more unresolved async calls. Wait for completions and
|
||||
// re-evaluate according to the configured drain strategy.
|
||||
var batch []async.Call
|
||||
|
||||
// Wait for at least one completion (or cancellation).
|
||||
select {
|
||||
case <-a.interrupt:
|
||||
return true, true
|
||||
default:
|
||||
return nil, false
|
||||
case id := <-completions:
|
||||
if call := frame.AsyncCall(id); call != nil {
|
||||
batch = append(batch, call)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()}
|
||||
return
|
||||
}
|
||||
|
||||
// Accumulate completions and consult the strategy.
|
||||
var timer *time.Timer
|
||||
reevaluate := false
|
||||
for !reevaluate {
|
||||
active := frame.ActiveAsyncCalls()
|
||||
action := p.drainStrategy.NextAction(batch, active)
|
||||
if action.Reevaluate {
|
||||
break
|
||||
}
|
||||
|
||||
var timeoutCh <-chan time.Time
|
||||
if action.WaitDuration > 0 {
|
||||
if timer == nil {
|
||||
timer = time.NewTimer(action.WaitDuration)
|
||||
} else {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
timer.Reset(action.WaitDuration)
|
||||
}
|
||||
timeoutCh = timer.C
|
||||
}
|
||||
|
||||
select {
|
||||
case id := <-completions:
|
||||
if call := frame.AsyncCall(id); call != nil {
|
||||
batch = append(batch, call)
|
||||
}
|
||||
case <-timeoutCh:
|
||||
reevaluate = true
|
||||
case <-ctx.Done():
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()}
|
||||
return
|
||||
}
|
||||
}
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
return a.parent.ResolveName(name)
|
||||
}()
|
||||
|
||||
return resCh
|
||||
}
|
||||
|
||||
func (a *ctxEvalActivation) Parent() Activation {
|
||||
return a.parent
|
||||
}
|
||||
|
||||
func (a *ctxEvalActivation) AsPartialActivation() (interpreter.PartialActivation, bool) {
|
||||
pa, ok := a.parent.(interpreter.PartialActivation)
|
||||
return pa, ok
|
||||
}
|
||||
|
||||
func newCtxEvalActivationPool() *ctxEvalActivationPool {
|
||||
return &ctxEvalActivationPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() any {
|
||||
return &ctxEvalActivation{}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ctxEvalActivationPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
// Setup initializes a pooled Activation with the ability check for context.Context cancellation
|
||||
func (p *ctxEvalActivationPool) Setup(vars Activation, done <-chan struct{}, interruptCheckRate uint) *ctxEvalActivation {
|
||||
a := p.Pool.Get().(*ctxEvalActivation)
|
||||
a.parent = vars
|
||||
a.interrupt = done
|
||||
a.interruptCheckCount = 0
|
||||
a.interruptCheckFrequency = interruptCheckRate
|
||||
return a
|
||||
}
|
||||
|
||||
type evalActivation struct {
|
||||
vars map[string]any
|
||||
lazyVars map[string]any
|
||||
}
|
||||
|
||||
// ResolveName looks up the value of the input variable name, if found.
|
||||
//
|
||||
// Lazy bindings may be supplied within the map-based input in either of the following forms:
|
||||
// - func() any
|
||||
// - func() ref.Val
|
||||
//
|
||||
// The lazy binding will only be invoked once per evaluation.
|
||||
//
|
||||
// Values which are not represented as ref.Val types on input may be adapted to a ref.Val using
|
||||
// the types.Adapter configured in the environment.
|
||||
func (a *evalActivation) ResolveName(name string) (any, bool) {
|
||||
v, found := a.vars[name]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
switch obj := v.(type) {
|
||||
case func() ref.Val:
|
||||
if resolved, found := a.lazyVars[name]; found {
|
||||
return resolved, true
|
||||
}
|
||||
lazy := obj()
|
||||
a.lazyVars[name] = lazy
|
||||
return lazy, true
|
||||
case func() any:
|
||||
if resolved, found := a.lazyVars[name]; found {
|
||||
return resolved, true
|
||||
}
|
||||
lazy := obj()
|
||||
a.lazyVars[name] = lazy
|
||||
return lazy, true
|
||||
default:
|
||||
return obj, true
|
||||
}
|
||||
}
|
||||
|
||||
// Parent implements the Activation interface
|
||||
func (a *evalActivation) Parent() Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEvalActivationPool() *evalActivationPool {
|
||||
return &evalActivationPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() any {
|
||||
return &evalActivation{lazyVars: make(map[string]any)}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type evalActivationPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
// Setup initializes a pooled Activation object with the map input.
|
||||
func (p *evalActivationPool) Setup(vars map[string]any) *evalActivation {
|
||||
a := p.Pool.Get().(*evalActivation)
|
||||
a.vars = vars
|
||||
return a
|
||||
}
|
||||
|
||||
func (p *evalActivationPool) Put(value any) {
|
||||
a := value.(*evalActivation)
|
||||
for k := range a.lazyVars {
|
||||
delete(a.lazyVars, k)
|
||||
}
|
||||
p.Pool.Put(a)
|
||||
}
|
||||
|
||||
var (
|
||||
// activationPool is an internally managed pool of Activation values that wrap map[string]any inputs
|
||||
activationPool = newEvalActivationPool()
|
||||
|
||||
// ctxActivationPool is an internally managed pool of Activation values that expose a special #interrupted variable
|
||||
ctxActivationPool = newCtxEvalActivationPool()
|
||||
)
|
||||
// errAsyncRequiresConcurrentEval is returned by the synchronous entry points (Eval, ContextEval)
|
||||
// when the expression contains asynchronous function calls, which only ConcurrentEval can resolve.
|
||||
var errAsyncRequiresConcurrentEval = errors.New(
|
||||
"expression contains asynchronous function calls; use ConcurrentEval")
|
||||
|
||||
+83
-5
@@ -23,15 +23,48 @@ import (
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
"github.com/google/cel-go/common/overloads"
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
//go:embed templates/authoring.tmpl
|
||||
var authoringPrompt string
|
||||
|
||||
// splitImpl splits a string into a list of strings.
|
||||
//
|
||||
// Normalizes extracted comments (trim common prefix whitespace and extra trailing newlines).
|
||||
func splitImpl(str string) []string {
|
||||
str = strings.TrimRight(str, " \n\t\r")
|
||||
out := strings.Split(str, "\n")
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
negative := strings.TrimLeft(out[0], " \t")
|
||||
lenNegative := len(negative)
|
||||
lenOut := len(out[0])
|
||||
if lenNegative == lenOut {
|
||||
return out
|
||||
}
|
||||
prefix := out[0][:lenOut-lenNegative]
|
||||
trimmed := make([]string, len(out))
|
||||
for i, line := range out {
|
||||
if line == "" {
|
||||
trimmed[i] = ""
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
return out
|
||||
}
|
||||
trimmed[i] = strings.TrimPrefix(line, prefix)
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// AuthoringPrompt creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
|
||||
func AuthoringPrompt(env *Env) (*Prompt, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"split": func(str string) []string { return strings.Split(str, "\n") },
|
||||
"split": splitImpl,
|
||||
"newlineToSpace": func(str string) string { return strings.ReplaceAll(str, "\n", " ") },
|
||||
}
|
||||
tmpl := template.New("cel").Funcs(funcMap)
|
||||
tmpl, err := tmpl.Parse(authoringPrompt)
|
||||
@@ -47,6 +80,17 @@ func AuthoringPrompt(env *Env) (*Prompt, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthoringPromptWithFieldPaths creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
|
||||
// Includes documentation for all of the reachable field paths in the environment.
|
||||
func AuthoringPromptWithFieldPaths(env *Env) (*Prompt, error) {
|
||||
p, err := AuthoringPrompt(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.fieldPaths = true
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Prompt represents the core components of an LLM prompt based on a CEL environment.
|
||||
//
|
||||
// All fields of the prompt may be overwritten / modified with support for rendering the
|
||||
@@ -64,14 +108,22 @@ type Prompt struct {
|
||||
// tmpl is the text template base-configuration for rendering text.
|
||||
tmpl *template.Template
|
||||
|
||||
// fieldPaths is a flag to include reachable field paths in the prompt.
|
||||
fieldPaths bool
|
||||
|
||||
// env reference used to collect variables, functions, and macros available to the prompt.
|
||||
env *Env
|
||||
}
|
||||
|
||||
type promptVariable struct {
|
||||
*common.Doc
|
||||
FieldPaths []*common.Doc
|
||||
}
|
||||
|
||||
type promptInst struct {
|
||||
*Prompt
|
||||
|
||||
Variables []*common.Doc
|
||||
Variables []*promptVariable
|
||||
Macros []*common.Doc
|
||||
Functions []*common.Doc
|
||||
UserPrompt string
|
||||
@@ -79,11 +131,36 @@ type promptInst struct {
|
||||
|
||||
// Render renders the user prompt with the associated context from the prompt template
|
||||
// for use with LLM generators.
|
||||
//
|
||||
// User-supplied input is passed as template data via the UserPrompt field, which
|
||||
// Go's text/template renders as a literal string value. Template action delimiters
|
||||
// such as {{.Persona}} in the user prompt are never evaluated as template directives
|
||||
// because text/template only executes directives present in the template definition
|
||||
// itself, not in data values interpolated at render time.
|
||||
func (p *Prompt) Render(userPrompt string) string {
|
||||
var buffer strings.Builder
|
||||
vars := make([]*common.Doc, len(p.env.Variables()))
|
||||
vars := make([]*promptVariable, len(p.env.Variables()))
|
||||
for i, v := range p.env.Variables() {
|
||||
vars[i] = v.Documentation()
|
||||
vars[i] = &promptVariable{Doc: v.Documentation()}
|
||||
if p.fieldPaths && v.Type().Kind() == types.StructKind {
|
||||
var fieldPaths []*common.Doc
|
||||
|
||||
paths := fieldPathsForType(p.env.CELTypeProvider(), v.Name(), v.Type())
|
||||
if len(paths) < 2 {
|
||||
paths = nil
|
||||
} else {
|
||||
// First path is the variable which is already documented.
|
||||
paths = paths[1:]
|
||||
}
|
||||
for _, path := range paths {
|
||||
fieldPaths = append(fieldPaths, path.Documentation())
|
||||
}
|
||||
|
||||
sort.SliceStable(fieldPaths, func(i, j int) bool {
|
||||
return fieldPaths[i].Name < fieldPaths[j].Name
|
||||
})
|
||||
vars[i].FieldPaths = fieldPaths
|
||||
}
|
||||
}
|
||||
sort.SliceStable(vars, func(i, j int) bool {
|
||||
return vars[i].Name < vars[j].Name
|
||||
@@ -107,7 +184,8 @@ func (p *Prompt) Render(userPrompt string) string {
|
||||
Variables: vars,
|
||||
Macros: macs,
|
||||
Functions: funcs,
|
||||
UserPrompt: userPrompt}
|
||||
UserPrompt: userPrompt,
|
||||
}
|
||||
p.tmpl.Execute(&buffer, inst)
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
+30
-8
@@ -1,8 +1,29 @@
|
||||
{{define "variable"}}{{.Name}} is a {{.Type}}
|
||||
{{define "fieldPath" }}
|
||||
* path: `{{.Name}}`
|
||||
type: `{{.Type}}`
|
||||
{{- if .Description }}
|
||||
description:
|
||||
{{range split .Description }} {{.}}
|
||||
{{end}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{define "variable" -}}
|
||||
* name: `{{.Name}}`
|
||||
type: `{{.Type}}`
|
||||
{{- if .Description}}
|
||||
description:
|
||||
{{range split .Description}} {{.}}
|
||||
{{end -}}
|
||||
{{- end -}}
|
||||
{{- if .FieldPaths }}
|
||||
attributes:
|
||||
{{- range .FieldPaths }}{{ template "fieldPath" . }}{{end}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{define "macro" -}}
|
||||
{{.Name}} macro{{if .Description}} - {{range split .Description}}{{.}} {{end}}
|
||||
{{.Name}} macro{{if .Description}} - {{newlineToSpace .Description}}
|
||||
{{end}}
|
||||
{{range .Children}}{{range split .Description}} {{.}}
|
||||
{{end}}
|
||||
@@ -18,7 +39,7 @@
|
||||
{{- end -}}
|
||||
|
||||
{{define "function" -}}
|
||||
{{.Name}}{{if .Description}} - {{range split .Description}}{{.}} {{end}}
|
||||
{{.Name}}{{if .Description}} - {{newlineToSpace .Description}}
|
||||
{{end}}
|
||||
{{range .Children}}{{template "overload" .}}{{end}}
|
||||
{{- end -}}
|
||||
@@ -32,25 +53,26 @@ Only use the following variables, macros, and functions in expressions.
|
||||
{{if .Variables}}
|
||||
Variables:
|
||||
|
||||
{{range .Variables}}* {{template "variable" .}}
|
||||
{{range .Variables -}}
|
||||
{{template "variable" .}}
|
||||
{{end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{end -}}
|
||||
{{if .Macros}}
|
||||
Macros:
|
||||
|
||||
{{range .Macros}}* {{template "macro" .}}
|
||||
{{end -}}
|
||||
|
||||
{{end -}}
|
||||
|
||||
{{if .Functions}}
|
||||
Functions:
|
||||
|
||||
{{range .Functions}}* {{template "function" .}}
|
||||
{{end -}}
|
||||
|
||||
{{end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{.GeneralUsage}}
|
||||
|
||||
{{.UserPrompt}}
|
||||
|
||||
+119
-12
@@ -15,6 +15,7 @@
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
@@ -25,11 +26,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
durationValidatorName = "cel.validator.duration"
|
||||
regexValidatorName = "cel.validator.matches"
|
||||
timestampValidatorName = "cel.validator.timestamp"
|
||||
homogeneousValidatorName = "cel.validator.homogeneous_literals"
|
||||
nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit"
|
||||
durationValidatorName = "cel.validator.duration"
|
||||
regexValidatorName = "cel.validator.matches"
|
||||
timestampValidatorName = "cel.validator.timestamp"
|
||||
homogeneousValidatorName = "cel.validator.homogeneous_literals"
|
||||
nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit"
|
||||
bindNestingLimitValidatorName = "cel.validator.bind_nesting_limit"
|
||||
|
||||
// HomogeneousAggregateLiteralExemptFunctions is the ValidatorConfig key used to configure
|
||||
// the set of function names which are exempt from homogeneous type checks. The expected type
|
||||
@@ -45,6 +47,14 @@ var (
|
||||
astValidatorFactories = map[string]ASTValidatorFactory{
|
||||
nestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) {
|
||||
if limit, found := val.ConfigValue("limit"); found {
|
||||
// In case of protos, config value is of type by google.protobuf.Value, which numeric values are always a double.
|
||||
if val, isDouble := limit.(float64); isDouble {
|
||||
if val != float64(int64(val)) {
|
||||
return nil, fmt.Errorf("invalid validator: %s, limit value is not a whole number: %v", nestingLimitValidatorName, limit)
|
||||
}
|
||||
return ValidateComprehensionNestingLimit(int(val)), nil
|
||||
}
|
||||
|
||||
if val, isInt := limit.(int); isInt {
|
||||
return ValidateComprehensionNestingLimit(val), nil
|
||||
}
|
||||
@@ -52,6 +62,23 @@ var (
|
||||
}
|
||||
return nil, fmt.Errorf("invalid validator: %s missing limit", nestingLimitValidatorName)
|
||||
},
|
||||
bindNestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) {
|
||||
if limit, found := val.ConfigValue("limit"); found {
|
||||
// In case of protos, config value is of type by google.protobuf.Value, which numeric values are always a double.
|
||||
if val, isDouble := limit.(float64); isDouble {
|
||||
if val != float64(int64(val)) {
|
||||
return nil, fmt.Errorf("invalid validator: %s, limit value is not a whole number: %v", bindNestingLimitValidatorName, limit)
|
||||
}
|
||||
return ValidateBindNestingLimit(int(val)), nil
|
||||
}
|
||||
|
||||
if val, isInt := limit.(int); isInt {
|
||||
return ValidateBindNestingLimit(val), nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid validator: %s unsupported limit type: %v", bindNestingLimitValidatorName, limit)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid validator: %s missing limit", bindNestingLimitValidatorName)
|
||||
},
|
||||
durationValidatorName: func(*env.Validator) (ASTValidator, error) {
|
||||
return ValidateDurationLiterals(), nil
|
||||
},
|
||||
@@ -72,12 +99,20 @@ type ASTValidatorFactory func(*env.Validator) (ASTValidator, error)
|
||||
|
||||
// ASTValidators configures a set of ASTValidator instances into the target environment.
|
||||
//
|
||||
// Validators are applied in the order in which the are specified and are treated as singletons.
|
||||
// The same ASTValidator with a given name will not be applied more than once.
|
||||
// Validators are applied in the order in which they are specified.
|
||||
// If an ASTValidator with the same name is already configured, it will be replaced.
|
||||
func ASTValidators(validators ...ASTValidator) EnvOption {
|
||||
return func(e *Env) (*Env, error) {
|
||||
for _, v := range validators {
|
||||
if !e.HasValidator(v.Name()) {
|
||||
found := false
|
||||
for i, existing := range e.validators {
|
||||
if existing.Name() == v.Name() {
|
||||
e.validators[i] = v
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
e.validators = append(e.validators, v)
|
||||
}
|
||||
}
|
||||
@@ -224,6 +259,13 @@ func ValidateComprehensionNestingLimit(limit int) ASTValidator {
|
||||
return nestingLimitValidator{limit: limit}
|
||||
}
|
||||
|
||||
// ValidateBindNestingLimit ensures that cel.bind() macro nesting does not exceed the specified limit.
|
||||
//
|
||||
// This validator can be useful for preventing arbitrarily nested cel.bind() macro calls.
|
||||
func ValidateBindNestingLimit(limit int) ASTValidator {
|
||||
return bindNestingLimitValidator{limit: limit}
|
||||
}
|
||||
|
||||
type argChecker func(env *Env, call, arg ast.Expr) error
|
||||
|
||||
func newFormatValidator(funcName string, argNum int, check argChecker) formatValidator {
|
||||
@@ -276,8 +318,9 @@ func evalCall(env *Env, call, arg ast.Expr) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = prg.Eval(NoVars())
|
||||
return err
|
||||
resCh := prg.ConcurrentEval(context.Background(), NoVars())
|
||||
res := <-resCh
|
||||
return res.Err
|
||||
}
|
||||
|
||||
func compileRegex(_ *Env, _, arg ast.Expr) error {
|
||||
@@ -422,8 +465,7 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i
|
||||
}
|
||||
// When the comprehension has an empty range, continue to the next ancestor
|
||||
// as this comprehension does not have any associated cost.
|
||||
iterRange := e.AsComprehension().IterRange()
|
||||
if iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 {
|
||||
if isEmptyRangeComprehension(e) {
|
||||
e, hasParent = e.Parent()
|
||||
continue
|
||||
}
|
||||
@@ -437,3 +479,68 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type bindNestingLimitValidator struct {
|
||||
limit int
|
||||
}
|
||||
|
||||
// Name returns the name of the cel.bind nesting limit validator.
|
||||
func (v bindNestingLimitValidator) Name() string {
|
||||
return bindNestingLimitValidatorName
|
||||
}
|
||||
|
||||
// ToConfig converts the ASTValidator to an env.Validator specifying the validator name and the nesting limit
|
||||
// as an integer value: {"limit": int}
|
||||
func (v bindNestingLimitValidator) ToConfig() *env.Validator {
|
||||
return env.NewValidator(v.Name()).SetConfig(map[string]any{"limit": v.limit})
|
||||
}
|
||||
|
||||
// Validate implements the ASTValidator interface method.
|
||||
func (v bindNestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Issues) {
|
||||
root := ast.NavigateAST(a)
|
||||
comprehensions := ast.MatchDescendants(root, ast.KindMatcher(ast.ComprehensionKind))
|
||||
var celBinds []ast.NavigableExpr
|
||||
for _, comp := range comprehensions {
|
||||
if isCelBind(comp) {
|
||||
celBinds = append(celBinds, comp)
|
||||
}
|
||||
}
|
||||
if len(celBinds) <= v.limit {
|
||||
return
|
||||
}
|
||||
for _, comp := range celBinds {
|
||||
count := 0
|
||||
e := comp
|
||||
hasParent := true
|
||||
for hasParent {
|
||||
if isCelBind(e) {
|
||||
count++
|
||||
if count > v.limit {
|
||||
iss.ReportErrorAtID(comp.ID(), "cel.bind exceeds nesting limit")
|
||||
break
|
||||
}
|
||||
}
|
||||
e, hasParent = e.Parent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isEmptyRangeComprehension(e ast.NavigableExpr) bool {
|
||||
if e.Kind() != ast.ComprehensionKind {
|
||||
return false
|
||||
}
|
||||
iterRange := e.AsComprehension().IterRange()
|
||||
return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0
|
||||
}
|
||||
|
||||
func isCelBind(e ast.NavigableExpr) bool {
|
||||
if !isEmptyRangeComprehension(e) {
|
||||
return false
|
||||
}
|
||||
compre := e.AsComprehension()
|
||||
loopCond := compre.LoopCondition()
|
||||
loopStep := compre.LoopStep()
|
||||
return compre.IterVar() == unusedIterVar &&
|
||||
loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral().Value() == false &&
|
||||
loopStep.Kind() == ast.IdentKind && loopStep.AsIdent() == compre.AccuVar()
|
||||
}
|
||||
|
||||
+56
-11
@@ -19,6 +19,8 @@ package checker
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/ast"
|
||||
@@ -65,6 +67,15 @@ func Check(parsed *ast.AST, source common.Source, env *Env) (*ast.AST, *common.E
|
||||
for id, t := range c.TypeMap() {
|
||||
c.SetType(id, substitute(c.mappings, t, true))
|
||||
}
|
||||
// Remove source info for IDs without a corresponding AST node. This can happen because
|
||||
// check() deletes some nodes while rewriting the AST. For example the Select operand is
|
||||
// deleted when a variable reference is replaced with a Ident expression.
|
||||
c.AST.ClearUnusedIDs()
|
||||
if env.jsonFieldNames {
|
||||
c.AST.SourceInfo().AddExtension(
|
||||
ast.NewExtension("json_name", ast.NewExtensionVersion(1, 1), ast.ComponentRuntime),
|
||||
)
|
||||
}
|
||||
return c.AST, errs
|
||||
}
|
||||
|
||||
@@ -104,11 +115,15 @@ func (c *checker) check(e ast.Expr) {
|
||||
func (c *checker) checkIdent(e ast.Expr) {
|
||||
identName := e.AsIdent()
|
||||
// Check to see if the identifier is declared.
|
||||
if ident := c.env.LookupIdent(identName); ident != nil {
|
||||
if ident := c.env.resolveSimpleIdent(identName); ident != nil {
|
||||
name := strings.TrimPrefix(ident.Name(), ".")
|
||||
if ident.requiresDisambiguation {
|
||||
name = "." + name
|
||||
}
|
||||
c.setType(e, ident.Type())
|
||||
c.setReference(e, ast.NewIdentReference(ident.Name(), ident.Value()))
|
||||
c.setReference(e, ast.NewIdentReference(name, ident.Value()))
|
||||
// Overwrite the identifier with its fully qualified name.
|
||||
e.SetKindCase(c.NewIdent(e.ID(), ident.Name()))
|
||||
e.SetKindCase(c.NewIdent(e.ID(), name))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,18 +134,22 @@ func (c *checker) checkIdent(e ast.Expr) {
|
||||
func (c *checker) checkSelect(e ast.Expr) {
|
||||
sel := e.AsSelect()
|
||||
// Before traversing down the tree, try to interpret as qualified name.
|
||||
qname, found := containers.ToQualifiedName(e)
|
||||
qualifiers, found := c.computeQualifiers(e)
|
||||
if found {
|
||||
ident := c.env.LookupIdent(qname)
|
||||
ident := c.env.resolveQualifiedIdent(qualifiers...)
|
||||
if ident != nil {
|
||||
// We don't check for a TestOnly expression here since the `found` result is
|
||||
// always going to be false for TestOnly expressions.
|
||||
|
||||
// Rewrite the node to be a variable reference to the resolved fully-qualified
|
||||
// variable name.
|
||||
name := ident.Name()
|
||||
if ident.requiresDisambiguation {
|
||||
name = "." + name
|
||||
}
|
||||
c.setType(e, ident.Type())
|
||||
c.setReference(e, ast.NewIdentReference(ident.Name(), ident.Value()))
|
||||
e.SetKindCase(c.NewIdent(e.ID(), ident.Name()))
|
||||
c.setReference(e, ast.NewIdentReference(name, ident.Value()))
|
||||
e.SetKindCase(c.NewIdent(e.ID(), name))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -142,6 +161,29 @@ func (c *checker) checkSelect(e ast.Expr) {
|
||||
c.setType(e, substitute(c.mappings, resultType, false))
|
||||
}
|
||||
|
||||
// computeQualifiers computes the qualified names parts of a select expression.
|
||||
func (c *checker) computeQualifiers(e ast.Expr) ([]string, bool) {
|
||||
var qualifiers []string
|
||||
for e.Kind() == ast.SelectKind {
|
||||
sel := e.AsSelect()
|
||||
// test only expressions are not considered for qualified name selection.
|
||||
if sel.IsTestOnly() {
|
||||
return qualifiers, false
|
||||
}
|
||||
// otherwise append the select field name to the qualifier list (reverse order)
|
||||
qualifiers = append(qualifiers, sel.FieldName())
|
||||
e = sel.Operand()
|
||||
// If the next operand is an identifier, then append it, reverse the name sequence
|
||||
// and return it to the caller.s
|
||||
if e.Kind() == ast.IdentKind {
|
||||
qualifiers = append(qualifiers, e.AsIdent())
|
||||
slices.Reverse(qualifiers)
|
||||
return qualifiers, true
|
||||
}
|
||||
}
|
||||
return qualifiers, false
|
||||
}
|
||||
|
||||
func (c *checker) checkOptSelect(e ast.Expr) {
|
||||
// Collect metadata related to the opt select call packaged by the parser.
|
||||
call := e.AsCall()
|
||||
@@ -234,7 +276,7 @@ func (c *checker) checkCall(e ast.Expr) {
|
||||
// Regular static call with simple name.
|
||||
if !call.IsMemberFunction() {
|
||||
// Check for the existence of the function.
|
||||
fn := c.env.LookupFunction(fnName)
|
||||
fn := c.env.lookupFunction(fnName)
|
||||
if fn == nil {
|
||||
c.errors.undeclaredReference(e.ID(), c.location(e), c.env.container.Name(), fnName)
|
||||
c.setType(e, types.ErrorType)
|
||||
@@ -256,7 +298,7 @@ func (c *checker) checkCall(e ast.Expr) {
|
||||
qualifiedPrefix, maybeQualified := containers.ToQualifiedName(target)
|
||||
if maybeQualified {
|
||||
maybeQualifiedName := qualifiedPrefix + "." + fnName
|
||||
fn := c.env.LookupFunction(maybeQualifiedName)
|
||||
fn := c.env.lookupFunction(maybeQualifiedName)
|
||||
if fn != nil {
|
||||
// The function name is namespaced and so preserving the target operand would
|
||||
// be an inaccurate representation of the desired evaluation behavior.
|
||||
@@ -269,7 +311,7 @@ func (c *checker) checkCall(e ast.Expr) {
|
||||
|
||||
// Regular instance call.
|
||||
c.check(target)
|
||||
fn := c.env.LookupFunction(fnName)
|
||||
fn := c.env.lookupFunction(fnName)
|
||||
// Function found, attempt overload resolution.
|
||||
if fn != nil {
|
||||
c.resolveOverloadOrError(e, fn, target, args)
|
||||
@@ -441,7 +483,7 @@ func (c *checker) checkCreateStruct(e ast.Expr) {
|
||||
msgVal := e.AsStruct()
|
||||
// Determine the type of the message.
|
||||
resultType := types.ErrorType
|
||||
ident := c.env.LookupIdent(msgVal.TypeName())
|
||||
ident := c.env.resolveTypeIdent(msgVal.TypeName())
|
||||
if ident == nil {
|
||||
c.errors.undeclaredReference(
|
||||
e.ID(), c.location(e), c.env.container.Name(), msgVal.TypeName())
|
||||
@@ -681,6 +723,9 @@ func (c *checker) lookupFieldType(exprID int64, structType, fieldName string) (*
|
||||
}
|
||||
|
||||
if ft, found := c.env.provider.FindStructFieldType(structType, fieldName); found {
|
||||
if c.env.jsonFieldNames && !ft.IsJSONField {
|
||||
c.errors.undefinedField(exprID, c.locationByID(exprID), fieldName)
|
||||
}
|
||||
return ft.Type, found
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -159,6 +159,11 @@ func (se SizeEstimate) Union(size SizeEstimate) SizeEstimate {
|
||||
return result
|
||||
}
|
||||
|
||||
// AsCost converts a size estimates to an equivalent cost estimate.
|
||||
func (se SizeEstimate) AsCost() CostEstimate {
|
||||
return se.MultiplyByCostFactor(1)
|
||||
}
|
||||
|
||||
// CostEstimate represents an estimated cost range and provides add and multiply operations
|
||||
// that do not overflow.
|
||||
type CostEstimate struct {
|
||||
@@ -786,18 +791,26 @@ func (c *coster) functionCost(e ast.Expr, function, overloadID string, target *A
|
||||
return CallEstimate{CostEstimate: c.sizeOrUnknown(args[1]).MultiplyByCostFactor(1).Add(argCostSum())}
|
||||
}
|
||||
// O(nm) functions
|
||||
case overloads.MatchesString:
|
||||
case overloads.Matches, overloads.MatchesString:
|
||||
// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
|
||||
if target != nil && len(args) == 1 {
|
||||
var strNode, regexNode AstNode
|
||||
if overloadID == overloads.MatchesString && target != nil && len(args) == 1 {
|
||||
strNode = *target
|
||||
regexNode = args[0]
|
||||
} else if overloadID == overloads.Matches && target == nil && len(args) == 2 {
|
||||
strNode = args[0]
|
||||
regexNode = args[1]
|
||||
}
|
||||
if strNode != nil && regexNode != nil {
|
||||
// Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0
|
||||
// in case where string is empty but regex is still expensive.
|
||||
strCost := c.sizeOrUnknown(*target).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor)
|
||||
strCost := c.sizeOrUnknown(strNode).Add(SizeEstimate{Min: 1, Max: 1}).MultiplyByCostFactor(common.StringTraversalCostFactor)
|
||||
// We don't know how many expressions are in the regex, just the string length (a huge
|
||||
// improvement here would be to somehow get a count the number of expressions in the regex or
|
||||
// how many states are in the regex state machine and use that to measure regex cost).
|
||||
// For now, we're making a guess that each expression in a regex is typically at least 4 chars
|
||||
// in length.
|
||||
regexCost := c.sizeOrUnknown(args[0]).MultiplyByCostFactor(common.RegexStringLengthCostFactor)
|
||||
regexCost := c.sizeOrUnknown(regexNode).MultiplyByCostFactor(common.RegexStringLengthCostFactor)
|
||||
return CallEstimate{CostEstimate: strCost.Multiply(regexCost).Add(argCostSum())}
|
||||
}
|
||||
case overloads.ContainsString:
|
||||
|
||||
+123
-32
@@ -74,6 +74,7 @@ type Env struct {
|
||||
declarations *Scopes
|
||||
aggLitElemType aggregateLiteralElementType
|
||||
filteredOverloadIDs map[string]struct{}
|
||||
jsonFieldNames bool
|
||||
}
|
||||
|
||||
// NewEnv returns a new *Env with the given parameters.
|
||||
@@ -104,6 +105,7 @@ func NewEnv(container *containers.Container, provider types.Provider, opts ...Op
|
||||
declarations: declarations,
|
||||
aggLitElemType: aggLitElemType,
|
||||
filteredOverloadIDs: filteredOverloadIDs,
|
||||
jsonFieldNames: envOptions.jsonFieldNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -129,45 +131,111 @@ func (e *Env) AddFunctions(declarations ...*decls.FunctionDecl) error {
|
||||
return formatError(errMsgs)
|
||||
}
|
||||
|
||||
// LookupIdent returns a Decl proto for typeName as an identifier in the Env.
|
||||
// Returns nil if no such identifier is found in the Env.
|
||||
func (e *Env) LookupIdent(name string) *decls.VariableDecl {
|
||||
// newAttrResolution creates a new attribute resolution value.
|
||||
func newAttrResolution(ident *decls.VariableDecl, requiresDisambiguation bool) *attributeResolution {
|
||||
return &attributeResolution{
|
||||
VariableDecl: ident,
|
||||
requiresDisambiguation: requiresDisambiguation,
|
||||
}
|
||||
}
|
||||
|
||||
// attributeResolution wraps an existing variable and denotes whether disambiguation is needed
|
||||
// during variable resolution.
|
||||
type attributeResolution struct {
|
||||
*decls.VariableDecl
|
||||
|
||||
// requiresDisambiguation indicates the variable name should be dot-prefixed.
|
||||
requiresDisambiguation bool
|
||||
}
|
||||
|
||||
// resolveSimpleIdent determines the resolved attribute for a single identifier.
|
||||
func (e *Env) resolveSimpleIdent(name string) *attributeResolution {
|
||||
local := e.lookupLocalIdent(name)
|
||||
if local != nil && !strings.HasPrefix(name, ".") {
|
||||
return newAttrResolution(local, false)
|
||||
}
|
||||
for _, candidate := range e.container.ResolveCandidateNames(name) {
|
||||
if ident := e.declarations.FindIdent(candidate); ident != nil {
|
||||
return ident
|
||||
}
|
||||
|
||||
// Next try to import the name as a reference to a message type. If found,
|
||||
// the declaration is added to the outest (global) scope of the
|
||||
// environment, so next time we can access it faster.
|
||||
if t, found := e.provider.FindStructType(candidate); found {
|
||||
decl := decls.NewVariable(candidate, t)
|
||||
e.declarations.AddIdent(decl)
|
||||
return decl
|
||||
}
|
||||
|
||||
if i, found := e.provider.FindIdent(candidate); found {
|
||||
if t, ok := i.(*types.Type); ok {
|
||||
decl := decls.NewVariable(candidate, types.NewTypeTypeWithParam(t))
|
||||
e.declarations.AddIdent(decl)
|
||||
return decl
|
||||
}
|
||||
}
|
||||
|
||||
// Next try to import this as an enum value by splitting the name in a type prefix and
|
||||
// the enum inside.
|
||||
if enumValue := e.provider.EnumValue(candidate); enumValue.Type() != types.ErrType {
|
||||
decl := decls.NewConstant(candidate, types.IntType, enumValue)
|
||||
e.declarations.AddIdent(decl)
|
||||
return decl
|
||||
if ident := e.lookupGlobalIdent(candidate); ident != nil {
|
||||
return newAttrResolution(ident, local != nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupFunction returns a Decl proto for typeName as a function in env.
|
||||
// resolveQualifiedIdent determines the resolved attribute for a qualified identifier.
|
||||
func (e *Env) resolveQualifiedIdent(qualifiers ...string) *attributeResolution {
|
||||
if len(qualifiers) == 1 {
|
||||
return e.resolveSimpleIdent(qualifiers[0])
|
||||
}
|
||||
local := e.lookupLocalIdent(qualifiers[0])
|
||||
if local != nil && !strings.HasPrefix(qualifiers[0], ".") {
|
||||
// this should resolve through a field selection rather than a qualified identifier
|
||||
return nil
|
||||
}
|
||||
// The qualifiers are concatenated together to indicate the qualified name to search
|
||||
// for as a global identifier. Since select expressions are resolved from leaf to root
|
||||
// if the fully concatenated string doesn't match a global identifier, indicate that
|
||||
// no variable was found to continue the traversal up to the next simpler name.
|
||||
varName := strings.Join(qualifiers, ".")
|
||||
for _, candidate := range e.container.ResolveCandidateNames(varName) {
|
||||
if ident := e.lookupGlobalIdent(candidate); ident != nil {
|
||||
return newAttrResolution(ident, local != nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveTypeIdent returns a Decl proto for typeName as an identifier in the Env.
|
||||
// Returns nil if no such identifier is found in the Env.
|
||||
func (e *Env) resolveTypeIdent(name string) *decls.VariableDecl {
|
||||
for _, candidate := range e.container.ResolveCandidateNames(name) {
|
||||
// Try to import the name as a reference to a message type.
|
||||
if i, found := e.provider.FindIdent(candidate); found {
|
||||
if t, ok := i.(*types.Type); ok {
|
||||
return decls.NewVariable(candidate, types.NewTypeTypeWithParam(t))
|
||||
}
|
||||
}
|
||||
// Next, try to find the struct type.
|
||||
if t, found := e.provider.FindStructType(candidate); found {
|
||||
return decls.NewVariable(candidate, t)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupLocalIdent finds the variable candidate in a local scope, returning nil if
|
||||
// the candidate variable name is not a local variable.
|
||||
func (e *Env) lookupLocalIdent(candidate string) *decls.VariableDecl {
|
||||
return e.declarations.FindLocalIdent(candidate)
|
||||
}
|
||||
|
||||
// lookupGlobalIdent finds a candidate variable name in the root scope, returning
|
||||
// nil if the identifier is not in the global scope.
|
||||
func (e *Env) lookupGlobalIdent(candidate string) *decls.VariableDecl {
|
||||
// Try to resolve the global identifier first.
|
||||
if ident := e.declarations.FindGlobalIdent(candidate); ident != nil {
|
||||
return ident
|
||||
}
|
||||
// Next try to import the name as a reference to a message type.
|
||||
if i, found := e.provider.FindIdent(candidate); found {
|
||||
if t, ok := i.(*types.Type); ok {
|
||||
return decls.NewVariable(candidate, types.NewTypeTypeWithParam(t))
|
||||
}
|
||||
}
|
||||
if t, found := e.provider.FindStructType(candidate); found {
|
||||
return decls.NewVariable(candidate, t)
|
||||
}
|
||||
// Next try to import this as an enum value by splitting the name in a type prefix and
|
||||
// the enum inside.
|
||||
if enumValue := e.provider.EnumValue(candidate); enumValue.Type() != types.ErrType {
|
||||
return decls.NewConstant(candidate, types.IntType, enumValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupFunction returns a Decl proto for typeName as a function in env.
|
||||
// Returns nil if no such function is found in env.
|
||||
func (e *Env) LookupFunction(name string) *decls.FunctionDecl {
|
||||
func (e *Env) lookupFunction(name string) *decls.FunctionDecl {
|
||||
for _, candidate := range e.container.ResolveCandidateNames(name) {
|
||||
if fn := e.declarations.FindFunction(candidate); fn != nil {
|
||||
return fn
|
||||
@@ -207,12 +275,31 @@ func (e *Env) setFunction(fn *decls.FunctionDecl) []errorMsg {
|
||||
return errMsgs
|
||||
}
|
||||
|
||||
func maybeMergeConstant(a *decls.VariableDecl, b *decls.VariableDecl) (*decls.VariableDecl, errorMsg) {
|
||||
if b.Value() != nil {
|
||||
if a.Value() == nil {
|
||||
return b, ""
|
||||
}
|
||||
eq, ok := a.Value().Equal(b.Value()).Value().(bool)
|
||||
if ok && eq {
|
||||
return a, ""
|
||||
}
|
||||
return nil, constantConflictError(b.Name())
|
||||
}
|
||||
return a, ""
|
||||
}
|
||||
|
||||
// addIdent adds the Decl to the declarations in the Env.
|
||||
// Returns a non-empty errorMsg if the identifier is already declared in the scope.
|
||||
func (e *Env) addIdent(decl *decls.VariableDecl) errorMsg {
|
||||
current := e.declarations.FindIdentInScope(decl.Name())
|
||||
if current != nil {
|
||||
if current.DeclarationIsEquivalent(decl) {
|
||||
decl, errMsg := maybeMergeConstant(current, decl)
|
||||
if errMsg != "" {
|
||||
return errMsg
|
||||
}
|
||||
e.declarations.AddIdent(decl)
|
||||
return ""
|
||||
}
|
||||
return overlappingIdentifierError(decl.Name())
|
||||
@@ -259,6 +346,10 @@ func (e *Env) exitScope() *Env {
|
||||
// may be accumulated into an error at a later point in execution.
|
||||
type errorMsg string
|
||||
|
||||
func constantConflictError(name string) errorMsg {
|
||||
return errorMsg(fmt.Sprintf("conflicting constant definitions for name '%s'", name))
|
||||
}
|
||||
|
||||
func overlappingIdentifierError(name string) errorMsg {
|
||||
return errorMsg(fmt.Sprintf("overlapping identifier for name '%s'", name))
|
||||
}
|
||||
|
||||
+9
@@ -18,6 +18,7 @@ type options struct {
|
||||
crossTypeNumericComparisons bool
|
||||
homogeneousAggregateLiterals bool
|
||||
validatedDeclarations *Scopes
|
||||
jsonFieldNames bool
|
||||
}
|
||||
|
||||
// Option is a functional option for configuring the type-checker
|
||||
@@ -40,3 +41,11 @@ func ValidatedDeclarations(env *Env) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// JSONFieldNames enables the use of json names instead of the standard protobuf snake_case field names
|
||||
func JSONFieldNames(enabled bool) Option {
|
||||
return func(opts *options) error {
|
||||
opts.jsonFieldNames = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -15,6 +15,8 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/common/decls"
|
||||
)
|
||||
|
||||
@@ -76,6 +78,7 @@ func (s *Scopes) AddIdent(decl *decls.VariableDecl) {
|
||||
// found.
|
||||
// Note: The search is performed from innermost to outermost.
|
||||
func (s *Scopes) FindIdent(name string) *decls.VariableDecl {
|
||||
name = strings.TrimPrefix(name, ".")
|
||||
if ident, found := s.scopes.idents[name]; found {
|
||||
return ident
|
||||
}
|
||||
@@ -89,12 +92,33 @@ func (s *Scopes) FindIdent(name string) *decls.VariableDecl {
|
||||
// nil if one does not exist.
|
||||
// Note: The search is only performed on the current scope and does not search outer scopes.
|
||||
func (s *Scopes) FindIdentInScope(name string) *decls.VariableDecl {
|
||||
name = strings.TrimPrefix(name, ".")
|
||||
if ident, found := s.scopes.idents[name]; found {
|
||||
return ident
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindLocalIdent finds a locally scoped variable with a given name, ignoring the root scope.
|
||||
func (s *Scopes) FindLocalIdent(name string) *decls.VariableDecl {
|
||||
if s == nil || s.parent == nil {
|
||||
return nil
|
||||
}
|
||||
if ident := s.FindIdentInScope(name); ident != nil {
|
||||
return ident
|
||||
}
|
||||
return s.parent.FindLocalIdent(name)
|
||||
}
|
||||
|
||||
// FindGlobalIdent finds an identifier in the global scope, ignoring all local scopes.
|
||||
func (s *Scopes) FindGlobalIdent(name string) *decls.VariableDecl {
|
||||
scope := s
|
||||
for scope.parent != nil {
|
||||
scope = scope.parent
|
||||
}
|
||||
return scope.FindIdentInScope(name)
|
||||
}
|
||||
|
||||
// SetFunction adds the function Decl to the current scope.
|
||||
// Note: Any previous entry for a function in the current scope with the same name is overwritten.
|
||||
func (s *Scopes) SetFunction(fn *decls.FunctionDecl) {
|
||||
@@ -105,6 +129,7 @@ func (s *Scopes) SetFunction(fn *decls.FunctionDecl) {
|
||||
// The search is performed from innermost to outermost.
|
||||
// Returns nil if no such function in Scopes.
|
||||
func (s *Scopes) FindFunction(name string) *decls.FunctionDecl {
|
||||
name = strings.TrimPrefix(name, ".")
|
||||
if fn, found := s.scopes.functions[name]; found {
|
||||
return fn
|
||||
}
|
||||
|
||||
+147
@@ -16,6 +16,8 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
@@ -160,6 +162,34 @@ func MaxID(a *AST) int64 {
|
||||
return visitor.maxID + 1
|
||||
}
|
||||
|
||||
// IDs returns the set of AST node IDs, including macro calls.
|
||||
func (a *AST) IDs() map[int64]bool {
|
||||
visitor := make(idVisitor)
|
||||
PostOrderVisit(a.Expr(), visitor)
|
||||
for _, call := range a.SourceInfo().MacroCalls() {
|
||||
PostOrderVisit(call, visitor)
|
||||
}
|
||||
return visitor
|
||||
}
|
||||
|
||||
// NodeCount returns the total number of expression nodes in the AST, including macro calls.
|
||||
func NodeCount(a *AST) int {
|
||||
if a == nil {
|
||||
return 0
|
||||
}
|
||||
return len(a.IDs())
|
||||
}
|
||||
|
||||
// ClearUnusedIDs removes IDs not used in the AST or macro calls from SourceInfo.
|
||||
func (a *AST) ClearUnusedIDs() {
|
||||
ids := a.IDs()
|
||||
for id := range a.SourceInfo().OffsetRanges() {
|
||||
if !ids[id] {
|
||||
a.SourceInfo().ClearOffsetRange(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Heights computes the heights of all AST expressions and returns a map from expression id to height.
|
||||
func Heights(a *AST) map[int64]int {
|
||||
visitor := make(heightVisitor)
|
||||
@@ -209,6 +239,11 @@ func CopySourceInfo(info *SourceInfo) *SourceInfo {
|
||||
for id, call := range info.macroCalls {
|
||||
callsCopy[id] = defaultFactory.CopyExpr(call)
|
||||
}
|
||||
var extCopy []Extension
|
||||
if len(info.extensions) > 0 {
|
||||
extCopy = make([]Extension, len(info.extensions))
|
||||
copy(extCopy, info.extensions)
|
||||
}
|
||||
return &SourceInfo{
|
||||
syntax: info.syntax,
|
||||
desc: info.desc,
|
||||
@@ -217,6 +252,7 @@ func CopySourceInfo(info *SourceInfo) *SourceInfo {
|
||||
baseCol: info.baseCol,
|
||||
offsetRanges: rangesCopy,
|
||||
macroCalls: callsCopy,
|
||||
extensions: extCopy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +266,26 @@ type SourceInfo struct {
|
||||
baseCol int32
|
||||
offsetRanges map[int64]OffsetRange
|
||||
macroCalls map[int64]Expr
|
||||
|
||||
// extensions indicate versioned optional features which affect the execution of one or more CEL component.
|
||||
extensions []Extension
|
||||
}
|
||||
|
||||
// RenumberIDs performs an in-place update of the expression IDs within the SourceInfo.
|
||||
func (s *SourceInfo) RenumberIDs(idGen IDGenerator) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
oldIDs := []int64{}
|
||||
for id := range s.offsetRanges {
|
||||
oldIDs = append(oldIDs, id)
|
||||
}
|
||||
slices.Sort(oldIDs)
|
||||
newRanges := make(map[int64]OffsetRange)
|
||||
for _, id := range oldIDs {
|
||||
newRanges[idGen(id)] = s.offsetRanges[id]
|
||||
}
|
||||
s.offsetRanges = newRanges
|
||||
}
|
||||
|
||||
// SyntaxVersion returns the syntax version associated with the text expression.
|
||||
@@ -365,6 +421,12 @@ func (s *SourceInfo) ComputeOffset(line, col int32) int32 {
|
||||
line = s.baseLine + line
|
||||
col = s.baseCol + col
|
||||
}
|
||||
return s.ComputeOffsetAbsolute(line, col)
|
||||
}
|
||||
|
||||
// ComputeOffsetAbsolute calculates the 0-based character offset from a 1-based line and 0-based column
|
||||
// based on the absolute line and column of the SourceInfo.
|
||||
func (s *SourceInfo) ComputeOffsetAbsolute(line, col int32) int32 {
|
||||
if line == 1 {
|
||||
return col
|
||||
}
|
||||
@@ -375,6 +437,34 @@ func (s *SourceInfo) ComputeOffset(line, col int32) int32 {
|
||||
return offset + col
|
||||
}
|
||||
|
||||
// Extensions returns the set of extensions present in the source.
|
||||
func (s *SourceInfo) Extensions() []Extension {
|
||||
var extensions []Extension
|
||||
if s == nil {
|
||||
return extensions
|
||||
}
|
||||
return s.extensions
|
||||
}
|
||||
|
||||
// HasExtension returns whether the source info contains the extension which satisfies the minimum version requirement.
|
||||
//
|
||||
// For an extension to be considered 'present' it must have the same major version as the minVersion and a minor version
|
||||
// at least as great as the lowest minor version specified.
|
||||
func (s *SourceInfo) HasExtension(id string, minVersion ExtensionVersion) bool {
|
||||
for _, ext := range s.Extensions() {
|
||||
return ext.ID == id && ext.Version.Major == minVersion.Major && ext.Version.Minor >= minVersion.Minor
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddExtension adds an extension record into the SourceInfo.
|
||||
func (s *SourceInfo) AddExtension(ext Extension) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.extensions = append(s.extensions, ext)
|
||||
}
|
||||
|
||||
// OffsetRange captures the start and stop positions of a section of text in the input expression.
|
||||
type OffsetRange struct {
|
||||
Start int32
|
||||
@@ -444,6 +534,53 @@ func (r *ReferenceInfo) Equals(other *ReferenceInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NewExtension creates an Extension to be recorded on the SourceInfo.
|
||||
func NewExtension(id string, version ExtensionVersion, components ...ExtensionComponent) Extension {
|
||||
return Extension{
|
||||
ID: id,
|
||||
Version: version,
|
||||
Components: components,
|
||||
}
|
||||
}
|
||||
|
||||
// Extension represents a versioned, optional feature present in the AST that affects CEL component behavior.
|
||||
type Extension struct {
|
||||
// ID indicates the unique name of the extension.
|
||||
ID string
|
||||
// Version indicates the major / minor version.
|
||||
Version ExtensionVersion
|
||||
// Components enumerates the CEL components affected by the feature.
|
||||
Components []ExtensionComponent
|
||||
}
|
||||
|
||||
// NewExtensionVersion creates a new extension version with a major, minor version.
|
||||
func NewExtensionVersion(major, minor int64) ExtensionVersion {
|
||||
return ExtensionVersion{Major: major, Minor: minor}
|
||||
}
|
||||
|
||||
// ExtensionVersion represents a semantic version with a major and minor number.
|
||||
type ExtensionVersion struct {
|
||||
// Major version of the extension.
|
||||
// All versions with the same major number are expected to be compatible with all minor version changes.
|
||||
Major int64
|
||||
|
||||
// Minor version of the extension which indicates that some small non-semantic change has been made to
|
||||
// the extension.
|
||||
Minor int64
|
||||
}
|
||||
|
||||
// ExtensionComponent indicates which CEL component is affected.
|
||||
type ExtensionComponent int
|
||||
|
||||
const (
|
||||
// ComponentParser means the feature affects expression parsing.
|
||||
ComponentParser ExtensionComponent = iota + 1
|
||||
// ComponentTypeChecker means the feature affects type-checking.
|
||||
ComponentTypeChecker
|
||||
// ComponentRuntime alters program planning or evaluation of the AST.
|
||||
ComponentRuntime
|
||||
)
|
||||
|
||||
type maxIDVisitor struct {
|
||||
maxID int64
|
||||
*baseVisitor
|
||||
@@ -533,3 +670,13 @@ func (hv heightVisitor) maxEntryHeight(entries ...EntryExpr) int {
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
type idVisitor map[int64]bool
|
||||
|
||||
func (v idVisitor) VisitExpr(e Expr) {
|
||||
v[e.ID()] = true
|
||||
}
|
||||
|
||||
func (v idVisitor) VisitEntryExpr(e EntryExpr) {
|
||||
v[e.ID()] = true
|
||||
}
|
||||
|
||||
+49
@@ -27,6 +27,19 @@ import (
|
||||
structpb "google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
var (
|
||||
pbComponentMap = map[exprpb.SourceInfo_Extension_Component]ExtensionComponent{
|
||||
exprpb.SourceInfo_Extension_COMPONENT_PARSER: ComponentParser,
|
||||
exprpb.SourceInfo_Extension_COMPONENT_TYPE_CHECKER: ComponentTypeChecker,
|
||||
exprpb.SourceInfo_Extension_COMPONENT_RUNTIME: ComponentRuntime,
|
||||
}
|
||||
componentPBMap = map[ExtensionComponent]exprpb.SourceInfo_Extension_Component{
|
||||
ComponentParser: exprpb.SourceInfo_Extension_COMPONENT_PARSER,
|
||||
ComponentTypeChecker: exprpb.SourceInfo_Extension_COMPONENT_TYPE_CHECKER,
|
||||
ComponentRuntime: exprpb.SourceInfo_Extension_COMPONENT_RUNTIME,
|
||||
}
|
||||
)
|
||||
|
||||
// ToProto converts an AST to a CheckedExpr protobouf.
|
||||
func ToProto(ast *AST) (*exprpb.CheckedExpr, error) {
|
||||
refMap := make(map[int64]*exprpb.Reference, len(ast.ReferenceMap()))
|
||||
@@ -534,6 +547,25 @@ func SourceInfoToProto(info *SourceInfo) (*exprpb.SourceInfo, error) {
|
||||
}
|
||||
sourceInfo.MacroCalls[id] = call
|
||||
}
|
||||
for _, ext := range info.Extensions() {
|
||||
var components []exprpb.SourceInfo_Extension_Component
|
||||
for _, c := range ext.Components {
|
||||
comp, found := componentPBMap[c]
|
||||
if found {
|
||||
components = append(components, comp)
|
||||
}
|
||||
}
|
||||
ver := &exprpb.SourceInfo_Extension_Version{
|
||||
Major: ext.Version.Major,
|
||||
Minor: ext.Version.Minor,
|
||||
}
|
||||
pbExt := &exprpb.SourceInfo_Extension{
|
||||
Id: ext.ID,
|
||||
Version: ver,
|
||||
AffectedComponents: components,
|
||||
}
|
||||
sourceInfo.Extensions = append(sourceInfo.Extensions, pbExt)
|
||||
}
|
||||
return sourceInfo, nil
|
||||
}
|
||||
|
||||
@@ -556,6 +588,23 @@ func ProtoToSourceInfo(info *exprpb.SourceInfo) (*SourceInfo, error) {
|
||||
}
|
||||
sourceInfo.SetMacroCall(id, call)
|
||||
}
|
||||
for _, pbExt := range info.GetExtensions() {
|
||||
var components []ExtensionComponent
|
||||
for _, c := range pbExt.GetAffectedComponents() {
|
||||
comp, found := pbComponentMap[*c.Enum()]
|
||||
if found {
|
||||
components = append(components, comp)
|
||||
}
|
||||
}
|
||||
sourceInfo.AddExtension(NewExtension(
|
||||
pbExt.GetId(),
|
||||
NewExtensionVersion(
|
||||
pbExt.GetVersion().GetMajor(),
|
||||
pbExt.GetVersion().GetMinor(),
|
||||
),
|
||||
components...,
|
||||
))
|
||||
}
|
||||
return sourceInfo, nil
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -181,6 +181,29 @@ func PreOrderVisit(expr Expr, visitor Visitor) {
|
||||
visit(expr, visitor, preOrder, 0, 0)
|
||||
}
|
||||
|
||||
// ExceedsDepth determines whether the AST contains expressions nested deeper than the specified
|
||||
// maxDepth. The root expression has depth 0, so a maxDepth of 250 permits expressions nested up
|
||||
// to and including 250 levels deep.
|
||||
//
|
||||
// The traversal is bounded: it descends at most maxDepth+1 levels, so it remains safe to call on
|
||||
// adversarially deep inputs that could otherwise exhaust the Go stack during later checking or
|
||||
// planning. A non-positive maxDepth disables the check and returns false.
|
||||
func ExceedsDepth(a *AST, maxDepth int) bool {
|
||||
if a == nil || maxDepth <= 0 {
|
||||
return false
|
||||
}
|
||||
exceedsDepth := false
|
||||
visitor := NewExprVisitor(func(e Expr) {
|
||||
if nav, ok := e.(NavigableExpr); ok && nav.Depth() >= maxDepth {
|
||||
exceedsDepth = true
|
||||
}
|
||||
})
|
||||
// Bound the walk to maxDepth+1 levels so it never recurses past the first level that exceeds
|
||||
// the limit, keeping the check itself safe on the deep inputs it guards against.
|
||||
visit(NavigateAST(a), visitor, postOrder, 0, maxDepth+1)
|
||||
return exceedsDepth
|
||||
}
|
||||
|
||||
type visitOrder int
|
||||
|
||||
const (
|
||||
|
||||
+8
-7
@@ -227,7 +227,7 @@ func Abbrevs(qualifiedNames ...string) ContainerOption {
|
||||
}
|
||||
alias := qn[ind+1:]
|
||||
var err error
|
||||
c, err = aliasAs("abbreviation", qn, alias)(c)
|
||||
c, err = aliasAs("abbreviation", qn, alias, true)(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,31 +236,32 @@ func Abbrevs(qualifiedNames ...string) ContainerOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Alias associates a fully-qualified name with a user-defined alias.
|
||||
// Alias associates a name with a user-defined alias.
|
||||
//
|
||||
// In general, Abbrevs is preferred to Alias since the names generated from the Abbrevs option
|
||||
// are more easily traced back to source code. The Alias option is useful for propagating alias
|
||||
// configuration from one Container instance to another, and may also be useful for remapping
|
||||
// poorly chosen protobuf message / package names.
|
||||
//
|
||||
// Note: all of the rules that apply to Abbrevs also apply to Alias.
|
||||
func Alias(qualifiedName, alias string) ContainerOption {
|
||||
return aliasAs("alias", qualifiedName, alias)
|
||||
return aliasAs("alias", qualifiedName, alias, false)
|
||||
}
|
||||
|
||||
func aliasAs(kind, qualifiedName, alias string) ContainerOption {
|
||||
func aliasAs(kind, qualifiedName, alias string, requireQualified bool) ContainerOption {
|
||||
return func(c *Container) (*Container, error) {
|
||||
if len(alias) == 0 || strings.Contains(alias, ".") {
|
||||
return nil, fmt.Errorf(
|
||||
"%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias)
|
||||
}
|
||||
|
||||
if len(qualifiedName) == 0 {
|
||||
return nil, fmt.Errorf("%s must refer to a valid name: %s", kind, qualifiedName)
|
||||
}
|
||||
if qualifiedName[0:1] == "." {
|
||||
return nil, fmt.Errorf("qualified name must not begin with a leading '.': %s",
|
||||
qualifiedName)
|
||||
}
|
||||
ind := strings.LastIndex(qualifiedName, ".")
|
||||
if ind <= 0 || ind == len(qualifiedName)-1 {
|
||||
if ind == len(qualifiedName)-1 || (requireQualified && ind <= 0) {
|
||||
return nil, fmt.Errorf("%s must refer to a valid qualified name: %s",
|
||||
kind, qualifiedName)
|
||||
}
|
||||
|
||||
+15
@@ -312,3 +312,18 @@ func (w *debugWriter) removeIndent() {
|
||||
func (w *debugWriter) String() string {
|
||||
return w.buffer.String()
|
||||
}
|
||||
|
||||
type idAdorner struct{}
|
||||
|
||||
func (a *idAdorner) GetMetadata(elem any) string {
|
||||
e, isExpr := elem.(ast.Expr)
|
||||
if !isExpr {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("@id:%d ", e.ID())
|
||||
}
|
||||
|
||||
// ToDebugStringWithIDs returns a string representation with AST node IDs.
|
||||
func ToDebugStringWithIDs(e ast.Expr) string {
|
||||
return ToAdornedDebugString(e, &idAdorner{})
|
||||
}
|
||||
|
||||
+105
-11
@@ -16,6 +16,7 @@
|
||||
package decls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -270,7 +271,7 @@ func (f *FunctionDecl) AddOverload(overload *OverloadDecl) error {
|
||||
if oID == overload.ID() {
|
||||
if o.SignatureEquals(overload) && o.IsNonStrict() == overload.IsNonStrict() {
|
||||
// Allow redefinition of an overload implementation so long as the signatures match.
|
||||
if overload.hasBinding() {
|
||||
if overload.HasBinding() {
|
||||
f.overloads[oID] = overload
|
||||
}
|
||||
// Allow redefinition of the doc string.
|
||||
@@ -303,13 +304,25 @@ func (f *FunctionDecl) OverloadDecls() []*OverloadDecl {
|
||||
return overloads
|
||||
}
|
||||
|
||||
// HasSingletonBinding indicates whether the function has a singleton binding definition.
|
||||
func (f *FunctionDecl) HasSingletonBinding() bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
return f.singleton != nil
|
||||
}
|
||||
|
||||
// HasLateBinding returns true if the function has late bindings. A function cannot mix late bindings with other bindings.
|
||||
func (f *FunctionDecl) HasLateBinding() bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
if f.singleton != nil && f.singleton.Async != nil {
|
||||
return true
|
||||
}
|
||||
for _, oID := range f.overloadOrdinals {
|
||||
if f.overloads[oID].HasLateBinding() {
|
||||
o := f.overloads[oID]
|
||||
if o.HasLateBinding() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -328,12 +341,13 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) {
|
||||
for _, oID := range f.overloadOrdinals {
|
||||
o := f.overloads[oID]
|
||||
hasLateBinding = hasLateBinding || o.HasLateBinding()
|
||||
if o.hasBinding() {
|
||||
if o.HasBinding() {
|
||||
overload := &functions.Overload{
|
||||
Operator: o.ID(),
|
||||
Unary: o.guardedUnaryOp(f.Name(), f.disableTypeGuards),
|
||||
Binary: o.guardedBinaryOp(f.Name(), f.disableTypeGuards),
|
||||
Function: o.guardedFunctionOp(f.Name(), f.disableTypeGuards),
|
||||
Async: o.guardedAsyncOp(f.Name(), f.disableTypeGuards),
|
||||
OperandTrait: o.OperandTrait(),
|
||||
NonStrict: o.IsNonStrict(),
|
||||
}
|
||||
@@ -354,6 +368,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) {
|
||||
Unary: f.singleton.Unary,
|
||||
Binary: f.singleton.Binary,
|
||||
Function: f.singleton.Function,
|
||||
Async: f.singleton.Async,
|
||||
OperandTrait: f.singleton.OperandTrait,
|
||||
},
|
||||
}
|
||||
@@ -372,6 +387,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) {
|
||||
Unary: overloads[0].Unary,
|
||||
Binary: overloads[0].Binary,
|
||||
Function: overloads[0].Function,
|
||||
Async: overloads[0].Async,
|
||||
NonStrict: overloads[0].NonStrict,
|
||||
OperandTrait: overloads[0].OperandTrait,
|
||||
}), nil
|
||||
@@ -530,6 +546,30 @@ func SingletonFunctionBinding(fn functions.FunctionOp, traits ...int) FunctionOp
|
||||
}
|
||||
}
|
||||
|
||||
// SingletonAsyncBinding creates a singleton async function definition to be used with all function overloads.
|
||||
// The provided function is called in its own goroutine with the provided context. The function should
|
||||
// block until the result is available, and the framework manages goroutine and channel lifecycle.
|
||||
//
|
||||
// Note, this approach works well if operand is expected to have a specific trait which it implements,
|
||||
// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings.
|
||||
func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt {
|
||||
trait := 0
|
||||
for _, t := range traits {
|
||||
trait = trait | t
|
||||
}
|
||||
return func(f *FunctionDecl) (*FunctionDecl, error) {
|
||||
if f.singleton != nil {
|
||||
return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name())
|
||||
}
|
||||
f.singleton = &functions.Overload{
|
||||
Operator: f.Name(),
|
||||
Async: wrapAsyncOp(fn),
|
||||
OperandTrait: trait,
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Overload defines a new global overload with an overload id, argument types, and result type. Through the
|
||||
// use of OverloadOpt options, the overload may also be configured with a binding, an operand trait, and to
|
||||
// be non-strict.
|
||||
@@ -614,6 +654,8 @@ type OverloadDecl struct {
|
||||
binaryOp functions.BinaryOp
|
||||
// functionOp is a catch-all for zero-arity and three-plus arity functions.
|
||||
functionOp functions.FunctionOp
|
||||
// asyncOp is an asynchronous function binding that returns a channel.
|
||||
asyncOp functions.AsyncOp
|
||||
}
|
||||
|
||||
// Examples returns a list of string examples for the overload.
|
||||
@@ -669,7 +711,7 @@ func (o *OverloadDecl) HasLateBinding() bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
return o.hasLateBinding
|
||||
return o.hasLateBinding || o.asyncOp != nil
|
||||
}
|
||||
|
||||
// OperandTrait returns the trait mask of the first operand to the overload call, e.g.
|
||||
@@ -740,9 +782,9 @@ func (o *OverloadDecl) SignatureOverlaps(other *OverloadDecl) bool {
|
||||
return argsOverlap
|
||||
}
|
||||
|
||||
// hasBinding indicates whether the overload already has a definition.
|
||||
func (o *OverloadDecl) hasBinding() bool {
|
||||
return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil)
|
||||
// HasBinding indicates whether the overload already has a definition.
|
||||
func (o *OverloadDecl) HasBinding() bool {
|
||||
return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil || o.asyncOp != nil)
|
||||
}
|
||||
|
||||
// guardedUnaryOp creates an invocation guard around the provided unary operator, if one is defined.
|
||||
@@ -784,6 +826,22 @@ func (o *OverloadDecl) guardedFunctionOp(funcName string, disableTypeGuards bool
|
||||
}
|
||||
}
|
||||
|
||||
// guardedAsyncOp creates an invocation guard around the provided async function binding, if one is provided.
|
||||
func (o *OverloadDecl) guardedAsyncOp(funcName string, disableTypeGuards bool) functions.AsyncOp {
|
||||
if o.asyncOp == nil {
|
||||
return nil
|
||||
}
|
||||
return func(ctx context.Context, args ...ref.Val) <-chan ref.Val {
|
||||
if !o.matchesRuntimeSignature(disableTypeGuards, args...) {
|
||||
ch := make(chan ref.Val, 1)
|
||||
ch <- MaybeNoSuchOverload(funcName, args...)
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
return o.asyncOp(ctx, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// matchesRuntimeUnarySignature indicates whether the argument type is runtime assiganble to the overload's expected argument.
|
||||
func (o *OverloadDecl) matchesRuntimeUnarySignature(disableTypeGuards bool, arg ref.Val) bool {
|
||||
return matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[0], arg) &&
|
||||
@@ -817,6 +875,8 @@ func matchRuntimeArgType(nonStrict, disableTypeGuards bool, argType *types.Type,
|
||||
if nonStrict && (disableTypeGuards || types.IsUnknownOrError(arg)) {
|
||||
return true
|
||||
}
|
||||
// Note, early returns and unknown aggregation happen in the interpretable.go file; however, this check is here
|
||||
// for defense in depth or for scenarios where someone manipulates bindings to offer their own dispatch logic.
|
||||
if types.IsUnknownOrError(arg) {
|
||||
return false
|
||||
}
|
||||
@@ -842,7 +902,7 @@ func OverloadExamples(examples ...string) OverloadOpt {
|
||||
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
|
||||
func UnaryBinding(binding functions.UnaryOp) OverloadOpt {
|
||||
return func(o *OverloadDecl) (*OverloadDecl, error) {
|
||||
if o.hasBinding() {
|
||||
if o.HasBinding() {
|
||||
return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
|
||||
}
|
||||
if len(o.ArgTypes()) != 1 {
|
||||
@@ -860,7 +920,7 @@ func UnaryBinding(binding functions.UnaryOp) OverloadOpt {
|
||||
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
|
||||
func BinaryBinding(binding functions.BinaryOp) OverloadOpt {
|
||||
return func(o *OverloadDecl) (*OverloadDecl, error) {
|
||||
if o.hasBinding() {
|
||||
if o.HasBinding() {
|
||||
return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
|
||||
}
|
||||
if len(o.ArgTypes()) != 2 {
|
||||
@@ -878,7 +938,7 @@ func BinaryBinding(binding functions.BinaryOp) OverloadOpt {
|
||||
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
|
||||
func FunctionBinding(binding functions.FunctionOp) OverloadOpt {
|
||||
return func(o *OverloadDecl) (*OverloadDecl, error) {
|
||||
if o.hasBinding() {
|
||||
if o.HasBinding() {
|
||||
return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
|
||||
}
|
||||
if o.hasLateBinding {
|
||||
@@ -889,11 +949,45 @@ func FunctionBinding(binding functions.FunctionOp) OverloadOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncBinding provides the implementation of an asynchronous overload. The provided function
|
||||
// is called in its own goroutine with the provided context. The function should block until
|
||||
// the result is available, and the framework manages goroutine and channel lifecycle.
|
||||
//
|
||||
// This follows the same pattern used by gRPC-Go and other major Go frameworks where user
|
||||
// code is synchronous and the framework manages concurrency.
|
||||
func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt {
|
||||
return func(o *OverloadDecl) (*OverloadDecl, error) {
|
||||
if o.HasBinding() {
|
||||
return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
|
||||
}
|
||||
if o.hasLateBinding {
|
||||
return nil, fmt.Errorf("overload already has a late binding: %s", o.ID())
|
||||
}
|
||||
o.asyncOp = wrapAsyncOp(fn)
|
||||
return o, nil
|
||||
}
|
||||
}
|
||||
|
||||
// wrapAsyncOp adapts a blocking function into the channel-based AsyncOp used internally.
|
||||
//
|
||||
// The blocking function is invoked synchronously and its result delivered on a buffered channel.
|
||||
// The interpreter always invokes an AsyncOp from a dedicated goroutine, so running the blocking
|
||||
// call inline here keeps the framework to a single goroutine per async call rather than spawning
|
||||
// an additional one to bridge blocking-to-channel.
|
||||
func wrapAsyncOp(fn functions.BlockingAsyncOp) functions.AsyncOp {
|
||||
return func(ctx context.Context, args ...ref.Val) <-chan ref.Val {
|
||||
ch := make(chan ref.Val, 1)
|
||||
ch <- fn(ctx, args...)
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
// LateFunctionBinding indicates that the function has a binding which is not known at compile time.
|
||||
// This is useful for functions which have side-effects or are not deterministically computable.
|
||||
func LateFunctionBinding() OverloadOpt {
|
||||
return func(o *OverloadDecl) (*OverloadDecl, error) {
|
||||
if o.hasBinding() {
|
||||
if o.HasBinding() {
|
||||
return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
|
||||
}
|
||||
o.hasLateBinding = true
|
||||
|
||||
+13
@@ -37,6 +37,8 @@ const (
|
||||
DocMacro
|
||||
// DocExample represents example documentation.
|
||||
DocExample
|
||||
// DocField represents documentation for a struct field.
|
||||
DocField
|
||||
)
|
||||
|
||||
// Doc holds the documentation details for a specific program element like
|
||||
@@ -163,6 +165,17 @@ func NewExampleDoc(ex string) *Doc {
|
||||
}
|
||||
}
|
||||
|
||||
// NewFieldDoc creates a new Doc struct for documenting a struct field.
|
||||
func NewFieldDoc(name, celType, description string, examples ...*Doc) *Doc {
|
||||
return &Doc{
|
||||
Kind: DocField,
|
||||
Name: name,
|
||||
Type: celType,
|
||||
Description: description,
|
||||
Children: examples,
|
||||
}
|
||||
}
|
||||
|
||||
// Documentor is an interface for types that can provide their own documentation.
|
||||
type Documentor interface {
|
||||
// Documentation returns the documentation coded by the DocKind to assist
|
||||
|
||||
+5
-1
@@ -23,12 +23,14 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"env.go",
|
||||
"io.go",
|
||||
],
|
||||
importpath = "github.com/google/cel-go/common/env",
|
||||
deps = [
|
||||
"//common:go_default_library",
|
||||
"//common/decls:go_default_library",
|
||||
"//common/types:go_default_library",
|
||||
"@in_yaml_go_yaml_v3//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -37,6 +39,7 @@ go_test(
|
||||
size = "small",
|
||||
srcs = [
|
||||
"env_test.go",
|
||||
"io_test.go",
|
||||
],
|
||||
data = glob(["testdata/**"]),
|
||||
embed = [":go_default_library"],
|
||||
@@ -45,6 +48,7 @@ go_test(
|
||||
"//common/operators:go_default_library",
|
||||
"//common/overloads:go_default_library",
|
||||
"//common/types:go_default_library",
|
||||
"@in_gopkg_yaml_v3//:go_default_library",
|
||||
"@com_github_google_go_cmp//cmp:go_default_library",
|
||||
"@in_yaml_go_yaml_v3//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
+117
-2
@@ -50,6 +50,7 @@ type Config struct {
|
||||
Functions []*Function `yaml:"functions,omitempty"`
|
||||
Validators []*Validator `yaml:"validators,omitempty"`
|
||||
Features []*Feature `yaml:"features,omitempty"`
|
||||
Limits []*Limit `yaml:"limits,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates the whole configuration is well-formed.
|
||||
@@ -92,6 +93,11 @@ func (c *Config) Validate() error {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, limit := range c.Limits {
|
||||
if err := limit.Validate(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, val := range c.Validators {
|
||||
if err := val.Validate(); err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -122,7 +128,7 @@ func (c *Config) AddVariableDecls(vars ...*decls.VariableDecl) *Config {
|
||||
return c.AddVariables(convVars...)
|
||||
}
|
||||
|
||||
// AddVariables adds one or more vairables to the config.
|
||||
// AddVariables adds one or more variables to the config.
|
||||
func (c *Config) AddVariables(vars ...*Variable) *Config {
|
||||
c.Variables = append(c.Variables, vars...)
|
||||
return c
|
||||
@@ -206,6 +212,12 @@ func (c *Config) AddFeatures(feats ...*Feature) *Config {
|
||||
return c
|
||||
}
|
||||
|
||||
// AddLimits appends one or more limits to the config.
|
||||
func (c *Config) AddLimits(limits ...*Limit) *Config {
|
||||
c.Limits = append(c.Limits, limits...)
|
||||
return c
|
||||
}
|
||||
|
||||
// NewImport returns a serializable import value from the qualified type name.
|
||||
func NewImport(name string) *Import {
|
||||
return &Import{Name: name}
|
||||
@@ -246,7 +258,9 @@ type Variable struct {
|
||||
|
||||
// Type represents the type declaration for the variable.
|
||||
//
|
||||
// Deprecated: use the embedded *TypeDesc fields directly.
|
||||
// When serialized, 'type' is used for shorthand specifier string.
|
||||
//
|
||||
// Use GetType() for getting the effective type.
|
||||
Type *TypeDesc `yaml:"type,omitempty"`
|
||||
|
||||
// TypeDesc is an embedded set of fields allowing for the specification of the Variable type.
|
||||
@@ -264,6 +278,9 @@ func (v *Variable) Validate() error {
|
||||
if err := v.GetType().Validate(); err != nil {
|
||||
return fmt.Errorf("invalid variable %q: %w", v.Name, err)
|
||||
}
|
||||
if v.GetType().IsTypeParam {
|
||||
return fmt.Errorf("invalid variable %q: variables cannot be type parameters", v.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -734,6 +751,29 @@ func (feat *Feature) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Limit represents a named limit in the CEL environment. This is used to control
|
||||
// the complexity tolerated before failing parsing, type checking, or planning.
|
||||
type Limit struct {
|
||||
Name string `yaml:"name"`
|
||||
Value int `yaml:"value"`
|
||||
}
|
||||
|
||||
// NewLimit creates a new limit.
|
||||
func NewLimit(name string, value int) *Limit {
|
||||
return &Limit{name, value}
|
||||
}
|
||||
|
||||
// Validate validates a limit.
|
||||
func (l *Limit) Validate() error {
|
||||
if l == nil {
|
||||
return errors.New("invalid limit: nil")
|
||||
}
|
||||
if l.Name == "" {
|
||||
return errors.New("invalid limit: missing name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTypeDesc describes a simple or complex type with parameters.
|
||||
func NewTypeDesc(typeName string, params ...*TypeDesc) *TypeDesc {
|
||||
return &TypeDesc{TypeName: typeName, Params: params}
|
||||
@@ -796,11 +836,47 @@ func (td *TypeDesc) Validate() error {
|
||||
return fmt.Errorf("invalid type: optional_type expects 1 parameter, got %d", len(td.Params))
|
||||
}
|
||||
return td.Params[0].Validate()
|
||||
case "type":
|
||||
if len(td.Params) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(td.Params) != 1 {
|
||||
return fmt.Errorf("invalid type: type expects 0 or 1 parameters, got %d", len(td.Params))
|
||||
}
|
||||
return td.Params[0].Validate()
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatSpecifierImpl(td *TypeDesc, sb *strings.Builder) {
|
||||
if td.IsTypeParam {
|
||||
sb.WriteRune('~')
|
||||
sb.WriteString(td.TypeName)
|
||||
return
|
||||
}
|
||||
sb.WriteString(td.TypeName)
|
||||
l := len(td.Params)
|
||||
if l < 1 {
|
||||
return
|
||||
}
|
||||
sb.WriteRune('<')
|
||||
for i, p := range td.Params {
|
||||
formatSpecifierImpl(p, sb)
|
||||
if i < l-1 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
}
|
||||
sb.WriteRune('>')
|
||||
}
|
||||
|
||||
// SpecifierFormat returns the short text representation of the type. e.g. "map<string, int>"
|
||||
func (td *TypeDesc) SpecifierFormat() string {
|
||||
var sb strings.Builder
|
||||
formatSpecifierImpl(td, &sb)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// AsCELType converts the serializable object to a *types.Type value.
|
||||
func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) {
|
||||
err := td.Validate()
|
||||
@@ -810,6 +886,27 @@ func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) {
|
||||
switch td.TypeName {
|
||||
case "dyn":
|
||||
return types.DynType, nil
|
||||
// short aliases for WKTs
|
||||
case "duration":
|
||||
return types.DurationType, nil
|
||||
case "timestamp":
|
||||
return types.TimestampType, nil
|
||||
case "any":
|
||||
return types.AnyType, nil
|
||||
case "null", "null_type":
|
||||
return types.NullType, nil
|
||||
case "bool_wrapper":
|
||||
return types.NewNullableType(types.BoolType), nil
|
||||
case "bytes_wrapper":
|
||||
return types.NewNullableType(types.BytesType), nil
|
||||
case "double_wrapper":
|
||||
return types.NewNullableType(types.DoubleType), nil
|
||||
case "int_wrapper":
|
||||
return types.NewNullableType(types.IntType), nil
|
||||
case "uint_wrapper":
|
||||
return types.NewNullableType(types.UintType), nil
|
||||
case "string_wrapper":
|
||||
return types.NewNullableType(types.StringType), nil
|
||||
case "map":
|
||||
kt, err := td.Params[0].AsCELType(tp)
|
||||
if err != nil {
|
||||
@@ -832,6 +929,15 @@ func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) {
|
||||
return nil, err
|
||||
}
|
||||
return types.NewOptionalType(et), nil
|
||||
case "type":
|
||||
if len(td.Params) == 0 {
|
||||
return types.TypeType, nil
|
||||
}
|
||||
pt, err := td.Params[0].AsCELType(tp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types.NewTypeTypeWithParam(pt), nil
|
||||
default:
|
||||
if td.IsTypeParam {
|
||||
return types.NewTypeParamType(td.TypeName), nil
|
||||
@@ -874,6 +980,15 @@ func SerializeTypeDesc(t *types.Type) *TypeDesc {
|
||||
for _, p := range t.Parameters() {
|
||||
params = append(params, SerializeTypeDesc(p))
|
||||
}
|
||||
// Special types, these aren't useful for describing environments.
|
||||
switch t.Kind() {
|
||||
case types.ErrorKind:
|
||||
typeName = "*error*"
|
||||
case types.UnknownKind:
|
||||
typeName = "*unknown*"
|
||||
case types.UnspecifiedKind:
|
||||
typeName = "*unspecified type*"
|
||||
}
|
||||
return NewTypeDesc(typeName, params...)
|
||||
}
|
||||
|
||||
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
// Copyright 2026 Google LLC
|
||||
//
|
||||
// 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 env
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
type internalTypeDesc struct {
|
||||
TypeName string `yaml:"type_name"`
|
||||
Params []*TypeDesc `yaml:"params,omitempty"`
|
||||
IsTypeParam bool `yaml:"is_type_param,omitempty"`
|
||||
}
|
||||
|
||||
// Embedding TypeDesc in variable causes issues with customizing
|
||||
// unmarshalling / marshalling. Work around with a parallel type.
|
||||
type internalVariable struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
|
||||
// Type represents the type declaration for the variable.
|
||||
Type *TypeDesc `yaml:"type,omitempty"`
|
||||
|
||||
TypeName string `yaml:"type_name"`
|
||||
Params []*TypeDesc `yaml:"params,omitempty"`
|
||||
IsTypeParam bool `yaml:"is_type_param,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshal
|
||||
func (v *Variable) UnmarshalYAML(n *yaml.Node) error {
|
||||
buf := internalVariable{}
|
||||
err := n.Decode(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Name = buf.Name
|
||||
v.Description = buf.Description
|
||||
if buf.TypeName != "" {
|
||||
v.TypeDesc = &TypeDesc{
|
||||
TypeName: buf.TypeName,
|
||||
Params: buf.Params,
|
||||
IsTypeParam: buf.IsTypeParam,
|
||||
}
|
||||
} else if buf.Type != nil {
|
||||
v.TypeDesc = buf.Type
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements yaml.Marshaler
|
||||
func (v *Variable) MarshalYAML() (any, error) {
|
||||
// The presence of an unmarshaller alters the default marshaller behavior so
|
||||
// provide a simple marshal implementation.
|
||||
buf := internalVariable{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
}
|
||||
if t := v.GetType(); t != nil {
|
||||
buf.TypeName = t.TypeName
|
||||
buf.Params = t.Params
|
||||
buf.IsTypeParam = t.IsTypeParam
|
||||
}
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler
|
||||
func (td *TypeDesc) UnmarshalYAML(n *yaml.Node) error {
|
||||
if td == nil {
|
||||
return fmt.Errorf("unexpected Unmarshal for TypeDesc at: %d", n.Line)
|
||||
}
|
||||
if n.Kind == yaml.ScalarNode {
|
||||
o, err := ParseTypeDesc(n.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*td = *o
|
||||
return nil
|
||||
}
|
||||
|
||||
if n.Kind != yaml.MappingNode {
|
||||
return errors.New("unsupported yaml for TypeDesc")
|
||||
}
|
||||
|
||||
buf := internalTypeDesc{}
|
||||
err := n.Decode(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
td.TypeName = buf.TypeName
|
||||
td.Params = buf.Params
|
||||
td.IsTypeParam = buf.IsTypeParam
|
||||
return nil
|
||||
}
|
||||
|
||||
type typeDescParser struct {
|
||||
text string
|
||||
pos int
|
||||
length int
|
||||
}
|
||||
|
||||
// ParseTypeDesc parses a TypeDesc from the type specifier format: "map<string, int>"
|
||||
func ParseTypeDesc(text string) (*TypeDesc, error) {
|
||||
p := &typeDescParser{text: text, length: len(text)}
|
||||
res, err := p.parseTypeElem()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse type %q: %v", text, err)
|
||||
}
|
||||
p.skipWhitespace()
|
||||
if p.pos < p.length {
|
||||
return nil, fmt.Errorf("unexpected character %q at position %d in %q", p.text[p.pos], p.pos, text)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *typeDescParser) parseConcreteType() (*TypeDesc, error) {
|
||||
id, err := p.parseNamespaceIdentifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.pos < p.length && p.text[p.pos] == '<' {
|
||||
p.pos++ // consume '<'
|
||||
var params []*TypeDesc
|
||||
for {
|
||||
p.skipWhitespace()
|
||||
param, err := p.parseTypeElem()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, param)
|
||||
p.skipWhitespace()
|
||||
if p.pos < p.length && p.text[p.pos] == ',' {
|
||||
p.pos++ // consume ','
|
||||
continue
|
||||
}
|
||||
if p.pos < p.length && p.text[p.pos] == '>' {
|
||||
p.pos++ // consume '>'
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("expected ',' or '>' at position %d", p.pos)
|
||||
}
|
||||
return NewTypeDesc(id, params...), nil
|
||||
}
|
||||
return NewTypeDesc(id), nil
|
||||
}
|
||||
|
||||
func (p *typeDescParser) parseTypeElem() (*TypeDesc, error) {
|
||||
p.skipWhitespace()
|
||||
if p.pos < p.length && p.text[p.pos] == '~' {
|
||||
p.pos++ // consume '~'
|
||||
id, err := p.parseTypeParamIdent()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTypeParam(id), nil
|
||||
}
|
||||
return p.parseConcreteType()
|
||||
}
|
||||
|
||||
func (p *typeDescParser) parseNamespaceIdentifier() (string, error) {
|
||||
p.skipWhitespace()
|
||||
var id string
|
||||
for p.pos < p.length && p.text[p.pos] != '<' {
|
||||
c := p.text[p.pos]
|
||||
if c == '.' {
|
||||
id += "."
|
||||
p.pos++ // consume '.'
|
||||
}
|
||||
ident, err := p.parseIdentifier()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id += ident
|
||||
p.skipWhitespace()
|
||||
if p.pos < p.length && p.text[p.pos] != '.' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("missing identifier at position %d", p.pos)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (p *typeDescParser) parseIdentifier() (string, error) {
|
||||
p.skipWhitespace()
|
||||
if p.pos >= p.length {
|
||||
return "", fmt.Errorf("unexpected end of input")
|
||||
}
|
||||
start := p.pos
|
||||
c := p.text[p.pos]
|
||||
if !isAlpha(c) && c != '_' {
|
||||
return "", fmt.Errorf("identifier is expected, but %q was found at position %d", c, p.pos)
|
||||
}
|
||||
p.pos++
|
||||
for p.pos < p.length {
|
||||
c := p.text[p.pos]
|
||||
if !isAlphaNumeric(c) && c != '_' {
|
||||
break
|
||||
}
|
||||
p.pos++
|
||||
}
|
||||
return p.text[start:p.pos], nil
|
||||
}
|
||||
|
||||
func (p *typeDescParser) parseTypeParamIdent() (string, error) {
|
||||
p.skipWhitespace()
|
||||
if p.pos >= p.length {
|
||||
return "", fmt.Errorf("unexpected end of input")
|
||||
}
|
||||
c := p.text[p.pos]
|
||||
if !isAlpha(c) {
|
||||
return "", fmt.Errorf("invalid type parameter identifier %q at position %d, must be a single character from A-Z", c, p.pos)
|
||||
}
|
||||
p.pos++
|
||||
if p.pos < p.length && isAlpha(p.text[p.pos]) {
|
||||
return "", fmt.Errorf("invalid type param, must have a single alphabetic character at position %d", p.pos)
|
||||
}
|
||||
return string(c), nil
|
||||
}
|
||||
|
||||
func (p *typeDescParser) skipWhitespace() {
|
||||
for p.pos < p.length && p.text[p.pos] == ' ' {
|
||||
p.pos++
|
||||
}
|
||||
}
|
||||
|
||||
func isAlpha(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
func isAlphaNumeric(c byte) bool {
|
||||
return isAlpha(c) || (c >= '0' && c <= '9')
|
||||
}
|
||||
|
||||
// ConfigFromYAML returns a config from YAML source.
|
||||
//
|
||||
// Adds custom parsing logic for normalizing shorthand for specifiying some fields
|
||||
// in a YAML document (mainly the type-specifier shorthand).
|
||||
//
|
||||
// Using yaml.Unmarshal with any implementation should be sufficient for most
|
||||
// cases.
|
||||
func ConfigFromYAML(data []byte) (*Config, error) {
|
||||
c := &Config{}
|
||||
e := yaml.Unmarshal(data, c)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ConfigToYAML returns the config serialized to YAML
|
||||
//
|
||||
// Provided as a convenience wrapper around a tested YAML Marshaler.
|
||||
func ConfigToYAML(c *Config) ([]byte, error) {
|
||||
return yaml.Marshal(c)
|
||||
}
|
||||
+26
-6
@@ -15,7 +15,11 @@
|
||||
// Package functions defines the standard builtin functions supported by the interpreter
|
||||
package functions
|
||||
|
||||
import "github.com/google/cel-go/common/types/ref"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
// Overload defines a named overload of a function, indicating an operand trait
|
||||
// which must be present on the first argument to the overload as well as one
|
||||
@@ -41,21 +45,37 @@ type Overload struct {
|
||||
// Binary defines the overload with a BinaryOp implementation. May be nil.
|
||||
Binary BinaryOp
|
||||
|
||||
// Function defines the overload with a FunctionOp implementation. May be
|
||||
// nil.
|
||||
// Function defines the overload with a FunctionOp implementation. May be nil.
|
||||
Function FunctionOp
|
||||
|
||||
// Async defines the overload with an AsyncOp implementation. May be nil.
|
||||
Async AsyncOp
|
||||
|
||||
// NonStrict specifies whether the Overload will tolerate arguments that
|
||||
// are types.Err or types.Unknown.
|
||||
NonStrict bool
|
||||
}
|
||||
|
||||
// UnaryOp is a function that takes a single value and produces an output.
|
||||
type UnaryOp func(value ref.Val) ref.Val
|
||||
type UnaryOp func(ref.Val) ref.Val
|
||||
|
||||
// BinaryOp is a function that takes two values and produces an output.
|
||||
type BinaryOp func(lhs ref.Val, rhs ref.Val) ref.Val
|
||||
type BinaryOp func(ref.Val, ref.Val) ref.Val
|
||||
|
||||
// FunctionOp is a function with accepts zero or more arguments and produces
|
||||
// a value or error as a result.
|
||||
type FunctionOp func(values ...ref.Val) ref.Val
|
||||
type FunctionOp func(...ref.Val) ref.Val
|
||||
|
||||
// AsyncOp is a function that accepts zero or more arguments and produces
|
||||
// a value or error asynchronously via a channel.
|
||||
//
|
||||
// AsyncOp is an internal interface intended for use by CEL to manage goroutines and
|
||||
// channels associated with async calls. For public API usage, use BlockingAsyncOp.
|
||||
// Implementers should listen for context cancellation on the provided context for
|
||||
// resource cleanup.
|
||||
type AsyncOp func(context.Context, ...ref.Val) <-chan ref.Val
|
||||
|
||||
// BlockingAsyncOp is a function that accepts zero or more arguments and blocks until
|
||||
// the result is available. When used with AsyncBinding, the framework runs the function
|
||||
// in its own goroutine and manages channel lifecycle internally.
|
||||
type BlockingAsyncOp func(context.Context, ...ref.Val) ref.Val
|
||||
|
||||
-1
@@ -291,7 +291,6 @@ const (
|
||||
const (
|
||||
DurationToDuration = "duration_to_duration"
|
||||
StringToDuration = "string_to_duration"
|
||||
IntToDuration = "int64_to_duration"
|
||||
)
|
||||
|
||||
// Convert to dyn
|
||||
|
||||
+52
-30
@@ -16,6 +16,7 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -113,45 +114,64 @@ var _ Buffer = &supplementalBuffer{}
|
||||
|
||||
var nilBuffer = &emptyBuffer{}
|
||||
|
||||
// SizeLimitError indicates that the input exceeded the configured code point limit.
|
||||
type SizeLimitError struct {
|
||||
Size int
|
||||
Limit int
|
||||
}
|
||||
|
||||
func (e *SizeLimitError) Error() string {
|
||||
return fmt.Sprintf("expression code point size exceeds limit: size: %d, limit %d", e.Size, e.Limit)
|
||||
}
|
||||
|
||||
// NewBuffer returns an efficient implementation of Buffer for the given text based on the ranges of
|
||||
// the encoded code points contained within.
|
||||
//
|
||||
// Code points are represented as an array of byte, uint16, or rune. This approach ensures that
|
||||
// each index represents a code point by itself without needing to use an array of rune. At first
|
||||
// we assume all code points are less than or equal to '\u007f'. If this holds true, the
|
||||
// underlying storage is a byte array containing only ASCII characters. If we encountered a code
|
||||
// point above this range but less than or equal to '\uffff' we allocate a uint16 array, copy the
|
||||
// elements of previous byte array to the uint16 array, and continue. If this holds true, the
|
||||
// underlying storage is a uint16 array containing only Unicode characters in the Basic Multilingual
|
||||
// Plane. If we encounter a code point above '\uffff' we allocate an rune array, copy the previous
|
||||
// elements of the byte or uint16 array, and continue. The underlying storage is an rune array
|
||||
// containing any Unicode character.
|
||||
func NewBuffer(data string) Buffer {
|
||||
buf, _ := newBuffer(data, false)
|
||||
buf, _, _ := newBufferWithLimit(data, false, -1)
|
||||
return buf
|
||||
}
|
||||
|
||||
// NewBufferAndLineOffsets returns an efficient implementation of Buffer for the given text based on
|
||||
// the ranges of the encoded code points contained within, as well as returning the line offsets.
|
||||
//
|
||||
// Code points are represented as an array of byte, uint16, or rune. This approach ensures that
|
||||
// each index represents a code point by itself without needing to use an array of rune. At first
|
||||
// we assume all code points are less than or equal to '\u007f'. If this holds true, the
|
||||
// underlying storage is a byte array containing only ASCII characters. If we encountered a code
|
||||
// point above this range but less than or equal to '\uffff' we allocate a uint16 array, copy the
|
||||
// elements of previous byte array to the uint16 array, and continue. If this holds true, the
|
||||
// underlying storage is a uint16 array containing only Unicode characters in the Basic Multilingual
|
||||
// Plane. If we encounter a code point above '\uffff' we allocate an rune array, copy the previous
|
||||
// elements of the byte or uint16 array, and continue. The underlying storage is an rune array
|
||||
// containing any Unicode character.
|
||||
func NewBufferAndLineOffsets(data string) (Buffer, []int32) {
|
||||
return newBuffer(data, true)
|
||||
buf, offs, _ := newBufferWithLimit(data, true, -1)
|
||||
return buf, offs
|
||||
}
|
||||
|
||||
func newBuffer(data string, lines bool) (Buffer, []int32) {
|
||||
if len(data) == 0 {
|
||||
return nilBuffer, []int32{0}
|
||||
// NewBufferAndLineOffsetsWithLimit returns an efficient implementation of Buffer for the given text
|
||||
// and enforces a code point limit while constructing the buffer.
|
||||
func NewBufferAndLineOffsetsWithLimit(data string, limit int) (Buffer, []int32, error) {
|
||||
if limit < 0 || len(data) <= limit {
|
||||
return newBufferWithLimit(data, true, -1)
|
||||
}
|
||||
return newBufferWithLimit(data, true, limit)
|
||||
}
|
||||
|
||||
func countRemainingCodePoints(data string, idx int, count int) int {
|
||||
for idx < len(data) {
|
||||
_, s := utf8.DecodeRuneInString(data[idx:])
|
||||
idx += s
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func newBufferWithLimit(data string, lines bool, limit int) (Buffer, []int32, error) {
|
||||
if len(data) == 0 {
|
||||
return nilBuffer, []int32{0}, nil
|
||||
}
|
||||
if limit >= 0 && len(data) > limit {
|
||||
size := countRemainingCodePoints(data, 0, 0)
|
||||
if size > limit {
|
||||
return nil, nil, &SizeLimitError{
|
||||
Size: size,
|
||||
Limit: limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The resulting buffers store one element per code point, so the worst case
|
||||
// element count never exceeds len(data).
|
||||
var (
|
||||
idx = 0
|
||||
off int32 = 0
|
||||
@@ -195,7 +215,8 @@ func newBuffer(data string, lines bool) (Buffer, []int32) {
|
||||
}
|
||||
return &asciiBuffer{
|
||||
arr: buf8,
|
||||
}, offs
|
||||
}, offs, nil
|
||||
|
||||
copy16:
|
||||
for idx < len(data) {
|
||||
r, s := utf8.DecodeRuneInString(data[idx:])
|
||||
@@ -222,7 +243,8 @@ copy16:
|
||||
}
|
||||
return &basicBuffer{
|
||||
arr: buf16,
|
||||
}, offs
|
||||
}, offs, nil
|
||||
|
||||
copy32:
|
||||
for idx < len(data) {
|
||||
r, s := utf8.DecodeRuneInString(data[idx:])
|
||||
@@ -238,5 +260,5 @@ copy32:
|
||||
}
|
||||
return &supplementalBuffer{
|
||||
arr: buf32,
|
||||
}, offs
|
||||
}, offs, nil
|
||||
}
|
||||
|
||||
+23
@@ -74,6 +74,12 @@ func NewTextSource(text string) Source {
|
||||
return NewStringSource(text, "<input>")
|
||||
}
|
||||
|
||||
// NewTextSourceWithLimit creates a new Source from the input text string while
|
||||
// enforcing a maximum code point count when needed.
|
||||
func NewTextSourceWithLimit(text string, limit int) (Source, error) {
|
||||
return NewStringSourceWithLimit(text, "<input>", limit)
|
||||
}
|
||||
|
||||
// NewStringSource creates a new Source from the given contents and description.
|
||||
func NewStringSource(contents string, description string) Source {
|
||||
// Compute line offsets up front as they are referred to frequently.
|
||||
@@ -85,6 +91,23 @@ func NewStringSource(contents string, description string) Source {
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringSourceWithLimit creates a new Source from the given contents and
|
||||
// description while enforcing a maximum code point count when needed.
|
||||
func NewStringSourceWithLimit(contents string, description string, limit int) (Source, error) {
|
||||
if limit < 0 || len(contents) <= limit {
|
||||
return NewStringSource(contents, description), nil
|
||||
}
|
||||
buf, offs, err := runes.NewBufferAndLineOffsetsWithLimit(contents, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sourceImpl{
|
||||
Buffer: buf,
|
||||
description: description,
|
||||
lineOffsets: offs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewInfoSource creates a new Source from a SourceInfo.
|
||||
func NewInfoSource(info *exprpb.SourceInfo) Source {
|
||||
return &sourceImpl{
|
||||
|
||||
+18
-2
@@ -16,6 +16,7 @@
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -310,6 +311,9 @@ func init() {
|
||||
argTypes(types.DurationType, types.DurationType), types.BoolType,
|
||||
decls.OverloadExamples(`duration('1ms') < duration('1s') // true`)),
|
||||
decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val {
|
||||
if isNaN(lhs) || isNaN(rhs) {
|
||||
return types.False
|
||||
}
|
||||
cmp := lhs.(traits.Comparer).Compare(rhs)
|
||||
if cmp == types.IntNegOne {
|
||||
return types.True
|
||||
@@ -367,6 +371,9 @@ func init() {
|
||||
argTypes(types.DurationType, types.DurationType), types.BoolType,
|
||||
decls.OverloadExamples(`duration('1ms') <= duration('1s') // true`)),
|
||||
decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val {
|
||||
if isNaN(lhs) || isNaN(rhs) {
|
||||
return types.False
|
||||
}
|
||||
cmp := lhs.(traits.Comparer).Compare(rhs)
|
||||
if cmp == types.IntNegOne || cmp == types.IntZero {
|
||||
return types.True
|
||||
@@ -424,6 +431,9 @@ func init() {
|
||||
argTypes(types.DurationType, types.DurationType), types.BoolType,
|
||||
decls.OverloadExamples(`duration('1ms') > duration('1us') // true`)),
|
||||
decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val {
|
||||
if isNaN(lhs) || isNaN(rhs) {
|
||||
return types.False
|
||||
}
|
||||
cmp := lhs.(traits.Comparer).Compare(rhs)
|
||||
if cmp == types.IntOne {
|
||||
return types.True
|
||||
@@ -481,6 +491,9 @@ func init() {
|
||||
argTypes(types.DurationType, types.DurationType), types.BoolType,
|
||||
decls.OverloadExamples(`duration('60s') >= duration('1m') // true`)),
|
||||
decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val {
|
||||
if isNaN(lhs) || isNaN(rhs) {
|
||||
return types.False
|
||||
}
|
||||
cmp := lhs.(traits.Comparer).Compare(rhs)
|
||||
if cmp == types.IntOne || cmp == types.IntZero {
|
||||
return types.True
|
||||
@@ -605,8 +618,6 @@ func init() {
|
||||
decls.Overload(overloads.DurationToDuration, argTypes(types.DurationType), types.DurationType,
|
||||
decls.OverloadExamples(`duration(duration('1s')) // duration('1s')`),
|
||||
decls.UnaryBinding(identity)),
|
||||
decls.Overload(overloads.IntToDuration, argTypes(types.IntType), types.DurationType,
|
||||
decls.UnaryBinding(convertToType(types.DurationType))),
|
||||
decls.Overload(overloads.StringToDuration, argTypes(types.StringType), types.DurationType,
|
||||
decls.OverloadExamples(`duration('1h2m3s') // duration('3723s')`),
|
||||
decls.UnaryBinding(convertToType(types.DurationType)))),
|
||||
@@ -928,6 +939,11 @@ func noBinaryOverrides(rhs, lhs ref.Val) ref.Val {
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
|
||||
func isNaN(val ref.Val) bool {
|
||||
d, ok := val.(types.Double)
|
||||
return ok && math.IsNaN(float64(d))
|
||||
}
|
||||
|
||||
func noFunctionOverrides(args ...ref.Val) ref.Val {
|
||||
return types.NoSuchOverloadErr()
|
||||
}
|
||||
|
||||
-1
@@ -40,7 +40,6 @@ go_library(
|
||||
"//common/types/pb:go_default_library",
|
||||
"//common/types/ref:go_default_library",
|
||||
"//common/types/traits:go_default_library",
|
||||
"@com_github_stoewer_go_strcase//:go_default_library",
|
||||
"@dev_cel_expr//:expr",
|
||||
"@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
|
||||
"@org_golang_google_protobuf//encoding/protojson:go_default_library",
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ func (b Bool) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
case boolWrapperType:
|
||||
// Convert the bool to a wrapperspb.BoolValue.
|
||||
return wrapperspb.Bool(bool(b)), nil
|
||||
case jsonValueType:
|
||||
case JSONValueType:
|
||||
// Return the bool as a new structpb.Value.
|
||||
return structpb.NewBoolValue(bool(b)), nil
|
||||
default:
|
||||
|
||||
+5
-2
@@ -44,7 +44,10 @@ func (b Bytes) Add(other ref.Val) ref.Val {
|
||||
if !ok {
|
||||
return ValOrErr(other, "no such overload")
|
||||
}
|
||||
return append(b, otherBytes...)
|
||||
sum := make([]byte, 0, len(b)+len(otherBytes))
|
||||
sum = append(sum, b...)
|
||||
sum = append(sum, otherBytes...)
|
||||
return Bytes(sum)
|
||||
}
|
||||
|
||||
// Compare implements traits.Comparer interface method by lexicographic ordering.
|
||||
@@ -79,7 +82,7 @@ func (b Bytes) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
case byteWrapperType:
|
||||
// Convert the bytes to a wrapperspb.BytesValue.
|
||||
return wrapperspb.Bytes([]byte(b)), nil
|
||||
case jsonValueType:
|
||||
case JSONValueType:
|
||||
// CEL follows the proto3 to JSON conversion by encoding bytes to a string via base64.
|
||||
// The encoding below matches the golang 'encoding/json' behavior during marshaling,
|
||||
// which uses base64.StdEncoding.
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ func (d Double) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
case floatWrapperType:
|
||||
// Convert to a wrapperspb.FloatValue (with truncation).
|
||||
return wrapperspb.Float(float32(d)), nil
|
||||
case jsonValueType:
|
||||
case JSONValueType:
|
||||
// Note, there are special cases for proto3 to json conversion that
|
||||
// expect the floating point value to be converted to a NaN,
|
||||
// Infinity, or -Infinity string values, but the jsonpb string
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ func (d Duration) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
case durationValueType:
|
||||
// Unwrap the CEL value to its underlying proto value.
|
||||
return dpb.New(d.Duration), nil
|
||||
case jsonValueType:
|
||||
case JSONValueType:
|
||||
// CEL follows the proto3 to JSON conversion.
|
||||
// Note, using jsonpb would wrap the result in extra double quotes.
|
||||
v := d.ConvertToType(StringType)
|
||||
|
||||
+3
@@ -113,6 +113,9 @@ func ValOrErr(val ref.Val, format string, args ...any) ref.Val {
|
||||
|
||||
// WrapErr wraps an existing Go error value into a CEL Err value.
|
||||
func WrapErr(err error) ref.Val {
|
||||
if err, ok := err.(*Err); ok {
|
||||
return err
|
||||
}
|
||||
return &Err{error: err}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ func (i Int) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
case int64WrapperType:
|
||||
// Convert the value to a wrapperspb.Int64Value.
|
||||
return wrapperspb.Int64(int64(i)), nil
|
||||
case jsonValueType:
|
||||
case JSONValueType:
|
||||
// The proto-to-JSON conversion rules would convert all 64-bit integer values to JSON
|
||||
// decimal strings. Because CEL ints might come from the automatic widening of 32-bit
|
||||
// values in protos, the JSON type is chosen dynamically based on the value.
|
||||
|
||||
+5
-4
@@ -22,8 +22,9 @@ import (
|
||||
|
||||
// JSON type constants representing the reflected types of protobuf JSON values.
|
||||
var (
|
||||
jsonValueType = reflect.TypeOf(&structpb.Value{})
|
||||
jsonListValueType = reflect.TypeOf(&structpb.ListValue{})
|
||||
jsonStructType = reflect.TypeOf(&structpb.Struct{})
|
||||
jsonNullType = reflect.TypeOf(structpb.NullValue_NULL_VALUE)
|
||||
// JSONValueType describes the protobuf native type for a JSON value.
|
||||
JSONValueType = reflect.TypeFor[*structpb.Value]()
|
||||
JSONListType = reflect.TypeFor[*structpb.ListValue]()
|
||||
JSONStructType = reflect.TypeFor[*structpb.Struct]()
|
||||
JSONNullType = reflect.TypeFor[structpb.NullValue]()
|
||||
)
|
||||
|
||||
+30
-27
@@ -126,16 +126,7 @@ func (l *baseList) Add(other ref.Val) ref.Val {
|
||||
if !ok {
|
||||
return MaybeNoSuchOverloadErr(other)
|
||||
}
|
||||
if l.Size() == IntZero {
|
||||
return other
|
||||
}
|
||||
if otherList.Size() == IntZero {
|
||||
return l
|
||||
}
|
||||
return &concatList{
|
||||
Adapter: l.Adapter,
|
||||
prevList: l,
|
||||
nextList: otherList}
|
||||
return newConcatList(l.Adapter, l, otherList)
|
||||
}
|
||||
|
||||
// Contains implements the traits.Container interface method.
|
||||
@@ -153,6 +144,9 @@ func (l *baseList) Contains(elem ref.Val) ref.Val {
|
||||
|
||||
// ConvertToNative implements the ref.Val interface method.
|
||||
func (l *baseList) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
if typeDesc == reflect.TypeFor[any]() {
|
||||
typeDesc = reflect.TypeFor[[]any]()
|
||||
}
|
||||
// If the underlying list value is assignable to the reflected type return it.
|
||||
if reflect.TypeOf(l.value).AssignableTo(typeDesc) {
|
||||
return l.value, nil
|
||||
@@ -164,19 +158,19 @@ func (l *baseList) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
// Attempt to convert the list to a set of well known protobuf types.
|
||||
switch typeDesc {
|
||||
case anyValueType:
|
||||
json, err := l.ConvertToNative(jsonListValueType)
|
||||
json, err := l.ConvertToNative(JSONListType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return anypb.New(json.(proto.Message))
|
||||
case jsonValueType, jsonListValueType:
|
||||
case JSONValueType, JSONListType:
|
||||
jsonValues, err :=
|
||||
l.ConvertToNative(reflect.TypeOf([]*structpb.Value{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jsonList := &structpb.ListValue{Values: jsonValues.([]*structpb.Value)}
|
||||
if typeDesc == jsonListValueType {
|
||||
if typeDesc == JSONListType {
|
||||
return jsonList, nil
|
||||
}
|
||||
return structpb.NewListValue(jsonList), nil
|
||||
@@ -350,9 +344,27 @@ func (l *mutableList) ToImmutableList() traits.Lister {
|
||||
// The `Adapter` enables native type to CEL type conversions.
|
||||
type concatList struct {
|
||||
Adapter
|
||||
value any
|
||||
prevList traits.Lister
|
||||
nextList traits.Lister
|
||||
value any
|
||||
prevList traits.Lister
|
||||
nextList traits.Lister
|
||||
cachedSize ref.Val
|
||||
}
|
||||
|
||||
func newConcatList(adapter Adapter, prevList, nextList traits.Lister) ref.Val {
|
||||
prevSize := prevList.Size().(Int)
|
||||
nextSize := nextList.Size().(Int)
|
||||
if prevSize == IntZero {
|
||||
return nextList.(ref.Val)
|
||||
}
|
||||
if nextSize == IntZero {
|
||||
return prevList.(ref.Val)
|
||||
}
|
||||
return &concatList{
|
||||
Adapter: adapter,
|
||||
prevList: prevList,
|
||||
nextList: nextList,
|
||||
cachedSize: prevSize.Add(nextSize),
|
||||
}
|
||||
}
|
||||
|
||||
// Add implements the traits.Adder interface method.
|
||||
@@ -361,16 +373,7 @@ func (l *concatList) Add(other ref.Val) ref.Val {
|
||||
if !ok {
|
||||
return MaybeNoSuchOverloadErr(other)
|
||||
}
|
||||
if l.Size() == IntZero {
|
||||
return other
|
||||
}
|
||||
if otherList.Size() == IntZero {
|
||||
return l
|
||||
}
|
||||
return &concatList{
|
||||
Adapter: l.Adapter,
|
||||
prevList: l,
|
||||
nextList: otherList}
|
||||
return newConcatList(l.Adapter, l, otherList)
|
||||
}
|
||||
|
||||
// Contains implements the traits.Container interface method.
|
||||
@@ -474,7 +477,7 @@ func (l *concatList) Iterator() traits.Iterator {
|
||||
|
||||
// Size implements the traits.Sizer interface method.
|
||||
func (l *concatList) Size() ref.Val {
|
||||
return l.prevList.Size().(Int).Add(l.nextList.Size())
|
||||
return l.cachedSize
|
||||
}
|
||||
|
||||
// String converts the concatenated list to a human-readable string.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user