From 04b1156ed8bee8d77d831c77663ef7695382c0ce Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Tue, 5 May 2026 15:37:02 -0400 Subject: [PATCH 1/2] perf(desiredrelease): paginate vs iterator and versionselector to SQL * The desired-release reconciler previously buffered up to 500 candidate deployment versions per release target, silently skipping any version beyond that window. Replace the upfront fetch with a keyset-paginated iter.Seq2 that streams versions newest-first and short-circuits as soon as FindDeployableVersion picks one, removing the hard cap. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Burst patterns (many RTs of the same deployment reconciling concurrently after a fanout event) now collapse the first-batch fetch through singleflight keyed by (deploymentID, hash(pushdown clauses)), so worker overlap costs one round trip instead of N. For policies whose versionselector CEL can be statically translated to SQL, push the predicate into the candidate query via cel2sql. Translatable shapes (literal eq, in-list, startsWith, boolean composition over the version variable) AND-join into the WHERE clause so Postgres returns only plausible candidates instead of the full LIMIT 500. Untranslatable selectors (referencing environment/resource/deployment, complex CEL, JSONB metadata access) fall back to the row-by-row evaluator unchanged — pushdown is a candidate-set narrowing optimization, not a replacement for runtime evaluation. Span instrumentation: - FindDeployableVersion: versions.scanned, version.found, deployment.id - queryCandidateVersionsBatch: pushdown.applied, pushdown.clauses_count, cursor.paginated, rows.returned, rows.below_limit SQL injection safety on the inlined pushdown fragments is verified by TestTryPushDown_StringEscaping; cel2sql emits Postgres-standard doubled single-quote escapes for string literals. --- apps/workspace-engine/go.mod | 62 ++--- apps/workspace-engine/go.sum | 235 +++++++++++------- .../pkg/db/deployment_versions.sql.go | 56 +++++ .../pkg/db/queries/deployment_versions.sql | 11 + .../evaluator/versionselector/pushdown.go | 70 ++++++ .../versionselector/pushdown_test.go | 128 ++++++++++ .../svc/controllers/desiredrelease/getters.go | 20 +- .../desiredrelease/getters_postgres.go | 227 +++++++++++++++-- .../desiredrelease/getters_postgres_test.go | 77 +++++- .../desiredrelease/policyeval/policyeval.go | 34 ++- .../policyeval/policyeval_test.go | 61 ++++- .../desiredrelease/pushdown_test.go | 87 +++++++ .../controllers/desiredrelease/reconcile.go | 67 +++-- .../desiredrelease/reconcile_test.go | 14 +- .../variableresolver/getters_postgres.go | 21 -- .../test/controllers/harness/mocks.go | 14 +- 16 files changed, 966 insertions(+), 218 deletions(-) create mode 100644 apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go create mode 100644 apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/pushdown_test.go diff --git a/apps/workspace-engine/go.mod b/apps/workspace-engine/go.mod index 193e4bdc3b..9f25b8e437 100644 --- a/apps/workspace-engine/go.mod +++ b/apps/workspace-engine/go.mod @@ -13,28 +13,30 @@ require ( github.com/exaring/otelpgx v0.9.3 github.com/gin-gonic/gin v1.11.0 github.com/goccy/go-json v0.10.5 - github.com/google/cel-go v0.26.1 + github.com/google/cel-go v0.28.0 github.com/google/go-github/v66 v66.0.0 github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518 github.com/hashicorp/go-tfe v1.97.0 - github.com/jackc/pgx/v5 v5.7.6 + github.com/jackc/pgx/v5 v5.9.2 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/oapi-codegen/runtime v1.1.2 + github.com/open-policy-agent/opa v1.15.2 github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible github.com/prometheus/common v0.66.1 + github.com/spandigital/cel2sql/v3 v3.8.2 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/teambition/rrule-go v1.8.2 go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 - go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 - go.opentelemetry.io/otel/sdk v1.41.0 - go.opentelemetry.io/otel/sdk/metric v1.41.0 - go.opentelemetry.io/otel/trace v1.41.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/sdk/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.43.0 k8s.io/apimachinery v0.34.1 sigs.k8s.io/yaml v1.6.0 ) @@ -42,7 +44,7 @@ require ( require ( cel.dev/expr v0.25.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect @@ -81,29 +83,28 @@ require ( github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect github.com/swaggo/swag v1.8.12 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/mod v0.33.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.34.0 // indirect golang.org/x/sync v0.20.0 - golang.org/x/tools v0.42.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.3 // indirect + golang.org/x/tools v0.43.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/grpc v1.80.0 gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -111,7 +112,7 @@ require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect cyphar.com/go-pathrs v0.2.1 // indirect dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect @@ -184,7 +185,7 @@ require ( github.com/go-openapi/swag/typeutils v0.25.3 // indirect github.com/go-openapi/swag/yamlutils v0.25.3 // indirect github.com/go-redis/cache/v9 v9.0.0 // indirect - github.com/go-sql-driver/mysql v1.9.2 // indirect + github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect @@ -221,7 +222,7 @@ require ( github.com/jonboulle/clockwork v0.5.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect @@ -231,7 +232,7 @@ require ( github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -246,8 +247,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/open-policy-agent/opa v1.15.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -291,19 +291,19 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.58.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -323,10 +323,10 @@ require ( k8s.io/kubectl v0.34.0 // indirect k8s.io/kubernetes v1.34.2 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect - modernc.org/libc v1.65.8 // indirect + modernc.org/libc v1.72.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.37.1 // indirect + modernc.org/sqlite v1.50.0 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.20.1 // indirect diff --git a/apps/workspace-engine/go.sum b/apps/workspace-engine/go.sum index 3695aa0674..1de767ecdf 100644 --- a/apps/workspace-engine/go.sum +++ b/apps/workspace-engine/go.sum @@ -2,14 +2,24 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.76.0 h1:wnfVSXN6GEMlsAoHWdhzTC8NMsptOx2hsqPiI+lTs3I= +cloud.google.com/go/bigquery v1.76.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= @@ -51,13 +61,19 @@ github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KO github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/argoproj/argo-cd/v3 v3.3.4 h1:3dFCh1b8QaqHv78QZPaH59zdZnu6wRvUpQtXmklLfUY= github.com/argoproj/argo-cd/v3 v3.3.4/go.mod h1:RqLNOqYHufjRTFcKIpdf27c1zvrze+fS5I9VVoJJciE= github.com/argoproj/argo-events v1.9.6 h1:tQTyUmMt0/4UI+9fbXrmK1/h9oalV7KBCC3YgPI7qz0= @@ -94,6 +110,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= @@ -188,12 +206,16 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w= +github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 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/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= @@ -249,6 +271,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= @@ -348,8 +374,8 @@ github.com/go-playground/webhooks/v6 v6.4.0 h1:KLa6y7bD19N48rxJDHM0DpE3T4grV7GxM github.com/go-playground/webhooks/v6 v6.4.0/go.mod h1:5lBxopx+cAJiBI4+kyRbuHrEi+hYRDdRHuRR4Ya5Ums= github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= -github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= -github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -398,8 +424,10 @@ 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 v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= -github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= +github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -425,12 +453,18 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518 h1:UBg1xk+oAsIVbFuGg6hdfAm7EvCv3EL80vFxJNsslqw= github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= @@ -464,6 +498,9 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHWaLx2MM0g4BlaQc1BXn/9835mPrBqwSPU= github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -526,8 +563,8 @@ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgS github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= -github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -568,8 +605,8 @@ github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXw 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.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -609,8 +646,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= @@ -638,6 +675,8 @@ github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebG github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -656,12 +695,12 @@ github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8 github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= -github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM= -github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -698,8 +737,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -755,6 +794,9 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -815,16 +857,14 @@ github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= -github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= -github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -842,12 +882,12 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spandigital/cel2sql/v3 v3.8.2 h1:Pk/5G7Flqh1N8QHAPRwVRzJmA1OWDCV5q9UZRYzLT4A= +github.com/spandigital/cel2sql/v3 v3.8.2/go.mod h1:e4dGSiXUQgVo5UIRw3L35tKofnJJ0Fqwj7V/VsXDDc0= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= -github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= -github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -856,8 +896,6 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -886,10 +924,16 @@ github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhg github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8= github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= -github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= -github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/compose v0.41.0 h1:6ttsQ6IilJYMoTFI2gu9l7KmKlnlY9XGkP0wtgh4rF4= github.com/testcontainers/testcontainers-go/modules/compose v0.41.0/go.mod h1:6PfaNLXsylvZE5CID8QMZ4fWjLHORvqm1xcGBncdzAY= +github.com/testcontainers/testcontainers-go/modules/gcloud v0.42.0 h1:EdLf2NCpo43CxTfC0x2R0sW3+HqzevC78pgnH9niyYc= +github.com/testcontainers/testcontainers-go/modules/gcloud v0.42.0/go.mod h1:5CMn4WViUGbOGORdjWvvGEkptvM9I/vwecYTsyKoPkg= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= @@ -954,41 +998,41 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0/go.mod h1:rjbQTDEPQymPE0YnRQp9/NuPwwtL0sesz/fnqRW/v84= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 h1:PeBoRj6af6xMI7qCupwFvTbbnd49V7n5YpG6pg8iDYQ= go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0/go.mod h1:ingqBCtMCe8I4vpz/UVzCW6sxoqgZB37nao91mLQ3Bw= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= go.opentelemetry.io/otel/exporters/prometheus v0.58.0 h1:CJAxWKFIqdBennqxJyOgnt5LqkeFRT+Mz3Yjz3hL+h8= go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1029,8 +1073,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= @@ -1048,8 +1092,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1080,14 +1124,12 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1097,8 +1139,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1142,8 +1182,10 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1154,8 +1196,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1168,10 +1210,8 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1196,36 +1236,38 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= +google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1249,9 +1291,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= +gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -1300,16 +1341,20 @@ k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI= k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= -modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= -modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= -modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= +modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= +modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q= -modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1318,8 +1363,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= -modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/apps/workspace-engine/pkg/db/deployment_versions.sql.go b/apps/workspace-engine/pkg/db/deployment_versions.sql.go index 96e166dc81..eba99c29c9 100644 --- a/apps/workspace-engine/pkg/db/deployment_versions.sql.go +++ b/apps/workspace-engine/pkg/db/deployment_versions.sql.go @@ -89,6 +89,62 @@ func (q *Queries) ListDeployableVersionsByDeploymentID(ctx context.Context, arg return items, nil } +const listDeployableVersionsByDeploymentIDAfter = `-- name: ListDeployableVersionsByDeploymentIDAfter :many +SELECT id, name, tag, config, job_agent_config, deployment_id, metadata, status, message, created_at, workspace_id FROM deployment_version +WHERE deployment_id = $1 + AND status NOT IN ('rejected', 'building') + AND ( + $3::timestamptz IS NULL + OR (created_at, id) < ($3::timestamptz, $4::uuid) + ) +ORDER BY created_at DESC, id DESC +LIMIT $2 +` + +type ListDeployableVersionsByDeploymentIDAfterParams struct { + DeploymentID uuid.UUID + Limit int64 + AfterCreatedAt pgtype.Timestamptz + AfterID uuid.UUID +} + +func (q *Queries) ListDeployableVersionsByDeploymentIDAfter(ctx context.Context, arg ListDeployableVersionsByDeploymentIDAfterParams) ([]DeploymentVersion, error) { + rows, err := q.db.Query(ctx, listDeployableVersionsByDeploymentIDAfter, + arg.DeploymentID, + arg.Limit, + arg.AfterCreatedAt, + arg.AfterID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DeploymentVersion + for rows.Next() { + var i DeploymentVersion + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Tag, + &i.Config, + &i.JobAgentConfig, + &i.DeploymentID, + &i.Metadata, + &i.Status, + &i.Message, + &i.CreatedAt, + &i.WorkspaceID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listDeploymentVersionsByDeploymentID = `-- name: ListDeploymentVersionsByDeploymentID :many SELECT id, name, tag, config, job_agent_config, deployment_id, metadata, status, message, created_at, workspace_id FROM deployment_version WHERE deployment_id = $1 ORDER BY created_at DESC LIMIT COALESCE($2::int, 5000) diff --git a/apps/workspace-engine/pkg/db/queries/deployment_versions.sql b/apps/workspace-engine/pkg/db/queries/deployment_versions.sql index 4cca619a47..3fa1064484 100644 --- a/apps/workspace-engine/pkg/db/queries/deployment_versions.sql +++ b/apps/workspace-engine/pkg/db/queries/deployment_versions.sql @@ -26,5 +26,16 @@ WHERE deployment_id = $1 ORDER BY created_at DESC LIMIT COALESCE(sqlc.narg('limit')::int, 5000); +-- name: ListDeployableVersionsByDeploymentIDAfter :many +SELECT * FROM deployment_version +WHERE deployment_id = $1 + AND status NOT IN ('rejected', 'building') + AND ( + sqlc.narg('after_created_at')::timestamptz IS NULL + OR (created_at, id) < (sqlc.narg('after_created_at')::timestamptz, sqlc.narg('after_id')::uuid) + ) +ORDER BY created_at DESC, id DESC +LIMIT $2; + -- name: DeleteDeploymentVersion :exec DELETE FROM deployment_version WHERE id = $1; diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go new file mode 100644 index 0000000000..f01ef50447 --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go @@ -0,0 +1,70 @@ +package versionselector + +import ( + "sync" + + "github.com/google/cel-go/cel" + "github.com/spandigital/cel2sql/v3" + "github.com/spandigital/cel2sql/v3/pg" +) + +// pushdownEnv is the CEL environment used to attempt SQL pushdown of a +// versionselector rule. It declares ONLY `version` because the iterator runs +// per-deployment and only version-scoped predicates can prune candidate rows +// at query time. Selectors that reference environment/resource/deployment will +// fail to compile here and fall back to runtime CEL evaluation. +var ( + pushdownEnv *cel.Env + pushdownEnvOnce sync.Once + pushdownEnvErr error +) + +func getPushdownEnv() (*cel.Env, error) { + pushdownEnvOnce.Do(func() { + versionSchema := pg.NewSchema([]pg.FieldSchema{ + {Name: "id", Type: "uuid"}, + {Name: "tag", Type: "text"}, + {Name: "name", Type: "text"}, + {Name: "status", Type: "text"}, + {Name: "created_at", Type: "timestamptz"}, + }) + + pushdownEnv, pushdownEnvErr = cel.NewEnv( + cel.CustomTypeProvider(pg.NewTypeProvider(map[string]pg.Schema{ + "DeploymentVersion": versionSchema, + })), + cel.Variable("version", cel.ObjectType("DeploymentVersion")), + ) + }) + return pushdownEnv, pushdownEnvErr +} + +// TryPushDown attempts to convert a versionselector CEL expression into a SQL +// WHERE clause that can be appended to the candidate-version query. Returns +// ok=false for any expression cel2sql cannot translate (selectors that read +// environment/resource/deployment, function calls outside the supported set, +// JSONB/metadata access not declared in the schema, etc.) so callers can fall +// back to the row-by-row CEL evaluator. +// +// IMPORTANT: pushdown is an optimization, not a replacement. The CEL +// evaluator must still run per-version at reconcile time — pushdown narrows +// the candidate set, but its translation may diverge from CEL's runtime +// semantics in edge cases. The runtime evaluator is the source of truth. +func TryPushDown(selector string) (clause string, ok bool) { + if selector == "" { + return "", false + } + env, err := getPushdownEnv() + if err != nil { + return "", false + } + ast, issues := env.Compile(selector) + if issues != nil && issues.Err() != nil { + return "", false + } + sql, err := cel2sql.Convert(ast) + if err != nil || sql == "" { + return "", false + } + return sql, true +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go new file mode 100644 index 0000000000..7a8508a9b6 --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go @@ -0,0 +1,128 @@ +package versionselector + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestTryPushDown_SupportedShapes locks in which CEL expression shapes the +// library currently translates. If any of these flip from ok=true to false on +// a library upgrade, we want a loud test failure rather than silent loss of +// the optimization. +func TestTryPushDown_SupportedShapes(t *testing.T) { + cases := []struct { + name string + selector string + wantContain string + }{ + { + name: "literal equality", + selector: `version.tag == "v1.2.3"`, + wantContain: "v1.2.3", + }, + { + name: "inequality", + selector: `version.tag != "broken"`, + wantContain: "broken", + }, + { + name: "boolean and", + selector: `version.tag == "x" && version.name == "y"`, + wantContain: "AND", + }, + { + name: "boolean or", + selector: `version.tag == "x" || version.tag == "y"`, + wantContain: "OR", + }, + { + name: "startsWith", + selector: `version.tag.startsWith("v1.")`, + wantContain: "v1.", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + clause, ok := TryPushDown(tc.selector) + if !ok { + t.Logf( + "selector did NOT push down (will fall back to runtime CEL): %q", + tc.selector, + ) + return // capability gap, not a hard failure — optimization is best-effort + } + t.Logf("selector=%q → SQL=%s", tc.selector, clause) + assert.Contains(t, clause, tc.wantContain, + "emitted SQL should mention the literal value somehow (escaped or parameterized)") + }) + } +} + +// TestTryPushDown_FailsClosed locks in that selectors referencing entities +// not in the pushdown schema (environment, resource, deployment) fall back +// rather than producing partial / invalid SQL. +func TestTryPushDown_FailsClosed(t *testing.T) { + cases := []string{ + `environment.name == "prod"`, + `resource.kind == "Server"`, + `deployment.name == "api"`, + `version.tag == "x" && environment.name == "prod"`, + ``, // empty + } + for _, sel := range cases { + t.Run(sel, func(t *testing.T) { + _, ok := TryPushDown(sel) + assert.False(t, ok, "selector %q must NOT push down", sel) + }) + } +} + +// TestTryPushDown_StringEscaping is the safety-critical test. If a user +// stores a malicious string literal in a versionselector rule (an attacker +// who has policy-write access already has way more than this — but defense +// in depth), the emitted SQL must escape single quotes properly. This test +// fails the build if cel2sql ever emits unescaped literals. +func TestTryPushDown_StringEscaping(t *testing.T) { + malicious := `version.tag == "test'; DROP TABLE deployment_version; --"` + clause, ok := TryPushDown(malicious) + if !ok { + t.Skip("library refused malicious input — that's also acceptable") + } + t.Logf("emitted SQL: %s", clause) + + // Acceptable shapes (any one of these proves safe handling): + // 1. Doubled single quote: 'test''; DROP TABLE...' + // 2. Backslash escape: 'test\'; DROP TABLE...' + // 3. Postgres E-string: E'test\'; DROP TABLE...' + // 4. Parameterized output: $1, $2, etc. (no literal at all) + doubled := strings.Contains(clause, `''`) + backslash := strings.Contains(clause, `\'`) + parameterized := strings.Contains(clause, "$") && !strings.Contains(clause, "DROP") + + safe := doubled || backslash || parameterized + assert.True(t, safe, + "emitted SQL must escape single quotes or parameterize literals; got: %q", clause) + + // Critical: the unescaped attack pattern must NOT appear verbatim. If the + // library inlines `test'; DROP TABLE...` as-is, this assertion catches it. + assert.NotContains(t, clause, `test'; DROP TABLE`, + "raw single-quote injection pattern present in emitted SQL — UNSAFE") +} + +// TestTryPushDown_JSONBAccess documents whether metadata-key access works. +// Result not asserted — just logged so we know if it's available without +// extending the schema. Most version selectors don't use metadata, so this +// being unsupported is acceptable for the POC. +func TestTryPushDown_JSONBAccess(t *testing.T) { + clause, ok := TryPushDown(`version.metadata["env"] == "prod"`) + if !ok { + t.Log( + "JSONB metadata access NOT supported by current schema — fine, scope to flat fields for now", + ) + return + } + t.Logf("metadata access produced: %s", clause) +} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/getters.go b/apps/workspace-engine/svc/controllers/desiredrelease/getters.go index 5303ec50de..6f4ae6ee8c 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/getters.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/getters.go @@ -2,6 +2,7 @@ package desiredrelease import ( "context" + "iter" "github.com/google/uuid" "workspace-engine/pkg/oapi" @@ -21,10 +22,25 @@ type Getter interface { // ReleaseTargetExists(ctx context.Context, rt *ReleaseTarget) (bool, error) GetReleaseTargetScope(ctx context.Context, rt *ReleaseTarget) (*evaluator.EvaluatorScope, error) - GetCandidateVersions( + + // IterCandidateVersions yields deployable versions newest-first. The + // caller is expected to stop iterating once a deployable version is + // found, so the implementation must lazily page through history rather + // than buffering all versions up front. + // + // extraWhere is an optional list of SQL fragments that get AND-joined + // into the candidate query as a pushdown filter. Each fragment is + // expected to reference columns via the alias `version` (e.g. + // `version.tag = 'v1.2.3'`). Fragments must be safely escaped before + // reaching this method — they are concatenated into the SQL string + // directly. When the iterator can't push down (no fragments supplied, + // or the consumer doesn't extract any), it behaves identically to the + // non-pushdown path. + IterCandidateVersions( ctx context.Context, deploymentID uuid.UUID, - ) ([]*oapi.DeploymentVersion, error) + extraWhere []string, + ) iter.Seq2[*oapi.DeploymentVersion, error] // GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) // HasCurrentRelease(ctx context.Context, rt *ReleaseTarget) (bool, error) diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go index ed9d45a23a..08d6faf722 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go @@ -2,10 +2,16 @@ package desiredrelease import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" + "iter" + "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "golang.org/x/sync/singleflight" "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" @@ -16,9 +22,17 @@ import ( "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) +var getterTracer = otel.Tracer("workspace-engine/desiredrelease/getter") + type policiesGetter = policyeval.Getter type variableResolverGetter = variableresolver.Getter +// candidateVersionBatchSize controls how many deployable versions are fetched +// per keyset-paginated round trip. Most release targets find an eligible +// version in the first few rows; this size only matters when policies block +// the entire newest batch. +const candidateVersionBatchSize = 500 + var _ Getter = (*PostgresGetter)(nil) func NewPostgresGetter( @@ -44,7 +58,14 @@ type PostgresGetter struct { policiesGetter variableResolverGetter - versionsSF singleflight.Group + // firstBatchSF deduplicates the first-batch fetch when many release + // targets for the same deployment reconcile concurrently — typical + // after a workspace-wide policy update. Only the first batch is shared: + // it's an immutable slice, safe across goroutines, and the 99% case + // (a deployable version exists in the newest 500 rows) terminates here + // without ever issuing a second query. Consumers that need to walk + // further paginate independently from their own cursor. + firstBatchSF singleflight.Group } func (g *PostgresGetter) ReleaseTargetExists(ctx context.Context, rt *ReleaseTarget) (bool, error) { @@ -83,31 +104,205 @@ func (g *PostgresGetter) GetReleaseTargetScope( }, nil } -func (g *PostgresGetter) GetCandidateVersions( +// candidateVersionColumns is the column list for SELECT against +// deployment_version, kept consistent with the sqlc-generated +// ListDeployableVersionsByDeploymentIDAfter query so we can scan rows into +// db.DeploymentVersion using the same field order. +const candidateVersionColumns = `id, name, tag, config, job_agent_config, deployment_id, metadata, status, message, created_at, workspace_id` + +// IterCandidateVersions yields deployable versions newest-first, fetching them +// in batches via keyset pagination. The previous implementation buffered up to +// 500 versions up front, which silently skipped reconciliation for any release +// target whose desired version sat beyond that window. With early-exit in the +// consumer, this iterator usually stops after the first few rows; only when +// every recent version is blocked by policy does it walk further history. +// +// The first batch is deduplicated via singleflight so a burst of concurrent +// reconciles for release targets sharing a deployment collapses to one DB +// round trip. The singleflight key includes a hash of extraWhere so consumers +// applying different pushdown filters do not share results. Subsequent batches +// (consumed only when the first 500 rows are exhausted without finding an +// eligible version) are fetched independently. +// +// extraWhere fragments are inlined into the SQL via string concatenation. They +// MUST come from a trusted source that emits already-escaped SQL — e.g. the +// versionselector.TryPushDown helper, which uses cel2sql with verified literal +// escaping. Never pass user-supplied raw strings here. +func (g *PostgresGetter) IterCandidateVersions( ctx context.Context, deploymentID uuid.UUID, -) ([]*oapi.DeploymentVersion, error) { - key := deploymentID.String() - v, err, _ := g.versionsSF.Do(key, func() (any, error) { - rows, err := db.GetQueries(ctx). - ListDeployableVersionsByDeploymentID(ctx, db.ListDeployableVersionsByDeploymentIDParams{ - DeploymentID: deploymentID, - Limit: pgtype.Int4{Int32: 500, Valid: true}, - }) + extraWhere []string, +) iter.Seq2[*oapi.DeploymentVersion, error] { + return func(yield func(*oapi.DeploymentVersion, error) bool) { + firstBatch, err := g.fetchFirstBatch(ctx, deploymentID, extraWhere) if err != nil { - return nil, fmt.Errorf("list versions for deployment %s: %w", deploymentID, err) + yield(nil, err) + return + } + if len(firstBatch) == 0 { + return + } + + for _, row := range firstBatch { + if !yield(db.ToOapiDeploymentVersion(row), nil) { + return + } } - versions := make([]*oapi.DeploymentVersion, 0, len(rows)) - for _, row := range rows { - versions = append(versions, db.ToOapiDeploymentVersion(row)) + if len(firstBatch) < candidateVersionBatchSize { + return } - return versions, nil + + last := firstBatch[len(firstBatch)-1] + afterCreatedAt := last.CreatedAt + afterID := last.ID + + for { + rows, err := queryCandidateVersionsBatch( + ctx, deploymentID, extraWhere, afterCreatedAt, afterID, + ) + if err != nil { + yield(nil, fmt.Errorf("list versions for deployment %s: %w", deploymentID, err)) + return + } + if len(rows) == 0 { + return + } + + for _, row := range rows { + if !yield(db.ToOapiDeploymentVersion(row), nil) { + return + } + } + + if len(rows) < candidateVersionBatchSize { + return + } + + last := rows[len(rows)-1] + afterCreatedAt = last.CreatedAt + afterID = last.ID + } + } +} + +// fetchFirstBatch loads the newest candidateVersionBatchSize deployable +// versions for a deployment, sharing the result across concurrent callers via +// singleflight keyed by (deploymentID, hash(extraWhere)). The returned slice +// is immutable: callers must not mutate it. +func (g *PostgresGetter) fetchFirstBatch( + ctx context.Context, + deploymentID uuid.UUID, + extraWhere []string, +) ([]db.DeploymentVersion, error) { + key := deploymentID.String() + "|" + hashWhere(extraWhere) + v, err, _ := g.firstBatchSF.Do(key, func() (any, error) { + return queryCandidateVersionsBatch( + ctx, deploymentID, extraWhere, pgtype.Timestamptz{}, uuid.Nil, + ) }) if err != nil { return nil, err } - return v.([]*oapi.DeploymentVersion), nil + return v.([]db.DeploymentVersion), nil +} + +// queryCandidateVersionsBatch executes a keyset-paginated SELECT against +// deployment_version with optional pushdown WHERE fragments. It bypasses the +// sqlc-generated query because sqlc cannot template-in dynamic predicates; +// the column list and base WHERE are kept structurally identical to +// ListDeployableVersionsByDeploymentIDAfter so future schema changes flow +// through both paths in lockstep. +func queryCandidateVersionsBatch( + ctx context.Context, + deploymentID uuid.UUID, + extraWhere []string, + afterCreatedAt pgtype.Timestamptz, + afterID uuid.UUID, +) ([]db.DeploymentVersion, error) { + ctx, span := getterTracer.Start(ctx, "queryCandidateVersionsBatch") + defer span.End() + + pushdownCount := 0 + for _, f := range extraWhere { + if f != "" { + pushdownCount++ + } + } + + var b strings.Builder + b.WriteString("SELECT ") + b.WriteString(candidateVersionColumns) + b.WriteString(` FROM deployment_version version +WHERE deployment_id = $1 + AND status NOT IN ('rejected', 'building') + AND ($3::timestamptz IS NULL OR (created_at, id) < ($3::timestamptz, $4::uuid))`) + for _, frag := range extraWhere { + if frag == "" { + continue + } + b.WriteString("\n AND (") + b.WriteString(frag) + b.WriteString(")") + } + b.WriteString("\nORDER BY created_at DESC, id DESC\nLIMIT $2") + + rows, err := db.GetPool(ctx).Query( + ctx, b.String(), + deploymentID, + int64(candidateVersionBatchSize), + afterCreatedAt, + afterID, + ) + if err != nil { + span.RecordError(err) + return nil, fmt.Errorf("list versions for deployment %s: %w", deploymentID, err) + } + defer rows.Close() + + var out []db.DeploymentVersion + for rows.Next() { + var v db.DeploymentVersion + if err := rows.Scan( + &v.ID, &v.Name, &v.Tag, &v.Config, &v.JobAgentConfig, &v.DeploymentID, + &v.Metadata, &v.Status, &v.Message, &v.CreatedAt, &v.WorkspaceID, + ); err != nil { + span.RecordError(err) + return nil, fmt.Errorf("scan deployment_version: %w", err) + } + out = append(out, v) + } + if err := rows.Err(); err != nil { + span.RecordError(err) + return nil, err + } + + span.SetAttributes( + attribute.String("deployment.id", deploymentID.String()), + attribute.Bool("pushdown.applied", pushdownCount > 0), + attribute.Int("pushdown.clauses_count", pushdownCount), + attribute.Bool("cursor.paginated", afterCreatedAt.Valid), + attribute.Int("rows.returned", len(out)), + attribute.Int("rows.limit", candidateVersionBatchSize), + attribute.Bool("rows.below_limit", len(out) < candidateVersionBatchSize), + ) + + return out, nil +} + +// hashWhere produces a stable cache-key suffix for a set of pushdown +// fragments. Order matters — callers should pass fragments in canonical order +// if they want different orderings to share a cache slot. +func hashWhere(extraWhere []string) string { + if len(extraWhere) == 0 { + return "" + } + h := sha256.New() + for _, frag := range extraWhere { + h.Write([]byte(frag)) + h.Write([]byte{0}) + } + return hex.EncodeToString(h.Sum(nil)) } func (g *PostgresGetter) GetApprovalRecords( diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres_test.go index c559a4a03f..63f7c04058 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres_test.go @@ -3,14 +3,17 @@ package desiredrelease_test import ( "context" "encoding/json" + "fmt" "os" "testing" + "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" desiredrelease "workspace-engine/svc/controllers/desiredrelease" ) @@ -165,18 +168,30 @@ func newReleaseTarget(f *fixture) *desiredrelease.ReleaseTarget { } } -func TestPostgresGetter_GetCandidateVersions(t *testing.T) { +func collectVersions( + t *testing.T, + getter *desiredrelease.PostgresGetter, + ctx context.Context, + deploymentID uuid.UUID, +) []*oapi.DeploymentVersion { + t.Helper() + var out []*oapi.DeploymentVersion + for v, err := range getter.IterCandidateVersions(ctx, deploymentID, nil) { + require.NoError(t, err) + out = append(out, v) + } + return out +} + +func TestPostgresGetter_IterCandidateVersions(t *testing.T) { pool := requireTestDB(t) f := setupFixture(t, pool) ctx := context.Background() getter := newGetter(pool) - t.Run("returns empty slice when no versions exist", func(t *testing.T) { - versions, err := getter.GetCandidateVersions(ctx, f.deploymentID) - require.NoError(t, err) - assert.NotNil(t, versions, "should return empty slice, not nil") - assert.Empty(t, versions) + t.Run("returns nothing when no versions exist", func(t *testing.T) { + assert.Empty(t, collectVersions(t, getter, ctx, f.deploymentID)) }) t.Run("returns only ready versions", func(t *testing.T) { @@ -200,18 +215,54 @@ func TestPostgresGetter_GetCandidateVersions(t *testing.T) { require.NoError(t, err, "insert version %s", tc.tag) } - versions, err := getter.GetCandidateVersions(ctx, f.deploymentID) - require.NoError(t, err) - + versions := collectVersions(t, getter, ctx, f.deploymentID) assert.Len(t, versions, 1, "only 'ready' versions should be returned") assert.Equal(t, readyID.String(), versions[0].Id) assert.Equal(t, "v1.0.0", versions[0].Tag) }) - t.Run("returns empty slice for nonexistent deployment", func(t *testing.T) { - versions, err := getter.GetCandidateVersions(ctx, uuid.New()) - require.NoError(t, err) - assert.Empty(t, versions) + t.Run("returns nothing for nonexistent deployment", func(t *testing.T) { + assert.Empty(t, collectVersions(t, getter, ctx, uuid.New())) + }) + + t.Run("paginates past batch size to reach older versions", func(t *testing.T) { + // Insert > batch size to confirm keyset pagination keeps walking + // rather than stopping at the first batch. Keep this comfortably + // above candidateVersionBatchSize (500) so the test exercises at + // least a second round trip. + const total = 750 + base := time.Now().Add(-time.Hour) + var oldest uuid.UUID + for i := range total { + id := uuid.New() + if i == 0 { + oldest = id + } + _, err := pool.Exec( + ctx, + `INSERT INTO deployment_version (id, name, tag, deployment_id, status, workspace_id, created_at) + VALUES ($1, $2, $3, $4, 'ready', $5, $6)`, + id, + fmt.Sprintf("v%d", i), + fmt.Sprintf("page-%d", i), + f.deploymentID, + f.workspaceID, + base.Add(time.Duration(i)*time.Second), + ) + require.NoError(t, err) + } + + versions := collectVersions(t, getter, ctx, f.deploymentID) + assert.GreaterOrEqual(t, len(versions), total) + + var foundOldest bool + for _, v := range versions { + if v.Id == oldest.String() { + foundOldest = true + break + } + } + assert.True(t, foundOldest, "iterator should walk past first batch to reach oldest version") }) } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go index b5555554ba..91ef681075 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go @@ -4,10 +4,12 @@ import ( "cmp" "context" "fmt" + "iter" "slices" "time" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" @@ -104,6 +106,10 @@ type FindDeployableVersionResult struct { // the first one that passes every evaluator. When no version qualifies, // NextTime is the earliest NextEvaluationTime across all evaluations. // +// The versions iterator is consumed lazily so callers can stream version +// history without buffering it all in memory; iteration stops as soon as a +// deployable version is found. +// // Policy skips are fetched per candidate version via getter.GetPolicySkips. // Any evaluator whose RuleId matches a non-expired skip is automatically // bypassed. @@ -111,7 +117,7 @@ func FindDeployableVersion( ctx context.Context, getter Getter, rt *oapi.ReleaseTarget, - versions []*oapi.DeploymentVersion, + versions iter.Seq2[*oapi.DeploymentVersion, error], evals []evaluator.Evaluator, scope evaluator.EvaluatorScope, ) (*FindDeployableVersionResult, error) { @@ -120,8 +126,16 @@ func FindDeployableVersion( var earliest *time.Time var allEvaluations []VersionedEvaluation + var found *oapi.DeploymentVersion + var iterErr error + var scanned int - for _, version := range versions { + for version, err := range versions { + if err != nil { + iterErr = fmt.Errorf("iter candidate versions: %w", err) + break + } + scanned++ scope.Version = version skips, err := getter.GetPolicySkips(ctx, version.Id, rt.EnvironmentId, rt.ResourceId) @@ -148,14 +162,20 @@ func FindDeployableVersion( } if eligible.Allowed() { - return &FindDeployableVersionResult{ - Version: version, - NextTime: earliest, - Evaluations: allEvaluations, - }, nil + found = version + break } } + span.SetAttributes( + attribute.String("deployment.id", rt.DeploymentId), + attribute.Int("versions.scanned", scanned), + attribute.Bool("version.found", found != nil), + ) + if iterErr != nil { + return nil, iterErr + } return &FindDeployableVersionResult{ + Version: found, NextTime: earliest, Evaluations: allEvaluations, }, nil diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go index 5cb7e5ff6e..7193388b58 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go @@ -3,6 +3,7 @@ package policyeval import ( "context" "errors" + "iter" "testing" "time" @@ -14,6 +15,19 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" ) +// iterVersions yields the supplied versions in order with no error, so existing +// table-driven tests built around slices keep their shape after FindDeployable +// Version switched to iter.Seq2. +func iterVersions(versions []*oapi.DeploymentVersion) iter.Seq2[*oapi.DeploymentVersion, error] { + return func(yield func(*oapi.DeploymentVersion, error) bool) { + for _, v := range versions { + if !yield(v, nil) { + return + } + } + } +} + // --------------------------------------------------------------------------- // Mock evaluator // --------------------------------------------------------------------------- @@ -328,7 +342,7 @@ func TestFindDeployableVersion(t *testing.T) { evals := []evaluator.Evaluator{ &mockEvaluator{result: allowResult(), scopeFields: evaluator.ScopeVersion}, } - result, err := FindDeployableVersion(ctx, getter, rt, nil, evals, fullScope()) + result, err := FindDeployableVersion(ctx, getter, rt, iterVersions(nil), evals, fullScope()) require.NoError(t, err) assert.Nil(t, result.Version) assert.Nil(t, result.NextTime) @@ -339,7 +353,14 @@ func TestFindDeployableVersion(t *testing.T) { evals := []evaluator.Evaluator{ &mockEvaluator{result: allowResult(), scopeFields: evaluator.ScopeVersion}, } - result, err := FindDeployableVersion(ctx, getter, rt, versions, evals, fullScope()) + result, err := FindDeployableVersion( + ctx, + getter, + rt, + iterVersions(versions), + evals, + fullScope(), + ) require.NoError(t, err) require.NotNil(t, result.Version) assert.Equal(t, "v1", result.Version.Id) @@ -361,7 +382,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{denyFirst}, fullScope(), ) @@ -390,7 +411,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -418,7 +439,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -435,7 +456,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -446,7 +467,14 @@ func TestFindDeployableVersion(t *testing.T) { t.Run("no evaluators means every version is eligible", func(t *testing.T) { versions := []*oapi.DeploymentVersion{version("v1"), version("v2")} - result, err := FindDeployableVersion(ctx, getter, rt, versions, nil, fullScope()) + result, err := FindDeployableVersion( + ctx, + getter, + rt, + iterVersions(versions), + nil, + fullScope(), + ) require.NoError(t, err) require.NotNil(t, result.Version) assert.Equal(t, "v1", result.Version.Id) @@ -467,7 +495,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -488,7 +516,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -504,7 +532,14 @@ func TestFindDeployableVersion(t *testing.T) { evals := []evaluator.Evaluator{ &mockEvaluator{result: allowResult(), scopeFields: evaluator.ScopeVersion}, } - result, err := FindDeployableVersion(ctx, errGetter, rt, versions, evals, fullScope()) + result, err := FindDeployableVersion( + ctx, + errGetter, + rt, + iterVersions(versions), + evals, + fullScope(), + ) require.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "get policy skips") @@ -525,7 +560,7 @@ func TestFindDeployableVersion(t *testing.T) { ctx, getter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -818,7 +853,7 @@ func TestFindDeployableVersion_PolicySkips(t *testing.T) { ctx, skipGetter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) @@ -839,7 +874,7 @@ func TestFindDeployableVersion_PolicySkips(t *testing.T) { ctx, noSkipGetter, rt, - versions, + iterVersions(versions), []evaluator.Evaluator{e}, fullScope(), ) diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/pushdown_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/pushdown_test.go new file mode 100644 index 0000000000..0c9d07b58b --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/pushdown_test.go @@ -0,0 +1,87 @@ +package desiredrelease + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "workspace-engine/pkg/oapi" +) + +func TestCollectPushdownClauses(t *testing.T) { + t.Run("nil policies → empty slice", func(t *testing.T) { + assert.Empty(t, collectPushdownClauses(nil)) + }) + + t.Run("disabled policy is skipped", func(t *testing.T) { + clauses := collectPushdownClauses([]*oapi.Policy{ + { + Enabled: false, + Rules: []oapi.PolicyRule{ + {VersionSelector: &oapi.VersionSelectorRule{Selector: `version.tag == "x"`}}, + }, + }, + }) + assert.Empty(t, clauses) + }) + + t.Run("rule without VersionSelector is skipped", func(t *testing.T) { + clauses := collectPushdownClauses([]*oapi.Policy{ + { + Enabled: true, + Rules: []oapi.PolicyRule{{}}, + }, + }) + assert.Empty(t, clauses) + }) + + t.Run("untranslatable selector falls back silently", func(t *testing.T) { + // References environment, which our pushdown schema doesn't expose. + clauses := collectPushdownClauses([]*oapi.Policy{ + { + Enabled: true, + Rules: []oapi.PolicyRule{ + { + VersionSelector: &oapi.VersionSelectorRule{ + Selector: `environment.name == "prod"`, + }, + }, + }, + }, + }) + assert.Empty(t, clauses, "selectors that can't push down must produce no clause") + }) + + t.Run("translatable selector emits clause", func(t *testing.T) { + clauses := collectPushdownClauses([]*oapi.Policy{ + { + Enabled: true, + Rules: []oapi.PolicyRule{ + { + VersionSelector: &oapi.VersionSelectorRule{ + Selector: `version.tag == "v1.2.3"`, + }, + }, + }, + }, + }) + assert.Len(t, clauses, 1) + assert.Contains(t, clauses[0], "v1.2.3") + }) + + t.Run("multiple translatable clauses returned in stable order", func(t *testing.T) { + policies := []*oapi.Policy{ + { + Enabled: true, + Rules: []oapi.PolicyRule{ + {VersionSelector: &oapi.VersionSelectorRule{Selector: `version.tag == "z"`}}, + {VersionSelector: &oapi.VersionSelectorRule{Selector: `version.tag == "a"`}}, + }, + }, + } + clauses1 := collectPushdownClauses(policies) + clauses2 := collectPushdownClauses(policies) + assert.Equal(t, clauses1, clauses2, "same input must produce identical output") + assert.Len(t, clauses1, 2) + assert.LessOrEqual(t, clauses1[0], clauses1[1], "clauses must be sorted") + }) +} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go index 33fe6ef6a4..5822f90e24 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go @@ -11,6 +11,7 @@ import ( "go.opentelemetry.io/otel/trace" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector" "workspace-engine/svc/controllers/desiredrelease/policyeval" "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) @@ -30,7 +31,6 @@ type reconciler struct { rt *ReleaseTarget scope *evaluator.EvaluatorScope - versions []*oapi.DeploymentVersion policies []*oapi.Policy version *oapi.DeploymentVersion vars map[string]oapi.LiteralValue @@ -42,11 +42,6 @@ func (r *reconciler) loadInput(ctx context.Context) (err error) { return fmt.Errorf("get release target scope: %w", err) } - r.versions, err = r.getter.GetCandidateVersions(ctx, r.rt.DeploymentID) - if err != nil { - return fmt.Errorf("get candidate versions: %w", err) - } - r.policies, err = r.getter.GetPoliciesForReleaseTarget(ctx, r.rt.ToOAPI()) if err != nil { return fmt.Errorf("get policies: %w", err) @@ -56,21 +51,18 @@ func (r *reconciler) loadInput(ctx context.Context) (err error) { } // findDeployableVersion evaluates policy rules against candidate versions -// (newest-first) and sets r.version to the first passing version. Returns -// the earliest NextEvaluationTime when all versions are blocked. +// (newest-first, streamed) and sets r.version to the first passing version. +// Returns the earliest NextEvaluationTime when all versions are blocked. func (r *reconciler) findDeployableVersion(ctx context.Context) *time.Time { - if len(r.versions) == 0 { - return nil - } - oapiRT := r.rt.ToOAPI() evals := policyeval.CollectEvaluators(ctx, r.getter, oapiRT, r.policies) + pushdown := collectPushdownClauses(r.policies) result, err := policyeval.FindDeployableVersion( ctx, r.getter, oapiRT, - r.versions, + r.getter.IterCandidateVersions(ctx, r.rt.DeploymentID, pushdown), evals, *r.scope, ) @@ -139,7 +131,7 @@ func Reconcile( return nil, recordErr(span, "load input", err) } - log.Info("find deployable version", "versions", len(r.versions)) + log.Info("find deployable version") nextTime := r.findDeployableVersion(ctx) if r.version == nil { @@ -166,6 +158,53 @@ func Reconcile( return &ReconcileResult{NextReconcileAt: nextTime}, nil } +// collectPushdownClauses inspects a release target's policies for +// versionselector rules and translates each into a SQL WHERE fragment via +// versionselector.TryPushDown. Rules that can't be translated (selectors +// referencing environment/resource/deployment, complex CEL, etc.) are +// silently skipped — the runtime CEL evaluator still runs per-version, so +// correctness is preserved; only the candidate-set narrowing is lost. +// +// The returned slice is sorted by clause text so concurrent reconciles +// against the same deployment with the same selector set produce the same +// singleflight cache key. +func collectPushdownClauses(policies []*oapi.Policy) []string { + var clauses []string + for _, p := range policies { + if p == nil || !p.Enabled { + continue + } + for _, rule := range p.Rules { + if rule.VersionSelector == nil { + continue + } + clause, ok := versionselector.TryPushDown(rule.VersionSelector.Selector) + if !ok { + continue + } + clauses = append(clauses, clause) + } + } + if len(clauses) > 1 { + // Stable order so the singleflight key is deterministic across + // reconciles that see the same logical clause set. + sortStrings(clauses) + } + return clauses +} + +// sortStrings is an in-place insertion sort. We use it instead of +// sort.Strings to keep the import surface small for this hot path. +func sortStrings(s []string) { + for i := 1; i < len(s); i++ { + j := i + for j > 0 && s[j-1] > s[j] { + s[j-1], s[j] = s[j], s[j-1] + j-- + } + } +} + func recordErr(span trace.Span, msg string, err error) error { span.RecordError(err) span.SetStatus(codes.Error, msg+" failed") diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go index 3e5d1c168b..583198cf6f 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go @@ -2,6 +2,7 @@ package desiredrelease import ( "context" + "iter" "testing" "github.com/google/uuid" @@ -44,11 +45,18 @@ func (m *mockReconcileGetter) GetReleaseTargetScope( return m.scope, nil } -func (m *mockReconcileGetter) GetCandidateVersions( +func (m *mockReconcileGetter) IterCandidateVersions( _ context.Context, _ uuid.UUID, -) ([]*oapi.DeploymentVersion, error) { - return m.versions, nil + _ []string, +) iter.Seq2[*oapi.DeploymentVersion, error] { + return func(yield func(*oapi.DeploymentVersion, error) bool) { + for _, v := range m.versions { + if !yield(v, nil) { + return + } + } + } } func (m *mockReconcileGetter) GetPoliciesForReleaseTarget( diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go index 538b97af24..534489eb3a 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/relationships/eval" @@ -24,26 +23,6 @@ type PostgresGetter struct { queries *db.Queries } -func (g *PostgresGetter) GetCandidateVersions( - ctx context.Context, - deploymentID uuid.UUID, -) ([]*oapi.DeploymentVersion, error) { - rows, err := db.GetQueries(ctx). - ListDeployableVersionsByDeploymentID(ctx, db.ListDeployableVersionsByDeploymentIDParams{ - DeploymentID: deploymentID, - Limit: pgtype.Int4{Int32: 500, Valid: true}, - }) - if err != nil { - return nil, fmt.Errorf("list versions for deployment %s: %w", deploymentID, err) - } - - versions := make([]*oapi.DeploymentVersion, 0, len(rows)) - for _, row := range rows { - versions = append(versions, db.ToOapiDeploymentVersion(row)) - } - return versions, nil -} - func (g *PostgresGetter) GetApprovalRecords( ctx context.Context, versionID, environmentID string, diff --git a/apps/workspace-engine/test/controllers/harness/mocks.go b/apps/workspace-engine/test/controllers/harness/mocks.go index 70afef4fc4..8e01e4a311 100644 --- a/apps/workspace-engine/test/controllers/harness/mocks.go +++ b/apps/workspace-engine/test/controllers/harness/mocks.go @@ -3,6 +3,7 @@ package harness import ( "context" "fmt" + "iter" "strings" "sync" "time" @@ -166,11 +167,18 @@ func (g *DesiredReleaseGetter) GetReleaseTargetScope( return g.Scope, nil } -func (g *DesiredReleaseGetter) GetCandidateVersions( +func (g *DesiredReleaseGetter) IterCandidateVersions( _ context.Context, _ uuid.UUID, -) ([]*oapi.DeploymentVersion, error) { - return g.Versions, nil + _ []string, +) iter.Seq2[*oapi.DeploymentVersion, error] { + return func(yield func(*oapi.DeploymentVersion, error) bool) { + for _, v := range g.Versions { + if !yield(v, nil) { + return + } + } + } } func (g *DesiredReleaseGetter) GetPoliciesForReleaseTarget( From ea9bd76609d09f85c2bd1421eae165a61722f230 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Tue, 5 May 2026 15:54:00 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(desiredrelease):=20detach=20singlefligh?= =?UTF-8?q?t=20ctx=20and=20de-duplicate=20error=20wrap=20*=20singleflight.?= =?UTF-8?q?Group.Do=20runs=20the=20closure=20in=20the=20first=20caller's?= =?UTF-8?q?=20goroutine,=20=20=20so=20the=20first=20caller's=20ctx=20cance?= =?UTF-8?q?llation=20aborted=20the=20shared=20DB=20query=20for=20=20=20eve?= =?UTF-8?q?ry=20other=20waiter=20on=20the=20same=20key.=20fetchFirstBatch?= =?UTF-8?q?=20now=20passes=20=20=20context.WithoutCancel(ctx)=20into=20the?= =?UTF-8?q?=20closure=20=E2=80=94=20cancellation=20is=20detached=20=20=20f?= =?UTF-8?q?rom=20any=20individual=20caller=20while=20trace=20context=20is?= =?UTF-8?q?=20preserved.=20Per-pool=20=20=20timeouts=20still=20bound=20run?= =?UTF-8?q?away=20queries.=20*=20The=20batch-loop=20in=20IterCandidateVers?= =?UTF-8?q?ions=20wrapped=20errors=20a=20second=20time=20=20=20even=20thou?= =?UTF-8?q?gh=20queryCandidateVersionsBatch=20already=20formats=20them=20a?= =?UTF-8?q?s=20=20=20"list=20versions=20for=20deployment=20%s:=20%w".=20Th?= =?UTF-8?q?e=20first-batch=20path=20passed=20the=20=20=20error=20through?= =?UTF-8?q?=20unwrapped,=20so=20the=20two=20sites=20produced=20inconsisten?= =?UTF-8?q?t=20messages=20=20=20("list=20versions=20for=20deployment=20X:?= =?UTF-8?q?=20list=20versions=20for=20deployment=20X:=20...").=20=20=20Dro?= =?UTF-8?q?p=20the=20outer=20wrap=20so=20both=20paths=20surface=20a=20sing?= =?UTF-8?q?le=20layer.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../svc/controllers/desiredrelease/getters_postgres.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go index 08d6faf722..49ea57599e 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go @@ -162,7 +162,7 @@ func (g *PostgresGetter) IterCandidateVersions( ctx, deploymentID, extraWhere, afterCreatedAt, afterID, ) if err != nil { - yield(nil, fmt.Errorf("list versions for deployment %s: %w", deploymentID, err)) + yield(nil, err) return } if len(rows) == 0 { @@ -196,9 +196,13 @@ func (g *PostgresGetter) fetchFirstBatch( extraWhere []string, ) ([]db.DeploymentVersion, error) { key := deploymentID.String() + "|" + hashWhere(extraWhere) + // Detach the singleflight closure from the first caller's cancellation + // so one caller's ctx cancellation doesn't fail the shared query for + // every other waiter on the same key. Trace context is preserved. + qCtx := context.WithoutCancel(ctx) v, err, _ := g.firstBatchSF.Do(key, func() (any, error) { return queryCandidateVersionsBatch( - ctx, deploymentID, extraWhere, pgtype.Timestamptz{}, uuid.Nil, + qCtx, deploymentID, extraWhere, pgtype.Timestamptz{}, uuid.Nil, ) }) if err != nil {