From cb9333f38cddb4e1ee619f12106d1b6b4a0d7328 Mon Sep 17 00:00:00 2001 From: Matthew Christopher Date: Thu, 24 Oct 2024 20:46:09 -0700 Subject: [PATCH] Add CEL Parser (#4362) * Includes json_type_provider for translating between v1.JSON and map[string]any in CEL. * Includes cache for CEL expressions and CEL Envs (one env per resource type). --- v2/cmd/asoctl/go.mod | 37 +- v2/cmd/asoctl/go.sum | 140 +----- v2/go.mod | 6 +- v2/go.sum | 10 +- v2/internal/metrics/cel_metrics.go | 110 ++++ v2/internal/util/cel/cel.go | 450 +++++++++++++++++ v2/internal/util/cel/cel_benchmark_test.go | 71 +++ v2/internal/util/cel/cel_test.go | 552 +++++++++++++++++++++ v2/internal/util/cel/compilation.go | 54 ++ v2/internal/util/cel/env_cache.go | 96 ++++ v2/internal/util/cel/env_cache_test.go | 65 +++ v2/internal/util/cel/json_type_provider.go | 178 +++++++ v2/internal/util/cel/program_cache.go | 129 +++++ v2/internal/util/cel/program_cache_test.go | 153 ++++++ v2/internal/util/cel/un_cache.go | 44 ++ 15 files changed, 1939 insertions(+), 156 deletions(-) create mode 100644 v2/internal/metrics/cel_metrics.go create mode 100644 v2/internal/util/cel/cel.go create mode 100644 v2/internal/util/cel/cel_benchmark_test.go create mode 100644 v2/internal/util/cel/cel_test.go create mode 100644 v2/internal/util/cel/compilation.go create mode 100644 v2/internal/util/cel/env_cache.go create mode 100644 v2/internal/util/cel/env_cache_test.go create mode 100644 v2/internal/util/cel/json_type_provider.go create mode 100644 v2/internal/util/cel/program_cache.go create mode 100644 v2/internal/util/cel/program_cache_test.go create mode 100644 v2/internal/util/cel/un_cache.go diff --git a/v2/cmd/asoctl/go.mod b/v2/cmd/asoctl/go.mod index 29de4174fba..a973ab1dee2 100644 --- a/v2/cmd/asoctl/go.mod +++ b/v2/cmd/asoctl/go.mod @@ -49,25 +49,17 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/subscription/armsubscription v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect - github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -78,14 +70,11 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.21.0 // indirect + github.com/google/cel-go v0.21.1-0.20241015195842-2a010f9960b1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hbollon/go-edlib v1.6.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -93,6 +82,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jellydator/ttlcache/v3 v3.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect @@ -102,34 +92,19 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microsoft/go-mssqldb v1.7.2 // indirect - github.com/moby/spdystream v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.47.0 // indirect github.com/spf13/pflag v1.0.5 // 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.5.14 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect - go.etcd.io/etcd/client/v3 v3.5.14 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/sdk v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect @@ -141,20 +116,14 @@ require ( gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect - google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.31.1 // indirect - k8s.io/component-base v0.31.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kms v0.31.1 // indirect k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 // indirect k8s.io/utils v0.0.0-20240821151609-f90d01438635 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/v2/cmd/asoctl/go.sum b/v2/cmd/asoctl/go.sum index c05da3ed63b..72a506c050e 100644 --- a/v2/cmd/asoctl/go.sum +++ b/v2/cmd/asoctl/go.sum @@ -8,6 +8,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement v1.1.1 h1:jCkNVNpsEevyic4bmjgVjzVA4tMGSJpXNGirf+S+mDI= @@ -22,16 +24,14 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprot github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dataprotection/armdataprotection/v3 v3.1.0/go.mod h1:4lNPcTKG4Zgad7aiZBmvLfIMX47eqr5BFzDjC4zggKU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventgrid/armeventgrid v1.0.0 h1:w6b0+FygDpqM7g5cjbeyPoBzgxVHwwt2vCUvTz1oFY8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventgrid/armeventgrid v1.0.0/go.mod h1:t8kRpcgm+RdImuJgHG6SfoQ0tpb9LGl7MF1E6u0yeeA= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.2.0 h1:+dggnR89/BIIlRlQ6d19dkhhdd/mQUiQbXhyHUFiB4w= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.2.0/go.mod h1:tI9M2Q/ueFi287QRkdrhb9LHm6ZnXgkVYLRC3FhYkPw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0 h1:4hGvxD72TluuFIXVr8f4XkKZfqAa7Pj61t0jmQ7+kes= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0/go.mod h1:TSH7DcFItwAufy0Lz+Ft2cyopExCpxbOxI5SkH4dRNo= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0 h1:Kb8eVvjdP6kZqYnER5w/PiGCFp91yVgaxve3d7kCEpY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0/go.mod h1:lYq15QkJyEsNegz5EhI/0SXQ6spvGfgwBH/Qyzkoc/s= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/iothub/armiothub v1.3.0 h1:NZP+oPbAVFy7PhQ4PTD3SuGWbEziNhp7lphGkkN707s= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/iothub/armiothub v1.3.0/go.mod h1:djbLk3ngutFfQ9fSOM29UzywAkcBI1YUsuUnxTQGsqU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0 h1:HlZMUZW8S4P9oob1nCHxCCKrytxyLc+24nUJGssoEto= @@ -58,37 +58,30 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= 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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= @@ -97,17 +90,12 @@ github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lSh github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= @@ -135,8 +123,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= -github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= +github.com/google/cel-go v0.21.1-0.20241015195842-2a010f9960b1 h1:veP4icZX+VneM77SADxZbc8SdQJ7EQrhlb5tkehWtr0= +github.com/google/cel-go v0.21.1-0.20241015195842-2a010f9960b1/go.mod h1:bQ8RrVxHJTP+uWpvUT8KjJf99HHy95iDOrREgU3H+vs= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -145,17 +133,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E= github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= @@ -166,20 +147,18 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/pgx/v5 v5.7.0 h1:FG6VLIdzvAPhnYqP14sQ2xhFLkiUQHCs6ySqO91kF4g= -github.com/jackc/pgx/v5 v5.7.0/go.mod h1:awP1KNnjylvpxHuHP63gzjhnGkI1iw+PMoIwvoleN/8= github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= +github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -204,8 +183,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= -github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= -github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -213,12 +190,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -228,10 +201,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= -github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= -github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= @@ -242,6 +211,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -251,6 +222,8 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -273,45 +246,15 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= -go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= -go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ= -go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI= -go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg= -go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= @@ -322,10 +265,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= @@ -342,28 +281,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -378,13 +303,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -396,44 +318,26 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= -k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= -k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= -k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= -k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= -k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= -k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= -k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= -k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.31.1 h1:cGLyV3cIwb0ovpP/jtyIe2mEuQ/MkbhmeBF2IYCA9Io= -k8s.io/kms v0.31.1/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94= k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI= k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= k8s.io/utils v0.0.0-20240821151609-f90d01438635 h1:2wThSvJoW/Ncn9TmQEYXRnevZXi2duqHWf5OX9S3zjI= k8s.io/utils v0.0.0-20240821151609-f90d01438635/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/v2/go.mod b/v2/go.mod index c5df0fdf6dc..44d5652f4c6 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -29,10 +29,12 @@ require ( github.com/go-logr/logr v1.4.2 github.com/go-logr/zapr v1.3.0 github.com/go-sql-driver/mysql v1.8.1 + github.com/google/cel-go v0.21.1-0.20241015195842-2a010f9960b1 // This is pinned to the commit for https://github.com/google/cel-go/pull/1009, we should update to an official release when we can github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/hbollon/go-edlib v1.6.0 github.com/jackc/pgx/v5 v5.7.1 + github.com/jellydator/ttlcache/v3 v3.3.0 github.com/kr/pretty v0.3.1 github.com/kylelemons/godebug v1.1.0 github.com/leanovate/gopter v0.2.11 @@ -40,6 +42,7 @@ require ( github.com/onsi/gomega v1.34.2 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.5 + github.com/samber/lo v1.47.0 github.com/spf13/cobra v1.8.1 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.28.0 @@ -47,6 +50,7 @@ require ( golang.org/x/sync v0.8.0 golang.org/x/text v0.19.0 golang.org/x/time v0.7.0 + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/dnaeon/go-vcr.v3 v3.2.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.31.1 @@ -85,7 +89,6 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.21.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect @@ -127,7 +130,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/v2/go.sum b/v2/go.sum index 811e25ed2d3..2398beb8abb 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -226,8 +226,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= -github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= +github.com/google/cel-go v0.21.1-0.20241015195842-2a010f9960b1 h1:veP4icZX+VneM77SADxZbc8SdQJ7EQrhlb5tkehWtr0= +github.com/google/cel-go v0.21.1-0.20241015195842-2a010f9960b1/go.mod h1:bQ8RrVxHJTP+uWpvUT8KjJf99HHy95iDOrREgU3H+vs= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -312,6 +312,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= +github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -341,6 +343,8 @@ github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matthchr/cel-go v0.0.0-20241014180305-bd921078b4c3 h1:Xd5M000v6o9gqtZ/UQnhhhL5XLNglLuBPBKa+gUrPEE= +github.com/matthchr/cel-go v0.0.0-20241014180305-bd921078b4c3/go.mod h1:/s8DV6NyGO2vqQQVrnd0uCUrSA+igcgu+iKzNbuIUCU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= @@ -402,6 +406,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= diff --git a/v2/internal/metrics/cel_metrics.go b/v2/internal/metrics/cel_metrics.go new file mode 100644 index 00000000000..4cb3dacc4d4 --- /dev/null +++ b/v2/internal/metrics/cel_metrics.go @@ -0,0 +1,110 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +type CEL interface { + Metrics + + RecordEnvCacheHit(resourceName string) + RecordEnvCacheMiss(resourceName string) + RecordProgramCacheHits(resourceName string) + RecordProgramCacheMisses(resourceName string) + RecordCompilationTime(resourceName string, requestTime time.Duration) +} + +type cel struct { + envCacheHits *prometheus.CounterVec + envCacheMisses *prometheus.CounterVec + programCacheHits *prometheus.CounterVec + programCacheMisses *prometheus.CounterVec + compilationTime *prometheus.HistogramVec +} + +var ( + _ Metrics = &cel{} + _ CEL = &cel{} +) + +func NewCEL() CEL { + envCacheHits := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "cel_env_cache_hits_total", + Help: "Total number of CEL env cache hits", + }, []string{"resource"}) + envCacheMisses := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "cel_env_cache_misses_total", + Help: "Total number of CEL env cache misses", + }, []string{"resource"}) + programCacheHits := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "cel_program_cache_hits_total", + Help: "Total number of CEL env cache hits", + }, []string{"resource"}) + programCacheMisses := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "cel_program_cache_misses_total", + Help: "Total number of CEL env cache misses", + }, []string{"resource"}) + compilationTime := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "cel_compilation_time_seconds", + Help: "Time spent on compiling/parsing CEL expressions", + }, []string{"resource"}) + + return &cel{ + envCacheHits: envCacheHits, + envCacheMisses: envCacheMisses, + programCacheHits: programCacheHits, + programCacheMisses: programCacheMisses, + compilationTime: compilationTime, + } +} + +// RegisterMetrics registers the collectors with prometheus server. +func (c *cel) RegisterMetrics() { + metrics.Registry.MustRegister(c.envCacheHits, c.envCacheMisses, c.programCacheHits, c.programCacheMisses, c.compilationTime) +} + +func (c *cel) RecordEnvCacheHit(resourceName string) { + c.envCacheHits.WithLabelValues(resourceName).Inc() +} + +func (c *cel) RecordEnvCacheMiss(resourceName string) { + c.envCacheMisses.WithLabelValues(resourceName).Inc() +} + +func (c *cel) RecordProgramCacheHits(resourceName string) { + c.programCacheHits.WithLabelValues(resourceName).Inc() +} + +func (c *cel) RecordProgramCacheMisses(resourceName string) { + c.programCacheMisses.WithLabelValues(resourceName).Inc() +} + +func (c *cel) RecordCompilationTime(resourceName string, requestTime time.Duration) { + c.compilationTime.WithLabelValues(resourceName).Observe(requestTime.Seconds()) +} + +type celNoOp struct{} + +func (c *celNoOp) RegisterMetrics() {} +func (c *celNoOp) RecordEnvCacheHit(resourceName string) {} +func (c *celNoOp) RecordEnvCacheMiss(resourceName string) {} +func (c *celNoOp) RecordProgramCacheHits(resourceName string) {} +func (c *celNoOp) RecordProgramCacheMisses(resourceName string) {} +func (c *celNoOp) RecordCompilationTime(resourceName string, requestTime time.Duration) {} + +var ( + _ Metrics = &celNoOp{} + _ CEL = &celNoOp{} +) + +func NewCELNoOp() CEL { + return &celNoOp{} +} diff --git a/v2/internal/util/cel/cel.go b/v2/internal/util/cel/cel.go new file mode 100644 index 00000000000..9e9aeefaa21 --- /dev/null +++ b/v2/internal/util/cel/cel.go @@ -0,0 +1,450 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel + +import ( + "reflect" + "strings" + + "github.com/go-logr/logr" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/ast" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/ext" + "github.com/pkg/errors" + "github.com/samber/lo" + + asometrics "github.com/Azure/azure-service-operator/v2/internal/metrics" + "github.com/Azure/azure-service-operator/v2/internal/set" +) + +// Note: if we ever make a breaking change here, we should allow users to control which "version" of CEL parsing they +// Note: get with an annotation or property on the operatorSpec. + +const ( + SelfIdent = "self" + SecretIdent = "secret" +) + +var evaluator ExpressionEvaluator + +// Evaluator returns the default expression evaluator +func Evaluator() ExpressionEvaluator { + return evaluator +} + +// TODO: We could put this instead in genruntime if we thought it made more sense to keep the "static value for use by webhooks" +// TODO: there. +// RegisterEvaluator registers an evaluator as the default expression evaluator. +func RegisterEvaluator(e ExpressionEvaluator) { + evaluator = e +} + +// ExpressionEvaluator defines an interface for evaluating expressions based on self (the resource) and an optional +// secret parameter (containing the secrets) +type ExpressionEvaluator interface { + CompileAndRun(expression string, self any, secret map[string]string) (*ExpressionResult, error) + Check(expression string, self any) (*cel.Type, error) + FindSecretUsage(expression string, self any) (set.Set[string], error) + Start() + Stop() +} + +type ExpressionEvaluatorOption func(e *expressionEvaluator) (*expressionEvaluator, error) + +// Cache configures the program cache for the expression evaluator. +// The program cache is used to avoid re-compiling expressions or creating +// cel.Envs more than necessary. +func Cache(cache ProgramCacher) ExpressionEvaluatorOption { + return func(e *expressionEvaluator) (*expressionEvaluator, error) { + e.programCache = cache + return e, nil + } +} + +// Metrics configures CEL prometheus metrics +func Metrics(metrics asometrics.CEL) ExpressionEvaluatorOption { + return func(e *expressionEvaluator) (*expressionEvaluator, error) { + e.metrics = metrics + return e, nil + } +} + +// Log configures CEL logging +func Log(log logr.Logger) ExpressionEvaluatorOption { + return func(e *expressionEvaluator) (*expressionEvaluator, error) { + e.log = log + return e, nil + } +} + +var _ ExpressionEvaluator = &expressionEvaluator{} + +type expressionEvaluator struct { + programCache ProgramCacher + metrics asometrics.CEL + log logr.Logger +} + +func NewExpressionEvaluator( + opts ...ExpressionEvaluatorOption, +) (ExpressionEvaluator, error) { + result := &expressionEvaluator{} + + for _, opt := range opts { + var err error + result, err = opt(result) + if err != nil { + return nil, err + } + } + + if result.metrics == nil { + // No metrics by default + result.metrics = asometrics.NewCELNoOp() + } + + if result.programCache == nil { + // Return an error if metrics wasn't configured + envCache := NewEnvCache(result.metrics, result.log, NewEnv) + result.programCache = NewProgramCache(envCache, result.metrics, result.log, Compile) + } + + return result, nil +} + +// Start starts the expressionEvaluator. +func (e *expressionEvaluator) Start() { + go e.programCache.Start() +} + +// Stop stops the expressionEvaluator. +func (e *expressionEvaluator) Stop() { + e.programCache.Stop() +} + +// NewEnv returns a new cel.Env accepting two parameters: +// +// self: The resource itself. The resulting Env contains knowledge of all types referenced +// by self, meaning CEL expressions involving self must all pass the type-checker. +// +// secret: Optional (may be nil/empty), a map[string]string containing secrets retrieved from Azure. +// The keys of the secret are the same as the keys in the operatorSpec.secrets structure. +func NewEnv(resource reflect.Type) (*cel.Env, error) { + types := findTypesRecursive(resource) + selfPath := getTypeImportPath(resource) + + // coerce between type slices + typesList := coerceList(types) + typesList = append(typesList, ext.ParseStructField(ParseStructTag)) + + // The typesList of supported env functions was taken from Kubernetes here: + // https://kubernetes.io/docs/reference/using-api/cel/#language-overview. + return cel.NewEnv( + ext.Strings(), + cel.DefaultUTCTimeZone(true), + // Kubernetes specifies HomogenousAggregateLiterals, but it could cause issues for + // construction of dynamic JSON payloads. This isn't a common operation, but then again + // neither is having Heterogeneous Aggregate (list|map) literals, as we currently restrict output + // types to string or map[string]string. + // cel.HomogeneousAggregateLiterals(), + cel.EagerlyValidateDeclarations(true), + cel.OptionalTypes(), + cel.CrossTypeNumericComparisons(true), + + ext.NativeTypes(typesList...), + newJSONProvider(), // This must come after ext.NativeTypes + + // TODO: We could consider adding support for the Kubernetes List Library + // TODO: https://kubernetes.io/docs/reference/using-api/cel/#kubernetes-list-library + // TODO: and the Kubernetes Regex Library + // TODO: https://kubernetes.io/docs/reference/using-api/cel/#kubernetes-regex-library + // TODO: if there is user need. + cel.Variable("self", cel.ObjectType(selfPath)), + cel.Variable("secret", cel.MapType(cel.StringType, cel.StringType)), + ) + // cel.ClearMacros can be used to disable macros. + // See https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros + // In our case many of the macros such as has, all, etc may be useful so we don't + // disable them. We don't support any custom macros though, only the builtins. +} + +type ExpressionResult struct { + Value string + Values map[string]string +} + +// CompileAndRun compiles the specified expression and returns the result of the compilation. +// expression is a CEL expression that must return either a string or map[string]string. +// self is the resource being reconciled. +// secret is the set of secrets associated with the resource (which may be empty). +func (e *expressionEvaluator) CompileAndRun(expression string, self any, secret map[string]string) (*ExpressionResult, error) { + if self == nil { + return nil, errors.New("self cannot be nil") + } + + // Cache lookup also compiles and checks the program for errors + program, err := e.programCache.Get(reflect.TypeOf(self), expression) + if err != nil { + return nil, err + } + + input := map[string]any{ + SelfIdent: self, + SecretIdent: secret, + } + // TODO: We may want to use ContextEval here, alongside prgm, err := env.cel.Program(ast, cel.InterruptCheckFrequency(10)) + out, _, err := program.Program.Eval(input) + if err != nil { + return nil, errors.Wrapf(err, "failed to eval CEL expression: %q", expression) + } + + // We could use out.Type(), which is a CEL type, rather than out.Value(), which is a Go value, but there's not + // much practical reason to do that since we've already evaluated the expression. + outStr, strOK := out.Value().(string) + outMap, mapErr := valToMap(out) + + if strOK { + return &ExpressionResult{ + Value: outStr, + }, nil + } else if mapErr == nil { + return &ExpressionResult{ + Values: outMap, + }, nil + } + + // If we get here, the result type was something unexpected + return nil, makeUnexpectedResultError(program.AST, AllowedOutputTypes()...) +} + +// Check compiles the expression and ensures that it type-checks and passes output requirements. +// Returns the cel OutputType of the expression. +func (e *expressionEvaluator) Check(expression string, self any) (*cel.Type, error) { + if self == nil { + return nil, errors.New("self cannot be nil") + } + + // Cache lookup also compiles, checks the program for errors, and ensures that it outputs an allowed type. + program, err := e.programCache.Get(reflect.TypeOf(self), expression) + if err != nil { + return nil, err + } + + return program.AST.OutputType(), nil +} + +// FindSecretUsage finds CEL expressions using `secret.xyz` and returns the set of secret values (xyz in the example). +func (e *expressionEvaluator) FindSecretUsage(expression string, self any) (set.Set[string], error) { + // Cache lookup also compiles and checks the program for errors + program, err := e.programCache.Get(reflect.TypeOf(self), expression) + if err != nil { + return nil, err + } + + result := findExprSelectedFields(SecretIdent, program.AST.NativeRep().Expr()) + return result, nil +} + +func parseStructTag(field reflect.StructField, tag string) string { + tag, found := field.Tag.Lookup(tag) + if found { + splits := strings.Split(tag, ",") + if len(splits) > 0 { + // We make the assumption that the leftmost entry in the tag is the name. + // This seems to be true for most tags that have the concept of a name/key, such as: + // https://pkg.go.dev/encoding/xml#Marshal + // https://pkg.go.dev/encoding/json#Marshal + // https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs + // https://pkg.go.dev/gopkg.in/yaml.v2#Marshal + name := splits[0] + return name + } + } + + return field.Name +} + +var fieldEscaper = strings.NewReplacer( + "__", "__underscores__", + ".", "__dot__", + "-", "__dash__", + "/", "__slash__") + +func escapeFieldName(field string) string { + return fieldEscaper.Replace(field) +} + +func ParseStructTag(field reflect.StructField) string { + tag := parseStructTag(field, "json") + + // We follow the same escaping sequences as Kubernetes + // https://kubernetes.io/docs/reference/using-api/cel/#escaping + return escapeFieldName(tag) +} + +// findExprSelectedFields finds all select expressions that start with the specified ident and returns +// the set of selected values. +// For example, an expression that includes .foo and .bar would return the set {foo, bar}. +func findExprSelectedFields(ident string, expr ast.Expr) set.Set[string] { + result := set.Set[string]{} + findExprSelectedFieldsImpl(ident, expr, result) + return result +} + +func findExprSelectedFieldsImpl(ident string, expr ast.Expr, result set.Set[string]) { + if expr == nil { + // Shouldn't happen but let's just be safe + return + } + + switch expr.Kind() { + case ast.CallKind: + t := expr.AsCall() + findExprSelectedFieldsImpl(ident, t.Target(), result) + for _, item := range t.Args() { + findExprSelectedFieldsImpl(ident, item, result) + } + case ast.UnspecifiedExprKind: + return + case ast.ComprehensionKind: + t := expr.AsComprehension() + findExprSelectedFieldsImpl(ident, t.IterRange(), result) + findExprSelectedFieldsImpl(ident, t.AccuInit(), result) + findExprSelectedFieldsImpl(ident, t.LoopStep(), result) + findExprSelectedFieldsImpl(ident, t.LoopCondition(), result) + case ast.IdentKind: + return + case ast.ListKind: + t := expr.AsList() + for _, item := range t.Elements() { + findExprSelectedFieldsImpl(ident, item, result) + } + case ast.LiteralKind: + return + case ast.MapKind: + t := expr.AsMap() + for _, item := range t.Entries() { + ent := item.AsMapEntry() + findExprSelectedFieldsImpl(ident, ent.Value(), result) + findExprSelectedFieldsImpl(ident, ent.Key(), result) + } + case ast.SelectKind: + t := expr.AsSelect() + op := t.Operand() + if op.Kind() != ast.IdentKind { + findExprSelectedFieldsImpl(ident, op, result) + } else { + selectIdent := op.AsIdent() + if selectIdent == ident { + result.Add(t.FieldName()) + } + } + case ast.StructKind: + t := expr.AsStruct() + for _, item := range t.Fields() { + ent := item.AsStructField() + findExprSelectedFieldsImpl(ident, ent.Value(), result) + } + } +} + +func CheckOutputTypeAllowed(ast *cel.Ast, allowed ...*cel.Type) error { + matched := false + for _, t := range allowed { + if ast.OutputType().IsExactType(t) { + matched = true + } + } + + if !matched { + return makeUnexpectedResultError(ast, allowed...) + } + + return nil +} + +func makeUnexpectedResultError(ast *cel.Ast, allowed ...*cel.Type) error { + expectedTypes := lo.Map( + allowed, + func(item *cel.Type, _ int) string { + return item.String() + }) + expectedTypesStr := strings.Join(expectedTypes, ",") + return errors.Errorf("expression %q must return one of [%s], but was %s", ast.Source().Content(), expectedTypesStr, ast.OutputType().String()) +} + +func valToMap(val ref.Val) (map[string]string, error) { + // Convert the CEL value to a native Go map + nativeVal, err := val.ConvertToNative(reflect.TypeOf(map[string]string{})) + if err != nil { + return nil, errors.Wrap(err, "error converting CEL value to native") + } + + // Type assert the native value to a map[string]string + result, ok := nativeVal.(map[string]string) + if !ok { + return nil, errors.Errorf("expectedStr map[string]string but got %T", nativeVal) + } + + return result, nil +} + +func simplePkgAlias(pkgPath string) string { + paths := strings.Split(pkgPath, "/") + if len(paths) == 0 { + return "" + } + return paths[len(paths)-1] +} + +// getResourceTypename should return the package name + +func getTypeImportPath(t reflect.Type) string { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + return simplePkgAlias(t.PkgPath()) + "." + t.Name() +} + +func findTypesRecursive(t reflect.Type) []reflect.Type { // Returns any here because that's what cel.Native expects + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + var result []reflect.Type + + switch t.Kind() { + case reflect.Struct: + // Special case to not include v1.JSON type in the exported reflect data. This is required because in Go + // this is just a string (a single .Raw field), but we want to transform it to a JSON map in CEL so that + // users can use it the same way they specified it in the CRD. See json_type_provider.go. + if t.PkgPath() == "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" && t.Name() == "JSON" { + break + } + + result = append(result, t) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if !field.IsExported() { + continue + } + + fieldType := field.Type + // Recurse into nested structs + result = append(result, findTypesRecursive(fieldType)...) + } + case reflect.Array, reflect.Slice: + result = append(result, findTypesRecursive(t.Elem())...) + case reflect.Map: + result = append(result, findTypesRecursive(t.Elem())...) + default: + // noop on all other types + } + + return result +} diff --git a/v2/internal/util/cel/cel_benchmark_test.go b/v2/internal/util/cel/cel_benchmark_test.go new file mode 100644 index 00000000000..ec4eca58f97 --- /dev/null +++ b/v2/internal/util/cel/cel_benchmark_test.go @@ -0,0 +1,71 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel_test + +import ( + "reflect" + "testing" + + asocel "github.com/Azure/azure-service-operator/v2/internal/util/cel" +) + +// Results from my machine for these tests are: +// pkg: github.com/Azure/azure-service-operator/v2/internal/util/cel +// cpu: AMD EPYC 7763 64-Core Processor +// BenchmarkEnv +// BenchmarkEnv-10 4224 242865 ns/op +// BenchmarkCompileAndRun_Cached +// BenchmarkCompileAndRun_Cached-10 317917 3236 ns/op +// BenchmarkCompileAndRun_Uncached +// BenchmarkCompileAndRun_Uncached-10 3324 357024 ns/op +// PASS + +// The main thing to note here is that creating the Env is the most expensive thing, +// and compilation at least of simple expressions is quite fast even uncached at +// ~357024 - 242865 == 114159 ns/op. + +func BenchmarkEnv(b *testing.B) { + simpleResource := newSimpleResource() + + for i := 0; i < b.N; i++ { + _, err := asocel.NewEnv(reflect.TypeOf(simpleResource)) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCompileAndRun_Cached(b *testing.B) { + evaluator, err := asocel.NewExpressionEvaluator() + if err != nil { + b.Fatal(err) + } + + simpleResource := newSimpleResource() + + for i := 0; i < b.N; i++ { + _, err = evaluator.CompileAndRun(`self.spec.location`, simpleResource, nil) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCompileAndRun_Uncached(b *testing.B) { + evaluator, err := asocel.NewExpressionEvaluator(asocel.Cache(asocel.NewUnCache(asocel.NewEnv, asocel.Compile))) + if err != nil { + b.Fatal(err) + } + + simpleResource := newSimpleResource() + + for i := 0; i < b.N; i++ { + _, err = evaluator.CompileAndRun(`self.spec.location`, simpleResource, nil) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/v2/internal/util/cel/cel_test.go b/v2/internal/util/cel/cel_test.go new file mode 100644 index 00000000000..557bea77e87 --- /dev/null +++ b/v2/internal/util/cel/cel_test.go @@ -0,0 +1,552 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel_test + +import ( + "reflect" + "testing" + + "github.com/google/cel-go/cel" + . "github.com/onsi/gomega" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/Azure/azure-service-operator/v2/internal/set" + asocel "github.com/Azure/azure-service-operator/v2/internal/util/cel" + "github.com/Azure/azure-service-operator/v2/pkg/genruntime" +) + +type SimpleResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SimpleSpec `json:"spec,omitempty"` + Status SimpleStatus `json:"status,omitempty"` +} + +type SimpleSpec struct { + Location string `json:"location,omitempty"` + Owner *genruntime.KnownResourceReference `group:"resources.azure.com" json:"owner,omitempty" kind:"ResourceGroup"` +} + +type SimpleStatus struct { + Location string `json:"location,omitempty"` + ID string `json:"id,omitempty"` +} + +type SimpleResource2 struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SimpleSpec2 `json:"spec,omitempty"` + Status SimpleStatus2 `json:"status,omitempty"` +} + +type SimpleSpec2 struct { + Location string `json:"location,omitempty"` + Owner *genruntime.KnownResourceReference `group:"resources.azure.com" json:"owner,omitempty" kind:"ResourceGroup"` + Untyped map[string]v1.JSON `json:"untyped,omitempty"` + UnderscoreField string `json:"underscore_field,omitempty"` + DoubleUnderscoreField string `json:"underscore__field,omitempty"` + DoubleDoubleUnderscoreField string `json:"underscore__field__,omitempty"` + UnderscoreStartField string `json:"_underscoreStartField,omitempty"` + DashField string `json:"dash-field,omitempty"` + DashUnderscoreField string `json:"dash-__field,omitempty"` + + // odata.type is the only field I'm seeing in ASO that has this format, and there aren't many of those + DotField string `json:"dot.field,omitempty"` + + // There aren't currently any fields in ASO that have this format, but testing it here anyway just to be safe + SlashField string `json:"slash/field,omitempty"` + + Const string `json:"const,omitempty"` // This is a CEL keyword + Break string `json:"break,omitempty"` // This is a CEL keyword + Loop string `json:"loop,omitempty"` // This is a CEL keyword +} + +type SimpleStatus2 struct { + Location string `json:"location,omitempty"` + ID string `json:"id,omitempty"` +} + +func Test_CompileAndRunAndCheck(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + self any + secret map[string]string + expression string + expectedStr string + expectedMap map[string]string + expectedErr string + }{ + { + name: "string constant", + self: newSimpleResource(), + expression: `"Hello"`, + expectedStr: "Hello", + }, + { + name: "simple concatenation", + self: newSimpleResource(), + expression: `"Hello, " + self.spec.location`, + expectedStr: "Hello, eastus", + }, + { + name: "direct property", + self: newSimpleResource(), + expression: `self.spec.location`, + expectedStr: "eastus", + }, + { + name: "direct metav1 property", + self: newSimpleResource(), + expression: `self.metadata.name`, + expectedStr: "mysimpleresource", + }, + { + name: "map output", + self: newSimpleResource(), + expression: `{"location": self.status.location, "namespacedName": self.metadata.namespace + ":" + self.metadata.name}`, + expectedMap: map[string]string{ + "location": "eastus", + "namespacedName": "default:mysimpleresource", + }, + }, + { + name: "heterogeneous string format arguments", + self: newSimpleResource(), + expression: `"%s:%d".format([self.spec.location, 7])`, + expectedStr: "eastus:7", + }, + { + name: "invalid expression error", + self: newSimpleResource(), + expression: `this expression is not valid`, + expectedErr: "failed to compile CEL expression: \"this expression is not valid\"", + }, + { + name: "invalid return type error", + self: newSimpleResource(), + expression: `7`, + expectedErr: "expression \"7\" must return one of [string,map(string, string)], but was int", + }, + { + name: "unstructured type simple string output", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.Untyped = map[string]v1.JSON{ + "mode": { + Raw: []byte(`"Test"`), + }, + } + }), + expression: `string(self.spec.untyped.mode)`, // Needs string cast here because output is dyn + expectedStr: "Test", + }, + { + name: "unstructured type that is a slice", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.Untyped = map[string]v1.JSON{ + "nestedArray": { + Raw: []byte(`["foo","bar","baz"]`), + }, + } + }), + expression: `string(self.spec.untyped.nestedArray[0])`, // Needs string cast here because output is dyn + expectedStr: "foo", + }, + { + name: "unstructured complex type access string property", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.Untyped = map[string]v1.JSON{ + "nestedStruct": { + Raw: []byte(`{"innerMode": "Test", "value": 7}`), + }, + } + }), + expression: `string(self.spec.untyped.nestedStruct.innerMode)`, // Needs string cast here because output is dyn + expectedStr: "Test", + }, + { + name: "unstructured complex type access int property", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.Untyped = map[string]v1.JSON{ + "nestedStruct": { + Raw: []byte(`{"innerMode": "Test", "value": 7}`), + }, + } + }), + expression: `string(self.spec.untyped.nestedStruct.value)`, // Needs string cast here because output is dyn + expectedStr: "7", + }, + { + name: "unstructured complex type access nonexistent property", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.Untyped = map[string]v1.JSON{ + "nestedStruct": { + Raw: []byte(`{"innerMode": "Test", "value": 7}`), + }, + } + }), + expression: `string(self.spec.untyped.nestedStruct.mode)`, // There is no mode type + expectedErr: "failed to eval CEL expression: \"string(self.spec.untyped.nestedStruct.mode)\": no such key: mode", + }, + { + name: "simple secret access", + self: newSimpleResource(), + secret: map[string]string{ + "pizza": "pepperoni", + }, + expression: `secret.pizza`, + expectedStr: "pepperoni", + }, + { + name: "slightly less simple secret access", + self: newSimpleResource(), + secret: map[string]string{ + "pizza": "pepperoni", + }, + expression: `self.spec.location + " " + secret.pizza`, + expectedStr: "eastus pepperoni", + }, + { + name: "missing secret value", + self: newSimpleResource(), + secret: map[string]string{ + "pizza": "pepperoni", + }, + expression: `self.spec.location + " " + secret.sauce`, + expectedErr: `failed to eval CEL expression: "self.spec.location + \" \" + secret.sauce": no such key: sauce`, + }, + { + name: "underscore field works as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.UnderscoreField = "hello" + }), + expression: `self.spec.underscore_field`, + expectedStr: "hello", + }, + { + name: "double underscores field escaped as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.DoubleUnderscoreField = "hello" + }), + expression: `self.spec.underscore__underscores__field`, + expectedStr: "hello", + }, + { + name: "double double underscores field escaped as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.DoubleDoubleUnderscoreField = "hello" + }), + expression: `self.spec.underscore__underscores__field__underscores__`, + expectedStr: "hello", + }, + { + name: "underscore start field works as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.UnderscoreStartField = "hello" + }), + expression: `self.spec._underscoreStartField`, + expectedStr: "hello", + }, + { + name: "dash field escaped as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.DashField = "hello" + }), + expression: `self.spec.dash__dash__field`, + expectedStr: "hello", + }, + { + name: "dash + underscore field escaped as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.DashUnderscoreField = "hello" + }), + expression: `self.spec.dash__dash____underscores__field`, + expectedStr: "hello", + }, + { + name: "dot field escaped as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.DotField = "hello" + }), + expression: `self.spec.dot__dot__field`, + expectedStr: "hello", + }, + { + name: "slash field works as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.SlashField = "hello" + }), + expression: `self.spec.slash__slash__field`, + expectedStr: "hello", + }, + { + name: "reserved fields works as expected", + self: newSimpleResource2Customized( + func(r *SimpleResource2) { + r.Spec.Const = "hello" + r.Spec.Break = " there" + r.Spec.Loop = " friend" + }), + expression: `self.spec.const + self.spec.break + self.spec.loop`, + expectedStr: "hello there friend", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + evaluator, err := asocel.NewExpressionEvaluator() + g.Expect(err).ToNot(HaveOccurred()) + + // Test CompileAndRun + result, err := evaluator.CompileAndRun(c.expression, c.self, c.secret) + if c.expectedErr != "" { + g.Expect(err).To(MatchError(ContainSubstring(c.expectedErr))) + return // Nothing more to assert + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + + if len(c.expectedMap) > 0 { + g.Expect(result.Values).To(Equal(c.expectedMap)) + g.Expect(result.Value).To(BeEmpty()) + } else { + g.Expect(result.Value).To(Equal(c.expectedStr)) + g.Expect(result.Values).To(BeEmpty()) + } + + // Test Check + _, err = evaluator.Check(c.expression, c.self) + if c.expectedErr != "" { + g.Expect(err).To(MatchError(ContainSubstring(c.expectedErr))) + return // Nothing more to assert + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} + +func Test_FindSecretUsage(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + self any + expression string + expected set.Set[string] + expectedErr string + }{ + { + name: "simple secret access", + self: newSimpleResource(), + expression: `secret.pizza`, + expected: set.Make("pizza"), + }, + { + name: "secret access in a function", + self: newSimpleResource(), + expression: `string(secret.pizza)`, + expected: set.Make("pizza"), + }, + { + name: "secret access in a format function (list)", + self: newSimpleResource(), + expression: `"%s:%s:%s".format([self.spec.location, secret.pizza, secret.sauce])`, + expected: set.Make("pizza", "sauce"), + }, + { + name: "multiple secrets accessed in a map", + self: newSimpleResource(), + expression: `{"key": secret.pizza, "key2": secret.sauce}`, + expected: set.Make("pizza", "sauce"), + }, + { + name: "multiple copies of the same secret accessed in a map", + self: newSimpleResource(), + expression: `{"key": secret.pizza, "key2": self.spec.location + " " + secret.pizza}`, + expected: set.Make("pizza"), + }, + { + name: "secrets accessed in a comprehension", + self: newSimpleResource(), + expression: `string(["foo", "bar", secret.pizza].exists(s, s.startsWith('pep')))`, + expected: set.Make("pizza"), + }, + { + name: "nonexistent property produces parser error", + self: newSimpleResource(), + expression: `self.this.doesnt.exist + secret.pizza`, + expectedErr: "failed to compile CEL expression: \"self.this.doesnt.exist + secret.pizza\": ERROR: :1:5: undefined field 'this'", + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + evaluator, err := asocel.NewExpressionEvaluator() + g.Expect(err).ToNot(HaveOccurred()) + + result, err := evaluator.FindSecretUsage(c.expression, c.self) + if c.expectedErr != "" { + g.Expect(err).To(MatchError(ContainSubstring(c.expectedErr))) + return // Nothing more to assert + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + + g.Expect(result).To(Equal(c.expected)) + }) + } +} + +// Test_NewEnv ensures that our CEL environment behaves as expected. Note that this test does +// a number of things that users cannot actually do in CEL expressions for ASO, because we have restrictions +// enforcing that the return type of the user supplied expressions must be string or map[string]string. +// This test is +func Test_NewEnv(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + expression string + expected any + expectedErr string + }{ + { + name: "construct simple resource works", + expression: `cel_test.SimpleResource2{ + spec: cel_test.SimpleSpec2{ + location: "test", + } + }`, + expected: &SimpleResource2{ + Spec: SimpleSpec2{ + Location: "test", + }, + }, + }, + // TODO: This isn't supported right now - we can probably make it work but there's + // TODO: not much value in doing so currently. + //{ + // name: "construct simple resource with map[string]v1.JSON", + // expression: `cel_test.SimpleResource2{ + // spec: cel_test.SimpleSpec2{ + // location: "test", + // untyped: {"test": {"innerMode": "Test", "value": 7}} + // } + // }`, + // expected: &SimpleResource2{ + // Spec: SimpleSpec2{ + // Location: "test", + // Untyped: map[string]v1.JSON{ + // "test": { + // Raw: []byte(`{"innerMode": "Test", "value": 7}`), + // }, + // }, + // }, + // }, + //}, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + env, err := asocel.NewEnv(reflect.TypeOf(c.expected)) + g.Expect(err).ToNot(HaveOccurred()) + + ast, iss := env.Compile(c.expression) + g.Expect(iss.Err()).ToNot(HaveOccurred()) + + program, err := env.Program(ast) + g.Expect(err).ToNot(HaveOccurred()) + + out, _, err := program.Eval(cel.NoVars()) + if c.expectedErr == "" { + g.Expect(err).ToNot(HaveOccurred()) + + var actual any + actual, err = out.ConvertToNative(reflect.TypeOf(c.expected)) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(actual).To(Equal(c.expected)) + } else { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(MatchError(ContainSubstring(c.expectedErr))) + } + }) + } +} + +func newSimpleResource() *SimpleResource { + return &SimpleResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mysimpleresource", + Namespace: "default", + }, + Spec: SimpleSpec{ + Location: "eastus", + Owner: &genruntime.KnownResourceReference{ + Name: "myrg", + }, + }, + Status: SimpleStatus{ + Location: "eastus", + ID: "/subscriptions/12345/resourceGroups/myrg/providers/Microsoft.Simple/simpleResource/mysimpleresource", + }, + } +} + +func newSimpleResource2() *SimpleResource2 { + return newSimpleResource2Customized(nil) +} + +func newSimpleResource2Customized(mutator func(r *SimpleResource2)) *SimpleResource2 { + r := &SimpleResource2{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mysimpleresource", + Namespace: "default", + }, + Spec: SimpleSpec2{ + Location: "eastus", + Owner: &genruntime.KnownResourceReference{ + Name: "myrg", + }, + }, + Status: SimpleStatus2{ + Location: "eastus", + ID: "/subscriptions/12345/resourceGroups/myrg/providers/Microsoft.Simple/simpleResource/mysimpleresource", + }, + } + + if mutator != nil { + mutator(r) + } + return r +} diff --git a/v2/internal/util/cel/compilation.go b/v2/internal/util/cel/compilation.go new file mode 100644 index 00000000000..5d5cada197c --- /dev/null +++ b/v2/internal/util/cel/compilation.go @@ -0,0 +1,54 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel + +import ( + "github.com/google/cel-go/cel" + "github.com/pkg/errors" +) + +var ( + StringType = cel.StringType + MapType = cel.MapType(cel.StringType, cel.StringType) +) + +// AllowedOutputTypes defines the set of allowed CEL expression output types supported by ASO. +// Any CEL expression whose result is a type other than one of these will be rejected. +func AllowedOutputTypes() []*cel.Type { + return []*cel.Type{ + StringType, + MapType, + } +} + +type CompilationResult struct { + AST *cel.Ast + Program cel.Program +} + +// Compile builds the specified expression and returns a CompilationResult containing +// the cel.AST and cel.Program. +func Compile(env *cel.Env, expression string) (*CompilationResult, error) { + ast, iss := env.Compile(expression) + if iss.Err() != nil { + return nil, errors.Wrapf(iss.Err(), "failed to compile CEL expression: %q", expression) + } + + err := CheckOutputTypeAllowed(ast, AllowedOutputTypes()...) + if err != nil { + return nil, err + } + + program, err := env.Program(ast) + if err != nil { + return nil, errors.Wrapf(err, "failed to generate program from CEL AST: %q", expression) + } + + return &CompilationResult{ + AST: ast, + Program: program, + }, nil +} diff --git a/v2/internal/util/cel/env_cache.go b/v2/internal/util/cel/env_cache.go new file mode 100644 index 00000000000..396260e667d --- /dev/null +++ b/v2/internal/util/cel/env_cache.go @@ -0,0 +1,96 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel + +import ( + "context" + "reflect" + "time" + + "github.com/go-logr/logr" + "github.com/google/cel-go/cel" + "github.com/jellydator/ttlcache/v3" + + . "github.com/Azure/azure-service-operator/v2/internal/logging" + asometrics "github.com/Azure/azure-service-operator/v2/internal/metrics" +) + +type envCacheItem struct { + env *cel.Env +} + +// EnvCache caches cel.Env's for a fixed duration. +type EnvCache struct { + cache *ttlcache.Cache[string, envCacheItem] + newEnv func(resource reflect.Type) (*cel.Env, error) + metrics asometrics.CEL + log logr.Logger +} + +// NewEnvCache creates a new EnvCache +func NewEnvCache( + metrics asometrics.CEL, + log logr.Logger, + newEnv func(resource reflect.Type) (*cel.Env, error), +) *EnvCache { + log = log.WithName("CELEnvCache") + + cache := ttlcache.New[string, envCacheItem]( + ttlcache.WithTTL[string, envCacheItem](24 * time.Hour), // TODO: Configurable? + ) + cache.OnInsertion( + func(ctx context.Context, item *ttlcache.Item[string, envCacheItem]) { + log.V(Debug).Info("Env cache item inserted", "key", item.Key(), "expiry", item.ExpiresAt()) + }) + cache.OnEviction( + func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, envCacheItem]) { + log.V(Debug).Info("Env cache item evicted", "key", item.Key(), "expiry", item.ExpiresAt(), "reason", reason) + }) + + return &EnvCache{ + cache: cache, + newEnv: newEnv, + metrics: metrics, + log: log, + } +} + +func (c *EnvCache) Start() { + go c.cache.Start() +} + +func (c *EnvCache) Stop() { + c.cache.Stop() +} + +func (c *EnvCache) Get(resource reflect.Type) (*cel.Env, error) { + key := getTypeImportPath(resource) + + item := c.cache.Get(key) + if item != nil { + c.metrics.RecordEnvCacheHit(key) + // We found what we wanted, return it + return item.Value().env, nil + } + + c.metrics.RecordEnvCacheMiss(key) + env, err := c.newEnv(resource) + if err != nil { + return nil, err + } + + c.cache.Set(key, envCacheItem{env: env}, ttlcache.DefaultTTL) + return env, nil +} + +func coerceList(types []reflect.Type) []any { + anyTypes := make([]any, 0, len(types)) + for _, t := range types { + anyTypes = append(anyTypes, t) + } + + return anyTypes +} diff --git a/v2/internal/util/cel/env_cache_test.go b/v2/internal/util/cel/env_cache_test.go new file mode 100644 index 00000000000..9d368b8a49d --- /dev/null +++ b/v2/internal/util/cel/env_cache_test.go @@ -0,0 +1,65 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel_test + +import ( + "reflect" + "testing" + + "github.com/go-logr/logr" + "github.com/google/cel-go/cel" + . "github.com/onsi/gomega" + + asometrics "github.com/Azure/azure-service-operator/v2/internal/metrics" + asocel "github.com/Azure/azure-service-operator/v2/internal/util/cel" +) + +func Test_EnvCache_SameResourceType_CacheHit(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + resource := newSimpleResource() + + called := 0 + newEnvWrapper := func(resource reflect.Type) (*cel.Env, error) { + called++ + return asocel.NewEnv(resource) + } + cache := asocel.NewEnvCache(asometrics.NewCEL(), logr.Discard(), newEnvWrapper) + for i := 0; i < 10; i++ { + _, err := cache.Get(reflect.TypeOf(resource)) + g.Expect(err).ToNot(HaveOccurred()) + } + + g.Expect(called).To(Equal(1)) +} + +func Test_EnvCache_DifferentResourceTypes_CacheHit(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + resource1 := newSimpleResource() + resource2 := newSimpleResource2() + + called := 0 + newEnvWrapper := func(resource reflect.Type) (*cel.Env, error) { + called++ + return asocel.NewEnv(resource) + } + cache := asocel.NewEnvCache(asometrics.NewCEL(), logr.Discard(), newEnvWrapper) + for i := 0; i < 10; i++ { + _, err := cache.Get(reflect.TypeOf(resource1)) + g.Expect(err).ToNot(HaveOccurred()) + } + g.Expect(called).To(Equal(1)) + + for i := 0; i < 10; i++ { + _, err := cache.Get(reflect.TypeOf(resource2)) + g.Expect(err).ToNot(HaveOccurred()) + } + + g.Expect(called).To(Equal(2)) +} diff --git a/v2/internal/util/cel/json_type_provider.go b/v2/internal/util/cel/json_type_provider.go new file mode 100644 index 00000000000..11fc040fffc --- /dev/null +++ b/v2/internal/util/cel/json_type_provider.go @@ -0,0 +1,178 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel + +import ( + "encoding/json" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/pkg/errors" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +const ( + typeName = "v1.JSON" +) + +var jsonMapType = types.NewMapType(types.StringType, types.NewObjectType(typeName)) + +type jsonProvider struct { + baseAdapter types.Adapter + baseProvider types.Provider +} + +// newJSONProvider creates a JSON type provider that defers all type lookups to the inner provider EXCEPT for lookups +// involving map[string]v1.JSON, which is the type used by ASO when a property doesn't have a concrete type. +// For map[string]v1.JSON, this provider transforms it into map[string]any. +// This means that (for example) a structure in Go looking like +// +// type MyResource struct { +// Spec MySpec `json:"spec,omitempty"` +// } +// +// type MySpec struct { +// Untyped map[string]v1.JSON `json:"untyped,omitempty"` +// } +// +// When accessed in CEL with an expression like self.spec.untyped.nestedStruct.value, the "nestedStruct.value" part +// of that expression will reach into the v1.JSON raw payload and return the value there (if it exists). If it's not found +// a missingKey error will be returned. +// You can think of this provider as "pretend map[string]v1.JSON is actually map[string]any, and that the v1.JSON type +// doesn't exist". +func newJSONProvider() cel.EnvOption { + return func(env *cel.Env) (*cel.Env, error) { + provider := &jsonProvider{ + baseAdapter: env.CELTypeAdapter(), + baseProvider: env.CELTypeProvider(), + } + + env, err := cel.CustomTypeAdapter(provider)(env) + if err != nil { + return nil, err + } + return cel.CustomTypeProvider(provider)(env) + } +} + +var ( + _ types.Adapter = &jsonProvider{} + _ types.Provider = &jsonProvider{} +) + +// types.Adapter impl +func (j *jsonProvider) NativeToValue(value any) ref.Val { + if value == nil { + return types.NullValue + } + + // TODO: NO idea + switch v := value.(type) { + case *v1.JSON: + return nativeToValue(j, v) + case v1.JSON: + return nativeToValue(j, &v) + default: + return j.baseAdapter.NativeToValue(value) + } +} + +func nativeToValue(a types.Adapter, v *v1.JSON) ref.Val { + if v == nil { + return types.NullValue + } + var data map[string]any + err := json.Unmarshal(v.Raw, &data) + if err != nil { + return types.NewErr("failed to unmarshal JSON: %s", err) + } + + return types.NewStringInterfaceMap(a, data) +} + +// types.Provider impl + +func (j *jsonProvider) EnumValue(enumName string) ref.Val { + return j.baseProvider.EnumValue(enumName) +} + +func (j *jsonProvider) FindIdent(identName string) (ref.Val, bool) { + if identName == typeName { + return types.MapType, true + } + return j.baseProvider.FindIdent(identName) +} + +func (j *jsonProvider) FindStructType(structType string) (*types.Type, bool) { + if structType == typeName { + return types.NewTypeTypeWithParam(types.MapType), true // OK so we're a map? + } + return j.baseProvider.FindStructType(structType) +} + +func (j *jsonProvider) FindStructFieldNames(structType string) ([]string, bool) { + if structType == typeName { + return nil, true + } + return j.baseProvider.FindStructFieldNames(typeName) +} + +func (j *jsonProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) { + ft, found := j.baseProvider.FindStructFieldType(structType, fieldName) + if !found { + return ft, found + } + + if ft.Type.IsExactType(jsonMapType) { + return &types.FieldType{ + Type: types.NewMapType(types.StringType, types.DynType), + IsSet: func(target any) bool { + return ft.IsSet(target) // For now we just proxy this, but that may not be correct + }, + GetFrom: func(target any) (any, error) { + inner, err := ft.GetFrom(target) + if err != nil { + return inner, err + } + + typed, ok := inner.(map[string]v1.JSON) + if !ok { + return nil, errors.Errorf("unexpected actual type for map[string]v1.JSON") + } + + var raw []byte + raw, err = json.Marshal(typed) + if err != nil { + return nil, err + } + + var result map[string]any + err = json.Unmarshal(raw, &result) + if err != nil { + return nil, err + } + + return result, nil + }, + }, true + } + + return ft, found +} + +func (j *jsonProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val { + // Just proxy to baseProvider NewValue here because we don't support creating a new value of type + // v1.JSON - as far as CEL is concerned no such type exists. + // TODO: If we want to support construction of map[string]dyn -> map[string]v1.JSON, we need a mapping of + // TODO: structs that contain map[string]v1.JSON, so that we can perform special handling for those. + // TODO: See for example the commented out "construct simple resource with map[string]v1.JSON" test + // TODO: in cel_test.go. This would boil down to performing our own conversion for that single field + // TODO: that just serializes the map to a JSON string. + // TODO: This isn't currently implemented because it doesn't seem likely to ever come up for our use-case (where + // TODO: return types must be string or map[string]string + return j.baseProvider.NewValue(structType, fields) +} diff --git a/v2/internal/util/cel/program_cache.go b/v2/internal/util/cel/program_cache.go new file mode 100644 index 00000000000..28dfc71af0c --- /dev/null +++ b/v2/internal/util/cel/program_cache.go @@ -0,0 +1,129 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/go-logr/logr" + "github.com/google/cel-go/cel" + "github.com/jellydator/ttlcache/v3" + "github.com/pkg/errors" + + . "github.com/Azure/azure-service-operator/v2/internal/logging" + asometrics "github.com/Azure/azure-service-operator/v2/internal/metrics" +) + +type ProgramCacher interface { + Start() + Stop() + Get(resource reflect.Type, expression string) (*CompilationResult, error) +} + +type programCacheItem struct { + result *CompilationResult + err error +} + +type ProgramCache struct { + // key is -. The key must contain a string uniquely identifying the env + // as the same expression may have different meanings depending on the env. + // Note that this is expressly safe to cache as + // per https://github.com/google/cel-go?tab=readme-ov-file#parse-and-check. + cache *ttlcache.Cache[string, *programCacheItem] + envCache *EnvCache + compile func(env *cel.Env, expression string) (*CompilationResult, error) + + metrics asometrics.CEL + log logr.Logger +} + +var _ ProgramCacher = &ProgramCache{} + +// NewProgramCache starts the program cache +func NewProgramCache( + envCache *EnvCache, + metrics asometrics.CEL, + log logr.Logger, + compile func(env *cel.Env, expression string) (*CompilationResult, error), +) *ProgramCache { + log = log.WithName("CELProgramCache") + + cache := ttlcache.New[string, *programCacheItem]( + ttlcache.WithTTL[string, *programCacheItem](24 * time.Hour), + ) + cache.OnInsertion( + func(ctx context.Context, item *ttlcache.Item[string, *programCacheItem]) { + log.V(Debug).Info("Program cache item inserted", "key", item.Key(), "expiry", item.ExpiresAt()) + }) + cache.OnEviction( + func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, *programCacheItem]) { + log.V(Debug).Info("Program cache item evicted", "key", item.Key(), "expiry", item.ExpiresAt(), "reason", reason) + }) + return &ProgramCache{ + cache: cache, + envCache: envCache, + compile: compile, + metrics: metrics, + log: log, + } +} + +func (c *ProgramCache) Start() { + go c.envCache.Start() + go c.cache.Start() +} + +func (c *ProgramCache) Stop() { + c.cache.Stop() + c.envCache.Stop() +} + +func (c *ProgramCache) Get(resource reflect.Type, expression string) (*CompilationResult, error) { + envKey := getTypeImportPath(resource) + + env, err := c.envCache.Get(resource) + if err != nil { + return nil, errors.Wrap(err, "failed to get CEL env") + } + key := fmt.Sprintf("%s-%s", envKey, expression) + + item := c.cache.Get(key) + if item != nil { + c.metrics.RecordProgramCacheHits(envKey) + + val := item.Value() + // Error was cached + if val.err != nil { + return nil, val.err + } + + // We found what we wanted, return it + return val.result, nil + } + + c.metrics.RecordEnvCacheMiss(envKey) + + start := time.Now() + result, err := c.compile(env, expression) + duration := time.Since(start) + c.metrics.RecordCompilationTime(envKey, duration) + + cacheItem := &programCacheItem{ + result: result, + err: err, + } + c.cache.Set(key, cacheItem, ttlcache.DefaultTTL) + + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/v2/internal/util/cel/program_cache_test.go b/v2/internal/util/cel/program_cache_test.go new file mode 100644 index 00000000000..2d7f4eb5cbe --- /dev/null +++ b/v2/internal/util/cel/program_cache_test.go @@ -0,0 +1,153 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel_test + +import ( + "reflect" + "testing" + + "github.com/go-logr/logr" + "github.com/google/cel-go/cel" + . "github.com/onsi/gomega" + "github.com/pkg/errors" + + asometrics "github.com/Azure/azure-service-operator/v2/internal/metrics" + asocel "github.com/Azure/azure-service-operator/v2/internal/util/cel" +) + +func Test_ProgramCache(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + objects []any + expressions []string + expectedEnvCalled int + expectedCompileCalled int + }{ + { + name: "single type single expression", + objects: []any{newSimpleResource()}, + expressions: []string{`"Hello"`}, + expectedCompileCalled: 1, + expectedEnvCalled: 1, + }, + { + name: "single type multiple expressions", + objects: []any{newSimpleResource()}, + expressions: []string{`"Hello"`, `self.spec.location`, `self.metadata.name`}, + expectedCompileCalled: 3, + expectedEnvCalled: 1, + }, + { + name: "single type multiple expressions (some repeat)", + objects: []any{newSimpleResource()}, + expressions: []string{`"Hello"`, `self.spec.location`, `self.metadata.name`, `self.spec.location`, `"Hello"`}, + expectedCompileCalled: 3, + expectedEnvCalled: 1, + }, + { + name: "multiple types single expressions", + objects: []any{newSimpleResource(), newSimpleResource2()}, + expressions: []string{`self.spec.location`}, + expectedCompileCalled: 2, + expectedEnvCalled: 2, + }, + { + name: "multiple types multiple expressions", + objects: []any{newSimpleResource(), newSimpleResource2()}, + expressions: []string{`"Hello"`, `self.spec.location`, `self.metadata.name`}, + expectedCompileCalled: 6, + expectedEnvCalled: 2, + }, + { + name: "multiple types multiple expressions (some repeat)", + objects: []any{newSimpleResource(), newSimpleResource2()}, + expressions: []string{`"Hello"`, `self.spec.location`, `self.metadata.name`, `self.spec.location`, `"Hello"`}, + expectedCompileCalled: 6, + expectedEnvCalled: 2, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + envCalled := 0 + compileCalled := 0 + newEnvWrapper := func(resource reflect.Type) (*cel.Env, error) { + envCalled++ + return asocel.NewEnv(resource) + } + envCache := asocel.NewEnvCache(asometrics.NewCEL(), logr.Discard(), newEnvWrapper) + + compileWrapper := func(env *cel.Env, expression string) (*asocel.CompilationResult, error) { + compileCalled++ + return simpleCompile(env, expression) + } + programCache := asocel.NewProgramCache(envCache, asometrics.NewCEL(), logr.Discard(), compileWrapper) + + for _, self := range c.objects { + for _, expression := range c.expressions { + _, err := programCache.Get(reflect.TypeOf(self), expression) + g.Expect(err).ToNot(HaveOccurred()) + } + } + + g.Expect(envCalled).To(Equal(c.expectedEnvCalled)) + g.Expect(compileCalled).To(Equal(c.expectedCompileCalled)) + }) + } +} + +func Test_ProgramCache_ErrorsAreCached(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + envCalled := 0 + compileCalled := 0 + newEnvWrapper := func(resource reflect.Type) (*cel.Env, error) { + envCalled++ + return asocel.NewEnv(resource) + } + envCache := asocel.NewEnvCache(asometrics.NewCEL(), logr.Discard(), newEnvWrapper) + + compileWrapper := func(env *cel.Env, expression string) (*asocel.CompilationResult, error) { + compileCalled++ + return simpleCompile(env, expression) + } + programCache := asocel.NewProgramCache(envCache, asometrics.NewCEL(), logr.Discard(), compileWrapper) + + self := newSimpleResource() + + for i := 0; i < 10; i++ { + _, err := programCache.Get(reflect.TypeOf(self), `self.loc`) + g.Expect(err).To(MatchError(ContainSubstring("undefined field 'loc'"))) + } + + g.Expect(envCalled).To(Equal(1)) + g.Expect(compileCalled).To(Equal(1)) +} + +// This is a simple compilation helper specifically for testing the cache +func simpleCompile(env *cel.Env, expression string) (*asocel.CompilationResult, error) { + ast, iss := env.Compile(expression) + if iss.Err() != nil { + return nil, errors.Wrapf(iss.Err(), "failed to compile CEL expression: %q", expression) + } + + program, err := env.Program(ast) + if err != nil { + return nil, errors.Wrapf(err, "failed to generate program from CEL AST: %q", expression) + } + + return &asocel.CompilationResult{ + AST: ast, + Program: program, + }, nil +} diff --git a/v2/internal/util/cel/un_cache.go b/v2/internal/util/cel/un_cache.go new file mode 100644 index 00000000000..f0ceb618700 --- /dev/null +++ b/v2/internal/util/cel/un_cache.go @@ -0,0 +1,44 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +package cel + +import ( + "reflect" + + "github.com/google/cel-go/cel" + "github.com/pkg/errors" +) + +// UnCache is a cache that doesn't cache, useful mostly for testing purposes +type UnCache struct { + newEnv func(resource reflect.Type) (*cel.Env, error) + compile func(env *cel.Env, expression string) (*CompilationResult, error) +} + +var _ ProgramCacher = &UnCache{} + +func NewUnCache( + newEnv func(resource reflect.Type) (*cel.Env, error), + compile func(env *cel.Env, expression string) (*CompilationResult, error), +) *UnCache { + return &UnCache{ + newEnv: newEnv, + compile: compile, + } +} + +func (c *UnCache) Start() {} + +func (c *UnCache) Stop() {} + +func (c *UnCache) Get(resource reflect.Type, expression string) (*CompilationResult, error) { + env, err := c.newEnv(resource) + if err != nil { + return nil, errors.Wrap(err, "failed to get CEL env") + } + + return c.compile(env, expression) +}